From 047a073dedf6c19690e745a813f940f3acc364ce Mon Sep 17 00:00:00 2001 From: Jhon Ross Date: Mon, 21 Sep 2026 17:03:26 -0300 Subject: [PATCH] feat: publish PT-BR Wi-Fi and Bluetooth community adaptation Bundle the Windows launcher, translated UI, hybrid device inventory, and Observatory radio fusion so Brazilian home users can run the project locally with clearer behavior and documentation. --- .gitignore | 6 + README.md | 51 + ...rativo-ruview-original-vs-adaptado-ptbr.md | 261 ++++ .../debug-basic-rssi-coverage-ptbr.md | 83 ++ .../debug-basic-rssi-invalid-ptbr.md | 65 + start.bat | 16 + start_ruview.ps1 | 687 ++++++++++ ui/app.js | 37 +- ui/components/DashboardTab.js | 313 ++++- ui/components/HardwareTab.js | 10 +- ui/components/LiveDemoTab.js | 226 +-- ui/components/SensingTab.js | 95 +- ui/index.html | 315 +++-- ui/observatory.html | 148 +- ui/observatory/css/observatory.css | 70 + ui/observatory/js/figure-pool.js | 110 +- ui/observatory/js/hud-controller.js | 234 +++- ui/observatory/js/main.js | 179 ++- ui/pose-fusion.html | 98 +- ui/pose-fusion/js/main.js | 47 +- ui/style.css | 143 +- ui/utils/i18n.js | 1213 +++++++++++++++-- ui/utils/local-dev-cache-guard.js | 83 ++ ui/utils/onboarding.js | 110 +- ui/utils/quick-settings.js | 5 +- .../src/hybrid_detection.rs | 511 +++++++ .../wifi-densepose-sensing-server/src/main.rs | 952 ++++++++++++- .../src/adapter/netsh_scanner.rs | 44 +- 28 files changed, 5456 insertions(+), 656 deletions(-) create mode 100644 docs/validation/comparativo-ruview-original-vs-adaptado-ptbr.md create mode 100644 docs/validation/debug-basic-rssi-coverage-ptbr.md create mode 100644 docs/validation/debug-basic-rssi-invalid-ptbr.md create mode 100644 start.bat create mode 100644 start_ruview.ps1 create mode 100644 ui/utils/local-dev-cache-guard.js create mode 100644 v2/crates/wifi-densepose-sensing-server/src/hybrid_detection.rs diff --git a/.gitignore b/.gitignore index a22f770048..4ef3035042 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,12 @@ firmware/esp32-csi-node/test/*.obj # Claude Flow swarm runtime state .swarm/ +# Local runtime/debug artifacts from the Windows PT-BR contribution workflow +/.dbg/ +/v2/.dbg/ +/v2/crates/wifi-densepose-sensing-server/.dbg/ +/v2/data/config.json + # CSI recordings (local training/capture data — CSI is person data per # CLAUDE.md; never commit). Covers current and legacy layouts. See ADR-299. data/recordings/ diff --git a/README.md b/README.md index 5233dcd778..39b772a398 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,57 @@ **Turn ordinary WiFi into a spatial intelligence / sensing system.** Detect people, measure breathing and heart rate, track movement, and monitor rooms — through walls, in the dark, with no cameras or wearables. Just physics. +## Fork roadmap: Hybrid Wi-Fi intelligence + +This fork is evolving RuView into a **hybrid sensing + network-awareness** platform for home and small-office environments. + +Our goal is to keep the upstream WiFi sensing pipeline intact while adding a practical layer that can: + +- inventory devices visible on the local network, +- heuristically classify likely **smartphones**, **smart TVs**, **computers**, and **IoT/media** devices, +- fuse that inventory with RuView's **presence / motion / estimated-person** outputs, +- help operators distinguish **human movement** from **stationary connected devices**. + +### What this fork is adding + +- `GET /api/v1/hybrid/latest` for a fused hybrid snapshot +- `GET /api/v1/hybrid/overrides`, `POST /api/v1/hybrid/overrides/upsert`, and `DELETE /api/v1/hybrid/overrides/delete` for manual device labeling +- `GET /api/v1/bluetooth/status` for an **experimental Windows-only Bluetooth helper** status +- LAN device discovery from the local machine +- heuristic device classification for common home devices +- manual override of device naming/category for home deployments +- dashboard visibility into: + - estimated humans, + - known devices, + - likely smartphones, + - likely smart TVs, + - Bluetooth helper availability / adapter / service state + +### Honest scope + +This does **not** mean RuView can magically identify every device purely from ambient RF disturbance. The fork uses a **hybrid approach**: + +- **WiFi sensing layer** for presence, motion, and human-related inference +- **network inventory layer** for connected-device hints +- **manual home-device labeling** for practical overrides in non-ideal networks +- **Bluetooth helper layer** as an optional Windows-only context signal +- **fusion layer** to keep devices like TVs and idle phones from being mistaken for people + +In the current notebook/basic-RSSI mode, this fork aims to extract the **maximum useful home-presence signal** without pretending it has CSI-grade spatial precision. Presence and movement can still be useful; stable multi-person separation remains a hardware-limited case until CSI-capable hardware is used. + +### Roadmap + +1. **Phase 1** + LAN discovery + heuristic classification + dashboard fusion +2. **Phase 2** + router/vendor integrations for richer client metadata +3. **Phase 3** + stronger room-level fusion with CSI-capable multi-node deployments +4. **Phase 4** + better semantic automation and publishable case studies + +If this fork proves useful in practice, the plan is to upstream the pieces that are generic and maintain the rest as optional extensions for hybrid deployments. + Works natively with the four major smart-home ecosystems: **[Home Assistant](docs/integrations/home-assistant.md)** via the HA-DISCO MQTT publisher, **[Apple Home & HomePod](docs/user-guide-apple-homepod.md)** as a discoverable HAP-1.1 bridge, **[Google Home](docs/integrations/home-assistant.md)** + **[Amazon Alexa](docs/integrations/home-assistant.md)** via the same HA bridge or a [Matter](docs/adr/ADR-122-bfld-ruview-ha-matter-exposure.md) endpoint. Siri, Google Assistant, and Alexa can voice presence and vitals by room with zero custom skills. [![Works with Home Assistant](https://img.shields.io/badge/Works%20with-Home%20Assistant-blue?logo=home-assistant&logoColor=white&labelColor=41BDF5)](docs/integrations/home-assistant.md) [![Works with Matter](https://img.shields.io/badge/Works%20with-Matter-blue?labelColor=4285F4)](docs/adr/ADR-122-bfld-ruview-ha-matter-exposure.md) [![Works with Apple Home](https://img.shields.io/badge/Works%20with-Apple%20Home-black?logo=apple)](docs/user-guide-apple-homepod.md) [![Works with Google Home](https://img.shields.io/badge/Works%20with-Google%20Home-blue?logo=googlehome)](docs/integrations/home-assistant.md) [![Works with Alexa](https://img.shields.io/badge/Works%20with-Alexa-blue?logo=amazon&logoColor=white&labelColor=00CAFF)](docs/integrations/home-assistant.md) diff --git a/docs/validation/comparativo-ruview-original-vs-adaptado-ptbr.md b/docs/validation/comparativo-ruview-original-vs-adaptado-ptbr.md new file mode 100644 index 0000000000..30a1eb2217 --- /dev/null +++ b/docs/validation/comparativo-ruview-original-vs-adaptado-ptbr.md @@ -0,0 +1,261 @@ +# Comparativo: RuView original (`HEAD`) vs nossa adaptacao BR + +## 1. Baseline usado + +Este comparativo usa como **projeto original** o estado limpo do repositório `RuView` em `HEAD` no branch `main`, porque este clone local não possui remoto `upstream` separado. +Ou seja: + +- **Original** = `HEAD` limpo do repo +- **Adaptado / nosso** = working tree local atual + arquivos novos criados na contribuição + +## 2. Visao executiva + +Nossa adaptacao transforma o RuView original em uma variante mais voltada para: + +- **usuarios brasileiros leigos** +- **instalacao local em Windows** +- **uso com Wi-Fi caseiro / notebook no modo basico** +- **inventario hibrido de dispositivos da rede** +- **interface traduzida e mais explicita** +- **launcher em PT-BR com logs operacionais** + +Em troca, esta adaptacao assume conscientemente um escopo mais pragmatico: + +- nao promete precisao de radar/CSI quando o hardware e apenas notebook + RSSI +- trata Bluetooth como **auxilio experimental** +- usa camadas hibridas e overrides manuais para separar melhor **pessoas x dispositivos** + +## 3. Diferencas principais por area + +### A. Inicializacao e experiencia para leigos + +**Original** +- inicializacao mais tecnica +- fluxo menos orientado a usuario final local Windows +- sem launcher PT-BR dedicado no nosso workspace local + +**Adaptado** +- adicionados: + - `start.bat` + - `start_ruview.ps1` +- o launcher: + - limpa conflitos de porta/processo + - abre a interface automaticamente + - mostra logs em portugues + - mantem o console visivel + - agora tambem informa o estado do **Bluetooth auxiliar experimental** + +### B. Traducao e localizacao PT-BR + +**Original** +- UI orientada ao ingles, com cobertura local limitada para nosso publico alvo + +**Adaptado** +- ampliada traducao em: + - `ui/index.html` + - `ui/components/DashboardTab.js` + - `ui/components/HardwareTab.js` + - `ui/components/LiveDemoTab.js` + - `ui/components/SensingTab.js` + - `ui/pose-fusion.html` + - `ui/pose-fusion/js/main.js` + - `ui/observatory.html` + - `ui/utils/i18n.js` +- persistencia de idioma e textos adicionais para o fluxo BR/leigo + +### C. Protecao contra cache e coerencia local + +**Original** +- sem a camada local especifica que resolvemos aqui para desenvolvimento e cache do navegador no nosso ambiente + +**Adaptado** +- novo arquivo: + - `ui/utils/local-dev-cache-guard.js` +- uso de `boot=` / cache-busting e origem canonica `localhost` +- mitigacao dos problemas reais que apareciam no Chrome com scripts antigos e divergencia `127.0.0.1` vs `localhost` + +### D. Modo basico Wi-Fi caseiro + +**Original** +- o caminho de sensing basico estava mais conservador +- no nosso uso real, isso derrubava demais a leitura multipessoa e fazia o sistema colapsar facil para `0/1` + +**Adaptado** +- ajustado: + - `v2/crates/wifi-densepose-wifiscan/src/adapter/netsh_scanner.rs` + - `v2/crates/wifi-densepose-sensing-server/src/main.rs` +- ganhos aplicados: + - parser `netsh` mais robusto para contexto PT-BR + - melhor aproveitamento do RSSI no modo `wifi:*` + - heuristica pratica menos travada + - pequena retencao temporal para evitar queda imediata em oscilacoes curtas + - preservacao melhor da contagem resolvida antes de montar a pose da UI + +### E. Camada hibrida de dispositivos de rede + +**Original** +- nao havia no baseline limpo do repo local esta camada domestica orientada a inventario de aparelhos da casa + +**Adaptado** +- novo backend: + - `v2/crates/wifi-densepose-sensing-server/src/hybrid_detection.rs` +- integracao no servidor: + - `v2/crates/wifi-densepose-sensing-server/src/main.rs` +- capacidade adicionada: + - varrer dispositivos da LAN + - inferir categoria heuristica + - contar provaveis smartphones / smart TVs / computadores / IoT + - combinar isso com presenca humana inferida pelo Wi-Fi + +### F. Cadastro manual de dispositivos conhecidos + +**Original** +- sem area dedicada para o usuario corrigir nome/categoria de aparelhos detectados + +**Adaptado** +- Dashboard ganhou area para: + - ver dispositivos detectados + - renomear + - corrigir categoria + - cadastrar manualmente + - salvar override persistente +- arquivos centrais: + - `ui/index.html` + - `ui/components/DashboardTab.js` + - `ui/style.css` + - `ui/utils/i18n.js` + - `v2/crates/wifi-densepose-sensing-server/src/main.rs` +- novos endpoints: + - `GET /api/v1/hybrid/overrides` + - `POST /api/v1/hybrid/overrides/upsert` + - `DELETE /api/v1/hybrid/overrides/delete` + +### G. Bluetooth auxiliar experimental + +**Original** +- sem trilha pronta de Bluetooth integrada para o nosso caso de uso + +**Adaptado** +- suporte **experimental e Windows-only** +- sem vender Bluetooth como detector humano principal +- usado como contexto operacional / indicio auxiliar +- pontos alterados: + - `start_ruview.ps1` + - `ui/index.html` + - `ui/components/DashboardTab.js` + - `ui/utils/i18n.js` + - `v2/crates/wifi-densepose-sensing-server/src/main.rs` +- novos endpoints: + - `GET /api/v1/bluetooth/status` + - `POST /api/v1/bluetooth/enabled` + +### H. Observatory mais honesto para leigo + +**Original** +- leitura visual mais ambigua no nosso uso pratico +- aura e destaque poderiam ser interpretados como algo mais preciso do que realmente eram + +**Adaptado** +- alterados: + - `ui/observatory.html` + - `ui/observatory/css/observatory.css` + - `ui/observatory/js/main.js` + - `ui/observatory/js/hud-controller.js` + - `ui/observatory/js/figure-pool.js` + - `ui/utils/i18n.js` +- melhorias: + - modo realista + - destaque por dispositivo condicionado ao contexto hibrido + - explicacao textual do significado da aura + - bloco visivel de fusao **Wi-Fi + Bluetooth auxiliar** + - visual mais discreto e menos enganoso + +### I. README e narrativa do fork + +**Original** +- README sem o recorte completo da nossa adaptacao BR/local-caseira + +**Adaptado** +- `README.md` atualizado para registrar: + - camada hibrida + - overrides manuais + - Bluetooth auxiliar experimental + - honestidade sobre os limites do notebook + RSSI + +## 4. Arquivos alterados no working tree + +### Arquivos rastreados modificados + +- `README.md` +- `ui/app.js` +- `ui/components/DashboardTab.js` +- `ui/components/HardwareTab.js` +- `ui/components/LiveDemoTab.js` +- `ui/components/SensingTab.js` +- `ui/index.html` +- `ui/observatory.html` +- `ui/observatory/css/observatory.css` +- `ui/observatory/js/figure-pool.js` +- `ui/observatory/js/hud-controller.js` +- `ui/observatory/js/main.js` +- `ui/pose-fusion.html` +- `ui/pose-fusion/js/main.js` +- `ui/style.css` +- `ui/utils/i18n.js` +- `v2/crates/wifi-densepose-sensing-server/src/main.rs` +- `v2/crates/wifi-densepose-wifiscan/src/adapter/netsh_scanner.rs` + +### Arquivos novos locais relevantes da contribuicao + +- `start.bat` +- `start_ruview.ps1` +- `ui/utils/local-dev-cache-guard.js` +- `v2/crates/wifi-densepose-sensing-server/src/hybrid_detection.rs` +- `docs/validation/comparativo-ruview-original-vs-adaptado-ptbr.md` +- `docs/validation/debug-basic-rssi-coverage-ptbr.md` +- `docs/validation/debug-basic-rssi-invalid-ptbr.md` + +## 5. Diferenca de filosofia entre original e adaptado + +### Original + +- mais proximo da base tecnica/generica do projeto +- maior foco em pipeline e pesquisa +- menos opinativo para o usuario final domestico brasileiro + +### Adaptado + +- assume explicitamente o papel de **versao amigavel para BR** +- prioriza: + - instalacao simples + - feedback visual claro + - textos em portugues + - controle operacional local + - honestidade sobre o que e deteccao humana e o que e apenas contexto de rede + +## 6. Limites que continuam existindo + +Mesmo com a adaptacao, alguns limites continuam reais: + +- notebook + RSSI basico **nao vira CSI** +- multipessoa em comodos separados continua instavel +- Bluetooth entra apenas como apoio experimental +- classificacao automatica de dispositivos ainda e heuristica +- para salto real de qualidade em multi-pessoa/localizacao, o caminho continua sendo **ESP32-S3 / CSI** + +## 7. Valor da contribuicao + +Esta contribuicao entrega algo que o baseline original nao estava focando diretamente no nosso contexto: + +- uma **versao mais utilizavel por brasileiros leigos** +- com **launcher operacional** +- **traducao mais ampla** +- **inventario hibrido** +- **cadastro manual de dispositivos** +- **Observatory mais explicativo** +- e uma abordagem mais honesta para o uso de **Wi-Fi caseiro como sensor de presenca** + +Em resumo: + +> O projeto original continua sendo a base tecnica. +> A nossa adaptacao transforma essa base em uma versao mais acessivel, mais local, mais transparente e mais util para uso domestico no Brasil. diff --git a/docs/validation/debug-basic-rssi-coverage-ptbr.md b/docs/validation/debug-basic-rssi-coverage-ptbr.md new file mode 100644 index 0000000000..779242ce48 --- /dev/null +++ b/docs/validation/debug-basic-rssi-coverage-ptbr.md @@ -0,0 +1,83 @@ +# Debug Session: basic-rssi-coverage +- **Status**: [CONCLUIDO — EVIDENCIA COLETADA E AJUSTE APLICADO] +- **Issue**: No modo basico Windows/RSSI (sem ESP32-S3), com varias pessoas em areas diferentes da casa (PC na mesa, cozinha, quarto), o Observatorio nao mostra sinal de cobertura multi-pessoa e o numero de pessoas nao passa de 1. Queremos confirmar se ainda estamos "deixando passar" sinal ou se isso ja e o limite real desse hardware/source. +- **Debug Server**: http://127.0.0.1:7777/event +- **Log File**: .dbg/trae-debug-log-basic-rssi-coverage.ndjson + +## Pipeline encontrado (leitura estatica de codigo) + +### Cap estrutural de codigo confirmado +- `practical_wifi_person_count_cap()`: havia um freio estrutural agressivo no modo `wifi:*`, principalmente quando a visibilidade caia para poucos BSSIDs. +- `score_to_person_count()`: limiares altos para adicionar pessoas: + - 1→2 exige `smoothed_score > 0.70` (histerese down 0.55) + - 2→3 exige `smoothed_score > 0.92` +- `practical_wifi_quality_override()`: havia um clamp pratico conservador demais no modo basico com baixa visibilidade. +- **Conclusao final da leitura + runtime**: o limite observado vinha de uma combinacao de: + 1. hardware RSSI de notebook oscilando entre fases boas (`obs_count` 4-6) e fases ruins (`obs_count` 1), + 2. heuristicas do software conservadoras demais para o caminho `wifi:*`. + +### Instrumentation points aplicados +Arquivo: `v2/crates/wifi-densepose-sensing-server/src/main.rs` +| Ponto | Local | O que captura | Hipoteses | +|-------|-------|---------------|-----------| +| B | `wifi_task::after_scan` | Lista completa de BSSIDs visiveis (SSID/BSSID/RSSI/sinal/ch/banda, obs_count) | A, B, D | +| AC | `wifi_task::after_cap` | `raw_score`, `smoothed_person_score`, `pure_person_count` (antes do cap via `s.person_count()`), `cap_branch` (qual regra do practical_cap pegou), `estimated_persons_after_cap`, variance, motion, presence, quality | A, C, D | +| AC | `windows_wifi_fallback_tick::after_cap` | Mesmo do AC para o caminho fallback single-RSSI | A, C | +| E | `hybrid_poll_task` | Snapshot hybrid: `total_devices`, `fusion`, `network_devices` list com ip/mac/hostname/categoria/vendor/confidence | E | + +Helpers atualizados para procurar `.dbg/` em multiplos paths relativos (WD até ../../../../). + +## Status das hipoteses apos instrumentacao +| ID | Hypothesis | Likelihood | Effort | Evidence | +|----|------------|------------|--------|----------| +| A | O modo basico netsh esta lendo apenas 1 BSSID valido e o pipeline/baseline esta descartando/limiting para 1 pessoa na mesma heuristica de min_bssids | High | Low | **Parcialmente confirmada**: no inicio do teste houve `obs_count=4` e em outro trecho `obs_count=6`, mas no estado final/atual o scan caiu para `obs_count=1` (`JHON_5G`). | +| B | Netsh wlan mostra outros BSSIDs (rede vizinha, band 2.4G vs 5G) mas eles estao com RSSI qualificado como ruido estavel e nao entram na contagem | High | Low | **Parcialmente confirmada**: o scanner enxergou multiplos BSSIDs no comeco (`2.4G + 5G + radios extras`), depois colapsou para um unico BSSID visivel. | +| C | A pipeline esta capturando RSSI/variance validos, mas a heuristica `estimated_persons` ainda esta clampada para 1 no fallback basico Windows | High | Low | **Confirmada por evidencia**: `pure_person_count_via_score_to_person_count` chegou a `2` e `estimated_persons_after_cap` tambem chegou a `2` quando havia `obs_count>=4` e qualidade `Permit`. No estado final, com `obs_count=1`, o branch passou para `force_1__live_le_2`. | +| D | A qualidade do canal esta ruim e ha 3 pessoas porem o sinal real de todas elas esta abaixo do minimo medivel do RSSI bruto (pessoas longe/obstaculos pesados) | Medium | Low | **Parcialmente confirmada**: no estado final a qualidade pratica caiu para `Warn`, `variance=0`, `motion_band_power=0` e o sistema ficou cego para cobertura multipessoa. | +| E | A camada hybrid detecta varios dispositivos mas nao esta repassando limite de pessoas para a UI live do Observatory | Low | Low | **Confirmada em parte**: havia `3` dispositivos conhecidos, mas `likely_smartphones=0` em todos os eventos registrados; portanto a UI nao tinha base para ligar destaque por smartphone. | + +## Findings principais +- O teste mostrou que o sistema **nao ficou preso em 1 o tempo todo**: + - `obs_count` variou de **1** ate **6** + - `pure_person_count_via_score_to_person_count` chegou a **2** + - `estimated_persons_after_cap` chegou a **2** +- O estado final do print atual corresponde a uma fase pior do radio: + - `obs_count=1` + - `quality_verdict=Warn` + - `variance=0` + - `motion_band_power=0` + - `estimated_persons=1` +- A camada hibrida detectou dispositivos na LAN, mas **nao classificou nenhum como smartphone** (`likely_smartphones=0`), entao qualquer "aura de dispositivo" percebida na UI estava ambigua visualmente e precisava de ajuste no frontend. +- O fork agora recebeu um ajuste pratico adicional no backend: + - cap de pessoas menos travado no modo `wifi:*`, + - quality override mais permissivo quando houver movimento real, + - pequena retencao temporal para evitar colapso imediato de `2 -> 0/1` em oscilacoes curtas, + - `derive_pose_from_sensing()` preservando melhor a contagem resolvida no tick. + +## Reproduction Steps +1. Manter Debug Server rodando em http://127.0.0.1:7777 (ver /health OK) +2. Garantir que portas 3000/3001/8765 LIVRES +3. NDJSON limpo em `.dbg/trae-debug-log-basic-rssi-coverage.ndjson` +4. **CENARIO**: voce no PC (movendo mouse/digitando), esposa na cozinha com celular, filha no quarto com celular/aparelho ligado — manter por **3 MINUTOS** +5. **EXECUCAO**: duplo clique em `RuView\start.bat` (launcher usa `cargo run` → carrega codigo instrumentado) +6. Abrir Observatory em `http://localhost:3000/ui/observatory.html` e conferir: + - Badge LIVE (nao DEMO) + - Contador de pessoas na HUD (canto superior direito?) + - Modo realista e destaque por dispositivo LIGADOS +7. Apos 3 minutos, parar a execucao (fechar janela do launcher) e coletar ndjson +8. Analisar: quantos % dos logs AC tem cap_branch force_1? Qual o max pure_person_count? + +## Conclusao final +- **Nao** era correto dizer que o notebook estava extraindo "tudo" no estado anterior. +- **Sim**, havia freio de software removivel e ele foi afrouxado nesta sessao. +- **Mesmo apos o ajuste**, o modo basico continua limitado por fisica/hardware: + - bom para **presenca geral**, + - util para **movimento**, + - pode sugerir **2 pessoas** em janelas boas, + - **nao** e um caminho confiavel para separar varias pessoas em comodos diferentes com consistencia de radar/CSI. + +## Proximo uso recomendado +- usar o modo basico como **sensor de presenca/movimento em tempo real**; +- usar o inventario hibrido + cadastro manual para separar melhor **pessoas x aparelhos conectados**; +- tratar Bluetooth apenas como **auxilio experimental de contexto** no Windows; +- reservar **ESP32-S3 / CSI** para o salto real de qualidade em multi-pessoa e localizacao. diff --git a/docs/validation/debug-basic-rssi-invalid-ptbr.md b/docs/validation/debug-basic-rssi-invalid-ptbr.md new file mode 100644 index 0000000000..b73eda098d --- /dev/null +++ b/docs/validation/debug-basic-rssi-invalid-ptbr.md @@ -0,0 +1,65 @@ +# Debug Session: basic-rssi-invalid +- **Status**: [OPEN] +- **Issue**: O modo basico via RSSI conecta ao backend e ao Observatory, mas o sinal chega com qualidade invalida ou quase nula (`quality_verdict: Deny`, `signal_quality_score: 0`, `mean_rssi: -100`, `variance: 0`, `motion_band_power: 0`), impedindo deteccao humana confiavel. +- **Debug Server**: http://127.0.0.1:7777/event +- **Log File**: .dbg/trae-debug-log-basic-rssi-invalid.ndjson + +## Reproduction Steps +1. Iniciar o RuView em Windows pelo `start.bat` com `--source wifi`. +2. Abrir `http://127.0.0.1:3000/ui/observatory.html`. +3. Observar o painel de sinal e a presenca no modo basico. +4. Comparar os snapshots de `GET /api/v1/sensing/latest` durante o teste. + +## Hypotheses & Verification +| ID | Hypothesis | Likelihood | Effort | Evidence | +|----|------------|------------|--------|----------| +| A | O coletor RSSI do Windows nao esta conseguindo ler amostras reais do adaptador e cai em valores sentinela/degenerados. | High | Low | Pending | +| B | O pipeline recebe amostras, mas a filtragem/deduplicacao zera a variancia e o motion score antes da classificacao. | High | Medium | Pending | +| C | O adaptador/rede atual entrega dados somente do AP conectado, com atualizacao lenta ou congelada, abaixo do minimo esperado pelo algoritmo. | Medium | Medium | Pending | +| D | O modo `wifi` esta pegando a interface errada ou um estado stale do Windows, produzindo snapshots validos do ponto de vista tecnico, mas sem relacao com o modem/rede testados. | Medium | Medium | Pending | +| E | A classificacao/adaptive classifier do modo basico esta inferindo `presence` de forma otimista mesmo quando o sinal base esta invalido. | Medium | Low | Pending | + +## Log Evidence +Instrumentacao ativa em: +- `main.rs:parse_netsh_interfaces_output` -> hipotese A +- `main.rs:wifi_task` -> hipoteses B/C +- `main.rs:windows_wifi_fallback_tick` -> hipotese D +- `main.rs:derive_pose_from_sensing` -> hipotese E + +- Evidencia pre-fix do parser localizado: + - `.dbg/trae-debug-log-basic-rssi-invalid.ndjson:995-1000` mostrou a transicao de `mean_rssi = -100` para `mean_rssi ~= -57/-58` quando o parser PT-BR passou a entender `Sinal`, `Banda`, `Canal` e `Tipo de Radio`. +- Evidencia de runtime apos reiniciar com o binario atualizado: + - `.dbg/trae-debug-log-basic-rssi-invalid.ndjson:2245-2264` mostra `observations = 1`, `bssid_count = 3`, `mean_rssi ~= -58`, `quality_verdict = Permit` e `source = wifi:JHON_5G`. +- Snapshot HTTP apos o ajuste de agregacao: + - `GET /api/v1/sensing/latest` passou a responder com `features.mean_rssi = -59.0` e `nodes[0].rssi_dbm = -59.0`, em vez de propagar `-100`. +- Snapshot HTTP apos o ajuste pratico para hardware comum: + - `GET /api/v1/sensing/latest` passou a responder com `source = wifi:JHON_5G`, `bssid_count = 1`, `quality_verdict = Warn`, `signal_quality_score ~= 0.25`, `estimated_persons = 1` e `presence = true`. + +Correcao minima aplicada apos evidencia: +- `wifi-densepose-wifiscan/src/adapter/netsh_scanner.rs` + - parser do `netsh wlan show networks mode=bssid` agora aceita rotulos PT-BR. +- `wifi-densepose-sensing-server/src/main.rs` + - o frame multi-BSSID deixou de herdar cegamente o RSSI da primeira observacao bruta; + - o snapshot agora usa um RSSI representativo do frame agregado; + - a expiracao do `BssidRegistry` no modo `netsh` foi reduzida de 30s para 10s para diminuir BSSIDs stale em testes locais. + - o modo Wi-Fi basico agora limita a contagem de pessoas em cenarios de baixa visibilidade (`1-2` observacoes vivas); + - o `quality_verdict` do modo basico pode subir de `Deny` para `Warn` quando houver sinal real suficiente para uso pratico, sem fingir qualidade de CSI. + +## Verification Conclusion +| ID | Hypothesis | Status | Evidence | +|----|------------|--------|----------| +| A | O coletor RSSI do Windows nao esta conseguindo ler amostras reais do adaptador e cai em valores sentinela/degenerados. | Parcialmente confirmada | O adaptador nao era o problema principal; o parser PT-BR fazia parte do caminho cair em `-100`. Depois do fix, o runtime passou a mostrar `-58/-59` em vez de `-100`. | +| B | O pipeline recebe amostras, mas a filtragem/deduplicacao zera a variancia e o motion score antes da classificacao. | Rejeitada como causa principal | Apos o fix, `variance`, `motion_band_power` e `spectral_power` ficaram positivos e estaveis; o pipeline nao estava zerando tudo por si so. | +| C | O adaptador/rede atual entrega dados somente do AP conectado, com atualizacao lenta ou congelada, abaixo do minimo esperado pelo algoritmo. | Parcialmente confirmada | Os logs mais recentes mostram `observations = 1` enquanto o frame ainda mantem `bssid_count = 3` por janela curta de historico/cache. O modo basico continua dependente de scans lentos e pouco ricos. | +| D | O modo `wifi` esta pegando a interface errada ou um estado stale do Windows, produzindo snapshots validos do ponto de vista tecnico, mas sem relacao com o modem/rede testados. | Inconclusiva | Houve sinais de cache/staleness, mas nao ficou provado que a interface esteja errada. O principal problema observado foi localizacao + agregacao de RSSI. | +| E | A classificacao/adaptive classifier do modo basico esta inferindo `presence` de forma otimista mesmo quando o sinal base esta invalido. | Confirmada historicamente, mitigada | Antes, havia presenca com `-100`. Agora os campos principais do snapshot deixam de carregar esse RSSI invalido, e a UI ja havia recebido gating visual para nao exagerar a confianca. | + +Diagnostico atual: +- O bug do `-100 fantasma` foi resolvido em dois niveis: + 1. parser localizado do `netsh`; + 2. agregacao incorreta entre `first_rssi` e frame multi-BSSID. +- O que sobra agora e uma limitacao estrutural do modo basico no Windows: + - scans lentos, + - poucos pontos observaveis, + - historico curto ainda sujeito a staleness/cache do `netsh`. +- A sessao continua `[OPEN]` ate confirmacao sua em uso real do Observatory, porque ainda precisamos validar a sensacao final na interface. diff --git a/start.bat b/start.bat new file mode 100644 index 0000000000..07f288d849 --- /dev/null +++ b/start.bat @@ -0,0 +1,16 @@ +@echo off +setlocal +chcp 65001 >nul + +set "SCRIPT_PATH=%~dp0start_ruview.ps1" +set "RUVIEW_CLOSE_CONFLICTS=1" + +if not exist "%SCRIPT_PATH%" ( + echo [ERRO] Nao encontrei o launcher PowerShell do RuView. + echo [ERRO] Caminho esperado: "%SCRIPT_PATH%" + pause + exit /b 1 +) + +powershell -NoLogo -NoExit -ExecutionPolicy Bypass -File "%SCRIPT_PATH%" -CloseConflicts +exit /b %errorlevel% diff --git a/start_ruview.ps1 b/start_ruview.ps1 new file mode 100644 index 0000000000..6aa1b54ef6 --- /dev/null +++ b/start_ruview.ps1 @@ -0,0 +1,687 @@ +param( + [switch]$CloseConflicts +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false) + +$RootDir = $PSScriptRoot +$V2Dir = Join-Path $RootDir "v2" +$Timestamp = Get-Date -Format "yyyyMMdd-HHmmss" +$UiUrl = "http://127.0.0.1:3000/ui/index.html" +$UiLaunchUrl = "{0}?boot={1}" -f $UiUrl, $Timestamp +$ApiUrl = "http://127.0.0.1:3000/api/v1/sensing/latest" +$HttpPort = 3000 +$WsPort = 3001 +$UdpPort = 5005 +$TickMs = 500 +$LogDir = Join-Path $RootDir "data\launcher-logs" +$LogPath = Join-Path $LogDir "ruview-launcher-$Timestamp.log" +$script:BrowserOpened = $false +$script:LastUiProbeError = $null +$script:ServerProcess = $null +$script:ProcessJobHandle = [IntPtr]::Zero + +New-Item -ItemType Directory -Force -Path $LogDir | Out-Null + +if (-not ("RuViewProcessJob" -as [type])) { + Add-Type -TypeDefinition @" +using System; +using System.Runtime.InteropServices; + +public static class RuViewProcessJob +{ + public const int JobObjectExtendedLimitInformation = 9; + public const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000; + + [StructLayout(LayoutKind.Sequential)] + public struct JOBOBJECT_BASIC_LIMIT_INFORMATION + { + public long PerProcessUserTimeLimit; + public long PerJobUserTimeLimit; + public uint LimitFlags; + public UIntPtr MinimumWorkingSetSize; + public UIntPtr MaximumWorkingSetSize; + public uint ActiveProcessLimit; + public UIntPtr Affinity; + public uint PriorityClass; + public uint SchedulingClass; + } + + [StructLayout(LayoutKind.Sequential)] + public struct IO_COUNTERS + { + public ulong ReadOperationCount; + public ulong WriteOperationCount; + public ulong OtherOperationCount; + public ulong ReadTransferCount; + public ulong WriteTransferCount; + public ulong OtherTransferCount; + } + + [StructLayout(LayoutKind.Sequential)] + public struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION + { + public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; + public IO_COUNTERS IoInfo; + public UIntPtr ProcessMemoryLimit; + public UIntPtr JobMemoryLimit; + public UIntPtr PeakProcessMemoryUsed; + public UIntPtr PeakJobMemoryUsed; + } + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern IntPtr CreateJobObject(IntPtr lpJobAttributes, string lpName); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool SetInformationJobObject( + IntPtr hJob, + int JobObjectInfoClass, + IntPtr lpJobObjectInfo, + uint cbJobObjectInfoLength); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool CloseHandle(IntPtr handle); +} +"@ +} + +function Write-LauncherLog { + param( + [Parameter(Mandatory = $true)][string]$Level, + [Parameter(Mandatory = $true)][string]$Message + ) + + $timestamp = Get-Date -Format "HH:mm:ss" + $line = "$timestamp [$Level] $Message" + + switch ($Level) { + "HEADER" { $emoji = "[RU]"; $color = "Cyan" } + "STEP" { $emoji = "[>>]"; $color = "Cyan" } + "INFO" { $emoji = "[i]"; $color = "Gray" } + "SUCCESS" { $emoji = "[OK]"; $color = "Green" } + "WARN" { $emoji = "[!]"; $color = "Yellow" } + "ERROR" { $emoji = "[X]"; $color = "Red" } + "SERVER" { $emoji = "[~]"; $color = "White" } + default { $emoji = "[ ]"; $color = "White" } + } + + Add-Content -Path $LogPath -Value $line + Write-Host "$emoji $line" -ForegroundColor $color +} + +function Test-RuViewUi { + try { + $response = Invoke-WebRequest -UseBasicParsing -Uri $UiUrl -Method Head -TimeoutSec 2 + $script:LastUiProbeError = $null + return ($response.StatusCode -eq 200) + } + catch { + $script:LastUiProbeError = $_.Exception.Message + return $false + } +} + +function Get-BluetoothDiagnostics { + $service = Get-Service bthserv -ErrorAction SilentlyContinue + $adapter = Get-PnpDevice -Class Bluetooth -ErrorAction SilentlyContinue | + Where-Object { $_.FriendlyName -and $_.FriendlyName -notmatch 'Enumerator' } | + Select-Object -First 1 + + [PSCustomObject]@{ + AdapterPresent = [bool]$adapter + AdapterName = if ($adapter) { $adapter.FriendlyName } else { $null } + ServiceRunning = [bool]($service -and $service.Status -eq 'Running') + HelperMode = 'experimental' + } +} + +function Get-CargoCommandPath { + $cargo = Get-Command cargo -ErrorAction SilentlyContinue + if (-not $cargo) { + throw "O comando 'cargo' nao foi encontrado no sistema." + } + return $cargo.Source +} + +function Initialize-ProcessLifetimeGuard { + if ($script:ProcessJobHandle -ne [IntPtr]::Zero) { + return + } + + $jobHandle = [RuViewProcessJob]::CreateJobObject([IntPtr]::Zero, "RuViewLauncher-$PID") + if ($jobHandle -eq [IntPtr]::Zero) { + throw "Nao foi possivel criar a protecao de ciclo de vida do processo." + } + + $jobInfo = New-Object RuViewProcessJob+JOBOBJECT_EXTENDED_LIMIT_INFORMATION + $jobInfo.BasicLimitInformation.LimitFlags = [RuViewProcessJob]::JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE + $jobInfoSize = [System.Runtime.InteropServices.Marshal]::SizeOf([type][RuViewProcessJob+JOBOBJECT_EXTENDED_LIMIT_INFORMATION]) + $jobInfoPtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($jobInfoSize) + + try { + [System.Runtime.InteropServices.Marshal]::StructureToPtr($jobInfo, $jobInfoPtr, $false) + $configured = [RuViewProcessJob]::SetInformationJobObject( + $jobHandle, + [RuViewProcessJob]::JobObjectExtendedLimitInformation, + $jobInfoPtr, + [uint32]$jobInfoSize + ) + + if (-not $configured) { + throw "Nao foi possivel configurar o encerramento automatico do processo filho." + } + } + finally { + [System.Runtime.InteropServices.Marshal]::FreeHGlobal($jobInfoPtr) + } + + $script:ProcessJobHandle = $jobHandle +} + +function Add-ProcessToLifetimeGuard { + param( + [Parameter(Mandatory = $true)] + [System.Diagnostics.Process]$Process + ) + + Initialize-ProcessLifetimeGuard + + $assigned = [RuViewProcessJob]::AssignProcessToJobObject($script:ProcessJobHandle, $Process.Handle) + if (-not $assigned) { + throw "Nao foi possivel vincular o servidor ao ciclo de vida desta janela." + } +} + +function Stop-RuViewRuntime { + param( + [string]$Reason = "Encerrando instancia atual do RuView." + ) + + if ($script:ServerProcess -and -not $script:ServerProcess.HasExited) { + Write-LauncherLog "WARN" $Reason + try { + & taskkill /PID $script:ServerProcess.Id /T /F | Out-Null + } + catch { + try { + Stop-Process -Id $script:ServerProcess.Id -Force -ErrorAction Stop + } + catch { + Write-LauncherLog "WARN" "Nao consegui encerrar o processo principal automaticamente: $($_.Exception.Message)" + } + } + } +} + +function Get-RuViewRelatedProcesses { + $currentPid = $PID + Get-CimInstance Win32_Process | Where-Object { + $_.ProcessId -ne $currentPid -and ( + $_.Name -in @("sensing-server.exe", "cargo.exe", "powershell.exe", "pwsh.exe") -or + $_.CommandLine -match "wifi-densepose-sensing-server|sensing-server\.exe|start_ruview" + ) + } +} + +function Stop-ProcessTree { + param( + [Parameter(Mandatory = $true)] + [int]$ProcessId + ) + + try { + & taskkill /PID $ProcessId /T /F | Out-Null + return $true + } + catch { + try { + Stop-Process -Id $ProcessId -Force -ErrorAction Stop + return $true + } + catch { + return $false + } + } +} + +function Test-PortsFree { + param( + [int[]]$Ports + ) + + $listeners = Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | + Where-Object { $Ports -contains $_.LocalPort } + + return ($null -eq $listeners -or $listeners.Count -eq 0) +} + +function Wait-ForPortsFree { + param( + [int[]]$Ports, + [int]$TimeoutSeconds = 15 + ) + + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + while ((Get-Date) -lt $deadline) { + if (Test-PortsFree -Ports $Ports) { + return $true + } + + Start-Sleep -Milliseconds 400 + } + + return (Test-PortsFree -Ports $Ports) +} + +function Stop-RuViewConflicts { + param( + [int[]]$Ports + ) + + $killed = @{} + + foreach ($port in $Ports) { + $listeners = Get-NetTCPConnection -State Listen -LocalPort $port -ErrorAction SilentlyContinue + foreach ($listener in $listeners) { + $procId = $listener.OwningProcess + if (-not $procId -or $procId -eq $PID -or $killed.ContainsKey($procId)) { + continue + } + + try { + $proc = Get-CimInstance Win32_Process -Filter "ProcessId = $procId" -ErrorAction Stop + Write-LauncherLog "WARN" "Encerrando processo na porta ${port}: PID $procId ($($proc.Name))" + if (Stop-ProcessTree -ProcessId $procId) { + $killed[$procId] = $true + Start-Sleep -Milliseconds 400 + } + else { + Write-LauncherLog "WARN" "Nao consegui encerrar a arvore do PID $procId preso na porta ${port}." + } + } + catch { + Write-LauncherLog "WARN" "Nao consegui encerrar o PID $procId preso na porta ${port}: $($_.Exception.Message)" + } + } + } + + $related = Get-RuViewRelatedProcesses + foreach ($proc in $related) { + if ($killed.ContainsKey($proc.ProcessId)) { + continue + } + + $commandLine = if ($null -ne $proc.CommandLine) { $proc.CommandLine } else { "" } + $isRuViewProcess = ( + $proc.Name -eq "sensing-server.exe" -or + $commandLine -match "wifi-densepose-sensing-server|sensing-server\.exe|start_ruview" + ) + + if (-not $isRuViewProcess) { + continue + } + + try { + Write-LauncherLog "WARN" "Encerrando instancia antiga: PID $($proc.ProcessId) ($($proc.Name))" + if (Stop-ProcessTree -ProcessId $proc.ProcessId) { + $killed[$proc.ProcessId] = $true + Start-Sleep -Milliseconds 400 + } + else { + Write-LauncherLog "WARN" "Nao consegui encerrar a arvore do PID $($proc.ProcessId)." + } + } + catch { + Write-LauncherLog "WARN" "Nao consegui encerrar o PID $($proc.ProcessId): $($_.Exception.Message)" + } + } + + if ($killed.Count -gt 0) { + Write-LauncherLog "SUCCESS" "Limpeza concluida. Processos encerrados: $($killed.Count)" + } + else { + Write-LauncherLog "INFO" "Nenhum processo antigo do RuView foi encontrado." + } + + if (-not (Wait-ForPortsFree -Ports $Ports -TimeoutSeconds 15)) { + $blocked = Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | + Where-Object { $Ports -contains $_.LocalPort } | + Select-Object -ExpandProperty LocalPort -Unique + throw "As portas do RuView ainda estao ocupadas apos a limpeza: $($blocked -join ', ')." + } + + Write-LauncherLog "SUCCESS" "Portas do RuView livres e prontas para uma nova inicializacao." +} + +function Convert-ServerLine { + param([string]$Line) + + $trimmed = $Line.Trim() + if ([string]::IsNullOrWhiteSpace($trimmed)) { + return + } + + if ($trimmed -match "^warning:") { + Write-LauncherLog "WARN" "Cargo: $trimmed" + return + } + + if ($trimmed -match "^error(\[[^\]]+\])?:") { + Write-LauncherLog "ERROR" "Cargo: $trimmed" + return + } + + if ($trimmed -match "^\s*Compiling\s+(.+)$") { + Write-LauncherLog "STEP" "Compilando componente: $($Matches[1])" + return + } + + if ($trimmed -match "^\s*Finished\s+.+target\(s\)\s+in\s+(.+)$") { + Write-LauncherLog "SUCCESS" "Compilacao concluida em $($Matches[1])." + return + } + + if ($trimmed -match "^\s*Running\s+(.+)$") { + Write-LauncherLog "STEP" "Executando: $($Matches[1])" + return + } + + if ($trimmed -like "*INFO sensing_server:*HTTP:*") { + $httpValue = ($trimmed.Substring($trimmed.IndexOf("HTTP:") + 5)).Trim() + Write-LauncherLog "SUCCESS" "Interface HTTP disponivel em $httpValue" + return + } + + if ($trimmed -like "*INFO sensing_server:*WebSocket:*") { + $wsValue = ($trimmed.Substring($trimmed.IndexOf("WebSocket:") + 10)).Trim() + Write-LauncherLog "INFO" "WebSocket ativo em $wsValue" + return + } + + if ($trimmed -like "*INFO sensing_server:*UDP:*") { + $udpValue = ($trimmed.Substring($trimmed.IndexOf("UDP:") + 4)).Trim() + Write-LauncherLog "INFO" "Escuta UDP preparada em $udpValue" + return + } + + if ($trimmed -like "*INFO sensing_server:*UI path:*") { + $uiValue = ($trimmed.Substring($trimmed.IndexOf("UI path:") + 8)).Trim() + Write-LauncherLog "INFO" "Pasta da interface: $uiValue" + return + } + + if ($trimmed -like "*INFO sensing_server:*Source:*") { + $sourceValue = ($trimmed.Substring($trimmed.IndexOf("Source:") + 7)).Trim() + Write-LauncherLog "INFO" "Fonte de dados selecionada: $sourceValue" + return + } + + if ($trimmed -like "*INFO sensing_server:*Data source:*") { + $dataValue = ($trimmed.Substring($trimmed.IndexOf("Data source:") + 12)).Trim() + Write-LauncherLog "INFO" "Motor de captura: $dataValue" + return + } + + if ($trimmed -like "*INFO sensing_server:*Vital sign detector sample rate:*") { + $sampleValue = ($trimmed.Substring($trimmed.IndexOf("Vital sign detector sample rate:") + 32)).Trim() + Write-LauncherLog "INFO" "Taxa do detector de sinais vitais: $sampleValue" + return + } + + if ($trimmed -like "*INFO sensing_server:*WiFi-DensePose Sensing Server*") { + Write-LauncherLog "SUCCESS" "Servidor RuView iniciado." + return + } + + if ($trimmed -match "INFO sensing_server:\s+HTTP:\s+(.+)$") { + Write-LauncherLog "SUCCESS" "Interface HTTP disponivel em $($Matches[1])" + return + } + + if ($trimmed -match "INFO sensing_server:\s+WebSocket:\s+(.+)$") { + Write-LauncherLog "INFO" "WebSocket ativo em $($Matches[1])" + return + } + + if ($trimmed -match "INFO sensing_server:\s+UDP:\s+(.+)$") { + Write-LauncherLog "INFO" "Escuta UDP preparada em $($Matches[1])" + return + } + + if ($trimmed -match "INFO sensing_server:\s+UI path:\s+(.+)$") { + Write-LauncherLog "INFO" "Pasta da interface: $($Matches[1])" + return + } + + if ($trimmed -match "INFO sensing_server:\s+Source:\s+(.+)$") { + Write-LauncherLog "INFO" "Fonte de dados selecionada: $($Matches[1])" + return + } + + if ($trimmed -match "INFO sensing_server:\s+Data source:\s+(.+)$") { + Write-LauncherLog "INFO" "Motor de captura: $($Matches[1])" + return + } + + if ($trimmed -match "INFO sensing_server:\s+Vital sign detector sample rate:\s+(.+)$") { + Write-LauncherLog "INFO" "Taxa do detector de sinais vitais: $($Matches[1])" + return + } + + if ($trimmed -match "INFO sensing_server:\s+Loaded runtime config:\s+(.+)$") { + Write-LauncherLog "INFO" "Configuracao carregada: $($Matches[1])" + return + } + + if ($trimmed -match "INFO sensing_server:\s+Loaded adaptive classifier:\s+(.+)$") { + Write-LauncherLog "INFO" "Classificador adaptativo carregado: $($Matches[1])" + return + } + + if ($trimmed -match "INFO sensing_server:\s+WiFi RSSI pipeline active\s+\((.+)\)$") { + Write-LauncherLog "SUCCESS" "Pipeline Wi-Fi RSSI ativo ($($Matches[1]))" + return + } + + if ($trimmed -match "INFO sensing_server:\s+HTTP server listening on\s+(.+)$") { + Write-LauncherLog "SUCCESS" "Servidor HTTP ouvindo em $($Matches[1])" + return + } + + if ($trimmed -match "INFO sensing_server:\s+WebSocket server listening on\s+(.+)$") { + Write-LauncherLog "SUCCESS" "Servidor WebSocket ouvindo em $($Matches[1])" + return + } + + if ($trimmed -match "INFO sensing_server:\s+Open\s+(.+)\s+in your browser$") { + Write-LauncherLog "INFO" "Abra no navegador: $($Matches[1])" + return + } + + if ($trimmed -match "INFO sensing_server:\s+API auth: OFF") { + Write-LauncherLog "WARN" "Autenticacao da API desativada para ambiente local." + return + } + + if ($trimmed -match "INFO sensing_server:\s+RuView discovery skipped") { + Write-LauncherLog "INFO" "Descoberta local ignorada porque o bind esta apenas em loopback." + return + } + + if ($trimmed -match "INFO wifi_densepose_sensing_server::browser_session:\s+browser session secret: loaded path=(.+)$") { + Write-LauncherLog "INFO" "Chave da sessao do navegador carregada de: $($Matches[1])" + return + } + + if ($trimmed -match "INFO sensing_server:\s+Discovered\s+(\d+)\s+model files,\s+(\d+)\s+recording files$") { + Write-LauncherLog "INFO" "Arquivos encontrados: modelos=$($Matches[1]) | gravacoes=$($Matches[2])" + return + } + + if ($trimmed -match "INFO sensing_server:\s+Host-header validation ON") { + Write-LauncherLog "INFO" "Validacao de host-header ativada." + return + } + + if ($trimmed -match "WARN") { + Write-LauncherLog "WARN" $trimmed + return + } + + if ($trimmed -match "ERROR|thread 'main' panicked|panicked at") { + Write-LauncherLog "ERROR" $trimmed + return + } + + Write-LauncherLog "SERVER" $trimmed +} + +function Start-RuViewBrowser { + if (-not $script:BrowserOpened) { + try { + Start-Process -FilePath $UiLaunchUrl -ErrorAction Stop | Out-Null + $script:BrowserOpened = $true + Write-LauncherLog "SUCCESS" "Interface aberta no navegador." + } + catch { + Write-LauncherLog "WARN" "Nao consegui abrir o navegador automaticamente: $($_.Exception.Message)" + Write-LauncherLog "INFO" "Abra manualmente: $UiLaunchUrl" + } + + Write-LauncherLog "INFO" "URL da interface: $UiLaunchUrl" + Write-LauncherLog "INFO" "API local: $ApiUrl" + } +} + +try { + Write-LauncherLog "HEADER" "RuView Launcher PT-BR" + Write-LauncherLog "INFO" "Log desta sessao: $LogPath" + Write-LauncherLog "STEP" "Validando estrutura do projeto..." + + if (-not (Test-Path (Join-Path $V2Dir "Cargo.toml"))) { + throw "Nao encontrei a pasta 'v2' do projeto em '$V2Dir'." + } + + $cargoPath = Get-CargoCommandPath + Write-LauncherLog "SUCCESS" "Cargo localizado em: $cargoPath" + + if ($CloseConflicts -or $env:RUVIEW_CLOSE_CONFLICTS -eq "1") { + Write-LauncherLog "STEP" "Encerrando instancias antigas e portas em conflito..." + Stop-RuViewConflicts -Ports @($HttpPort, $WsPort, 8765) + } + + if (Test-RuViewUi) { + Write-LauncherLog "SUCCESS" "O RuView ja esta respondendo na porta $HttpPort." + Start-RuViewBrowser + Write-LauncherLog "INFO" "Nenhuma nova instancia foi iniciada." + return + } + + Write-LauncherLog "STEP" "Preparando subida do servidor..." + Write-LauncherLog "INFO" "Modo: basico Wi-Fi (RSSI-only)" + Write-LauncherLog "INFO" "Portas esperadas: HTTP=$HttpPort | WS=$WsPort | UDP=$UdpPort" + Write-LauncherLog "INFO" "Intervalo de leitura: ${TickMs}ms" + + $bluetooth = Get-BluetoothDiagnostics + if ($bluetooth.AdapterPresent) { + Write-LauncherLog "INFO" "Bluetooth auxiliar detectado: $($bluetooth.AdapterName)" + } + else { + Write-LauncherLog "WARN" "Nenhum adaptador Bluetooth utilizavel foi detectado para o modo auxiliar." + } + + if ($bluetooth.ServiceRunning) { + Write-LauncherLog "INFO" "Servico Bluetooth ativo. O modo auxiliar experimental pode ser consultado pela interface." + } + else { + Write-LauncherLog "WARN" "Servico Bluetooth inativo. O RuView segue funcionando, mas sem o apoio auxiliar experimental." + } + + $arguments = "run -p wifi-densepose-sensing-server -- --source wifi --http-port $HttpPort --ws-port $WsPort --tick-ms $TickMs" + + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = $cargoPath + $psi.Arguments = $arguments + $psi.WorkingDirectory = $V2Dir + $psi.UseShellExecute = $false + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + $psi.StandardOutputEncoding = [System.Text.UTF8Encoding]::new($false) + $psi.StandardErrorEncoding = [System.Text.UTF8Encoding]::new($false) + + $process = New-Object System.Diagnostics.Process + $process.StartInfo = $psi + $process.EnableRaisingEvents = $true + + $stdoutEvent = Register-ObjectEvent -InputObject $process -EventName OutputDataReceived -Action { + if ($EventArgs.Data) { + Convert-ServerLine -Line $EventArgs.Data + } + } + + $stderrEvent = Register-ObjectEvent -InputObject $process -EventName ErrorDataReceived -Action { + if ($EventArgs.Data) { + Convert-ServerLine -Line $EventArgs.Data + } + } + + Write-LauncherLog "STEP" "Iniciando processo do servidor..." + $null = $process.Start() + $script:ServerProcess = $process + Add-ProcessToLifetimeGuard -Process $process + $process.BeginOutputReadLine() + $process.BeginErrorReadLine() + + Write-LauncherLog "SUCCESS" "Processo iniciado com PID $($process.Id)." + Write-LauncherLog "INFO" "Aguarde alguns segundos enquanto o projeto compila/inicializa." + Write-LauncherLog "INFO" "Esta janela permanece visivel durante toda a execucao." + Write-LauncherLog "INFO" "Se voce fechar esta janela, o RuView sera encerrado junto." + + $deadline = (Get-Date).AddSeconds(90) + while (-not $process.HasExited) { + if (-not $script:BrowserOpened -and (Test-RuViewUi)) { + Start-RuViewBrowser + } + + if (-not $script:BrowserOpened -and (Get-Date) -ge $deadline) { + Write-LauncherLog "WARN" "A interface ainda nao respondeu dentro de 90 segundos." + Write-LauncherLog "WARN" "Se a compilacao ainda estiver rolando, aguarde mais um pouco." + $deadline = (Get-Date).AddYears(10) + } + + Start-Sleep -Seconds 2 + } + + Start-Sleep -Milliseconds 500 + + if ($process.ExitCode -eq 0) { + Write-LauncherLog "SUCCESS" "Servidor encerrado normalmente." + } + else { + Write-LauncherLog "ERROR" "Servidor encerrado com falha. Codigo de saida: $($process.ExitCode)" + if ($script:LastUiProbeError) { + Write-LauncherLog "WARN" "Ultimo erro ao testar a interface: $script:LastUiProbeError" + } + } +} +catch { + Write-LauncherLog "ERROR" $_.Exception.Message +} +finally { + Stop-RuViewRuntime -Reason "Fechando launcher: encerrando o RuView junto com esta janela." + + Get-EventSubscriber | Where-Object { + $_.SourceObject -is [System.Diagnostics.Process] + } | Unregister-Event -Force -ErrorAction SilentlyContinue + + if ($script:ProcessJobHandle -ne [IntPtr]::Zero) { + [RuViewProcessJob]::CloseHandle($script:ProcessJobHandle) | Out-Null + $script:ProcessJobHandle = [IntPtr]::Zero + } + + Write-LauncherLog "INFO" "Janela pronta para diagnostico. Feche quando quiser." +} diff --git a/ui/app.js b/ui/app.js index 55e4f741eb..4afb6a3dad 100644 --- a/ui/app.js +++ b/ui/app.js @@ -29,6 +29,10 @@ import { ScreenshotTool } from './utils/screenshot.js'; import { UptimeClock } from './utils/uptime-clock.js'; import { QuickSettings } from './utils/quick-settings.js'; import { dataSourceBanner } from './utils/data-source-banner.js'; +import { + enforceLocalDevFreshness, + shouldBypassPersistentCaching +} from './utils/local-dev-cache-guard.js'; class WiFiDensePoseApp { constructor() { @@ -296,6 +300,11 @@ class WiFiDensePoseApp { // Register service worker for offline capability registerServiceWorker() { + if (shouldBypassPersistentCaching()) { + console.info('Local loopback detected - skipping service worker registration'); + return; + } + if ('serviceWorker' in navigator) { navigator.serviceWorker.register('./sw.js').then(reg => { console.info('Service worker registered:', reg.scope); @@ -487,11 +496,27 @@ class WiFiDensePoseApp { } } -// Initialize app when DOM is ready -document.addEventListener('DOMContentLoaded', () => { - window.wifiDensePoseApp = new WiFiDensePoseApp(); - window.wifiDensePoseApp.init(); -}); +async function bootWiFiDensePoseApp() { + if (window.wifiDensePoseApp?.isReady?.()) { + return; + } + + if (!window.wifiDensePoseApp) { + window.wifiDensePoseApp = new WiFiDensePoseApp(); + } + + await enforceLocalDevFreshness(); + await window.wifiDensePoseApp.init(); +} + +// Initialize app reliably even when this module is loaded via dynamic import. +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => { + bootWiFiDensePoseApp(); + }, { once: true }); +} else { + bootWiFiDensePoseApp(); +} // Export for testing -export { WiFiDensePoseApp }; \ No newline at end of file +export { WiFiDensePoseApp }; diff --git a/ui/components/DashboardTab.js b/ui/components/DashboardTab.js index 5e505fa99f..ae4907582a 100644 --- a/ui/components/DashboardTab.js +++ b/ui/components/DashboardTab.js @@ -3,6 +3,8 @@ import { healthService } from '../services/health.service.js'; import { poseService } from '../services/pose.service.js'; import { sensingService } from '../services/sensing.service.js'; +import { apiService } from '../services/api.service.js'; +import { i18n } from '../utils/i18n.js'; export class DashboardTab { constructor(containerElement) { @@ -15,6 +17,19 @@ export class DashboardTab { // Initialize component async init() { this.cacheElements(); + this.wireManualForm(); + this.wireBluetoothToggle(); + this._localeUnsub = i18n.onLocaleChange(() => { + i18n.applyTranslations(this.container); + this.updateDataSourceIndicator(); + if (this._lastHealth) this.updateHealthStatus(this._lastHealth); + if (this._lastHybrid) this.updateHybridSummary(this._lastHybrid); + if (this._lastBluetoothStatus) this.updateBluetoothStatus(this._lastBluetoothStatus); + this.renderKnownDeviceRegistry( + this._lastHybrid?.network_devices || [], + this._lastHybridOverrides || [] + ); + }); await this.loadInitialData(); this.startMonitoring(); } @@ -50,6 +65,7 @@ export class DashboardTab { // Get current stats const stats = await poseService.getStats(1); this.updateStats(stats); + await this.refreshHybridSummary(); } catch (error) { // DensePose API may not be running (sensing-only mode) — fail silently @@ -92,11 +108,11 @@ export class DashboardTab { const statusText = el.querySelector('.status-text'); const statusMsg = el.querySelector('.status-message'); const config = { - 'live': { text: 'ESP32', status: 'healthy', msg: 'Real hardware connected' }, - 'server-simulated': { text: 'SIMULATED', status: 'warning', msg: 'Server running without hardware' }, - 'reconnecting': { text: 'RECONNECTING', status: 'degraded', msg: 'Attempting to connect...' }, - 'unreachable': { text: 'NO DATA', status: 'unhealthy', msg: 'Server unreachable — readings below are stale' }, - 'simulated': { text: 'INVENTED', status: 'unhealthy', msg: 'Browser-generated data, not measured' }, + 'live': { text: 'ESP32', status: 'healthy', msg: i18n.t('status.realHardwareConnected') }, + 'server-simulated': { text: i18n.t('common.simulated').toUpperCase(), status: 'warning', msg: i18n.t('status.serverRunningWithoutHardware') }, + 'reconnecting': { text: i18n.t('common.reconnecting').toUpperCase(), status: 'degraded', msg: i18n.t('status.attemptingConnect') }, + 'unreachable': { text: 'NO DATA', status: 'unhealthy', msg: i18n.t('status.serverUnreachableStale') }, + 'simulated': { text: 'INVENTED', status: 'unhealthy', msg: i18n.t('status.browserGeneratedData') }, }; const cfg = config[ds] || config['reconnecting']; el.className = `component-status status-${cfg.status}`; @@ -154,12 +170,13 @@ export class DashboardTab { // Update health status updateHealthStatus(health) { if (!health) return; + this._lastHealth = health; // Update overall status const overallStatus = this.container.querySelector('.overall-health'); if (overallStatus) { overallStatus.className = `overall-health status-${health.status}`; - overallStatus.textContent = health.status.toUpperCase(); + overallStatus.textContent = i18n.t(`common.${health.status}`) || health.status.toUpperCase(); } // Update component statuses @@ -210,11 +227,11 @@ export class DashboardTab { const apiStatusMessage = apiElement.querySelector('.status-message'); if (apiStatusText) { - apiStatusText.textContent = 'HEALTHY'; + apiStatusText.textContent = i18n.t('common.healthy').toUpperCase(); } if (apiStatusMessage) { - apiStatusMessage.textContent = 'API server is running normally'; + apiStatusMessage.textContent = i18n.t('status.apiRunningNormally'); } } } @@ -280,6 +297,7 @@ export class DashboardTab { // Get zones summary const zonesSummary = await poseService.getZonesSummary(); this.updateZonesDisplay(zonesSummary); + await this.refreshHybridSummary(); } catch (error) { console.error('Failed to update live stats:', error); @@ -344,7 +362,7 @@ export class DashboardTab { const zoneCountSpan = document.createElement('span'); zoneCountSpan.className = 'zone-count'; - zoneCountSpan.textContent = 'undefined'; + zoneCountSpan.textContent = i18n.t('common.undefined'); zoneElement.appendChild(zoneNameSpan); zoneElement.appendChild(zoneCountSpan); @@ -389,6 +407,280 @@ export class DashboardTab { } } + async refreshHybridSummary() { + try { + const [hybrid, overrides, bluetooth] = await Promise.all([ + apiService.get('/api/v1/hybrid/latest'), + apiService.get('/api/v1/hybrid/overrides').catch(() => ({ items: [] })), + apiService.get('/api/v1/bluetooth/status').catch(() => null) + ]); + this._lastHybridOverrides = overrides?.items || []; + this._lastBluetoothStatus = bluetooth; + this.updateHybridSummary(hybrid); + this.updateBluetoothStatus(bluetooth); + this.renderKnownDeviceRegistry(hybrid?.network_devices || [], this._lastHybridOverrides); + } catch (error) { + console.log('Dashboard: hybrid snapshot not available yet'); + } + } + + updateHybridSummary(hybrid) { + if (!hybrid) return; + this._lastHybrid = hybrid; + + const fusion = hybrid.fusion || {}; + const devices = hybrid.network_devices || []; + + const setText = (selector, value) => { + const el = this.container.querySelector(selector); + if (el) el.textContent = String(value); + }; + + setText('.hybrid-humans', fusion.estimated_humans || 0); + setText('.hybrid-devices', devices.length); + setText('.hybrid-smartphones', fusion.likely_smartphones || 0); + setText('.hybrid-smarttvs', fusion.likely_smart_tvs || 0); + + const list = this.container.querySelector('.hybrid-device-list'); + if (!list) return; + list.innerHTML = ''; + + if (devices.length === 0) { + const empty = document.createElement('div'); + empty.className = 'hybrid-device-item'; + empty.textContent = i18n.t('hybrid.empty'); + list.appendChild(empty); + return; + } + + devices.slice(0, 12).forEach(device => { + const item = document.createElement('div'); + item.className = 'hybrid-device-item'; + + const name = document.createElement('div'); + name.className = 'hybrid-device-name'; + name.textContent = device.display_name || device.hostname || device.ip; + + const category = document.createElement('div'); + category.className = 'hybrid-device-meta'; + const badge = device.manual_override ? i18n.t('hybrid.manualBadge') : i18n.t('hybrid.autoBadge'); + category.textContent = `${i18n.t(`hybrid.category.${device.category}`)} • ${Math.round((device.confidence || 0) * 100)}% • ${badge}`; + + const meta = document.createElement('div'); + meta.className = 'hybrid-device-meta'; + meta.textContent = [device.vendor, device.ip, device.mac].filter(Boolean).join(' • '); + + item.appendChild(name); + item.appendChild(category); + item.appendChild(meta); + list.appendChild(item); + }); + } + + updateBluetoothStatus(status) { + if (!status) return; + + const availability = this.container.querySelector('.hybrid-bluetooth-availability'); + const mode = this.container.querySelector('.hybrid-bluetooth-mode'); + const adapter = this.container.querySelector('.hybrid-bluetooth-adapter'); + const adapterName = this.container.querySelector('.hybrid-bluetooth-adapter-name'); + const service = this.container.querySelector('.hybrid-bluetooth-service'); + const serviceNote = this.container.querySelector('.hybrid-bluetooth-service-note'); + const note = this.container.querySelector('.hybrid-bluetooth-note'); + const toggle = this.container.querySelector('.hybrid-bluetooth-toggle'); + + if (availability) availability.textContent = status.enabled + ? (status.effective ? i18n.t('common.active') : i18n.t('common.warning')) + : i18n.t('common.inactive'); + if (mode) mode.textContent = status.helper_mode || 'experimental'; + if (adapter) adapter.textContent = status.adapter_present ? i18n.t('common.connected') : i18n.t('common.disconnected'); + if (adapterName) adapterName.textContent = status.adapter_name || i18n.t('common.unknown'); + if (service) service.textContent = status.service_running ? i18n.t('common.active') : i18n.t('common.inactive'); + if (serviceNote) serviceNote.textContent = status.service_running + ? i18n.t('hybrid.bluetoothServiceRunning') + : i18n.t('hybrid.bluetoothServiceStopped'); + if (note) note.textContent = status.note || i18n.t('hybrid.bluetoothNote'); + if (toggle) toggle.textContent = status.enabled + ? i18n.t('hybrid.bluetoothDisable') + : i18n.t('hybrid.bluetoothEnable'); + } + + wireBluetoothToggle() { + const button = this.container.querySelector('.hybrid-bluetooth-toggle'); + if (!button) return; + + button.addEventListener('click', async () => { + const nextEnabled = !this._lastBluetoothStatus?.enabled; + button.disabled = true; + try { + const bluetooth = await apiService.post('/api/v1/bluetooth/enabled', { + enabled: nextEnabled + }); + this._lastBluetoothStatus = bluetooth; + this.updateBluetoothStatus(bluetooth); + } catch (error) { + this.showError(error.message || i18n.t('common.error')); + } finally { + button.disabled = false; + } + }); + } + + wireManualForm() { + const form = this.container.querySelector('.hybrid-manual-form'); + if (!form) return; + + form.addEventListener('submit', async (event) => { + event.preventDefault(); + const formData = new FormData(form); + const payload = { + display_name: String(formData.get('display_name') || '').trim() || null, + category: String(formData.get('category') || 'unknown_device'), + match_mac: String(formData.get('match_mac') || '').trim() || null, + match_ip: String(formData.get('match_ip') || '').trim() || null, + match_hostname: String(formData.get('match_hostname') || '').trim() || null, + notes: String(formData.get('notes') || '').trim() || null + }; + + if (!payload.display_name && !payload.match_mac && !payload.match_ip && !payload.match_hostname) { + this.showError(i18n.t('hybrid.form.validation')); + return; + } + + try { + await apiService.post('/api/v1/hybrid/overrides/upsert', payload); + form.reset(); + await this.refreshHybridSummary(); + } catch (error) { + this.showError(error.message || i18n.t('common.error')); + } + }); + } + + renderKnownDeviceRegistry(devices, overrides) { + const container = this.container.querySelector('.hybrid-known-device-list'); + if (!container) return; + container.innerHTML = ''; + + const overrideMap = new Map((overrides || []).map(entry => [entry.id, entry])); + + if (!devices.length && !overrideMap.size) { + const empty = document.createElement('div'); + empty.className = 'hybrid-known-device-card'; + empty.textContent = i18n.t('hybrid.registryEmpty'); + container.appendChild(empty); + return; + } + + const matchedOverrideIds = new Set(); + devices.forEach(device => { + if (device.matched_override_id) matchedOverrideIds.add(device.matched_override_id); + container.appendChild(this.buildKnownDeviceCard(device, overrideMap.get(device.matched_override_id))); + }); + + overrides + .filter(entry => !matchedOverrideIds.has(entry.id)) + .forEach(entry => { + container.appendChild(this.buildKnownDeviceCard(null, entry)); + }); + } + + buildKnownDeviceCard(device, overrideEntry) { + const card = document.createElement('div'); + card.className = 'hybrid-known-device-card'; + + const title = document.createElement('div'); + title.className = 'hybrid-device-name'; + title.textContent = overrideEntry?.display_name || device?.display_name || device?.hostname || device?.ip || i18n.t('hybrid.manualEntry'); + + const identity = document.createElement('div'); + identity.className = 'hybrid-device-meta'; + identity.textContent = [ + device?.mac || overrideEntry?.match_mac, + device?.ip || overrideEntry?.match_ip, + device?.hostname || overrideEntry?.match_hostname + ].filter(Boolean).join(' • ') || i18n.t('hybrid.noIdentity'); + + const controls = document.createElement('div'); + controls.className = 'hybrid-known-device-controls'; + + const nameInput = document.createElement('input'); + nameInput.className = 'hybrid-manual-input'; + nameInput.value = overrideEntry?.display_name || device?.display_name || device?.hostname || ''; + nameInput.placeholder = i18n.t('hybrid.form.namePlaceholder'); + + const categorySelect = document.createElement('select'); + categorySelect.className = 'hybrid-manual-select'; + ['smartphone', 'smart_tv', 'computer', 'iot_or_media', 'unknown_device'].forEach(category => { + const option = document.createElement('option'); + option.value = category; + option.textContent = i18n.t(`hybrid.category.${category}`); + option.selected = (overrideEntry?.category || device?.category || 'unknown_device') === category; + categorySelect.appendChild(option); + }); + + const notesInput = document.createElement('input'); + notesInput.className = 'hybrid-manual-input'; + notesInput.value = overrideEntry?.notes || ''; + notesInput.placeholder = i18n.t('hybrid.form.notesPlaceholder'); + + const actions = document.createElement('div'); + actions.className = 'hybrid-known-device-actions'; + + const saveButton = document.createElement('button'); + saveButton.type = 'button'; + saveButton.className = 'hybrid-inline-btn hybrid-inline-btn--primary'; + saveButton.textContent = i18n.t('hybrid.save'); + saveButton.addEventListener('click', async () => { + const payload = { + id: overrideEntry?.id || device?.matched_override_id || '', + display_name: nameInput.value.trim() || null, + category: categorySelect.value, + match_mac: device?.mac || overrideEntry?.match_mac || null, + match_ip: device?.ip || overrideEntry?.match_ip || null, + match_hostname: device?.hostname || overrideEntry?.match_hostname || null, + notes: notesInput.value.trim() || null + }; + + try { + await apiService.post('/api/v1/hybrid/overrides/upsert', payload); + await this.refreshHybridSummary(); + } catch (error) { + this.showError(error.message || i18n.t('common.error')); + } + }); + + const deleteButton = document.createElement('button'); + deleteButton.type = 'button'; + deleteButton.className = 'hybrid-inline-btn'; + deleteButton.textContent = i18n.t('hybrid.delete'); + deleteButton.disabled = !(overrideEntry?.id || device?.matched_override_id); + deleteButton.addEventListener('click', async () => { + const id = overrideEntry?.id || device?.matched_override_id; + if (!id) return; + try { + await apiService.delete('/api/v1/hybrid/overrides/delete', { + body: JSON.stringify({ id }) + }); + await this.refreshHybridSummary(); + } catch (error) { + this.showError(error.message || i18n.t('common.error')); + } + }); + + controls.appendChild(nameInput); + controls.appendChild(categorySelect); + controls.appendChild(notesInput); + actions.appendChild(saveButton); + actions.appendChild(deleteButton); + + card.appendChild(title); + card.appendChild(identity); + card.appendChild(controls); + card.appendChild(actions); + return card; + } + // Format feature name formatFeatureName(name) { return name.replace(/_/g, ' ') @@ -428,6 +720,7 @@ export class DashboardTab { } if (this._sensingUnsub) this._sensingUnsub(); if (this._sensingDataUnsub) this._sensingDataUnsub(); + if (this._localeUnsub) this._localeUnsub(); if (this.statsInterval) { clearInterval(this.statsInterval); @@ -435,4 +728,4 @@ export class DashboardTab { healthService.stopHealthMonitoring(); } -} \ No newline at end of file +} diff --git a/ui/components/HardwareTab.js b/ui/components/HardwareTab.js index 7f36113a93..6e62244f24 100644 --- a/ui/components/HardwareTab.js +++ b/ui/components/HardwareTab.js @@ -1,5 +1,7 @@ // Hardware Tab Component +import { i18n } from '../utils/i18n.js'; + export class HardwareTab { constructor(containerElement) { this.container = containerElement; @@ -127,9 +129,9 @@ export class HardwareTab { return div; }; - arrayStatus.appendChild(createInfoDiv('Active TX:', `${txActive}/3`)); - arrayStatus.appendChild(createInfoDiv('Active RX:', `${rxActive}/6`)); - arrayStatus.appendChild(createInfoDiv('Signal Quality:', `${this.calculateSignalQuality(txActive, rxActive)}%`)); + arrayStatus.appendChild(createInfoDiv(i18n.t('hardware.activeTx'), `${txActive}/3`)); + arrayStatus.appendChild(createInfoDiv(i18n.t('hardware.activeRx'), `${rxActive}/6`)); + arrayStatus.appendChild(createInfoDiv(i18n.t('hardware.signalQuality'), `${this.calculateSignalQuality(txActive, rxActive)}%`)); } // Calculate signal quality based on active antennas @@ -171,4 +173,4 @@ export class HardwareTab { antenna.removeEventListener('click', this.toggleAntenna); }); } -} \ No newline at end of file +} diff --git a/ui/components/LiveDemoTab.js b/ui/components/LiveDemoTab.js index b4de941c3c..4761be9683 100644 --- a/ui/components/LiveDemoTab.js +++ b/ui/components/LiveDemoTab.js @@ -5,6 +5,7 @@ import { poseService } from '../services/pose.service.js'; import { streamService } from '../services/stream.service.js'; import { wsService } from '../services/websocket.service.js'; import { sensingService } from '../services/sensing.service.js'; +import { i18n } from '../utils/i18n.js'; // Optional services - loaded lazily in init() to avoid blocking module graph let modelService = null; @@ -97,6 +98,12 @@ export class LiveDemoTab { // Create enhanced DOM structure this.createEnhancedStructure(); + this._localeUnsub = i18n.onLocaleChange(() => { + this.refreshStaticTexts(); + this.updateUI(); + this.updateModelUI(); + this.updateTrainingStatus(); + }); // Initialize pose detection canvas this.initializePoseCanvas(); @@ -148,26 +155,26 @@ export class LiveDemoTab {
- Detecting data source... + ${i18n.t('demo.detectingSource')}
-

Live Human Pose Detection

+

${i18n.t('demo.title')}

- Ready + ${i18n.t('common.ready')}
- - - - + + + +
@@ -179,45 +186,45 @@ export class LiveDemoTab {
-

Performance Metrics

+

${i18n.t('demo.performanceMetrics')}

- - Disconnected + + ${i18n.t('common.disconnected')}
- + 0
- + 0s
- + 0
- - Never + + ${i18n.t('common.never')}
-

Estimation Mode

+

${i18n.t('demo.estimationMode')}

- Unknown + ${i18n.t('common.unknown')}

- Waiting for first frame... + ${i18n.t('demo.waitingFrame')}

-

Model Control

+

${i18n.t('demo.modelControl')}

- +
- - + +
-
No model loaded
+
${i18n.t('demo.noModelLoaded')}
- - + +
-

Training

+

${i18n.t('demo.training')}

- Idle + ${i18n.t('demo.idle')}
- - + +
-

Setup Guide

+

${i18n.t('demo.setupGuide')}

1x
- 1 ESP32 + 1 AP -

Presence, breathing, gross motion

+ ${i18n.t('demo.setup1Title')} +

${i18n.t('demo.setup1Desc')}

3x
- 2-3 ESP32s -

Body localization, motion direction

+ ${i18n.t('demo.setup2Title')} +

${i18n.t('demo.setup2Desc')}

4x+
- 4+ ESP32s + trained model -

Individual limb tracking, full pose

+ ${i18n.t('demo.setup3Title')} +

${i18n.t('demo.setup3Desc')}

-

- Signal-Derived mode uses aggregate CSI features. - For per-limb tracking, load a trained .rvf model - with --model path.rvf and use 4+ sensors. -

+

${i18n.t('demo.setupNote')}

-

System Health

+

${i18n.t('demo.systemHealth')}

- - Unknown + + ${i18n.t('common.unknown')}
- - Unknown + + ${i18n.t('common.unknown')}
- - Unknown + + ${i18n.t('common.unknown')}