Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
734748e
feat: identidade por produto, containers padronizados e o painel para…
elielsousa-pathbit Sep 12, 2026
f080959
Isolar cada stack em rede, hostname e faixa de enderecos proprios
elielsousa-pathbit Sep 12, 2026
57a2eec
Nao marcar como invalida a credencial que nao conseguimos ler
elielsousa-pathbit Sep 12, 2026
01f72e8
Criar /app/logs na imagem com o dono que o compose usa
elielsousa-pathbit Sep 12, 2026
6aaebba
Prender o painel do Makefile ao loopback, com guarda que impede o ret…
elielsousa-pathbit Sep 13, 2026
40dbf94
Guarda contra credencial de fabrica em texto de ajuda
elielsousa-pathbit Sep 13, 2026
b89c09f
Doc de licencas x usuarios, com os scripts que reproduzem cada numero
elielsousa-pathbit Sep 13, 2026
7fded49
Contrato de identidade visual travado por teste
elielsousa-pathbit Sep 13, 2026
d6e5ab3
Calar o cabecalho Server, e travar as duas simetrias por teste
elielsousa-pathbit Sep 13, 2026
235c188
Dar icone a aba do painel, que era a unica pagina sem ele
elielsousa-pathbit Sep 13, 2026
7ff56cb
Tela de login propria, e o grid que finalmente cabe na tela
elielsousa-pathbit Sep 13, 2026
dbd88f1
Freio contra forca bruta no login: 429, espera que cresce e prova de …
elielsousa-pathbit Sep 13, 2026
43754ae
Barra do topo com tres controles, e o carimbo que se estragava sozinho
elielsousa-pathbit Sep 13, 2026
0914892
Guarda: nada responde sem sessao, exceto o que tem motivo declarado
elielsousa-pathbit Sep 13, 2026
56c3851
Tirar a senha real de dentro do teste que existe para impedir isso
elielsousa-pathbit Sep 13, 2026
7686d86
Encadear o LiteLLM nos gateways, e um teste que nao mente sobre isso
elielsousa-pathbit Sep 13, 2026
1d004a7
Dificuldade da prova de trabalho cresce com a insistencia
elielsousa-pathbit Sep 13, 2026
6865549
Fazer o botao Sair sair de verdade, e 404 para rota que nao existe
elielsousa-pathbit Sep 13, 2026
ee19252
SSO por OIDC e SAML2, e os seis cartoes iguais nos tres paineis
elielsousa-pathbit Sep 13, 2026
bc8191f
O canone dos irmaos, e a catraca que o mede sem mentir
elielsousa-pathbit Sep 14, 2026
31fbe14
O dimensionador do 9RTKSync para de citar o gateway dos irmaos
elielsousa-pathbit Sep 14, 2026
a404e72
A unica excecao a "container_name igual ao hostname", explicada
elielsousa-pathbit Sep 14, 2026
7c68d9f
O catalogo de traducoes passa a ser o MESMO arquivo nos tres
elielsousa-pathbit Sep 14, 2026
d73ea0c
Um leitor de data so: o normalizer e o models discordavam em 3 horas
elielsousa-pathbit Sep 14, 2026
89bce9b
A celula do grid de modelos nunca mais fica em branco
elielsousa-pathbit Sep 14, 2026
46746c3
O README parava de atribuir aos botoes uma rota que eles nao usam
elielsousa-pathbit Sep 14, 2026
9af35f5
O modal de SSO parava de dar o mesmo aviso duas vezes
elielsousa-pathbit Sep 14, 2026
934dd9d
A tela do LiteLlm parava de dizer que o gateway respondeu quando ele …
elielsousa-pathbit Sep 14, 2026
238347f
O rodape da Pathbit em toda tela do produto -- menos em uma, de propo…
elielsousa-pathbit Sep 14, 2026
c8406a8
A barra de paginacao mostrava a chave crua, e o historico nao paginava
elielsousa-pathbit Sep 14, 2026
d7d7c4d
As tres portas do SAML2 nos tres paineis
elielsousa-pathbit Sep 14, 2026
dd70058
Duas rotas que o servidor prometia e nao servia
elielsousa-pathbit Sep 14, 2026
735d97a
feat(auth): desafio interativo direto de selecao de icones no login a…
elielsousa-pathbit Sep 14, 2026
3619141
chore(vscode): ignorar repositorios de tmp no source control do editor
elielsousa-pathbit Sep 14, 2026
3bc2f30
chore: sincronizar master em feat/identidade-e-padronizacao
elielsousa-pathbit Sep 14, 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
45 changes: 42 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ DB_PATH=/app/data/db/data.sqlite
# 2. 9Router Gateway Connectivity
# ------------------------------------------------------------------------------
# Base URL of the 9Router gateway for diagnostic checks and integration
ROUTER_URL=http://127.0.0.1:20128
ROUTER_URL=http://9rtk-router:20128

# ------------------------------------------------------------------------------
# 3. Synchronization and Cron Scheduler Parameters
Expand Down Expand Up @@ -89,7 +89,7 @@ DASHBOARD_PASSWORD=

# Deixou DASHBOARD_PASSWORD vazia? O container gera uma credencial de
# recuperacao no primeiro boot. Leia e entre com ela como usuario 'admin':
# docker exec router-sync cat /app/data/db/.dashboard_recovery
# docker exec 9rtk-sync cat /app/data/db/.dashboard_recovery
# Depois defina a sua senha pela tela.
#
# Com DASHBOARD_PASSWORD preenchida, o ambiente vira a fonte da verdade e a
Expand All @@ -103,11 +103,33 @@ DASHBOARD_PASSWORD=
# quando e necessaria.
# DASHBOARD_RECOVERY_HASH=

# ------------------------------------------------------------------------------
# ENTRADA FEDERADA (SSO) / SINGLE SIGN-ON
# ------------------------------------------------------------------------------
# O SSO e OPCIONAL e nasce desligado: sem configuracao, o painel funciona
# exatamente como hoje. O resto da configuracao (emissor, identificador do
# cliente, lista de quem pode entrar) e feito pela tela, no botao Configuracoes.
#
# Single sign-on is OPTIONAL and starts off. Everything but the secret below is
# configured from the screen, under the Settings button.

# Segredo do cliente OIDC. O ambiente VENCE o arquivo gravado pela tela: com
# esta variavel preenchida, o campo da tela fica travado. Vazia aqui de
# proposito -- um valor publicado num arquivo de exemplo e uma credencial
# publica. Deixe-a vazia para administrar o segredo pela tela, que o grava em
# .sso_client_secret com modo 0600.
OIDC_CLIENT_SECRET=

# Interruptor de emergencia. Com 1, o SSO fica desligado mesmo com tudo
# configurado, sem tocar no banco -- e o que devolve o formulario local quando o
# provedor de identidade cai e o painel esta atras de um tunel.
# SSO_DISABLED=0

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

# Retention in days before rotated files are purged (default: 30).
LOG_RETENTION_DAYS=30
Expand Down Expand Up @@ -138,3 +160,20 @@ REQUIRE_LOGIN=false
# que uma conexao esta saudavel so por carregar uma credencial.
CREDENTIAL_CHECK_ENABLED=1
CREDENTIAL_CHECK_TIMEOUT=8

# --- Acesso remoto (opcional) ------------------------------------------------
# Só têm efeito quando você sobe o perfil correspondente:
# docker compose --profile tunel up -d
# docker compose --profile tailnet up -d
# Leia docs/wiki/Remote-Access.md ANTES de ligar qualquer um dos dois: com a
# porta em 127.0.0.1 o painel só é alcançado por esta máquina, e um túnel
# inverte isso.

# Vazio = quick tunnel da Cloudflare: URL nova a cada subida, pública para quem
# a tiver. Preenchido com o token de um túnel nomeado = URL estável e a
# possibilidade de pôr o Cloudflare Access na frente.
TUNNEL_TOKEN=

# Chave efêmera gerada em https://login.tailscale.com/admin/settings/keys
# (efêmera para o nó sumir sozinho quando o contêiner morrer).
TS_AUTHKEY=
2 changes: 1 addition & 1 deletion .github/workflows/cleanup-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ name: Package Retention
# 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.
# estivesse puxando ghcr.io/pathbit/9rtksync 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
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -1231,6 +1231,10 @@ temp/
# Credenciais do painel: o hash de recuperacao e a senha em texto herdada.
.dashboard_recovery
.dashboard_auth.json
# Segredo do cliente OIDC, gravado com modo 0600. Mesma classe de arquivo e
# mesmo motivo: numa execucao local o diretorio de dados pode cair dentro da
# arvore do repositorio.
.sso_client_secret
# Log persistente: pode conter endereco, nome de conexao e mensagem de erro.
*.log
logs/
Expand Down
21 changes: 11 additions & 10 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnPaste": true,
"explorer.autoReveal": true,
"explorer.compactFolders": false,
"files.exclude": {
"**/.git": false
},
"claudeCode.includeCoAuthoredBy": false,
"git.includeCoAuthoredBy": false
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnPaste": true,
"explorer.autoReveal": true,
"explorer.compactFolders": false,
"files.exclude": {
"**/.git": false
},
"claudeCode.includeCoAuthoredBy": false,
"git.includeCoAuthoredBy": false,
"git.ignoredRepositories": ["**/tmp/**"]
}
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ COPY pyproject.toml /app/
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -e .

# Criado na imagem, com o dono que o compose usa: um volume nomeado herda o
# dono do diretorio que cobre. Sem isto ele nasce root e o processo (uid 1000)
# nao consegue escrever o proprio log.
RUN mkdir -p /app/logs && chown -R 1000:1000 /app/logs

EXPOSE 9090

HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=3 \
Expand Down
21 changes: 18 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
.PHONY: setup test test-container venv run status docker-build docker-run clean
.PHONY: rede-de-inferencia setup test test-container venv run status docker-build docker-run clean

# Cria o .env a partir do .env.example. Nunca sobrescreve um .env existente:
# ele carrega os seus segredos, e um `make setup` distraido nao pode apaga-los.
# O docker compose le esse .env sozinho, por estar ao lado do compose.
setup:
# A rede de inferencia e compartilhada pelos tres gateways e declarada como
# externa nos tres composes -- externa justamente para que nenhuma das stacks
# seja dona dela: qualquer uma pode subir primeiro, e derrubar uma nao leva a
# rede junto. O preco e que ela precisa existir antes do primeiro `up`, e o
# compose so diz "network declared as external, but could not be found".
rede-de-inferencia:
@docker network inspect rtk-inference-net >/dev/null 2>&1 \
|| docker network create rtk-inference-net >/dev/null \
&& echo "rede rtk-inference-net pronta."

setup: rede-de-inferencia
@if [ -f .env ]; then \
echo ".env ja existe — preservado."; \
else \
Expand Down Expand Up @@ -52,8 +62,13 @@ status:
docker-build:
docker build -t 9rtksync:latest -t ghcr.io/pathbit/9rtksync:latest .

# O bind em 127.0.0.1 nao e detalhe: este painel le o banco do gateway e mostra
# a saude das credenciais. Sem o prefixo, "-p PORTA:9090" publica em TODA
# interface -- o Wi-Fi do cafe, a VLAN do escritorio -- enquanto os composes
# deste repo publicam so no loopback. Comentario FORA da receita: linha iniciada
# por # dentro de um alvo vai para o shell e aparece na saida.
docker-run:
docker run --rm -it --name 9rtksync -p 9091:9090 9rtksync:latest
docker run --rm -it --name 9rtk-sync -p 127.0.0.1:9091:9090 9rtksync:latest

clean:
find . -type d -name "__pycache__" -exec rm -rf {} +
Expand Down
117 changes: 80 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ On first boot with `DASHBOARD_PASSWORD` empty, the container generates a
**recovery credential** and writes it inside the data directory. Read it:

```bash
docker exec router-sync cat /app/data/db/.dashboard_recovery
docker exec 9rtk-sync cat /app/data/db/.dashboard_recovery
```

Sign in as `admin` with that value, then set a real password on the screen. The
Expand All @@ -83,46 +83,46 @@ need it.

## Running with Docker

### Configuração: `.env` a partir do exemplo
### Configuration: `.env` from the example

A configuração inteira vem de variáveis de ambiente, lidas de um `.env` ao lado
do `docker-compose.yml` — o Compose o encontra sozinho, sem nenhuma flag.
The whole configuration comes from environment variables, read from a `.env`
next to `docker-compose.yml` — Compose finds it on its own, with no flag.

```bash
make setup # cria o .env a partir do .env.example, sem sobrescrever um existente
make setup # creates .env from .env.example, never overwriting an existing one
```

O alvo lista, ao final, exatamente quais variáveis ficaram em branco e precisam
ser preenchidas. Preencha e suba a stack.
At the end, the target lists exactly which variables were left blank and need
filling in. Fill them and bring the stack up.

O `.env` **nunca** é versionado, e o `.env.example` não carrega nenhum valor de
segredo — um valor publicado num arquivo de exemplo é, por definição, uma
credencial pública. Um teste garante que toda variável exigida por um compose
existe no exemplo, para que `cp .env.example .env` nunca produza um `.env`
incompleto.
The `.env` is **never** committed, and `.env.example` carries no secret value —
a value published in an example file is, by definition, a public credential. A
test guarantees that every variable a compose file requires exists in the
example, so that `cp .env.example .env` never produces an incomplete `.env`.

### Portas, e por que cada uma é diferente
### Ports, and why each one differs

Os três sincronizadores escutam na **mesma porta dentro do container** (`9090`)
e publicam em portas diferentes no host, para que os três possam rodar lado a
lado. O mesmo vale para os gateways: cada um tem a sua.
The three synchronizers listen on the **same port inside the container**
(`9090`) and publish on different host ports, so that all three can run side by
side. The same goes for the gateways: each one has its own.

| Serviço | Porta interna | Publicada no host |
| Service | Internal port | Published on the host |
| :--- | :--- | :--- |
| 9Router | `20128` | `8081` |
| OmniRoute | `20128` | `8082` |
| LiteLLM | `4000` | `8083` |
| 9RTKSync (painel) | `9090` | `9091` |
| OminiRTkSync (painel) | `9090` | `9092` |
| LiteLlmRTKSync (painel) | `9090` | `9093` |
| 9RTKSync (dashboard) | `9090` | `9091` |
| OminiRTkSync (dashboard) | `9090` | `9092` |
| LiteLlmRTKSync (dashboard) | `9090` | `9093` |

A stack dos artigos (`claudegravity`) fica com a **`20128`**, a porta padrão do
9Router. As stacks dos repositórios saem dessa faixa de propósito: assim você
roda o artigo e os três sincronizadores ao mesmo tempo, sem conflito.
The article stack (`claudegravity`) keeps **`20128`**, the default 9Router port.
The repository stacks deliberately move out of that range: that way you can run
the article and all three synchronizers at the same time, with no conflict.

Tudo preso a `127.0.0.1`: o gateway carrega credenciais reais e não deve ficar
acessível na rede local. Para mudar qualquer uma, altere o lado esquerdo do
mapeamento no compose — o lado direito é a porta interna, que o processo escuta.
Everything is bound to `127.0.0.1`: the gateway carries real credentials and
must not be reachable on the local network. To change any of them, edit the left
side of the mapping in the compose file — the right side is the internal port,
the one the process listens on.


Official multi-architecture Docker images (`linux/amd64` and `linux/arm64`) are published automatically to the GitHub Container Registry (GHCR):
Expand All @@ -136,15 +136,32 @@ docker pull ghcr.io/pathbit/9rtksync:latest
Add `9rtksync` to your `docker-compose.yml` alongside [9Router](https://github.com/decolua/9router):

```yaml
name: 9rtksync-stack

services:
9router:
9rtk-router:
image: decolua/9router:latest
container_name: claudegravity-router
container_name: 9rtk-router
hostname: 9rtk-router
networks:
- 9rtksync-net
restart: unless-stopped
ports:
# 20128 dentro do container; 8081 no host, para nao disputar a porta
# padrao do 9Router com a stack do artigo.
- "127.0.0.1:8081:20128"
environment:
- DATA_DIR=/app/data
- PORT=20128
- HOSTNAME=0.0.0.0
# Sem esta linha o fluxo de login e redirecionado para a porta interna,
# que nao existe no host.
- NEXT_PUBLIC_BASE_URL=http://localhost:8081
- NODE_ENV=production
# Sem valor de fallback: um default publicado em arquivo de exemplo vira
# a senha real de toda implantacao que so copiou e colou.
- INITIAL_PASSWORD=${INITIAL_PASSWORD:?defina INITIAL_PASSWORD no .env}
- JWT_SECRET=${JWT_SECRET:?defina JWT_SECRET no .env (openssl rand -hex 32)}
volumes:
- 9router_data:/app/data
healthcheck:
Expand All @@ -154,27 +171,35 @@ services:
retries: 3
start_period: 20s

9rtksync:
9rtk-sync:
# Mesmo uid do gateway: os dois compartilham o volume de dados.
user: "1000:1000"
image: ghcr.io/pathbit/9rtksync:latest
container_name: 9rtksync
container_name: 9rtk-sync
hostname: 9rtk-sync
networks:
- 9rtksync-net
restart: unless-stopped
ports:
- "127.0.0.1:9091:9090"
volumes:
- 9router_data:/app/data
- ${HOME}:/root/host:ro
- 9rtksync_logs:/app/logs
environment:
- HOST_HOME=/root/host
- DB_PATH=/app/data/db/data.sqlite
- ROUTER_URL=http://9router:20128
- ROUTER_URL=${ROUTER_URL:-http://9rtk-router:20128}
- SYNC_INTERVAL=${SYNC_INTERVAL:-300}
- REFRESH_MARGIN=${REFRESH_MARGIN:-900}
- ENABLE_WEB_DASHBOARD=${ENABLE_WEB_DASHBOARD:-1}
- WEB_PORT=${WEB_PORT:-9090}
- DASHBOARD_USER=${DASHBOARD_USER:-admin}
- DASHBOARD_PASSWORD=${DASHBOARD_PASSWORD:-}
- LOG_DIR=${LOG_DIR:-/app/logs}
- LOG_RETENTION_DAYS=${LOG_RETENTION_DAYS:-30}
depends_on:
9router:
9rtk-router:
condition: service_healthy
healthcheck:
test: ["CMD", "/opt/venv/bin/python3", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:9090/healthz', timeout=3)"]
Expand All @@ -185,6 +210,14 @@ services:

volumes:
9router_data:
9rtksync_logs:

networks:
9rtksync-net:
name: 9rtksync-net
# Rede propria da stack. Na rede default, duas stacks no mesmo daemon
# resolvem o mesmo nome curto e nao da para saber a qual gateway o
# sincronizador se conectou.
```

---
Expand Down Expand Up @@ -257,10 +290,18 @@ When running with `ENABLE_WEB_DASHBOARD=1`, access the dashboard in your browser

Dashboard capabilities:
* Live operational metrics (Total Connections, OAuth Accounts, API Keys, Resilience Combos).
* Six domain cards, in the same order as the sibling panels: gateway connection, scheduler, monitored connections, virtual keys, registered models, resilience combos.
* Real-time countdown meters with visual health badges for every connection.
* Gateway diagnostic card with millisecond latency testing (`POST /api/test-gateway`).
* Password change modal for credential rotation (`POST /api/change-password`).
* Manual sync trigger via REST API (`POST /api/sync` and `POST /api/cron-run`).
* Gateway diagnostic card with millisecond latency testing.
* Password change modal for credential rotation.
* Manual sync trigger, from the panel or from a script.

The buttons on the panel post to `/acoes/…` and answer with a redirect
(POST-Redirect-GET), so a reload never repeats the action. The `/api/…` routes
(`POST /api/test-gateway`, `/api/change-password`, `/api/sync`, `/api/cron-run`)
do the same work for `curl` and for monitoring, and they answer JSON. Both exist
on purpose; naming only the API here read as if the buttons used it, which they
do not.

---

Expand All @@ -279,8 +320,10 @@ The only requirement is Docker. Nothing else needs to be installed on your machi
# Or via Makefile target
make test-container

# Or via Docker Compose
docker compose -f docker-compose.test.yml run --rm test
# docker-compose.test.yml nao tem servico de teste: e uma bancada viva
# (gateway real + este sincronizador) para conferir a stack de ponta a ponta.
docker compose -f docker-compose.test.yml up -d
docker compose -f docker-compose.test.yml down -v
```

### Option 2. Local Virtual Environment (Optional Prerequisites)
Expand Down
Loading
Loading