Skip to content

fix: BrokenPipeError no healthz + painel server-side, i18n, log persistente e credencial de recuperacao - #1

Merged
elielsousa-pathbit merged 18 commits into
masterfrom
feat/env-only-configuration
Sep 12, 2026
Merged

elielsousa-pathbit merged 18 commits into
masterfrom
feat/env-only-configuration

Conversation

@elielsousa-pathbit

@elielsousa-pathbit elielsousa-pathbit commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Descrição das Alterações

Corrige o BrokenPipeError que aparecia nos logs do container e adiciona o que faltava para operar o sincronizador sem depender da tela.

1. Servidor web — a causa do BrokenPipeError

File "/app/src/nine_rtksync/web/server.py", line 116, in serve_healthz
    self.wfile.write(b"OK")
BrokenPipeError: [Errno 32] Broken pipe

Causa raiz: o servidor era HTTPServer (uma thread só) apesar de o docstring do módulo prometer multi-thread, e /healthz fazia uma chamada HTTP de saída de até 3s ao gateway a cada probe. O healthcheck do Docker (timeout 5s, a cada 15s) desistia e fechava o socket antes de a resposta ser escrita — e o socketserver imprimia o traceback inteiro.

  • ThreadingHTTPServer com daemon_threads, cumprindo o que o docstring já dizia.
  • QuietThreadingHTTPServer.handle_error engole desconexão do cliente (BrokenPipe/ConnectionReset/ConnectionAborted); erros reais seguem chegando ao handler padrão.
  • write_body() tolera o cliente ter fechado a conexão antes de ler o corpo.
  • A sondagem ao gateway ganha cache de 30s, então /healthz deixa de custar uma ida à rede por chamada.

2. Render server-side, ícones e i18n

  • O HTML passa a ser montado em web/render.py com os dados já embutidos. O navegador não consulta mais /api/status para desenhar a tela — o SQLite fica inteiramente do lado do servidor. Ações viram POST-Redirect-GET (/acoes/*) e a página funciona sem JavaScript.
  • Removido o Access-Control-Allow-Origin: * de /api/status; adicionados Cache-Control: no-store, X-Frame-Options: DENY, X-Content-Type-Options e Referrer-Policy.
  • Bootstrap 5 + Bootstrap Icons + flag-icons + jQuery no lugar dos emojis.
  • Idioma padrão inglês, com português e espanhol no seletor de bandeiras, persistido em SQLite próprio (prefs.py) — nunca no banco do gateway, nunca no localStorage.
  • Botão Atualizar explícito, além de Sincronizar agora, Executar agora e Testar conexão.

3. Diagnóstico da renovação e logs do cron

O painel mostrava só 0 renovadas e não havia como distinguir "nada precisava ser renovado" de "a renovação falhou". Agora:

  • Cada conexão traz uma frase explicando por que foi ou não renovada. O caso observado — o cron rodou 4 ciclos sem renovar o Antigravity — era comportamento correto: 24 min restantes contra uma margem (REFRESH_MARGIN) de 15 min. A tela agora diz isso.
  • O cron guarda o log de cada ciclo e o painel tem o botão Logs com o histórico por execução; um ciclo que falhou aparece marcado em vermelho.

4. Provedores locais (Ollama)

  • ConnectionRecord.is_local reconhece Ollama/vLLM/LM Studio/LocalAI e qualquer baseUrl apontando para o host. Uma instância local costuma usar chave de fachada e por isso aparecia rotulada como provedor de nuvem.
  • LocalProvider consulta /api/tags e /v1/models, grava os modelos descobertos e marca a conexão como unreachable quando a instância não responde — antes ela era dada como saudável às cegas.
  • A tabela mostra a baseUrl e os modelos servidos.

5. Log persistente com retenção

logs.py: arquivo rotativo diário, retenção configurável por LOG_RETENTION_DAYS (padrão 30 dias) e expurgo dos rotacionados vencidos no boot. LOG_DIR, LOG_LEVEL e LOG_TO_STDOUT completam o contrato. O stdout do container continua espelhado por padrão.

6. Credencial de recuperação e configuração por ambiente

  • auth.py: credenciais salvas pela tela mandam; se nada foi salvo valem as de fábrica; e o usuário admin com o hash de recuperação entra sempre. Qualquer outra combinação é inválida. Comparações com hmac.compare_digest.
  • O hash vem de DASHBOARD_RECOVERY_HASH ou é gerado no primeiro boot, salvo com permissão 0600 e registrado uma única vez no log.
  • DASHBOARD_USER/DASHBOARD_PASSWORD explícitas vencem o arquivo gravado pela tela — sem isso, uma única troca de senha tornava as variáveis inertes. Novas CRON_INTERVAL e CRON_ENABLED.

7. Portas e container

Porta interna 9090 (igual no OminiRTKSync), publicada em 9091 no host, com bind em 127.0.0.1. Container renomeado de router-sync para 9rtksync, para não colidir com o sincronizador irmão.

Tipo de Alteração

  • Correção de bug (bug fix)
  • Nova funcionalidade ou suporte a novo provedor
  • Refatoração de código sem impacto em comportamento
  • Atualização de documentação
  • Melhorias em testes ou pipeline de CI

Checklist de Validação

  • Código executado e validado em virtual environment local.
  • Suíte de testes unitários passando: 96 testes (eram 41).
  • Sem conflitos com a branch master (reconciliado com 183229f).
$ PYTHONPATH=src python3 -m unittest discover -s tests -p "test_*.py"
Ran 96 tests in 2.994s

OK

Arquivos de teste novos: test_logs.py, test_auth_recovery.py, test_web_render.py, test_web_resilience.py.

Nota: os comentários dos módulos novos seguem a convenção em inglês adotada em 183229f, não o padrão em português. Se preferir voltar ao português, é uma troca de uma passada.


Escopo adicionado depois da abertura

Validação viva de credenciais

O painel declarava toda conexão saudável por ela carregar uma chave: o ApiKeyProvider definia HEALTH_CHECK_ENDPOINTS e nunca os chamava, carimbando o status às cegas. Uma chave revogada seguia verde até uma requisição real falhar.

credential_check.py pergunta ao provedor. 401/403 = recusada, 429 = rate limited, falha de rede = unreachable (não comprovadamente ruim), qualquer outra resposta HTTP = credencial aceita — valida-se a credencial, não o modelo.

Os endpoints foram medidos, não supostos:

Provedor Endpoint Motivo
OpenRouter /api/v1/key /api/v1/models responde 200 sem credencial nenhuma
Ollama Cloud POST /v1/chat/completions o catálogo é público
Google AI Studio /v1beta/models + x-goog-api-key responde 400, não 401
OAuth Google oauth2.googleapis.com/tokeninfo responde 400 quando o token morreu

A validação nasce desligada no construtor: ligá-la por padrão faria qualquer teste que monta o engine sair para a internet — foi assim que a CI quebrou uma vez.

Autenticação

  • Não existe mais senha de fábrica. Um valor estático é, por definição, uma credencial pública. O primeiro acesso usa a credencial sorteada no primeiro boot, gravada com modo 0600 e nunca impressa no log.
  • Senha passa a viver no SQLite do painel como hash PBKDF2-SHA256 com sal, em vez de texto puro em JSON.
  • Política obrigatória: mínimo de 6 caracteres com maiúscula, minúscula, número e caractere especial, validada no formulário, na ação da tela e no endpoint JSON.
  • O corpo do 401 — que é o que o navegador exibe quando se aperta ESC no diálogo — deixou de imprimir as credenciais padrão.
  • Trocar a senha terminava em 401 cru. Agora redireciona para /credenciais-atualizadas, servida antes do require_auth.

CSRF

POST de outra origem é recusado: o Basic Auth é anexado pelo navegador mesmo em formulário de outro site, e urlencoded não dispara preflight. O Origin decide primeiro; checar Sec-Fetch-Site antes dele fazia um valor inesperado do navegador recusar um POST legítimo do próprio painel.

Detecção de instância local

baseUrl mora em providerSpecificData, não na raiz de data — lendo só a raiz, nenhuma instância local exibia seus modelos. A classificação passou a ser pelo endereço: "ollama" também é o nome do Ollama Cloud, que era tratado como local.

Retenção de packages

O cleanup-packages.yml não era limpeza: com min-versions-to-keep: 0 e delete-only-untagged-versions: false apagava todas as versões e depois removia o package via API. Agora guarda as 3 versões marcadas mais recentes.

…vidor web

Corrige o BrokenPipeError reportado em producao e adiciona o que faltava para
operar o sincronizador sem depender da tela.

Servidor web
- Troca HTTPServer por ThreadingHTTPServer: o docstring do modulo ja prometia
  multi-thread, mas uma unica thread atendia tudo. Com /healthz fazendo uma
  chamada HTTP de saida de ate 3s ao gateway, o probe do Docker (timeout 5s)
  estourava, fechava o socket e a escrita da resposta morria com
  "BrokenPipeError: [Errno 32] Broken pipe" em serve_healthz.
- QuietThreadingHTTPServer.handle_error passa a engolir desconexao do cliente
  (BrokenPipe/ConnectionReset/ConnectionAborted) em vez de imprimir traceback;
  erros de verdade continuam chegando ao handler padrao.
- write_body() tolera o cliente ter fechado a conexao antes de ler o corpo.
- A sondagem ao gateway ganha cache de 30s, entao /healthz deixa de custar uma
  ida a rede por chamada.

Log persistente
- Novo modulo logs.py: arquivo rotativo diario com retencao configuravel por
  LOG_RETENTION_DAYS (padrao 30 dias) e expurgo dos rotacionados vencidos no
  boot. LOG_DIR, LOG_LEVEL e LOG_TO_STDOUT completam o contrato.
- daemon.log_msg e o CronScheduler passam a escrever no logger; o stdout do
  container continua espelhado por padrao.

Credencial de recuperacao
- Novo modulo auth.py com a regra: credenciais salvas pela tela mandam; se nada
  foi salvo valem as de fabrica; e o usuario 'admin' com o hash de recuperacao
  entra sempre. Qualquer outra combinacao e invalida.
- O hash vem de DASHBOARD_RECOVERY_HASH ou e gerado no primeiro boot, salvo com
  permissao 0600 e registrado uma unica vez no log.
- Comparacoes por hmac.compare_digest.

Configuracao por ambiente
- DASHBOARD_USER/DASHBOARD_PASSWORD explicitas passam a vencer o arquivo salvo
  pela tela; sem isso, uma unica troca de senha tornava as variaveis inertes.
- Novas CRON_INTERVAL e CRON_ENABLED.

Portas e container
- Porta interna padronizada em 9090 (igual no OminiRTKSync); o host publica
  9091. Container renomeado de router-sync para 9rtksync para nao colidir com o
  sincronizador irmao.

Testes: 69 -> 69 passando (tests/test_logs.py, tests/test_auth_recovery.py e
tests/test_web_resilience.py sao novos).
…provedores locais

Render server-side
- O HTML passa a ser montado em web/render.py com os dados ja embutidos. O
  navegador nao consulta mais /api/status para desenhar a tela, entao o SQLite
  fica inteiramente do lado do servidor.
- Acoes viram POST-Redirect-GET (/acoes/*), e a pagina funciona sem JavaScript.
- Removido o Access-Control-Allow-Origin: * de /api/status; adicionados
  Cache-Control: no-store, X-Frame-Options, X-Content-Type-Options e
  Referrer-Policy. A tela nunca mais vem do cache do navegador.
- Botao "Atualizar" explicito, alem de "Sincronizar agora", "Executar agora" e
  "Testar conexao".

Icones e i18n
- Bootstrap 5 + Bootstrap Icons + flag-icons + jQuery no lugar dos emojis.
- Idioma padrao ingles, com portugues e espanhol no seletor de bandeiras.
- A escolha e persistida em SQLite proprio (prefs.py), nunca no banco do
  gateway e nunca no localStorage.

Diagnostico de renovacao
- Cada conexao mostra por que foi ou nao renovada. O caso reportado - "o cron
  rodou 4 vezes e nao renovou o Antigravity" - era comportamento correto: 24min
  restantes contra uma margem de 15min. Agora a tela diz isso em vez de so
  exibir "0 renovadas".
- O cron guarda o log de cada ciclo e o painel tem o botao Logs com o historico
  por execucao; um ciclo que falhou aparece marcado em vermelho.

Provedores locais
- ConnectionRecord.is_local reconhece Ollama/vLLM/LM Studio/LocalAI e qualquer
  baseUrl apontando para o host. Uma instancia local costuma usar chave de
  fachada e por isso aparecia rotulada como provedor de nuvem.
- LocalProvider consulta /api/tags e /v1/models, grava os modelos descobertos e
  marca a conexao como unreachable quando a instancia nao responde - antes ela
  era dada como saudavel as cegas.
- A tabela mostra a baseUrl e os modelos servidos.

Testes: 69 -> 94 passando (test_web_render.py e novo).
Resolve o encontro entre duas linhas de trabalho sobre os mesmos arquivos:
a traducao do repo para ingles (183229f) e a reestruturacao do painel.

- Comentarios e mensagens dos modulos novos passam para ingles, seguindo a
  convencao que o repo acabou de adotar.
- health_status agora devolve os codigos em ingles (active, expiring_soon,
  expired, rate_limited, no_expiration, unknown); HEALTH_PRESENTATION e as
  chaves de i18n foram realinhadas, com um teste travando essa correspondencia
  para os tres idiomas.
- Emojis removidos da saida de terminal introduzida pela traducao.
- .env.example e Makefile mantem o texto em ingles deles e recebem as variaveis
  novas (CRON_INTERVAL, CRON_ENABLED, DASHBOARD_RECOVERY_HASH, LOG_*) e a porta
  interna 9090 / externa 9091.
- web/index.html removido: a pagina agora e renderizada no servidor.

Testes: 96 passando.
A wiki do GitHub nao passa por review: quem tem acesso edita direto e nao ha
diff, historico util nem gate. As paginas passam a viver em docs/wiki/ e sao
publicadas na wiki por um workflow a cada push em master.

Paginas: Home, Installation, Configuration, Dashboard, Authentication, Logging,
Architecture, Troubleshooting e Upstream-Fixes, alem de _Sidebar e _Footer.

Configuration documenta o contrato completo de variaveis de ambiente - o
requisito de operar sem nunca abrir a tela. Troubleshooting parte de sintomas
reais (o BrokenPipeError do healthz, "o cron nao renovou o token", o Ollama
local sem modelos, senha esquecida). Upstream-Fixes registra os bugs achados
nos dois gateways e os PRs enviados, incluindo a correcao da afirmacao do
README antigo sobre o expiresAt em ISO, que a leitura do codigo upstream nao
sustenta.

Primeira execucao do workflow exige que a wiki ja exista: o GitHub so cria o
repositorio <repo>.wiki.git depois que a primeira pagina e salva pela interface.
O job avisa isso em vez de falhar.
O teste apontava para http://127.0.0.1:11434 de verdade: passava na maquina de
quem tem Ollama rodando e falhava no CI, que nao tem. Agora discover_models e
stubado e o teste cobre as duas pontas - instancia respondendo (ok + modelos) e
instancia fora (unreachable).

discover_models tambem deixa de tentar os tres endpoints quando nada esta
escutando: um erro de conexao encerra a sondagem em vez de multiplicar o
timeout por 3 a cada varredura. Um HTTP de verdade (404/405) continua tentando
o proximo caminho.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ebe67a5b58

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 20 to +21
def can_handle(self, conn: ConnectionRecord) -> bool:
p = conn.provider.lower()
return (
"ollama" in p
or "openai-compatible" in p
or "vllm" in p
or "lmstudio" in p
or bool(conn.data.get("baseUrl") and not conn.is_oauth and not conn.has_api_key)
)
return conn.is_local

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Route facade-key local connections to LocalProvider

Local instances commonly use the facade API key that is_local now explicitly supports, but SyncEngine iterates ApiKeyProvider before this provider and ApiKeyProvider.can_handle() accepts every apiKey record. The loop therefore stops at the generic handler, which stamps the connection ok, and this catalog probe never runs: models are never discovered and an unavailable Ollama/vLLM instance is reported healthy. Put the local handler before the generic API-key handler or exclude is_local records from the latter.

Useful? React with 👍 / 👎.

Comment on lines +82 to +83
models, probe_error = self.discover_models(conn.base_url or "", conn.api_key or "")
now_iso = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update the local-provider test for the network probe

This newly unconditional network probe makes the pre-existing TestDiscoveryEngine.test_providers_with_discovery host-dependent: that test creates the Ollama connection without starting or mocking a catalog server but still asserts testStatus == "ok". On a normal CI host with no service at 127.0.0.1:11434, all three requests fail and this branch writes unreachable; PYTHONPATH=src python3 -m unittest discover -s tests -p 'test_*.py' currently fails on that assertion. Mock the catalog response or update the fixture and expectation.

Useful? React with 👍 / 👎.

Comment thread src/nine_rtksync/models.py Outdated
Comment on lines +120 to +122
if self.is_local:
# A local instance is only healthy when its model catalog answered.
return "unknown" if self.data.get("testStatus") == "unreachable" else "active"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check local health before generic API-key health

For a local connection carrying a facade apiKey whose catalog probe has set testStatus to unreachable, health_status returns active from the preceding API-key branch and never reaches this new local-health branch. Consequently the dashboard can display an offline local instance as active even when persisted probe state says it is unreachable; evaluate is_local before has_api_key (while preserving any desired local rate-limit behavior).

Useful? React with 👍 / 👎.

Comment thread src/nine_rtksync/cli.py Outdated
Comment on lines +117 to +122
# The file log must exist before any event from the sync engine.
logger = setup_logging(settings.db_path)

# Break-glass credential: generated once and written to the log, so the operator
# can get back into the panel after forgetting the password set on the screen.
recovery_hash, generated_now = settings.ensure_recovery_hash()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply --db-path before initializing persistent state

When the documented --db-path points somewhere other than the initially discovered/env database, logging and ensure_recovery_hash() run against the old path here, then line 131 changes settings.db_path. Subsequent dashboard authentication resolves the recovery file from the new database directory, so the generated-and-logged recovery password is never found and cannot unlock the dashboard; logs likewise go to the wrong default directory. Apply CLI overrides before deriving either path-dependent resource.

Useful? React with 👍 / 👎.

Comment on lines +200 to +203
if route == "/acoes/credenciais":
fields = parse_qs(raw_body.decode("utf-8", errors="replace"))
new_user = (fields.get("user", [""])[0] or "").strip()
new_pass = (fields.get("password", [""])[0] or "").strip()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Protect credential changes against cross-site form posts

Once a browser has cached the dashboard's Basic Auth credentials, a malicious site can submit a cross-origin HTML form to this new URL with user and password fields; the browser's same-origin response protections do not prevent that form POST, and this handler has no CSRF token or Origin/Referer validation. The request reaches update_auth_credentials and can replace the operator's dashboard password, whereas the retained JSON endpoint was not form-parsable. Require an anti-CSRF mechanism before accepting this state-changing form.

Useful? React with 👍 / 👎.

Comment thread src/nine_rtksync/auth.py
Comment on lines +91 to +94
except OSError:
# Sem disco gravável o hash vira efêmero (válido só nesta execução),
# mas o serviço continua subindo.
pass

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the generated recovery hash available after write failures

The claimed unwritable-disk fallback is not actually valid during the current process: after this exception, ensure_recovery_hash() returns and the CLI logs generated, but every authentication attempt calls get_recovery_hash(), which rereads the nonexistent file and gets an empty string. Thus deployments with an existing but unwritable DATA_DIR advertise a recovery password that can never authenticate; cache the generated value in process state or do not present it as usable.

Useful? React with 👍 / 👎.

Comment thread Dockerfile
ENV SYNC_INTERVAL=300
ENV REFRESH_MARGIN=900
ENV WEB_PORT=9190
ENV WEB_PORT=9090

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve or migrate the previous dashboard port

Changing the default listener to 9090 breaks existing deployments that published the previously documented -p 9190:9190 mapping without also setting WEB_PORT: after upgrading, the host port forwards to container port 9190 while the server listens on 9090. The repository README still publishes that mapping, its 9190 default, and http://localhost:9190, so both upgrades and documented default CLI use lead users to an unreachable dashboard. Retain compatibility or update the public configuration and migration guidance together.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4a96f3e5ad

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +53 to +57
models = self._extract_model_names(payload)
if models:
return models, ""

return [], last_error or "no model returned by the local instance"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat empty model catalogs as a successful probe

When an Ollama/vLLM instance responds successfully with an empty models or data array (for example, a fresh instance with no model installed), this treats that response as a failed probe and eventually returns an error. check_and_refresh then persists testStatus as unreachable, so an available local service is shown as offline; preserve probe-success separately from the discovered model names and mark a successful empty catalog as ok.

Useful? React with 👍 / 👎.

Comment thread src/nine_rtksync/models.py Outdated
Comment on lines +62 to +63
base_url = str(self.data.get("baseUrl") or "")
return any(host in base_url for host in ("localhost", "127.0.0.1", "0.0.0.0", "host.docker.internal"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Detect local connections that use baseURL

For a custom-named local connection that uses the supported baseURL spelling and a facade API key, local detection reads only baseUrl and returns false. It is therefore handled by the generic API-key provider and stamped active without a local catalog probe; use the normalized base_url accessor here so both accepted spellings receive the same local-health behavior.

Useful? React with 👍 / 👎.

Comment thread src/nine_rtksync/daemon.py Outdated
Comment on lines +22 to +24
def log_msg(prefix: str, text: str):
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"[{ts}] [{prefix}] {text}", flush=True)
"""Record an event in the persistent log (and on stdout, if LOG_TO_STDOUT allows)."""
get_logger().info(f"[{prefix}] {text}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Emit failures at their actual log level

When LOG_LEVEL is set to WARNING or ERROR, every daemon event—including calls such as log_msg("ERROR", ...) for an unavailable database and log_msg("FAILURE", ...) for provider exceptions—is emitted as INFO and filtered out before reaching either handler. This leaves operators with no recorded failures precisely when using the documented higher-severity log modes; map error/failure prefixes to logger.error or logger.warning instead of always using info.

Useful? React with 👍 / 👎.

Comment thread src/nine_rtksync/cron.py
Comment on lines +25 to +26
actions = detail.get("actions") or []
if not actions:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Record provider exceptions in scheduler history

If an individual provider refresh raises, SyncEngine catches it without adding an action or an error to its summary; this extractor consequently skips that connection, while the scheduler defaults the cycle to success=True. The newly added dashboard history therefore shows a successful cycle with no failure detail even when an OAuth renewal failed, defeating the operator-facing failure log; propagate caught provider errors into the summary/detail and mark the cycle unsuccessful.

Useful? React with 👍 / 👎.

Comment thread src/nine_rtksync/web/server.py Outdated
Comment on lines +153 to +154
try:
req = urllib.request.Request(
self.router_url,
headers={"User-Agent": "9RTKSync-Healthcheck/1.0"},
res = self.sync_trigger_callback() or {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Serialize concurrent dashboard syncs

Because the server now handles requests in separate threads, two authorized POSTs to this new action can enter SyncEngine.sync_all() concurrently. Those runs read and then write the same connection JSON independently, so simultaneous OAuth renewals can race and overwrite a newly rotated token; protect the sync callback with a single-run lock (shared with cron/manual triggers) before invoking it.

Useful? React with 👍 / 👎.

Comment thread src/nine_rtksync/web/server.py Outdated
Comment on lines +153 to +154
try:
req = urllib.request.Request(
self.router_url,
headers={"User-Agent": "9RTKSync-Healthcheck/1.0"},
res = self.sync_trigger_callback() or {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Invoke the sync callback without descriptor binding

When start_web_server receives an ordinary function or lambda for its documented Callable[[], ...] callback, looking it up through self binds the handler as an extra positional argument. The new form action then redirects with a takes 0 positional arguments but 1 was given failure, although the retained /api/sync path works because it calls the class attribute directly; invoke DashboardHandler.sync_trigger_callback here or store the callback on the server instance.

Useful? React with 👍 / 👎.

…al e fecha CSRF

O painel pintava toda conexao de verde apenas por existir uma chave: o
ApiKeyProvider carimbava testStatus = "ok" sem nunca perguntar nada ao
provedor, entao uma chave revogada seguia "ativa" ate uma requisicao real
falhar. Agora cada credencial e verificada de fato.

Validacao viva (credential_check.py):
- API keys: 401/403 = recusada, 429 = rate limited, qualquer outra resposta
  HTTP = credencial aceita (validamos a credencial, nao o modelo).
- Tokens OAuth: consulta o tokeninfo do Google, que responde 400 quando o
  token morreu, em vez de inferir vida a partir da validade gravada.
- Endpoints escolhidos por medicao, nao por suposicao: /api/v1/models do
  OpenRouter responde 200 sem credencial nenhuma e validaria qualquer lixo,
  entao usa-se /api/v1/key; o catalogo do Ollama Cloud e publico pelo mesmo
  motivo; o Google AI Studio autentica por header e responde 400.
- Desligada por padrao no construtor, para que teste algum gere trafego de
  saida por acidente.

Deteccao de instancia local:
- baseUrl mora em providerSpecificData, nao na raiz de data. Lendo so a raiz,
  nenhuma instancia local exibia seus modelos.
- A classificacao passa a ser pelo endereco, nao pelo nome: "ollama" tambem e
  o nome do Ollama Cloud, que era tratado como local e sondado em /api/tags.
- is_local e avaliado antes do ramo de API key, senao a instancia local, que
  carrega chave de fachada, nunca chegava ao proprio teste.

Seguranca do painel:
- POST de outra origem passa a ser recusado. O Basic Auth e anexado pelo
  navegador mesmo em formulario de outro site, e urlencoded nao dispara
  preflight: dava para trocar a senha do painel a partir de uma pagina
  maliciosa.
- /api/status devolvia accessToken, refreshToken, apiKey e a linha bruta do
  banco. Passa a projetar apenas os campos que a tela consome.

Interface:
- Tipografia via Google Fonts, com pilha de sistema como reserva.
- Linha de diagnostico do gateway alinhada a direita como as demais.
- Badges e traducoes (en/pt/es) para os estados novos da credencial.

READMEs dos dois projetos publicavam a porta antiga e davam o mesmo
container_name aos dois sincronizadores, que colidiriam ao subir juntos.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

O master recebeu dois commits diretos que quebraram a CI e precisaram de um
hotfix. Este merge concilia os dois lados:

- Padronizacao do master adotada: testStatus passa a ser "active" em todo o
  projeto, inclusive no LocalProvider e no ApiKeyProvider; health_status
  continua aceitando "ok" e "success" para nao quebrar bancos existentes.
- O import de HTTPServer do master foi mantido, e a segunda definicao de
  QuietThreadingHTTPServer, que sobrescrevia silenciosamente a primeira, foi
  removida.
- A validacao viva de credenciais e a descoberta de modelos locais prevalecem
  sobre o carimbo cego de "active" que voltara pelo master: era justamente o
  bug que fazia toda conexao aparecer saudavel sem nunca ter sido verificada.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

…sh que releia tudo

O aviso de seguranca ficava na tela comparando a senha ativa com o texto
"pathbit". Agora ele depende do que interessa: existir ou nao uma senha
definida pelo usuario no banco do painel. Definida a senha, o aviso some.

Senha:
- Passa a viver no SQLite do sincronizador como hash PBKDF2-SHA256 com sal,
  em vez de texto puro no .dashboard_auth.json, que e apagado na troca.
- Politica obrigatoria: minimo de 6 caracteres com maiuscula, minuscula,
  numero e caractere especial, validada no formulario, na acao da tela e no
  endpoint JSON. A recusa lista de uma vez todas as regras violadas, no
  idioma escolhido, em vez de revelar a politica a cada tentativa.
- Quem ja tinha senha no arquivo antigo continua entrando: password_matches
  reconhece o texto puro herdado ate a proxima troca.

Refresh:
- O botao Atualizar virou uma acao que zera o cache da sondagem ao gateway
  antes de remontar a pagina; como link simples, o painel podia repetir por
  ate 30s o estado anterior a acao recem-disparada. A sincronizacao manual
  passa a invalidar o mesmo cache.

Tipografia do Google Fonts, que estava declarada mas nunca inserida no head.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

…o log

O merge com o master reverteu o serve_healthz dos dois projetos para a versao
antiga e, no 9RTKSync, deixou o rabo da versao nova colado nela: o handler
escrevia a resposta e so depois levantava NameError: name "status" is not
defined, a cada probe do Docker, de 15 em 15 segundos.

O teste existente passava porque o cliente ja tinha recebido corpo e status
antes da excecao. Os testes novos conferem Content-Length e Cache-Control, que
so existem quando a resposta e montada antes de qualquer escrita.

No OminiRTKSync o merge tambem desfez o cache da sondagem e o write_body
tolerante: era exatamente o BrokenPipeError que motivou a correcao original.

Credenciais que vazavam:
- O corpo do 401 imprimia "Default credentials: admin / pathbit" para quem
  ainda nao tinha entrado. Agora diz apenas que a autenticacao e requerida.
- O banner de seguranca exibia a credencial de fabrica na tela; passa a dizer
  que falta definir uma senha, sem mostrar qual.
- A credencial de recuperacao era escrita por inteiro no stdout. O stdout do
  container costuma ser coletado, encaminhado e lido por muita gente; o valor
  fica so no arquivo com modo 0600 e o log diz onde encontra-lo.

Barreira de CSRF: a ordem estava invertida. Checar Sec-Fetch-Site antes do
Origin fazia um valor inesperado do navegador recusar um POST legitimo do
proprio painel, e a recusa era uma pagina 403 crua sem caminho de volta. O
Origin passa a decidir primeiro e a recusa volta ao painel com o aviso.

Validade em tela: token OAuth aparecia como "Ilimitado" quando nao havia
expiresAt legivel. Todo token OAuth expira; a ausencia e dado faltando, nao
credencial eterna. So chave estatica e apresentada como sem expiracao.

Coluna nova de ultima renovacao, alimentada pelo lastRefreshAt que o gateway
ja grava, para o painel mostrar quando cada credencial foi renovada.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

…tudo

Senha padrao estatica e, por definicao, uma credencial publica: ela viaja no
README, e copiada para toda implantacao e e a primeira coisa que qualquer um
tenta. O painel deixa de ter uma.

- dashboard_password nasce vazio. Sem nada gravado, quem abre a porta e a
  credencial sorteada no primeiro boot, escrita com modo 0600 e nunca impressa
  no log. "admin/pathbit" para de existir como caminho de entrada.
- O corpo do 401 e o que o navegador mostra quando se aperta ESC no dialogo do
  Basic Auth. Passa a ser uma pagina limpa, sem credencial nem dica dela.
- Trocar a senha terminava em 401 cru: o navegador ainda envia a anterior. A
  acao passa a redirecionar para /credenciais-atualizadas, servida antes do
  require_auth, que explica o que houve e leva de volta ao painel.

Retencao de packages:

O cleanup-packages.yml nao era limpeza. Com min-versions-to-keep: 0 e
delete-only-untagged-versions: false ele apagava TODAS as versoes e em seguida
removia o proprio package via API; quem tivesse um container puxando :latest
ficava sem imagem para reiniciar. Agora guarda as 3 versoes marcadas mais
recentes, descarta as camadas orfas dos builds multi-arch, roda apos um release
bem-sucedido e registra no summary o que sobreviveu.

Documentacao, compose e .env deixam de sugerir qualquer senha.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

actions/delete-package-versions@v5 nao tem ignore-versions-with-tags; o
workflow rodava com um aviso de input desconhecido. O unico filtro disponivel e
ignore-versions, casado contra o nome da versao, que para container e o digest,
entao nao da para preservar por tag. "latest" fica seguro assim mesmo, porque
ela sempre aponta para o digest mais novo, que min-versions-to-keep mantem por
construcao.
Comment thread src/nine_rtksync/auth.py Fixed
Comment thread src/nine_rtksync/auth.py Fixed
Comment thread src/nine_rtksync/config.py Fixed
Comment thread src/nine_rtksync/web/render.py Fixed
Comment thread tests/test_auth_recovery.py Fixed
Comment thread tests/test_logs.py Fixed
Comment thread tests/test_credential_check.py Fixed
Comment thread tests/test_auth_recovery.py Fixed
Comment thread tests/test_web_resilience.py Fixed
…emplo

A retencao de packages usava actions/delete-package-versions, que trata cada
manifesto como uma versao independente. O build publica linux/amd64 e
linux/arm64, entao o buildx grava um manifest list com a tag mais um manifesto
sem tag por plataforma. Descartar "versoes sem tag" apagava justamente as
camadas que a tag referencia: a tag continuava listada e o pull falhava com
"manifest unknown". Trocada por dataaxiom/ghcr-cleanup-action, que resolve o
manifest list antes de apagar e so descarta um manifesto sem tag quando
nenhuma tag preservada aponta para ele. Ficam habilitados delete-ghost-images
e delete-partial-images para recolher o que execucoes anteriores quebraram, e
validate para conferir no registro que cada digest referenciado existe.

O docker-compose.example.yml trazia INITIAL_PASSWORD e JWT_SECRET com valor de
fallback. Um default publicado em arquivo de exemplo nao e exemplo: e a
credencial real de toda implantacao que copiou e colou. Os dois passam a usar
a forma obrigatoria ${VAR:?...}, e o .env.example vem com os campos vazios e a
instrucao de como gerar cada um.

A fixture de senha dos testes imitava o formato de uma senha real; virou um
placeholder evidente.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 185f1a8ea5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

cron_scheduler = CronScheduler(
sync_callback=engine.sync_all,
interval_seconds=settings.sync_interval,
interval_seconds=settings.cron_interval,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply the CLI interval to the scheduler

When the daemon is started with --interval, cli.py updates only settings.sync_interval, but this changed scheduler construction now reads settings.cron_interval, which was already derived from the environment before CLI overrides were applied. Consequently --interval 60 still schedules runs at the default/environment cron interval while the startup banner claims 60 seconds; update both settings or keep the scheduler tied to sync_interval when no explicit cron override exists.

Useful? React with 👍 / 👎.

Comment on lines +72 to +75
result = check_api_key(
conn.provider,
conn.api_key or "",
base_url=conn.base_url,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate the newly discovered API key

When host discovery finds a rotated API key, the new value is placed in data["apiKey"], but this probe still sends conn.api_key, which is the stale value loaded before discovery. If the old key has been revoked, the same write persists the new key together with credentialState/testStatus set to invalid, potentially disabling or misreporting a successfully repaired connection until the next sweep; validate the key from the updated data mapping.

Useful? React with 👍 / 👎.

Comment on lines +79 to 81
data.update(result.to_dict())
data["lastTested"] = result.checked_at
modified = True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not count every validation as a credential renewal

With the default live validation enabled, every sweep changes credentialCheckedAt and unconditionally sets modified=True, even when the API key itself is unchanged. SyncEngine interprets this returned boolean as renewed, increments summary["refreshed"], and logs that credentials were updated, so cron renewal totals grow by the number of API-key connections on every run; distinguish probe metadata persistence from an actual credential refresh.

Useful? React with 👍 / 👎.

Comment on lines +112 to +117
def _classify(status: int, spec_invalid: tuple = ()) -> str:
if status in (401, 403) or status in spec_invalid:
return STATE_INVALID
if status == 429:
return STATE_RATE_LIMITED
return STATE_VALID

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat provider 5xx responses as unavailable

If a credential probe receives HTTP 500, 502, or 503, urlopen raises HTTPError, _execute passes that status here, and this fallback classifies it as valid. ApiKeyProvider then stamps the connection active even though the provider is failing and the credential was not proven usable; server-error responses should map to an unavailable/error state rather than STATE_VALID.

Useful? React with 👍 / 👎.

Comment thread src/nine_rtksync/web/render.py Outdated
Comment on lines +464 to +466
db_ok = bool(gateway.get("dbSummary"))
if online and db_ok:
diagnosis = translate("gateway.diag_ok", lang)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Derive database health from an explicit status

When the configured SQLite file is missing, collect_dashboard_state supplies the nonempty string "Banco nao encontrado" as dbSummary; converting that string to bool therefore sets db_ok=True. If the router is reachable, the dashboard displays the green “Gateway and SQLite database fully operational” diagnosis while simultaneously saying the database was not found; pass or test an explicit database-health boolean instead.

Useful? React with 👍 / 👎.

Comment on lines +39 to +42
ApiKeyProvider(
discovery=self.discovery,
validate_credentials=settings.validate_credentials,
validation_timeout=settings.validation_timeout,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Wire live validation into the OAuth providers

When CREDENTIAL_CHECK_ENABLED is enabled, this setting and its timeout are passed only to ApiKeyProvider; GoogleProvider and GenericOAuthProvider never invoke the newly added check_oauth_token or check_connection functions. A revoked OAuth access token whose stored expiry is still in the future therefore remains reported as active despite live credential validation being enabled; dispatch the OAuth connections through the validator as well.

Useful? React with 👍 / 👎.

Comment thread src/nine_rtksync/prefs.py
Comment on lines +26 to +28
def _connect(path: str) -> sqlite3.Connection:
conn = sqlite3.connect(path, timeout=10.0)
conn.execute(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict the preferences database permissions

When ui_prefs.sqlite is first created under the usual 022 umask, this plain sqlite3.connect leaves it mode 0644, even though the new database stores the dashboard username and auth.password_hash. Any other local user or sidecar that can read the mounted directory can copy the verifier for offline password guessing; create the file with mode 0600 and also tighten permissions on an existing database.

Useful? React with 👍 / 👎.

Comment thread src/nine_rtksync/auth.py
Comment on lines +145 to +147
ok_user = set_preference(prefs_path, AUTH_USER_KEY, user)
ok_pass = set_preference(prefs_path, AUTH_PASSWORD_KEY, hash_password(password))
return bool(ok_user and ok_pass)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Save the username and password hash atomically

Credential rotation currently commits auth.user and auth.password_hash in two independent SQLite transactions. If the process stops or the second write fails after the first commit, the method returns failure but leaves a hybrid credential consisting of the new username and the old password, so neither the previously documented pair nor the requested new pair works; write both values through one connection and transaction.

Useful? React with 👍 / 👎.

Duas correcoes encontradas validando os containers vivos.

1. auth_from_env vinha de bool(env_user or env_pass). O docker-compose de
   exemplo define DASHBOARD_USER=admin e deixa DASHBOARD_PASSWORD vazia, entao
   o nome de usuario sozinho ja marcava a autenticacao como autoritativa do
   ambiente. Consequencia: o painel respondia "Credenciais definidas por
   variavel de ambiente" em toda tentativa de definir senha, a instalacao
   ficava presa na credencial de recuperacao e o aviso de seguranca nunca
   sumia, porque nunca havia senha gravada no SQLite. Quem decide e a senha:
   nome de usuario sozinho nao e credencial.

2. Os cabecalhos de seguranca eram emitidos num unico ponto, o da pagina do
   painel. O corpo do 401 -- que e o que o navegador exibe quando se aperta ESC
   no dialogo do Basic Auth -- e a pagina de credenciais atualizadas saiam sem
   nenhum deles, e nao havia Content-Security-Policy em resposta alguma.
   Passaram para end_headers(), entao valem em toda resposta, com uma CSP
   restrita ao que a pagina realmente carrega: jsDelivr para Bootstrap e
   icones, Google para as fontes, connect-src proprio, sem frame e sem
   base-uri.

Cobertura nova para os dois casos, incluindo a forma exata que o
docker-compose.example.yml produz e a ausencia de cabecalho duplicado.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 620b702e47

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

"""
base_url = str(self.base_url or "").lower()
if base_url:
return any(host in base_url for host in self.LOCAL_HOSTS)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Recognize container-network endpoints as local

When a self-hosted Ollama/vLLM connection uses a Docker service hostname or private-network address such as http://ollama:11434, this allowlist returns false because the URL contains none of the listed literals. The connection is consequently handled by ApiKeyProvider; for provider ollama, select_probe() ignores its baseUrl and sends the facade API key to https://ollama.com/v1/chat/completions, both leaking it to the wrong service and preventing local model discovery. Classify non-cloud self-hosted endpoints without restricting them to localhost spellings.

Useful? React with 👍 / 👎.

# This used to stamp testStatus = "ok" unconditionally, which is why the
# panel showed every API key as healthy: nothing had ever been verified,
# and a revoked key stayed green until a real request failed.
if self.validate_credentials:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear probe state when live validation is disabled

When an operator changes CREDENTIAL_CHECK_ENABLED from 1 to 0, this branch simply stops probing and leaves the previously persisted credentialState untouched. ConnectionRecord.health_status continues to prioritize that stale value, so connections remain displayed as invalid, unreachable, or active indefinitely rather than falling back to not_checked as documented in docs/wiki/Configuration.md; remove or disregard prior probe metadata when validation is disabled.

Useful? React with 👍 / 👎.

Comment on lines +95 to +96
docker logs 9rtksync 2>&1 | grep "Recovery hash"
docker exec 9rtksync cat /app/data/.dashboard_recovery

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Correct the recovery credential retrieval commands

On a fresh installation with no dashboard password, both documented recovery commands fail: the implementation logs Recovery credential generated, not Recovery hash, and it stores the file beside the default database at /app/data/db/.dashboard_recovery, not /app/data/.dashboard_recovery. Operators following this section therefore cannot retrieve the only first-login credential; update the grep text and file path to match cli.py and Settings.get_recovery_file_path().

Useful? React with 👍 / 👎.

Comment on lines +93 to +94
if remaining is None:
return translate("reason.no_expiry", lang)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match the no-expiry diagnosis to provider behavior

For a generic OAuth connection whose expiresAt is missing or unreadable, this tells the operator that the token will be renewed on the next sweep. GenericOAuthProvider.check_and_refresh() does the opposite for the same remaining_seconds is None condition: it treats the token as long-lived and returns without attempting renewal. The dashboard therefore promises recovery that will never occur for Claude, GitHub, Codex, and other generic OAuth providers; make this diagnosis provider-specific or align the provider behavior.

Useful? React with 👍 / 👎.

Comment on lines +168 to 169
if self.dashboard_auth_from_env:
return False

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Report env-managed password changes as conflicts

When DASHBOARD_PASSWORD makes authentication environment-managed, this returns the same False used for storage failures. The retained /api/change-password handler consequently responds with HTTP 500, although this is an expected immutable-configuration conflict and the new documentation explicitly promises 409 Conflict; API clients can misclassify it as a transient server failure and retry. Have the handler distinguish this mode before calling the boolean writer, as the server-side form action already does.

Useful? React with 👍 / 👎.

auth_from_env = False
refresh_margin = 900
if self.settings:
current_user, _ = self.settings.get_auth_credentials()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Display the identity authenticated by the request

The footer and /api/status.currentUser derive the displayed identity from configured credentials rather than the Basic Auth header that authenticated this request. A break-glass login as admin therefore appears as the stored operator account, and after a restart a stored SQLite username such as operator authenticates successfully while this method falls back to and displays admin because get_auth_credentials() never reads the new SQLite store. Preserve the authenticated username for the request instead of presenting configuration state as “Signed in as.”

Useful? React with 👍 / 👎.

…no startup

Rodada a partir dos apontamentos da revisao automatica dos PRs.

DESPACHO
Uma conexao local carrega uma chave de fachada, e o handler generico de chave
respondia `can_handle` para ela. Como esse handler e consultado antes do
handler local, o laco dava break e o catalogo local nunca era descoberto: era
por isso que o Ollama local aparecia sem modelo nenhum no painel. O handler de
chave passa a recusar explicitamente conexao local, e a classificacao de "local"
virou um unico ponto compartilhado.

CONTAGEM E PERSISTENCIA
Gravar e contar eram a mesma decisao. Carimbar o horario de uma verificacao
marcava a conexao como renovada, e o ciclo anunciava "N renovadas" para chaves
que ninguem trocou. Agora o motor grava sempre que o provider devolve dado e so
conta quando a credencial mudou de fato. No OmniRoute o resultado da sondagem
era devolvido pelo provider e simplesmente descartado pelo motor -- o painel
recarregava a linha antiga e uma chave recusada continuava verde na tela. Entra
update_connection_health(), que grava test_status, last_tested, last_error e o
catalogo local sem tocar em token nenhum, escrevendo apenas em coluna existente.

VALIDACAO
5xx do provedor era classificado como credencial valida; virou indisponivel,
porque um erro do fornecedor nao prova nada sobre a chave. Um baseUrl declarado
na conexao passa a vencer o casamento por substring do nome: "azure-openai" casa
com "openai" e um "anthropic" atras de proxy tambem casa, e a chave do cliente
saia daqui para o endereco publico do fornecedor. Catalogo vazio deixou de ser
queda: instalacao nova, sem modelo baixado, esta no ar.

CORRIDA E ORDEM DE INICIALIZACAO
O botao da tela e o cron chamam a mesma instancia do motor, e o servidor atende
em threads: duas renovacoes OAuth concorrentes podiam sobrescrever um token
recem-rotacionado. sync_all() passa a ser serializado. As opcoes de linha de
comando sao aplicadas antes de resolver estado persistente -- com --db-path a
credencial de recuperacao nascia ao lado do banco antigo e a autenticacao a
procurava ao lado do novo -- e --interval alcanca o agendador, que le
cron_interval.

ARMAZENAMENTO NO STARTUP
Quando o diretorio do DB_PATH ainda nao existia, o caminho caia para $HOME e o
banco de preferencias, onde mora a senha do painel, era gravado fora do volume:
a senha sumia ao recriar o container. O diretorio passa a ser criado.

OUTROS
O --help anunciava uma senha padrao que nao existe mais. A linha crua do banco
deixou de ser devolvida na projecao de conexoes (ela carregava access_token,
refresh_token e api_key juntos); o que o codigo realmente usava era o endereco
base, agora projetado explicitamente. O compose de exemplo do OmniRoute exigia
service_healthy de um servico sem healthcheck, entao a stack anunciada nao
subia. Estado do banco virou bandeira explicita: o resumo textual nunca esta
vazio, e converte-lo em booleano fazia a tela declarar tudo operacional
justamente quando o arquivo sumia. Nivel de log passa a seguir o prefixo, senao
LOG_LEVEL=WARNING escondia toda falha. Arquivo de credencial malformado deixou
de levantar AttributeError. Os .gitignore ganharam, ao final e sem alterar o que
ja havia, cobertura para banco, log e credencial de execucao.
Comment thread tests/test_auth_recovery.py Fixed
Comment thread tests/test_provider_dispatch.py Fixed
Comment thread tests/test_startup_storage.py Fixed
Varias sessoes na mesma conta nao sao o problema; o problema e todas as contas
de um gateway sairem pelo mesmo endereco. Os dois gateways ja modelam a solucao,
e o painel nao mostrava nada disso.

O que foi lido das imagens em execucao:

OmniRoute guarda os enderecos em proxy_registry, o vinculo em proxy_assignments
(proxy_id, scope, scope_id, position) e a rotacao em proxy_scope_rotation
(strategy, cursor, sticky_window_minutes). Os escopos no codigo sao global,
provider e account, e a propria linha da conexao tem proxy_enabled e
per_key_proxy_enabled. O seletor devolve o proxy direto quando o escopo resolve
para exatamente um -- entao um unico proxy em scope=account fixa a saida
daquela conta, sem rotacao nenhuma. Quando ha rotacao, a janela padrao gravada e
de 30 minutos.

9Router guarda os pools em proxyPools e o vinculo dentro da conexao, em
providerSpecificData: proxyPoolId mais o interruptor connectionProxyEnabled.

O que entra aqui: leitura, nunca escrita. A listagem de conexoes resolve, em uma
consulta, qual saida o gateway usaria para cada conta, e o registro passa a
expor "vinculada", "compartilhada" ou "desconhecida". Instalacao sem as tabelas
de proxy continua listando normalmente. Nada cria, altera ou apaga proxy,
vinculo ou estrategia: quem manda no roteamento e o gateway, o unico que
consegue aplica-lo a uma requisicao.

docs/Egress-And-Multi-Session.md, em ingles com versao em portugues, registra o
schema, a consulta SQL que mostra quais contas compartilham endereco, o passo a
passo pelas telas do gateway e o que o Tailscale resolve e o que nao resolve: um
exit node da um endereco estavel ao HOST, nao por conta -- separar contas ainda
depende do vinculo por conta. Nenhuma afirmacao sobre politica de fornecedor,
que nao temos como verificar.

Tambem nesta rodada: DATA_DIR, GOOGLE_CLIENT_ID e GOOGLE_CLIENT_SECRET passam a
constar do .env.example, e um teste compara as variaveis que o codigo le com as
que o exemplo documenta, falhando com a diferenca nomeada -- a defasagem que o
usuario encontrou no arquivo agora aparece no CI. O guia de instalacao do
OminiRTkSync apontava para um script removido; passa a apontar para o Makefile.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

defasagem aparece no CI em vez de aparecer em producao.
"""

import os

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0cb959db82

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +127 to +130
return (
self.data.get("lastRefreshAt")
or self.data.get("credentialCheckedAt")
or self.data.get("lastTested")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist the actual renewal timestamp

Neither GoogleProvider nor GenericOAuthProvider writes lastRefreshAt when it successfully replaces an access token, while the fallback fields here are probe timestamps rather than renewal timestamps. Consequently an OAuth token renewed by this daemon is still displayed as “Never renewed,” whereas an API-key validation or local health check can be displayed as a renewal; stamp lastRefreshAt on successful credential changes and do not substitute check timestamps in the “Last renewal” column.

Useful? React with 👍 / 👎.

Comment thread src/nine_rtksync/providers/local.py Outdated
Comment on lines +108 to +110
if models and data.get("discoveredModels") != models:
data["discoveredModels"] = models
modified = True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear models after a successful empty catalog probe

When an instance previously exposed models and later removes all of them, a successful empty response skips this assignment, and if testStatus is already active the method returns no updated data at all. The old discoveredModels therefore remains persisted and the dashboard continues claiming that the instance serves models that no longer exist; persist an empty list when the successful catalog result differs from the stored list.

Useful? React with 👍 / 👎.

Comment on lines +156 to +158
if self.is_local:
# A local instance is only healthy when its model catalog answered.
return "unknown" if self.data.get("testStatus") == "unreachable" else "active"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require a successful local probe before reporting active

For a newly created local connection with no testStatus—especially when CRON_ENABLED=0 prevents the startup probe—or one carrying any status other than the single unreachable literal, this branch reports active despite no catalog ever answering. This recreates the blind-health behavior the local probe is intended to remove; return an unchecked/unknown state unless testStatus explicitly records a successful probe.

Useful? React with 👍 / 👎.

Comment on lines +162 to +164
summary.setdefault("errors", []).append(
f"[{conn.provider} · {conn.name}] {e}"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mark summaries containing provider errors as failed

When a provider throws, the current handler now captures the exception only in summary["errors"]; CronScheduler._execute_cycle() still derives success from res.get("success", True) and its error field from singular res["error"], so the resulting history entry remains successful and green. Fresh evidence in the current tree is this newly added errors list without a corresponding success=False or top-level error; propagate that failure state into the summary.

Useful? React with 👍 / 👎.

Comment thread src/nine_rtksync/web/server.py Outdated
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net "
"https://fonts.googleapis.com; "
"font-src 'self' https://cdn.jsdelivr.net https://fonts.gstatic.com data:; "
"img-src 'self' data:; "

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow the flag asset host in the image policy

In browsers enforcing this CSP, the language flags do not render: flag-icons is loaded from jsDelivr and its CSS references the flag SVG files on that same CDN, but img-src permits only same-origin and data URLs. The selector therefore contains blank flag spans even though choosing languages is presented as a flag-based UI; allow the jsDelivr origin for images or serve the SVG assets locally.

Useful? React with 👍 / 👎.

Comment thread src/nine_rtksync/credential_check.py Outdated
Comment on lines +211 to +212
if spec is not None and base_url and not _same_host(base_url, spec.url):
spec = ProbeSpec(base_url.rstrip("/") + "/models")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve provider-specific auth headers for proxy probes

When an Anthropic or Gemini connection declares a custom proxy baseUrl, this replacement discards the selected provider's authentication metadata and creates a default Bearer-token probe. A valid Anthropic key is therefore sent as Authorization: Bearer instead of x-api-key, and a Gemini key loses x-goog-api-key; with live validation enabled by default, those proxies can reject the request and the synchronizer persists a false invalid state. Replace only the probe URL while preserving the selected header template and invalid-status rules.

Useful? React with 👍 / 👎.

… trava vencida

Segunda rodada dos apontamentos da revisao automatica.

AUTENTICACAO DO FORNECEDOR
Quando a conexao declara um proxy proprio, a sonda era substituida inteira e
perdia o jeito de autenticar do fornecedor. A Anthropic espera x-api-key e o
Gemini x-goog-api-key; trocar por um Bearer generico fazia o proxy recusar uma
chave perfeitamente valida. Agora so o endereco muda.

OLLAMA HOSPEDADO
O marcador "ollama" tambem casa com a conta em https://ollama.com/v1, e ela era
classificada como local. O sincronizador ia sondar um catalogo que nao existe
ali e a conexao saia do caminho de validacao de chave. Endereco declarado passa
a decidir sozinho; o nome so vale quando nao ha endereco.

BANDEIRAS INVISIVEIS
img-src permitia apenas a propria origem, mas as bandeiras do seletor de idioma
sao SVG que o CSS do flag-icons busca no mesmo CDN. Em navegador que aplica a
politica elas simplesmente nao apareciam, sem erro visivel na tela.

TRAVA DE RATE LIMIT VENCIDA
O motor inferia "limpar a trava" da ausencia do campo no dicionario, mas o
provider ja havia removido a trava vencida dali: a condicao invertia o sentido e
preservava justamente a trava que devia sair.

CICLO COM ERRO
Excecao de provider entrava no resumo, e o agendador continuava lendo o ciclo
como bem-sucedido porque derivava isso de success, que ninguem atualizava.

CATALOGO QUE ESVAZIOU
Uma instancia que tinha modelos e passou a nao ter mantinha os antigos na tela:
a gravacao so acontecia quando havia modelo novo.

COMPOSE
CREDENTIAL_CHECK_ENABLED e CREDENTIAL_CHECK_TIMEOUT eram anunciadas no
.env.example e lidas pelo Settings, mas nao chegavam ao container.

Mais os apontamentos de qualidade: unittest importado das duas formas em 11
arquivos de teste, e import sem uso.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

Ate agora a validacao local usava a stack de um artigo, que existe para outro
proposito e pode mudar por razoes que nada tem a ver com este codigo.

docker-compose.test.yml sobe o gateway real mais este sincronizador, com nome de
projeto, nomes de container e portas proprios, para conviver com qualquer outra
stack na mesma maquina. Tudo preso em 127.0.0.1: o painel le credencial, e nada
disso vai para a rede. As variaveis de segredo do gateway sao obrigatorias e sem
valor padrao, entao o compose recusa subir sem elas. A validacao viva de
credenciais vem desligada aqui, porque teste nao deve sair para a internet.

Validado de pe: gateway saudavel, sincronizador saudavel, /healthz em 200, raiz
em 401 sem credencial, zero traceback e zero vazamento no log.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@elielsousa-pathbit
elielsousa-pathbit merged commit 5b1a923 into master Sep 12, 2026
5 checks passed
@elielsousa-pathbit
elielsousa-pathbit deleted the feat/env-only-configuration branch September 12, 2026 20:55
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.

1 participant