Skip to content

fix: interop com OmniRoute (expires_at ISO, testStatus active) + painel server-side, i18n e log persistente - #1

Merged
elielsousa-pathbit merged 18 commits into
masterfrom
fix/expires-at-iso-and-test-status
Sep 12, 2026
Merged

elielsousa-pathbit merged 18 commits into
masterfrom
fix/expires-at-iso-and-test-status

Conversation

@elielsousa-pathbit

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

Copy link
Copy Markdown
Contributor

Descrição das Alterações

Dois blocos: uma correção de interoperabilidade com o OmniRoute e a evolução do painel.

1. Correção — o gateway nunca renovava as conexões que sincronizamos

update_connection() gravava expires_at como epoch numérico em texto (str(expires_at_ms)) numa coluna TEXT. O OmniRoute lê esse campo com new Date(...) em src/lib/tokenHealthCheck.ts, e new Date("1789999999000") é um Invalid Date:

NaN -> getEffectiveTokenExpiryMs() devolve 0 -> hasKnownExpiry = false
   -> isAboutToExpire = false
   -> para provedores rotativos (codex, claude, kiro...) shouldRefreshByInterval também é false
   -> `if (!isAboutToExpire && !shouldRefreshByInterval) return;`  ← a conexão nunca é renovada

Também gravava test_status = 'ok', valor que o OmniRoute não reconhece — só 'active' conta como saudável (clearAccountError em src/sse/services/auth.ts e as checagens em tokenHealthCheck.ts), então a conexão aparecia como estando em erro.

Agora grava ISO-8601 UTC e 'active', os formatos nativos do gateway. No schema JSON (9Router) o expiresAt continua numérico, que é o que aquele gateway espera.

O fix correspondente do lado do gateway foi enviado em diegosouzapw/OmniRoute#13444.

2. Servidor web — corrige o BrokenPipeError de produção

File "/app/src/omini_rtksync/web.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 do docstring 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) desistia e fechava o socket antes da resposta.

  • ThreadingHTTPServer + handle_error que engole desconexão do cliente (erros reais seguem chegando ao handler padrão).
  • write_body() tolera o cliente ter fechado a conexão.
  • A sondagem ao gateway ganha cache de 30s.

3. Render server-side, i18n e diagnóstico

  • O HTML passa a ser montado em 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: *; 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.
  • Cada conexão mostra por que foi ou não renovada, e o cron guarda o log de cada ciclo com botão de detalhe; um ciclo que falhou aparece marcado em vermelho.
  • Instâncias locais (Ollama/vLLM/LM Studio) deixam de ser rotuladas como provedor de nuvem por causa da chave de fachada: aparecem como Local, com a baseUrl e os modelos servidos.

4. Log persistente, credencial de recuperação, configuração e portas

  • 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.
  • auth.py: credenciais salvas mandam; sem nada salvo valem as de fábrica; e admin + hash de recuperação entra sempre. 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.
  • Porta interna 9090 (igual no 9RTKSync), publicada em 9092 no host, com bind em 127.0.0.1. Container renomeado para ominirtksync, para não colidir com o 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 21).
  • Sem conflitos com a branch master (reconciliado com 4c4a356).
$ PYTHONPATH=src python3 -m unittest discover -s tests -p "test_*.py"
Ran 96 tests in 2.982s

OK

Arquivos de teste novos: test_logs.py, test_auth_recovery.py, test_web_render.py, test_web_resilience.py, test_config_env.py, além de casos novos em test_database.py.


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.

O sincronizador gravava expires_at como epoch numerico em texto
(str(expires_at_ms)) numa coluna TEXT. O OmniRoute le esse campo com
new Date(...) em src/lib/tokenHealthCheck.ts, e "1789999999000" e um Invalid
Date -> NaN -> getEffectiveTokenExpiryMs devolve 0 -> hasKnownExpiry falso ->
a renovacao preventiva do gateway nunca dispara para aquela conexao.

Tambem gravava test_status = 'ok'. O OmniRoute so reconhece 'active' como
saudavel (clearAccountError em src/sse/services/auth.ts e as checagens em
tokenHealthCheck.ts), entao a conexao aparecia como estando em erro.

Passa a gravar ISO-8601 UTC e 'active' - os mesmos formatos que o gateway usa
nativamente. No schema JSON (9Router) expiresAt continua numerico, que e o
formato que aquele gateway espera.

O fix correspondente do lado do gateway foi enviado em
diegosouzapw/OmniRoute#13444.
…cuperacao

Espelha no OminiRTKSync a mesma evolucao aplicada ao projeto irmao 9RTKSync.

Servidor web
- ThreadingHTTPServer no lugar do HTTPServer de uma thread so, e handle_error
  passa a engolir desconexao do cliente em vez de imprimir traceback. Era a
  origem do "BrokenPipeError: [Errno 32] Broken pipe" em serve_healthz: o probe
  do Docker desistia enquanto o /healthz fazia uma chamada HTTP de saida de ate
  3s ao gateway, com o servidor bloqueado numa unica thread.
- A sondagem ao gateway ganha cache de 30s.
- Removido o Access-Control-Allow-Origin curinga; adicionados Cache-Control
  no-store, X-Frame-Options, X-Content-Type-Options e Referrer-Policy.

Render server-side
- O HTML e montado em render.py com os dados ja embutidos; o navegador nao
  consulta mais /api/status para desenhar a tela e o SQLite fica do lado do
  servidor. Acoes viram POST-Redirect-GET (/acoes/*).
- Bootstrap 5 + Bootstrap Icons + flag-icons + jQuery no lugar dos emojis.
- Idioma padrao ingles, com portugues e espanhol no seletor de bandeiras,
  persistido em SQLite proprio (prefs.py) - nunca no banco do gateway.
- Cada conexao mostra por que foi ou nao renovada, e o cron guarda o log de
  cada ciclo com botao de detalhe; ciclo com falha aparece marcado.
- models.py embrulha as linhas relacionais do OmniRoute na interface que o
  render consome, reconhecendo instancias locais (Ollama/vLLM/LM Studio) que
  antes eram rotuladas como provedor de nuvem por causa da chave de fachada.

Log persistente
- logs.py: arquivo rotativo diario, retencao configuravel por
  LOG_RETENTION_DAYS (padrao 30 dias) e expurgo dos vencidos no boot.
  LOG_DIR, LOG_LEVEL e LOG_TO_STDOUT completam o contrato.

Autenticacao
- auth.py: credenciais salvas mandam; sem nada salvo valem as de fabrica; e
  'admin' com o hash de recuperacao entra sempre. Comparacoes em tempo
  constante. O hash vem de DASHBOARD_RECOVERY_HASH ou e gerado no primeiro
  boot, salvo com permissao 0600 e registrado uma vez no log.

Configuracao e portas
- DASHBOARD_USER/DASHBOARD_PASSWORD explicitas vencem o arquivo da tela.
- Novas CRON_INTERVAL e CRON_ENABLED.
- Porta interna padronizada em 9090 (igual no 9RTKSync); o host publica 9092.
  Container renomeado para ominirtksync, para nao colidir com o irmao.

Testes: 21 -> 96 passando.

@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: 38d7be9808

ℹ️ 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 thread src/omini_rtksync/cli.py
Comment on lines 213 to +215
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 --interval to the scheduler

When the daemon is started with --interval N, main() only updates settings.sync_interval, but the scheduler now reads settings.cron_interval; the latter keeps its environment/default value. Thus ominirtksync --daemon --interval 17 still schedules at 300 seconds unless CRON_INTERVAL is also set, making the documented CLI override a no-op for the only daemon loop.

Useful? React with 👍 / 👎.

Comment thread src/omini_rtksync/cli.py Outdated
Comment on lines +268 to +270
# Credencial de emergencia: gerada uma unica vez e registrada no log, para o
# operador conseguir voltar ao painel caso esqueca a senha trocada pela tela.
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 Initialize recovery storage after applying --db-path

For a local invocation using --db-path without DB_PATH/DATA_DIR, this generates and logs the recovery credential using the initial fallback directory (typically $HOME), then line 279 changes db_path; subsequent authentication resolves the recovery file beside the selected database and cannot find the generated value. The advertised break-glass password therefore fails precisely for the common --db-path setup unless the recovery hash is explicitly supplied in the environment.

Useful? React with 👍 / 👎.

Comment thread src/omini_rtksync/auth.py
Comment on lines +48 to +51
with open(auth_file, "r", encoding="utf-8") as f:
data = json.load(f)
user = data.get("user")
password = data.get("password")

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 non-object credential files as invalid

A syntactically valid but malformed credential file such as [] or "text" reaches this .get call and raises AttributeError, which is not caught here. Settings.verify_credentials() then propagates the error (and the HTTP handler rejects the request), so neither the configured credentials nor the advertised recovery credential can log in; the previous reader caught this malformed-file case and fell back safely.

Useful? React with 👍 / 👎.

Comment thread README.md Outdated
| `SYNC_INTERVAL` | `300` | Intervalo em segundos entre varreduras no modo daemon e cron |
| `REFRESH_MARGIN` | `900` | Margem prévia em segundos para renovação de tokens |
| `ENABLE_WEB_DASHBOARD` | `1` | Ativa o dashboard web embutido (`1` para sim, `0` para não) |
| `WEB_PORT` | `9191` | Porta do dashboard web HTTP |

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 Update the documented default dashboard port

The table still declares WEB_PORT defaults to 9191, while the new runtime default and Docker image listen on 9090. A local/default deployment that follows the dashboard URL guidance will therefore try port 9191 and fail to reach the service unless the user independently discovers the changed port.

Useful? React with 👍 / 👎.

Comment thread src/omini_rtksync/web.py
Comment on lines +524 to +527
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 from cross-site form posts

When a browser has cached dashboard Basic credentials, a third-party page can submit a simple cross-site form to this newly added endpoint; there is no Origin/Referer or CSRF-token validation before the supplied user and password are persisted. Visiting such a page can therefore replace the dashboard credentials and lock the operator out (especially where no working recovery credential is configured).

Useful? React with 👍 / 👎.

Comment thread src/omini_rtksync/cli.py Outdated
Comment on lines +20 to +22
def log_msg(prefix: str, text: str):
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"[{ts}] [{prefix}] {text}", flush=True)
"""Registra um evento no log persistente (e no stdout, se LOG_TO_STDOUT permitir)."""
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 Preserve failure logs at warning/error thresholds

All engine events are emitted at INFO, including the OAuth refresh failure path that calls log_msg("FALHA", ...). Consequently, deployments using the newly documented LOG_LEVEL=WARNING or ERROR suppress those failures entirely instead of retaining the events that motivated the persistent log feature.

Useful? React with 👍 / 👎.

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.
…ndo ela cai

O LocalProvider so devolvia 'Conexao local operacional', sem nunca falar com a
instancia. Um Ollama local fora do ar continuava aparecendo como saudavel, e os
modelos que ele serve nunca chegavam ao painel.

- can_handle passa a reconhecer ollama/vllm/lmstudio/localai/llamacpp e qualquer
  baseUrl apontando para o host. Uma instancia local costuma usar chave de
  fachada, e por isso caia na classificacao de provedor de nuvem.
- discover_models consulta /api/tags e /v1/models e grava discoveredModels.
- Sem resposta do catalogo a conexao vira 'unreachable' em vez de 'active'.
- Erro de conexao encerra a sondagem no primeiro endpoint: tentar os tres com
  nada escutando so multiplica o timeout por 3 a cada varredura.

Testes: 96 -> 106 (tests/test_local_provider.py e novo).

@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: 08bdc01159

ℹ️ 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 +295 to +300
if models:
return (
True,
{"discoveredModels": models, "testStatus": "active"},
[f"Local instance answered with {len(models)} model(s): {', '.join(models[:5])}"],
)

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 local probe results before rendering dashboard

When a local provider answers (or fails to answer), this returns discoveredModels/testStatus, but OmniSyncEngine.sync_all() discards both returned values in its local-provider branch and performs no database update. The dashboard subsequently reloads only database rows, so local instances never show their discovered models and an unreachable instance continues to receive the healthy badge rather than the intended Unknown state.

Useful? React with 👍 / 👎.

Comment thread src/omini_rtksync/cron.py
Comment on lines +24 to +26
for detail in res.get("details", []) or []:
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 Populate scheduler history with engine actions

For the daemon's OmniSyncEngine.sync_all callback, the result contains only success, total, and refreshed; it never supplies the details collection this new loop consumes. Consequently every successful scheduled run records an empty log, and the new Logs modal renders “No cycle has run yet” inside entries for cycles that did run, hiding renewals and provider outcomes that the feature is meant to expose.

Useful? React with 👍 / 👎.

…al e fecha CSRF

Espelha no OminiRTKSync as mesmas correcoes do projeto irmao 9RTKSync.

O painel declarava toda conexao "operacional e ativa" apenas por existir uma
chave, sem nunca perguntar nada ao provedor: uma chave revogada seguia verde
ate uma requisicao real falhar.

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: /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.
- Desligada por padrao no construtor. O teste do engine e o modulo da CLI
  passam a desliga-la explicitamente: sem isso o ciclo chamava a API do
  provedor de verdade e a suite passava a depender da internet, que foi
  exatamente como a CI quebrou antes.

Deteccao de instancia local:
- baseUrl mora em providerSpecificData, nao na raiz. Lendo so a raiz, nenhuma
  instancia local exibia seus modelos.
- Classificacao pelo endereco, nao pelo nome: "ollama" tambem e o nome do
  Ollama Cloud, que era tratado como local e sondado em /api/tags.
- ConnectionRecord passa a expor access_token e refresh_token, que faltavam.

Seguranca do painel:
- POST de outra origem recusado: o Basic Auth e anexado pelo navegador mesmo
  em formulario de outro site, e urlencoded nao dispara preflight.
- /api/status devolvia accessToken, refreshToken, apiKey e a linha bruta do
  banco; passa a projetar apenas os campos que a tela consome.

Interface: Google Fonts, linha de diagnostico do gateway alinhada a direita
como as demais e badges/traducoes (en/pt/es) para os estados novos.
@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.

…or teste

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

- O import de HTTPServer corrigido no master foi mantido, e a segunda
  definicao de QuietThreadingHTTPServer, que sobrescrevia a primeira em
  silencio, foi removida junto com o "import sys" duplicado.
- A conversao de expires_at para ISO permanece na funcao to_iso_utc, que e
  a versao documentada e coberta por teste; o master havia reimplementado a
  mesma conversao inline, sem teste.
@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.

A normalizacao acontecia so junto de uma renovacao bem-sucedida. Quando a
renovacao falha -- refresh token revogado, client_id ausente -- o epoch
numerico gravado como texto permanecia, e e justamente ele que o OmniRoute le
com new Date(...) e obtem Invalid Date, concluindo que a conexao nao tem
validade conhecida e desligando a propria renovacao preventiva. Ou seja: o
caso em que a cura mais importa era exatamente o caso em que ela nao ocorria.

normalize_expiry_format regrava expires_at em ISO-8601 sem tocar nos tokens, e
roda antes de qualquer tentativa de renovacao.

Verificado contra um OmniRoute real subido localmente: um expires_at gravado
como "1789226422362" e Invalid Date no Node; depois do ciclo vira
"2026-09-12T15:20:22.362Z", o gateway passa a le-lo e marca a conexao como
expirada por conta propria -- exatamente o comportamento que estava desligado.

O compose de exemplo passa a esperar service_healthy do OmniRoute: ele cria o
storage.sqlite durante o proprio boot, e subir antes disso fazia o primeiro
ciclo encontrar o banco ausente.

Documentada a criptografia em repouso do gateway: escrever texto puro e seguro
porque decrypt() devolve inalterado o que nao tem o prefixo enc:v1:.
@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/omini_rtksync/auth.py Fixed
Comment thread src/omini_rtksync/auth.py Fixed
Comment thread src/omini_rtksync/config.py Fixed
Comment thread src/omini_rtksync/render.py Fixed
Comment thread tests/test_auth_recovery.py Fixed
Comment thread tests/test_local_provider.py Fixed
Comment thread tests/test_logs.py Fixed
Comment thread tests/test_credential_check.py Fixed
Comment thread tests/test_credential_check.py Fixed
Comment thread tests/test_web_render.py Fixed

@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: 06b4dd7421

ℹ️ 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 +235 to +236
res.update(result.to_dict())
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 Persist API-key validation outcomes

With credential checking enabled by default, this stores credentialState and the updated testStatus only in the transient res dictionary. OmniSyncEngine.sync_all() never writes the returned API-key data to SQLite, so the next dashboard render reloads the old row and cannot show that a key was rejected; it also counts every probe as a refresh because modified is always set. Persist the probe metadata/status and reserve the refreshed counter for an actual credential update.

Useful? React with 👍 / 👎.

Comment thread src/omini_rtksync/cli.py
Comment on lines +39 to +42
self.api_provider = 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 Run the live check for Google OAuth tokens

The validation settings are wired only into ApiKeyProvider, while Google/Antigravity OAuth connections are consumed earlier by the dedicated Google branch and never call the newly implemented check_oauth_token(). A revoked Google access token whose stored expiry is still in the future therefore remains reported as active, despite CREDENTIAL_CHECK_ENABLED=1; integrate and persist the Google token probe in that branch.

Useful? React with 👍 / 👎.

Comment on lines 43 to +44
- DASHBOARD_USER=${DASHBOARD_USER:-admin}
- DASHBOARD_PASSWORD=${DASHBOARD_PASSWORD:-pathbit}
- DASHBOARD_PASSWORD=${DASHBOARD_PASSWORD:-}

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 Leave dashboard credentials screen-managed by default

In the example Compose deployment, DASHBOARD_USER is always exported as admin, even when the operator configured neither credential. Settings.from_env() treats either variable as authoritative, so after signing in with the generated recovery value the dashboard enters headless mode and refuses/hides password changes, contradicting the documented first-sign-in flow of setting a password on screen. Do not export these variables by default, or only enable headless mode when a nonempty password is supplied.

Useful? React with 👍 / 👎.

Comment thread src/omini_rtksync/cli.py Outdated
Comment on lines +77 to +79
raw_expiry = str(c.get("expiresAt") or "")
if exp_ms and raw_expiry.isdigit():
if normalize_expiry_format(self.settings.db_path, cid, exp_ms):

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 Heal numeric expiries in every OAuth branch

The new format-repair block runs only inside the antigravity/gemini-cli branch. A legacy numeric-text expires_at belonging to Claude, Codex, GitHub, or Kiro proceeds directly to GenericOAuthProvider; if its refresh cannot complete because client credentials are absent or the refresh token is rejected, the malformed value remains an Invalid Date to OmniRoute indefinitely. Apply the format-only repair before provider dispatch so it does not depend on provider type or refresh success.

Useful? React with 👍 / 👎.

Comment on lines +175 to +179
matches = [marker for marker in API_KEY_PROBES if marker in name]
if not matches:
return None
# Sort by length then alphabetically so the choice never depends on dict order.
return API_KEY_PROBES[sorted(matches, key=lambda m: (-len(m), m))[0]]

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 Honor custom base URLs before matching vendor probes

With live validation enabled by default, substring matching sends the stored key to a public vendor endpoint even when the connection declares a different baseUrl. For example, an azure-openai or proxied anthropic connection is matched as OpenAI/Anthropic and its Azure/proxy credential is transmitted to api.openai.com or api.anthropic.com instead of its configured service. Prefer an explicitly configured endpoint, or require an exact provider identity before sending credentials to a vendor URL.

Useful? React with 👍 / 👎.

Comment on lines +454 to +458
online = bool(gateway.get("online"))
tone = "text-success" if online else "text-danger"
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 actual status flag

The renderer treats any nonempty dbSummary as a healthy database, but collect_dashboard_state() always supplies a nonempty string, including "Banco nao encontrado". When the gateway responds but the SQLite file is missing, the card therefore says the gateway and database are fully operational and renders the diagnosis in green. Pass a real database-health boolean and use it for both the diagnosis and presentation.

Useful? React with 👍 / 👎.

Comment thread docs/wiki/Installation.md Outdated
Comment on lines +128 to +131
Or with no local install at all:

```bash
./run_tests.sh

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 Replace the deleted test runner in the installation guide

The newly added installation guide tells users without a local installation to execute ./run_tests.sh, but this same commit deletes that script and repo-wide search finds no replacement at that path. Following the documented container-based test path therefore immediately fails with “No such file or directory”; point the guide to an existing Make target or include the equivalent Docker command.

Useful? React with 👍 / 👎.

Comment on lines +125 to +129
return (
self.data.get("lastRefreshAt")
or self.data.get("credentialCheckedAt")
or self.data.get("lastTested")
or None

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 a timestamp that the renewal column can read

The new “Last renewal” column reads only lastRefreshAt, credentialCheckedAt, or lastTested, but no successful OAuth update writes lastRefreshAt, and get_all_connections() does not project any of these fields from the relational row. Consequently a connection still renders “Never renewed” immediately after update_connection() refreshes it. Persist a dedicated refresh timestamp during the update and return it when loading the row.

Useful? React with 👍 / 👎.

Comment on lines 53 to +58
depends_on:
- omniroute
omniroute:
# Esperar o gateway ficar saudavel, e nao apenas iniciado: o OmniRoute
# cria o storage.sqlite durante o proprio boot, e subir antes disso faz
# o primeiro ciclo encontrar o banco ausente.
condition: service_healthy

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 Add a healthcheck for the service-health dependency

The example stack now gates ominirtksync on omniroute with condition: service_healthy, but the omniroute service defined earlier in this same Compose file has no healthcheck. Starting this advertised stack therefore cannot satisfy the dependency condition and Compose reports that the dependency has no healthcheck instead of launching the synchronizer. Define an OmniRoute healthcheck or retain the plain service-started dependency.

Useful? React with 👍 / 👎.

…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: f5fa658937

ℹ️ 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 thread src/omini_rtksync/cli.py
Comment on lines 153 to 160
if self.api_provider.can_handle(c):
mod, data, notes = self.api_provider.check_and_refresh(c)
for note in notes:
log_msg("STATUS", f"[{provider} · {name}] {note}")
if mod and data:
refreshed += 1
log_msg("SUCCESS", f"[{provider} · {name}] API key synchronized in storage.sqlite")
log_msg("SUCESSO", f"[{provider} · {name}] Chave de API sincronizada no storage.sqlite")
continue

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 Route API-key local connections through local probing

When a local Ollama/OpenAI-compatible connection carries a real or placeholder API key, ApiKeyProvider.can_handle() returns true and this continue prevents the later LocalProvider branch from running. The API-key validator recognizes the connection as local and merely returns unsupported, so the model catalog is never queried and local health/model discovery does not occur; check the local-provider predicate before this branch or exclude local records from ApiKeyProvider.

Useful? React with 👍 / 👎.

Comment thread src/omini_rtksync/auth.py
Comment on lines +182 to +186
except OSError:
# Sem disco gravável o hash vira efêmero (válido só nesta execução),
# mas o serviço continua subindo.
pass
return generated, 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 Retain the recovery credential when persistence fails

If the recovery directory is read-only or otherwise unwritable and no dashboard password/hash is configured, this handler discards the write failure but returns a generated value that is never retained. Authentication later calls resolve_recovery_hash() again and receives an empty string, while startup logs only the nonexistent file path and deliberately does not reveal the generated value; with the new empty factory password, the operator is therefore completely locked out of the dashboard.

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 Do not classify provider 5xx responses as valid credentials

When a validation endpoint returns a server-side error such as HTTP 500 or 503, _classify() falls through to STATE_VALID. During a provider outage this makes the synchronizer report that the key was accepted and stamp the transient result as active, even though the response provides no evidence that authentication succeeded; 5xx responses should remain unreachable/indeterminate rather than valid.

Useful? React with 👍 / 👎.

Comment on lines +83 to +86
return (
datetime.fromtimestamp(epoch_ms / 1000, tz=timezone.utc)
.isoformat(timespec="milliseconds")
.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.

P2 Badge Handle out-of-range expiries without aborting the cycle

When a Google connection contains a malformed but numeric expires_at value outside datetime's supported range, parse_expiry_to_ms() accepts it and the new normalization path reaches this conversion, which raises OSError, OverflowError, or ValueError. normalize_expiry_format() catches only sqlite3.Error, so the exception escapes sync_all(), marks the whole cycle failed, and prevents every later connection from being processed; reject or contain invalid timestamp ranges per connection.

Useful? React with 👍 / 👎.

| `HOST_HOME` | auto-detected | Host home directory mounted into the container. Falls back to `/root/host`, then `/host`, then the process home. |
| `DATA_DIR` | — | Base directory for the panel's own state files (`.dashboard_auth.json`, `.dashboard_recovery`, `ui_prefs.sqlite`). Defaults to the directory holding `DB_PATH`. |
| `ANTIGRAVITY_TOKEN_PATH` | — | Extra path to an Antigravity/Gemini credential file, searched before the built-in list. |
| `MODULE` | `all` | Which combos to sync: `all`, `antigravity`, `oauth`, `gemini`. |

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 Remove the unsupported MODULE setting from the guide

The new configuration guide advertises MODULE as a way to limit synchronization scope, but a repo-wide search finds no code or environment parser that reads this variable, and OmniSyncEngine.sync_all() always iterates every connection. An operator who sets MODULE=antigravity or MODULE=oauth will therefore still probe and process all providers; either implement the filter or remove this documented contract.

Useful? React with 👍 / 👎.

Comment thread src/omini_rtksync/logs.py
Comment on lines +3 to +6
O stdout/stderr de um container é volátil: ele some no `docker rm`, é truncado pelo
driver de log e não sobrevive a um restart. Os eventos que importam para auditoria
(renovação de token, falha de sincronização, acesso ao dashboard) passam a ser
gravados também em arquivo, com rotação diária e retenção configurável.

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 the promised dashboard access audit events

The persistent logging feature explicitly includes dashboard access among the events retained for auditing, but the HTTP handler still suppresses BaseHTTPRequestHandler.log_message() and never calls the new logger for successful or rejected authentication. As a result, visits and failed sign-in attempts leave no persistent record at any log level; wire authentication/access outcomes into the persistent logger or stop claiming they are audited.

Useful? React with 👍 / 👎.

Comment thread src/omini_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 Make credential updates atomic

The username and password hash are committed by two independent set_preference() calls, so a failure during the second write leaves the new username paired with the previous password hash even though the endpoint reports that saving failed. Concurrent password-change requests can also interleave these calls and persist a username from one request with a password from another, locking both operators out of normal authentication; write both keys under one SQLite transaction and lock.

Useful? React with 👍 / 👎.

Comment thread src/omini_rtksync/cli.py
parser.add_argument("--no-web", action="store_true", help="Desativa dashboard web")
parser.add_argument("--port", type=int, help="Porta do dashboard web (padrão: 9090)")
parser.add_argument("--user", type=str, help="Usuário para autenticação no dashboard web (padrão: admin)")
parser.add_argument("--password", type=str, help="Senha para autenticação no dashboard web (padrão: pathbit)")

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 Update the CLI help to reflect the missing factory password

The runtime default was changed to an empty password with first access through the generated recovery credential, but --help still tells operators that the default password is pathbit. On a fresh deployment, following this CLI guidance always produces a failed login and obscures the required recovery-file workflow; describe the empty/recovery-based default instead.

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: e7b28d5b4e

ℹ️ 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 thread src/omini_rtksync/web.py
Comment on lines +642 to +644
res = self.sync_callback() or {}
self.redirect_to_dashboard(
"success",

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 Honor unsuccessful manual synchronization results

When sync_callback() reports a handled failure rather than raising—such as OmniSyncEngine.sync_all() returning {"success": false, "error": "db_not_found"}—this branch unconditionally redirects with a green success message saying the synchronization completed. Operators are therefore told a manual sync succeeded even though nothing ran; inspect res["success"] and surface its error before selecting the success tone.

Useful? React with 👍 / 👎.

Comment thread src/omini_rtksync/auth.py
Comment on lines +178 to +180
# 0600: apenas o dono do processo lê o segredo de emergência.
fd = os.open(recovery_file, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(fd, "w", encoding="utf-8") as f:

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 Enforce mode 0600 when replacing a recovery file

When .dashboard_recovery already exists but is empty—for example, after deployment tooling creates a placeholder with mode 0644—passing 0o600 to os.open(..., O_CREAT) does not change that existing file's permissions. The newly generated break-glass credential is then left readable by other local users despite the stated 0600 guarantee; apply the mode to the opened descriptor or reject an insecure existing file before writing.

Useful? React with 👍 / 👎.

Comment on lines +313 to +315
models = self._extract_model_names(payload)
if models:
return 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 Treat an empty model catalog as a successful probe

A reachable but newly installed Ollama, LM Studio, or compatible instance can legitimately return HTTP 200 with an empty models/data array. Because only a nonempty extracted list returns success, this code exhausts the paths and check_and_refresh() stamps that responding instance as unreachable, so the dashboard falsely reports an outage; track whether a valid catalog response was received separately from whether it contained models.

Useful? React with 👍 / 👎.

Comment thread src/omini_rtksync/logs.py
Comment on lines +63 to +66
for entry in os.listdir(log_dir):
# Só mexe nos arquivos rotacionados deste serviço; o arquivo ativo é preservado.
if not entry.startswith(LOG_FILE_NAME) or entry == LOG_FILE_NAME:
continue

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 retention cleanup to rotated log filenames

When the configured log directory contains an old manual backup such as ominirtksync.log.manual-backup, this prefix-only check treats it as a rotated log and deletes it once it crosses the retention cutoff. That contradicts the documented promise to remove only the service's date-suffixed rotations and can destroy operator-created backups; match the actual ominirtksync.log.YYYY-MM-DD rotation pattern instead of every filename sharing the prefix.

Useful? React with 👍 / 👎.

Comment on lines +327 to +330
models = combo.get("models") or []
if isinstance(models, str):
models = [models]
preview = ", ".join(str(m) for m in models[:4])

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 Reject non-list combo model payloads before rendering

If a combo row contains syntactically valid JSON of the wrong shape, such as models = '{"primary":"gpt-5"}' or a scalar, get_all_combos() passes that value through and this slice raises KeyError or TypeError. One malformed combo therefore aborts rendering the entire dashboard instead of degrading that row to an empty model list; normalize the decoded value to a list before slicing it.

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: a052b68b1d

ℹ️ 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 thread src/omini_rtksync/cli.py Outdated
test_status=data.get("testStatus"),
credential_state=data.get("credentialState"),
last_error=data.get("lastError"),
clear_rate_limit=not data.get("rateLimitedUntil"),

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 Clear expired rate-limit locks instead of preserving them

When a connection already has an expired rateLimitedUntil, ApiKeyProvider.check_and_refresh() copies that value into data, so this expression passes clear_rate_limit=False. The stale timestamp is therefore never removed (and backoffLevel is never reset), allowing OmniRoute to keep skipping the connection indefinitely after its cooldown; determine whether the timestamp has expired rather than testing only whether the field is present.

Useful? React with 👍 / 👎.

Comment on lines +294 to +295
if any(marker in provider for marker in LOCAL_PROVIDER_MARKERS):
return 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 Keep hosted Ollama connections in the API-key path

For an Ollama Cloud connection with a non-local baseUrl such as https://ollama.com/v1, the provider-name marker still makes this return true. ApiKeyProvider.can_handle() consequently declines the record, so the engine performs local model discovery instead of the dedicated authenticated Ollama Cloud credential probe; use provider markers only when no address is declared, as ConnectionRecord.is_local already does.

Useful? React with 👍 / 👎.

Comment on lines +83 to +87
if conn.is_local:
models = conn.local_models
if models:
return translate("reason.local_ok", lang, count=len(models))
return translate("reason.local_unreachable", 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 Base local diagnostics on probe status, not model count

The fresh provider fix marks an HTTP-successful empty catalog as active, but this renderer still reports that the instance did not answer whenever local_models is empty. The inverse also occurs after an outage because the failure result omits discoveredModels, leaving a previously persisted nonempty catalog and producing a success diagnosis; derive the message from testStatus/health_status and treat the catalog size separately.

Useful? React with 👍 / 👎.


# credentialState e o catalogo local nao tem coluna propria: vao para o
# JSON de provider_specific_data, preservando o que ja estava la.
if (credential_state or discovered_models is not None) and "provider_specific_data" in cols:

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 health results in the supported JSON schema

The fresh persistence helper still handles only relational columns. For the explicitly supported providerConnections schema (id, data, updatedAt), this condition never writes credentialState, testStatus, or discovered models into data, and none of the other branches adds an assignment, so the function returns false and both API-key validation and local-probe results are discarded on that schema.

Useful? React with 👍 / 👎.

Comment on lines +150 to +158
if self.is_oauth:
remaining = self.remaining_seconds
if remaining is None:
return "no_expiration"
if remaining <= 0:
return "expired"
if remaining < EXPIRING_SOON_SECONDS:
return "expiring_soon"
return "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 Honor invalid gateway status for generic OAuth connections

For a non-Google OAuth connection that OmniRoute has already marked with test_status='invalid', no live credentialState is produced because the validation probe is wired only into the Google branch. This branch ignores testStatus and derives health solely from a future expiresAt, so a rejected Claude, Codex, GitHub, or Kiro connection is displayed as active; apply a nonhealthy gateway status before the expiry-derived classification.

Useful? React with 👍 / 👎.

Comment on lines +66 to +70
- CRON_ENABLED=${CRON_ENABLED:-1}
# - CRON_INTERVAL=300 # herda SYNC_INTERVAL quando omitido
- LOG_DIR=${LOG_DIR:-/app/data/logs}
- LOG_RETENTION_DAYS=${LOG_RETENTION_DAYS:-30}
- LOG_LEVEL=${LOG_LEVEL:-INFO}

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 Forward credential-check controls into the Compose container

The example stack never passes CREDENTIAL_CHECK_ENABLED or CREDENTIAL_CHECK_TIMEOUT into ominirtksync, even though both are advertised in .env.example and read by Settings.from_env(). Compose uses .env for interpolation but does not automatically inject unused keys, so an operator setting CREDENTIAL_CHECK_ENABLED=0 still gets the runtime default of enabled and the container continues making outbound validation requests with stored credentials; explicitly forward both variables in this environment block.

Useful? React with 👍 / 👎.

Comment on lines +113 to +116
- SYNC_INTERVAL=60
- REFRESH_MARGIN=1200
- ENABLE_WEB_DASHBOARD=0
- LOG_DIR=/app/data/logs

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 container healthy when running without the dashboard

The new fully headless example sets ENABLE_WEB_DASHBOARD=0, but the inspected Dockerfile and docker-compose.example.yml healthchecks both unconditionally request http://127.0.0.1:9090/healthz. In this documented mode run_daemon() never starts an HTTP server, so every probe fails and the otherwise functioning synchronizer remains permanently unhealthy; use a non-HTTP liveness check in headless mode or make the healthcheck conditional.

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 4496dc5 into master Sep 12, 2026
5 checks passed
@elielsousa-pathbit
elielsousa-pathbit deleted the fix/expires-at-iso-and-test-status 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