fix(ui): erro em /api/start|stop nao trava mais o painel (helper postJSON) - #52
Conversation
…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
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
Parecer do PR Doctor — aprovada para merge (HANDBOOK §7.3, autonomia normal)Head analisado: Classificação: §7.3 (autonomia total em Gate rodado localmente na worktree
Verificação de risco (o ponto que a própria PR levanta): a mudança de tipo de retorno de Outros pontos conferidos:
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.
Squash-merge liberado. |
…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
Contexto
API.starteAPI.stop(static/app.js) eram os únicos verbos sem checagem der.oke sem.catch— ao contrário deAPI.devices(corrigido em #33) eAPI.gain. Fora do caminho feliz do 400 previsto pelo backend, o painel travava ou mentia, e sóF5recuperava:start(exceção emDirectionWorker.start()):res.errorvinhaundefined, oifnão disparava e a UI faziastate.running.add(dir)— status congelado em "carregando", ▶ Iniciar desabilitado, app aparentando ter iniciado sem worker.fetchrejeitava dentro destartDirection(async, semcatch) → unhandled rejection; as linhas de recuperação nunca rodavam.■ Pararque não parava: rejeição emAPI.stoppulava tudo depois doawait—state.runningsujo, meters vivos, nenhuma mensagem.O que mudou e por quê
Diff contido em
static/(HANDBOOK §7.3), sem tocar no backend.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 espalhartry/catch:fetchrejeitado →{ok:false}comerror.request_failede{detail}= mensagem do erro;datapassa intacto comerror_key/args/error;500 {"detail":"Internal Server Error"}) ou corpo não-JSON →error.request_failedcomHTTP <status>[: <detail>].resolveEvent()renderiza sem caminho novo.startDirection:!ok || data.error→setStatus(panel, 'error', …)traduzível +toggleButtons(panel, false);state.runningnão recebe a direção. Nunca mais "rodando" sem worker.stopDirection:try/finally— o painel volta paraidle(limpastate.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.error.request_failedadicionada em PT e EN.Nenhuma chamada de rede nova, nenhum caminho quente de áudio tocado.
Gate (resultado real)
python -m compileall -q .→COMPILE_OK;import fase0_poc, laguna_core, laguna_server→IMPORTS_OK.laguna_core.py/fase0_poc.py).node --check static/app.js+node --check static/i18n.js→JS_OK;pytest tests_unit/test_i18n_parity.py→ 2 passed. Suíte completapytest tests_unit→ 48 passed.Riscos
API.gainmudou o tipo de retorno (null/objeto →{ok, data}). Único call site épushGain()(app.js), que ignora o retorno e não usaawait— epostJSONnunca rejeita, então não há unhandled rejection.{ok:true, data}e o backend continua devolvendo{ok:true, direction}.Closes #50