Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
cb2b74c
feat: log persistente, credencial de recuperacao e resiliencia do ser…
elielsousa-pathbit Sep 12, 2026
670f6f8
feat(web): dashboard server-side, i18n com bandeiras, logs do cron e …
elielsousa-pathbit Sep 12, 2026
6886c79
chore: remove emojis da saida de terminal
elielsousa-pathbit Sep 12, 2026
ebe67a5
merge: reconcilia a evolucao do painel com a traducao para ingles
elielsousa-pathbit Sep 12, 2026
dcd3d02
docs: wiki versionada no repo com publicacao automatica
elielsousa-pathbit Sep 12, 2026
4a96f3e
fix(tests): torna a sondagem do provedor local deterministica
elielsousa-pathbit Sep 12, 2026
d5ebd65
fix: valida credenciais de verdade, corrige deteccao de instancia loc…
elielsousa-pathbit Sep 12, 2026
9fea9cf
merge: integra master e padroniza testStatus como "active"
elielsousa-pathbit Sep 12, 2026
8f1d630
feat: senha com politica de forca gravada como hash no sqlite e refre…
elielsousa-pathbit Sep 12, 2026
aa5313c
fix: healthz voltou a quebrar no merge, e credenciais vivas sumiram d…
elielsousa-pathbit Sep 12, 2026
b2fa106
fix: elimina a senha de fabrica e a retencao de packages que apagava …
elielsousa-pathbit Sep 12, 2026
91e51db
fix(ci): remove input inexistente da politica de retencao
elielsousa-pathbit Sep 12, 2026
185f1a8
chore(security): retencao ciente de multi-arch e fim das senhas de ex…
elielsousa-pathbit Sep 12, 2026
620b702
fix(auth): senha vazia no ambiente nao pode travar a troca pela tela
elielsousa-pathbit Sep 12, 2026
0cb959d
fix: despacho de providers, persistencia da sondagem e armazenamento …
elielsousa-pathbit Sep 12, 2026
5781cc7
feat: saida de rede por conta (leitura) e documentacao de multi-sessao
elielsousa-pathbit Sep 12, 2026
65f6ebe
fix: cabecalho de autenticacao, Ollama hospedado, CSP das bandeiras e…
elielsousa-pathbit Sep 12, 2026
b12cbb2
test: stack de teste propria do repositorio, sem depender de artigo
elielsousa-pathbit Sep 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 70 additions & 8 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ DB_PATH=/app/data/db/data.sqlite
# Optional custom path for direct Antigravity OAuth credentials
# ANTIGRAVITY_TOKEN_PATH=/root/host/.gemini/oauth_creds.json

# Base directory for everything this container writes: the panel preferences
# database, the recovery credential and the log directory. Defaults to the
# directory of DB_PATH. Set it when the gateway database lives somewhere the
# synchronizer should not write to.
# DATA_DIR=/app/data

# Google OAuth client used to renew Antigravity / Gemini CLI credentials.
# Leave BOTH empty to let the synchronizer read them from the host credential
# file it discovers. Fill them in only for an isolated deployment that has no
# host credentials mounted. These are secrets: they belong in your .env, which
# is never committed, and never in this example file.
# GOOGLE_CLIENT_ID=
# GOOGLE_CLIENT_SECRET=

# ------------------------------------------------------------------------------
# 2. 9Router Gateway Connectivity
# ------------------------------------------------------------------------------
Expand All @@ -33,9 +47,17 @@ ROUTER_URL=http://127.0.0.1:20128
# Interval in seconds between synchronization passes and cron renewals (default: 300s / 5min)
SYNC_INTERVAL=300

# Margin in seconds before expiration to trigger proactive renewal (default: 900s / 15min)
# Margin in seconds before expiration to trigger proactive renewal (default: 900s / 15min).
# A token is only renewed once its remaining validity drops below this margin.
REFRESH_MARGIN=900

# Dedicated interval for the cron scheduler. Inherits SYNC_INTERVAL when omitted.
# CRON_INTERVAL=300

# Enable/disable the automatic scheduler (1=on, 0=off).
# With 0, synchronization only happens on manual trigger (--once or POST /api/sync).
CRON_ENABLED=1

# Execution module: all, antigravity, oauth, gemini
MODULE=all

Expand All @@ -48,19 +70,59 @@ ENABLE_WEB_DASHBOARD=1
# Network interface for the web dashboard server
WEB_HOST=0.0.0.0

# HTTP port for the 9RTKSync web dashboard (default: 9190)
WEB_PORT=9190
# INTERNAL port of the web server inside the container (default: 9090).
# It is the same in both synchronizers; what differs is the port published on the
# host (9091 for 9RTKSync, 9092 for OminiRTKSync).
WEB_PORT=9090

# HTTP Basic Auth credentials for dashboard protection
# HTTP Basic Auth credentials for dashboard protection.
# IMPORTANT: Update these credentials upon first login via web interface or via .env!
#
# Headless mode: when DASHBOARD_USER and/or DASHBOARD_PASSWORD are set, they become
# the source of truth and the .dashboard_auth.json file written by the screen is
# ignored. Changing the password from the panel then answers 409 Conflict. Comment
# the two lines below to hand control back to the dashboard.
DASHBOARD_USER=admin
DASHBOARD_PASSWORD=pathbit
# DASHBOARD_PASSWORD= # vazio: usa a credencial de recuperacao do primeiro boot

# Break-glass recovery credential. Sign in with user 'admin' and this value as the
# password to regain access if the dashboard password is forgotten. When left empty,
# a random value is generated on first boot, stored in .dashboard_recovery (mode
# 0600) and written once to the log file.
# DASHBOARD_RECOVERY_HASH=

# ------------------------------------------------------------------------------
# Persistent file log
# ------------------------------------------------------------------------------
# Directory for log files. Default: <database directory>/logs.
LOG_DIR=/app/data/logs

# Retention in days before rotated files are purged (default: 30).
LOG_RETENTION_DAYS=30

# Minimum level recorded: DEBUG, INFO, WARNING or ERROR (default: INFO).
LOG_LEVEL=INFO

# Mirror events on the container stdout as well (1=yes, 0=no).
LOG_TO_STDOUT=1

# ------------------------------------------------------------------------------
# 5. 9Router Stack Integration (Docker Compose)
# ------------------------------------------------------------------------------
# Initial credentials and JWT secret for 9Router
INITIAL_PASSWORD=PathbitDevs2026!
JWT_SECRET=9router-jwt-secret-key-pathbit
# Initial credentials and JWT secret for 9Router.
# Both are REQUIRED and intentionally shipped empty: a value published in an
# example file is a public credential, and it becomes the real password of
# every deployment that copied the file. `docker compose up` refuses to start
# until you fill them in.
# INITIAL_PASSWORD -> at least 12 characters, generated, not typed
# JWT_SECRET -> openssl rand -hex 32
INITIAL_PASSWORD=
JWT_SECRET=
REQUIRE_API_KEY=false
REQUIRE_LOGIN=false

# --- Validacao viva de credenciais -----------------------------------------
# Pergunta a cada provedor se a chave/token ainda e aceito, em vez de assumir
# que uma conexao esta saudavel so por carregar uma credencial.
CREDENTIAL_CHECK_ENABLED=1
CREDENTIAL_CHECK_TIMEOUT=8
86 changes: 72 additions & 14 deletions .github/workflows/cleanup-packages.yml
Original file line number Diff line number Diff line change
@@ -1,29 +1,87 @@
name: Purge Packages
name: Package Retention

# Mantem o registro enxuto sem nunca derrubar o que esta em uso.
#
# Historico: a primeira versao deste arquivo nao era limpeza. Com
# min-versions-to-keep: 0 e delete-only-untagged-versions: false ela apagava
# TODAS as versoes e em seguida removia o proprio package via API. Quem
# estivesse puxando ghcr.io/pathbit/9rtksyncatest ficava sem imagem.
#
# A segunda versao corrigia isso, mas usava actions/delete-package-versions,
# que trata cada manifesto como uma versao independente. O build e multi-arch
# (linux/amd64 + linux/arm64): o buildx publica um manifest list com a tag e um
# manifesto SEM TAG por plataforma. Descartar "versoes sem tag" apaga
# justamente as camadas que a tag referencia, e o pull passa a falhar com
# "manifest unknown" mesmo com a tag intacta no registro.
#
# Por isso a limpeza aqui usa uma action que resolve o manifest list antes de
# apagar: um manifesto sem tag so e descartado se nenhuma tag preservada
# apontar para ele.

on:
workflow_dispatch:
inputs:
keep:
description: "Quantas versoes marcadas manter"
required: false
default: "3"
dry-run:
description: "Simular: lista o que seria apagado sem apagar"
required: false
default: "false"
schedule:
# Semanal, domingo 04:00 UTC: o acumulo vem dos builds, nao do relogio.
- cron: "0 4 * * 0"
workflow_run:
workflows: ["Release and Docker Package"]
types: [completed]

permissions:
packages: write

concurrency:
group: package-retention-9rtksync
cancel-in-progress: false

jobs:
purge:
retention:
name: Apply retention policy
runs-on: ubuntu-latest
# Depois de um release, so faz sentido limpar se o release funcionou.
if: >-
github.event_name != 'workflow_run' ||
github.event.workflow_run.conclusion == 'success'
steps:
- name: Purge package versions
uses: actions/delete-package-versions@v5
- name: Apply retention policy
uses: dataaxiom/ghcr-cleanup-action@v1.2.2
with:
package-name: '9rtksync'
package-type: 'container'
min-versions-to-keep: 0
delete-only-untagged-versions: 'false'
continue-on-error: true
token: ${{ secrets.GITHUB_TOKEN }}
owner: ${{ github.repository_owner }}
packages: 9rtksync
# Guarda as N imagens marcadas mais recentes, com as camadas de
# plataforma de cada uma. 'latest' fica de fora da contagem por
# seguranca, ainda que por construcao ela aponte para a mais nova.
keep-n-tagged: ${{ github.event.inputs.keep || '3' }}
exclude-tags: latest
# Apaga apenas manifestos orfaos de verdade: os que sobraram de
# builds ja aposentados e nao pertencem a nenhuma tag preservada.
delete-untagged: true
# Restos de execucoes anteriores: manifest list cujas camadas de
# plataforma ja nao existem (nao ha o que puxar delas).
delete-ghost-images: true
delete-partial-images: true
delete-orphaned-images: true
# Confere no registro se cada digest referenciado existe mesmo, e
# registra o resultado no log da execucao.
validate: true
dry-run: ${{ github.event.inputs.dry-run || 'false' }}

- name: Force delete package via GitHub API
- name: Report what survived
if: always()
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
curl -X DELETE \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GH_TOKEN" \
https://api.github.com/orgs/pathbit/packages/container/9rtksync || true
echo "### Versoes mantidas em 9rtksync" >> "$GITHUB_STEP_SUMMARY"
gh api "/orgs/${{ github.repository_owner }}/packages/container/9rtksync/versions" \
--jq '.[] | "- \(.name[0:19]) tags: \(.metadata.container.tags | join(", "))"' \
>> "$GITHUB_STEP_SUMMARY" || echo "- (nao foi possivel listar)" >> "$GITHUB_STEP_SUMMARY"
62 changes: 62 additions & 0 deletions .github/workflows/publish-wiki.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Publish Wiki

# The wiki is generated from docs/wiki/ so the documentation is reviewed in pull
# requests like any other change, instead of being edited straight in the wiki
# where nothing gates it.
#
# First run requires the wiki to already exist: GitHub only creates the
# <repo>.wiki.git repository after the first page is saved through the web UI.
# Create any page once and this workflow takes over from there.

on:
push:
branches: [master]
paths:
- "docs/wiki/**"
- ".github/workflows/publish-wiki.yml"
workflow_dispatch:

permissions:
contents: write

concurrency:
group: publish-wiki
cancel-in-progress: false

jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Clone the wiki
id: clone
run: |
if git clone "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.wiki.git" wiki; then
echo "ok=true" >> "$GITHUB_OUTPUT"
else
echo "ok=false" >> "$GITHUB_OUTPUT"
echo "::warning::Wiki repository not found. Create the first page through the GitHub UI once, then re-run this workflow."
fi

- name: Sync pages
if: steps.clone.outputs.ok == 'true'
run: |
# Replace the whole page set so a deleted source file disappears from the wiki too.
find wiki -maxdepth 1 -name '*.md' -delete
cp docs/wiki/*.md wiki/

- name: Commit and push
if: steps.clone.outputs.ok == 'true'
working-directory: wiki
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -A
if git diff --cached --quiet; then
echo "Wiki already up to date."
exit 0
fi
git commit -m "docs: sync wiki from ${{ github.sha }}"
git push
18 changes: 18 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -1218,3 +1218,21 @@ temp/

### NOT IGNORE
!.vscode/

# --- Artefatos de execucao do RTKSync (acrescentado ao final; o conteudo acima
# --- permanece exatamente como estava) ---
# O SQLite e criado no startup do proprio projeto: e estado de execucao, nunca
# fonte. Versiona-lo publicaria conexoes, tokens e chaves de quem rodou.
*.sqlite
*.sqlite3
*.sqlite-journal
*.sqlite-wal
*.sqlite-shm
# Credenciais do painel: o hash de recuperacao e a senha em texto herdada.
.dashboard_recovery
.dashboard_auth.json
# Log persistente: pode conter endereco, nome de conexao e mensagem de erro.
*.log
logs/
# Clones do upstream para leitura (9router, OmniRoute, litellm).
tmp/
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ ENV DB_PATH=/app/data/db/data.sqlite
ENV ROUTER_URL=http://127.0.0.1:20128
ENV SYNC_INTERVAL=300
ENV REFRESH_MARGIN=900
ENV WEB_PORT=9190
ENV WEB_PORT=9090

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve or migrate the previous dashboard port

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

Useful? React with 👍 / 👎.

ENV WEB_HOST=0.0.0.0
ENV ENABLE_WEB_DASHBOARD=1

Expand All @@ -34,10 +34,10 @@ COPY pyproject.toml /app/
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -e .

EXPOSE 9190
EXPOSE 9090

HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=3 \
CMD /opt/venv/bin/python3 -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:9190/healthz', timeout=3)" || exit 1
CMD /opt/venv/bin/python3 -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:9090/healthz', timeout=3)" || exit 1

ENTRYPOINT ["/opt/venv/bin/python3", "-m", "nine_rtksync"]
CMD ["--daemon"]
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ docker-build:
docker build -t 9rtksync:latest -t ghcr.io/pathbit/9rtksync:latest .

docker-run:
docker run --rm -it --name router-sync -p 9190:9190 9rtksync:latest
docker run --rm -it --name 9rtksync -p 9091:9090 9rtksync:latest

clean:
find . -type d -name "__pycache__" -exec rm -rf {} +
Expand Down
Loading
Loading