Skip to content

fix(ui): erro em /api/start|stop nao trava mais o painel (helper postJSON) - #52

Merged
caioross merged 1 commit into
mainfrom
auto/issue-50-ui-start-stop-errors
Jul 29, 2026
Merged

caioross merged 1 commit into
mainfrom
auto/issue-50-ui-start-stop-errors

Conversation

@caioross

Copy link
Copy Markdown
Owner

Contexto

API.start e API.stop (static/app.js) eram os únicos verbos sem checagem de r.ok e sem .catch — ao contrário de API.devices (corrigido em #33) e API.gain. Fora do caminho feliz do 400 previsto pelo backend, o painel travava ou mentia, e só F5 recuperava:

  1. 500 no start (exceção em DirectionWorker.start()): res.error vinha undefined, o if não disparava e a UI fazia state.running.add(dir) — status congelado em "carregando", ▶ Iniciar desabilitado, app aparentando ter iniciado sem worker.
  2. Servidor fora do ar: fetch rejeitava dentro de startDirection (async, sem catch) → unhandled rejection; as linhas de recuperação nunca rodavam.
  3. ■ Parar que não parava: rejeição em API.stop pulava tudo depois do await — state.running sujo, meters vivos, nenhuma mensagem.

O que mudou e por quê

Diff contido em static/ (HANDBOOK §7.3), sem tocar no backend.

  • Novo helper postJSON(url, body) que nunca lança e devolve {ok, data}, usado pelos três verbos (start/stop/gain). Ele cobre os três buracos numa função só, em vez de espalhar try/catch:
    • fetch rejeitado → {ok:false} com error.request_failed e {detail} = mensagem do erro;
    • HTTP não-OK com contrato de erro (o 400 do servidor) → data passa intacto com error_key/args/error;
    • HTTP não-OK sem contrato (ex.: 500 {"detail":"Internal Server Error"}) ou corpo não-JSON → error.request_failed com HTTP <status>[: <detail>].
    • O erro sintético usa o mesmo formato do backend, então resolveEvent() renderiza sem caminho novo.
  • startDirection: !ok || data.error → setStatus(panel, 'error', …) traduzível + toggleButtons(panel, false); state.running não recebe a direção. Nunca mais "rodando" sem worker.
  • stopDirection: try/finally — o painel volta para idle (limpa state.running, zera meters, libera botões) mesmo com a chamada falhando; a falha vai para o console. O worker local já era; travar a UI só piora.
  • i18n: chave error.request_failed adicionada em PT e EN.

Nenhuma chamada de rede nova, nenhum caminho quente de áudio tocado.

Gate (resultado real)

  • T1 — python -m compileall -q . → COMPILE_OK; import fase0_poc, laguna_core, laguna_server → IMPORTS_OK.
  • T2 — não aplicável (nenhuma mudança em laguna_core.py/fase0_poc.py).
  • T3 — node --check static/app.js + node --check static/i18n.js → JS_OK; pytest tests_unit/test_i18n_parity.py → 2 passed. Suíte completa pytest tests_unit → 48 passed.

Riscos

  • API.gain mudou o tipo de retorno (null/objeto → {ok, data}). Único call site é pushGain() (app.js), que ignora o retorno e não usa await — e postJSON nunca rejeita, então não há unhandled rejection.
  • Comportamento no caminho feliz é idêntico: {ok:true, data} e o backend continua devolvendo {ok:true, direction}.
  • Verificação foi estática (gate T3 + leitura); não houve teste manual com o servidor derrubado no meio do clique.

Closes #50

…JSON)

`API.start`/`API.stop` nao checavam `r.ok` nem tinham `.catch`: um 500 do
backend virava `res.error === undefined` e a UI marcava a direcao como
rodando sem worker; `fetch` rejeitado (servidor fora do ar) virava unhandled
rejection e deixava o painel em "carregando" com os botoes travados ate F5.

- novo helper `postJSON(url, body)` que NUNCA lanca e devolve `{ok, data}`,
  usado pelos tres verbos (start/stop/gain). Resposta nao-OK sem contrato de
  erro vira `error.request_failed` com `{detail}`, no mesmo formato do 400 do
  backend — `resolveEvent()` renderiza sem caminho novo.
- `startDirection`: qualquer falha -> status `error` traduzivel + botoes
  liberados; `state.running` nao recebe a direcao.
- `stopDirection`: `try/finally` garante volta pra `idle` (limpa running,
  zera meters, libera botoes) mesmo com a chamada falhando.
- chave `error.request_failed` em PT e EN.

Refs #50
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@caioross

Copy link
Copy Markdown
Owner Author

Parecer do PR Doctor — aprovada para merge (HANDBOOK §7.3, autonomia normal)

Head analisado: 9ccc079. Diff lido inteiro (59+/24-, 2 arquivos, 100% em static/).

Classificação: §7.3 (autonomia total em static//i18n). Não é núcleo §7.1 nem área de quórum §7.2 — o backend não é tocado; o contrato REST/WS de laguna_server.py continua idêntico, a PR só passa a consumir corretamente o que o servidor já devolvia.

Gate rodado localmente na worktree Laguna-wt/i50 (HEAD confere com o head da PR, árvore limpa), com C:\Python313\python.exe:

  • T1 — compileall → COMPILE_OK; import fase0_poc, laguna_core, laguna_server → IMPORTS_OK.
  • T2 — não aplicável (nenhum arquivo do pipeline no diff).
  • T3 — node --check static/app.js + static/i18n.js → JS_OK; pytest tests_unit → 48 passed (inclui test_i18n_parity.py).
  • CI do GitHub: 2/2 verdes.

Verificação de risco (o ponto que a própria PR levanta): a mudança de tipo de retorno de API.gain (null/objeto → {ok, data}) foi conferida por call site. API.gain tem um único consumidor, pushGain() (static/app.js:302-308), que não usa await e descarta o retorno — e postJSON nunca rejeita, então não há unhandled rejection. API.start/API.stop também têm um call site cada (startDirection/stopDirection), ambos migrados no mesmo diff. Nenhum consumidor órfão.

Outros pontos conferidos:

  • resolveEvent() (app.js:97-105) trata key ausente com fallback para msg e devolve '' no pior caso — o erro sintético de requestFailed() sempre traz error_key + args.detail, então cai no caminho traduzido.
  • Resposta OK com corpo vazio/não-JSON → data || {}, e data.error fica undefined: caminho feliz preservado.
  • !ok || data.error faz short-circuit antes de desreferenciar data no ramo de falha.
  • error.request_failed presente em PT e EN (a paridade é garantida pelo teste versionado, não por inspeção).
  • Nenhuma chamada de rede nova, nenhum caminho quente de áudio tocado — promessa 100% local (§1) intacta.

Closes #50 está correto: os 6 acceptance criteria da issue são atendidos (checagem de r.ok, corpo não-JSON sem lançar, 500 → status error traduzível + botões liberados + state.running não poluído, fetch rejeitado tratado igual, stopDirection com try/finally, chaves PT/EN, gate T3).

Ressalva registrada, não bloqueante: como o próprio autor declara, a verificação foi estática — não houve teste manual derrubando o servidor no meio do clique. Aceito: o diff é defensivo por construção (o helper não pode lançar) e o custo de um falso negativo aqui é menor que o do bug atual, em que o painel mente dizendo "rodando" sem worker.

API.devices segue com throw próprio (corrigido em #33, fora do escopo desta PR).

Squash-merge liberado.

@caioross
caioross merged commit e706782 into main Jul 29, 2026
2 checks passed
@caioross
caioross deleted the auto/issue-50-ui-start-stop-errors branch July 29, 2026 19:12
caioross added a commit that referenced this pull request Aug 5, 2026
…ry recuperavel antes do terminal (#54) (#64)

* fix(core): soluco de UM device de saida nao para mais a direcao (#54)

Toda falha de `_OutputSink` virava `error.play`, e `static/app.js` trata erro
nao-recuperavel como fim de sessao: status 'error', `running.delete(dir)` e
`toggleButtons(panel, false)` — ou seja, Stop DESABILITADO com o worker ainda
capturando, traduzindo e mandando audio pro Discord. Um fone entrando em power
save deixava a UI e o backend dessincronizados, e a unica saida era recarregar
a pagina.

O sink ja se recuperava por dentro (o stream cai, a frase seguinte reabre): o
que faltava era o degrau no relato. Agora `_OutputSink` conta falhas
CONSECUTIVAS por device (o contador vive no sink, que e quem sabe se o
`write()` deu certo, e um contador por sink ja e um por device) e o worker
decide: abaixo de `OUT_SINK_MAX_ERRORS` emite `error.output_retry` com
`recoverable=True` — aviso ambar transitorio, direcao segue rodando, Stop
segue habilitado — e no teto emite o terminal `error.play`, agora identificando
o device e o numero de falhas. Uma frase tocada com sucesso zera o orcamento,
para que falhas espacadas nunca somem ate o teto.

Mesma doutrina ja aplicada a captura (CAPTURE_MAX_RETRIES) e ao laco de
traducao (SEGMENT_MAX_FAILURES, #45). Reusar o `recoverable` da #45 em vez de
um `kind:"status"` novo mantem `static/app.js` intocado — status pintaria o
painel de verde "rodando" com um texto de falha, e nao voltaria sozinho.

O guarda de shutdown do `_report()` (#52) fica intacto: o write abortado pelo
`close()` nao emite nem incrementa o contador.

Closes #54

* fix(core): terminal de saida perdida e sem volta para aquele device (#54)

O quorum adversarial (Lente Produto/UX) vetou com vetor concreto: depois do
terminal `error.play` a UI ja derrubou os botoes (`app.js`: setStatus('error')
+ toggleButtons(false) + running.delete), mas o zerar-em-sucesso do orcamento
deixava a falha SEGUINTE do mesmo device voltar como `recoverable` — e o
caminho recuperavel repinta o painel de VERDE "Rodando" 4s depois, com o Parar
desabilitado. Estado que nao existia antes desta issue: na main, `error.play`
nunca era recoverable.

`DirectionWorker._sinks_perdidos` trava o veredito por device: device que ja
custou OUT_SINK_MAX_ERRORS frases seguidas nao volta a ser soluco, e o terminal
tambem para de ser re-emitido a cada frase. `_open_sinks` limpa a trava, para
que uma direcao nova nunca herde o veredito da anterior. Sem lock: so a thread
de cada sink escreve, e cada uma escreve a propria chave.

Testes (3 novos, sem PortAudio/modelo/GPU): terminal nao volta a recuperavel
(verificado que falha sem a trava), vizinho vivo segue reportando normalmente,
e `_open_sinks` zera o veredito.

Refs #54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ui: erro em /api/start ou /api/stop trava o painel — a UI diz "rodando" sem worker, e só F5 recupera

1 participant