fix: interop com OmniRoute (expires_at ISO, testStatus active) + painel server-side, i18n e log persistente - #1
Conversation
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.
There was a problem hiding this comment.
💡 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".
| cron_scheduler = CronScheduler( | ||
| sync_callback=engine.sync_all, | ||
| interval_seconds=settings.sync_interval, | ||
| interval_seconds=settings.cron_interval, |
There was a problem hiding this comment.
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 👍 / 👎.
| # 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() |
There was a problem hiding this comment.
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 👍 / 👎.
| with open(auth_file, "r", encoding="utf-8") as f: | ||
| data = json.load(f) | ||
| user = data.get("user") | ||
| password = data.get("password") |
There was a problem hiding this comment.
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 👍 / 👎.
| | `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 | |
There was a problem hiding this comment.
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 👍 / 👎.
| 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() |
There was a problem hiding this comment.
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 👍 / 👎.
| 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}") |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
💡 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".
| if models: | ||
| return ( | ||
| True, | ||
| {"discoveredModels": models, "testStatus": "active"}, | ||
| [f"Local instance answered with {len(models)} model(s): {', '.join(models[:5])}"], | ||
| ) |
There was a problem hiding this comment.
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 👍 / 👎.
| for detail in res.get("details", []) or []: | ||
| actions = detail.get("actions") or [] | ||
| if not actions: |
There was a problem hiding this comment.
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.
|
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.
|
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.
|
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.
|
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.
|
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:.
|
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.
There was a problem hiding this comment.
💡 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".
| res.update(result.to_dict()) | ||
| modified = True |
There was a problem hiding this comment.
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 👍 / 👎.
| self.api_provider = ApiKeyProvider( | ||
| discovery=self.discovery, | ||
| validate_credentials=settings.validate_credentials, | ||
| validation_timeout=settings.validation_timeout, |
There was a problem hiding this comment.
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 👍 / 👎.
| - DASHBOARD_USER=${DASHBOARD_USER:-admin} | ||
| - DASHBOARD_PASSWORD=${DASHBOARD_PASSWORD:-pathbit} | ||
| - DASHBOARD_PASSWORD=${DASHBOARD_PASSWORD:-} |
There was a problem hiding this comment.
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 👍 / 👎.
| 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): |
There was a problem hiding this comment.
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 👍 / 👎.
| 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]] |
There was a problem hiding this comment.
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 👍 / 👎.
| 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) |
There was a problem hiding this comment.
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 👍 / 👎.
| Or with no local install at all: | ||
|
|
||
| ```bash | ||
| ./run_tests.sh |
There was a problem hiding this comment.
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 👍 / 👎.
| return ( | ||
| self.data.get("lastRefreshAt") | ||
| or self.data.get("credentialCheckedAt") | ||
| or self.data.get("lastTested") | ||
| or None |
There was a problem hiding this comment.
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 👍 / 👎.
| 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
💡 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".
| 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 |
There was a problem hiding this comment.
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 👍 / 👎.
| 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 |
There was a problem hiding this comment.
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 👍 / 👎.
| 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 |
There was a problem hiding this comment.
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 👍 / 👎.
| return ( | ||
| datetime.fromtimestamp(epoch_ms / 1000, tz=timezone.utc) | ||
| .isoformat(timespec="milliseconds") | ||
| .replace("+00:00", "Z") |
There was a problem hiding this comment.
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`. | |
There was a problem hiding this comment.
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 👍 / 👎.
| 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. |
There was a problem hiding this comment.
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 👍 / 👎.
| 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) |
There was a problem hiding this comment.
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 👍 / 👎.
| 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)") |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
💡 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".
| res = self.sync_callback() or {} | ||
| self.redirect_to_dashboard( | ||
| "success", |
There was a problem hiding this comment.
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 👍 / 👎.
| # 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: |
There was a problem hiding this comment.
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 👍 / 👎.
| models = self._extract_model_names(payload) | ||
| if models: | ||
| return models, "" |
There was a problem hiding this comment.
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 👍 / 👎.
| 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 |
There was a problem hiding this comment.
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 👍 / 👎.
| models = combo.get("models") or [] | ||
| if isinstance(models, str): | ||
| models = [models] | ||
| preview = ", ".join(str(m) for m in models[:4]) |
There was a problem hiding this comment.
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.
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.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
| defasagem aparece no CI em vez de aparecer em producao. | ||
| """ | ||
|
|
||
| import os |
There was a problem hiding this comment.
💡 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".
| test_status=data.get("testStatus"), | ||
| credential_state=data.get("credentialState"), | ||
| last_error=data.get("lastError"), | ||
| clear_rate_limit=not data.get("rateLimitedUntil"), |
There was a problem hiding this comment.
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 👍 / 👎.
| if any(marker in provider for marker in LOCAL_PROVIDER_MARKERS): | ||
| return True |
There was a problem hiding this comment.
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 👍 / 👎.
| 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) |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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 👍 / 👎.
| 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" |
There was a problem hiding this comment.
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 👍 / 👎.
| - 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} |
There was a problem hiding this comment.
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 👍 / 👎.
| - SYNC_INTERVAL=60 | ||
| - REFRESH_MARGIN=1200 | ||
| - ENABLE_WEB_DASHBOARD=0 | ||
| - LOG_DIR=/app/data/logs |
There was a problem hiding this comment.
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.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
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.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
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()gravavaexpires_atcomo epoch numérico em texto (str(expires_at_ms)) numa coluna TEXT. O OmniRoute lê esse campo comnew Date(...)emsrc/lib/tokenHealthCheck.ts, enew Date("1789999999000")é um Invalid Date:Também gravava
test_status = 'ok', valor que o OmniRoute não reconhece — só'active'conta como saudável (clearAccountErroremsrc/sse/services/auth.tse as checagens emtokenHealthCheck.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) oexpiresAtcontinua numérico, que é o que aquele gateway espera.2. Servidor web — corrige o
BrokenPipeErrorde produçãoCausa raiz: o servidor era
HTTPServer(uma thread só) apesar do docstring prometer multi-thread, e/healthzfazia 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_errorque engole desconexão do cliente (erros reais seguem chegando ao handler padrão).write_body()tolera o cliente ter fechado a conexão.3. Render server-side, i18n e diagnóstico
render.pycom os dados já embutidos. O navegador não consulta mais/api/statuspara 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.Access-Control-Allow-Origin: *; adicionadosCache-Control: no-store,X-Frame-Options: DENY,X-Content-Type-OptionseReferrer-Policy.prefs.py) — nunca no banco do gateway, nunca no localStorage.baseUrle os modelos servidos.4. Log persistente, credencial de recuperação, configuração e portas
logs.py: arquivo rotativo diário, retenção configurável porLOG_RETENTION_DAYS(padrão 30 dias) e expurgo dos rotacionados vencidos no boot.LOG_DIR,LOG_LEVELeLOG_TO_STDOUTcompletam o contrato.auth.py: credenciais salvas mandam; sem nada salvo valem as de fábrica; eadmin+ hash de recuperação entra sempre. Comparações comhmac.compare_digest. O hash vem deDASHBOARD_RECOVERY_HASHou é gerado no primeiro boot, salvo com permissão0600e registrado uma única vez no log.DASHBOARD_USER/DASHBOARD_PASSWORDexplícitas vencem o arquivo gravado pela tela — sem isso, uma única troca de senha tornava as variáveis inertes. NovasCRON_INTERVALeCRON_ENABLED.127.0.0.1. Container renomeado paraominirtksync, para não colidir com o irmão.Tipo de Alteração
Checklist de Validação
master(reconciliado com4c4a356).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 emtest_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
ApiKeyProviderdefiniaHEALTH_CHECK_ENDPOINTSe nunca os chamava, carimbando o status às cegas. Uma chave revogada seguia verde até uma requisição real falhar.credential_check.pypergunta 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:
/api/v1/key/api/v1/modelsresponde 200 sem credencial nenhumaPOST /v1/chat/completions/v1beta/models+x-goog-api-keyoauth2.googleapis.com/tokeninfoA 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
0600e nunca impressa no log./credenciais-atualizadas, servida antes dorequire_auth.CSRF
POSTde outra origem é recusado: o Basic Auth é anexado pelo navegador mesmo em formulário de outro site, e urlencoded não dispara preflight. OOrigindecide primeiro; checarSec-Fetch-Siteantes dele fazia um valor inesperado do navegador recusar um POST legítimo do próprio painel.Detecção de instância local
baseUrlmora emproviderSpecificData, não na raiz dedata— 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.ymlnão era limpeza: commin-versions-to-keep: 0edelete-only-untagged-versions: falseapagava todas as versões e depois removia o package via API. Agora guarda as 3 versões marcadas mais recentes.