From 6d8af0121137b7a76452777ee31f4e69d22e0f7d Mon Sep 17 00:00:00 2001 From: Eliel Sousa Date: Sat, 12 Sep 2026 18:29:40 -0300 Subject: [PATCH] feat: entrar no painel deixa de ser um enigma, e as portas deixam de colidir Quem subia a stack nao conseguia entrar no painel. Nao era falta de senha de fabrica -- era o caminho para definir a propria senha estar escondido. **A causa.** No .env.example, DASHBOARD_PASSWORD estava COMENTADA. Quem copiava o arquivo, preenchia e subia a stack nunca via a variavel, ficava sem senha e caia no fluxo da credencial de recuperacao, que nao tinha como adivinhar onde estava. O bloco de credenciais foi reescrito: as duas linhas agora existem, descomentadas e vazias, com o passo a passo em cima e o comando exato para ler a credencial de recuperacao caso a stack ja tenha subido sem senha. **Nao existe senha de fabrica, e continua nao existindo.** Um valor fixo publicado na imagem e uma credencial publica no instante em que a imagem e publicada. O que faltava era clareza, nao um default. Um bloco "como entrar no painel" abre o README e a Home da wiki: endereco, usuario, onde a senha e definida e o que fazer se nao foi. **Configuracao.** `make setup` cria o .env a partir do exemplo sem sobrescrever um existente, e lista quais variaveis ficaram em branco. Mantivemos a substituicao ${VAR} em vez de env_file: env_file despeja o arquivo inteiro em cada container, e o sincronizador receberia a senha do Postgres, que ele nao usa. Novo teste: toda variavel exigida por um compose tem de existir no .env.example. Ele pega exatamente a falha que originou isto -- verificado removendo DASHBOARD_PASSWORD do exemplo numa copia. **Portas.** As stacks de exemplo do 9RTKSync e do OminiRTkSync publicavam as duas em 20128: subir as duas na mesma maquina, que e o caso de quem compara os gateways, dava conflito. O OmniRoute passa a publicar em 20129 e o LiteLLM em 20130, na mesma faixa, com a tabela de portas no README e na wiki. **Documentacao.** Egress-And-Multi-Session.md sai de docs/ e entra em docs/wiki/, que e de onde a wiki e gerada -- estava fora dela, sem aparecer no indice nem no sidebar. Entra tambem na Home e no _Sidebar. **Painel.** O seletor de idioma ficava mais alto que os botoes vizinhos: ele carrega so a bandeira, um elemento com altura propria, e sem texto ao lado para definir a linha ele esticava o botao. A barra de acoes passa a fixar a altura de todos os controles. --- .env.example | 34 ++++++--- Makefile | 20 +++++- README.md | 80 +++++++++++++++++++++ docs/{ => wiki}/Egress-And-Multi-Session.md | 0 docs/wiki/Home.md | 24 +++++++ docs/wiki/Installation.md | 35 +++++++++ docs/wiki/_Sidebar.md | 1 + src/nine_rtksync/web/render.py | 12 +++- tests/test_env_cobre_composes.py | 60 ++++++++++++++++ 9 files changed, 253 insertions(+), 13 deletions(-) rename docs/{ => wiki}/Egress-And-Multi-Session.md (100%) create mode 100644 tests/test_env_cobre_composes.py diff --git a/.env.example b/.env.example index b0868c9..1ec29fa 100644 --- a/.env.example +++ b/.env.example @@ -75,20 +75,32 @@ WEB_HOST=0.0.0.0 # host (9091 for 9RTKSync, 9092 for OminiRTKSync). WEB_PORT=9090 -# HTTP Basic Auth credentials for dashboard protection. -# IMPORTANT: Update these credentials upon first login via web interface or via .env! +# ------------------------------------------------------------------------------ +# COMO ENTRAR NO PAINEL / HOW TO SIGN IN +# ------------------------------------------------------------------------------ +# O painel pede usuario e senha. Preencha as DUAS linhas abaixo antes de subir a +# stack -- e so isso. Nao existe senha de fabrica: um valor fixo publicado na +# imagem seria uma credencial publica no instante em que a imagem e publicada. # -# 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. +# The panel asks for a user and a password. Fill in BOTH lines below before +# bringing the stack up. There is no factory password, on purpose. DASHBOARD_USER=admin -# DASHBOARD_PASSWORD= # vazio: usa a credencial de recuperacao do primeiro boot +DASHBOARD_PASSWORD= -# 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. +# 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 +# Depois defina a sua senha pela tela. +# +# Com DASHBOARD_PASSWORD preenchida, o ambiente vira a fonte da verdade e a +# troca de senha pela tela e recusada com aviso -- mude aqui e recrie o +# container. Deixe-a vazia se preferir administrar a senha pela tela. + +# Credencial de recuperacao (break-glass). Se ficar vazia, um valor aleatorio e +# gerado no primeiro boot, salvo em .dashboard_recovery (modo 0600) e o log +# registra o ARQUIVO, nunca o valor. Continua valendo depois de definir a senha: +# uma credencial de socorro que caduca ao definir a senha e inutil justamente +# quando e necessaria. # DASHBOARD_RECOVERY_HASH= # ------------------------------------------------------------------------------ diff --git a/Makefile b/Makefile index b4e55ca..6583aa2 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,22 @@ -.PHONY: test test-container venv run status docker-build docker-run clean +.PHONY: 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: + @if [ -f .env ]; then \ + echo ".env ja existe — preservado."; \ + else \ + cp .env.example .env; \ + echo ".env criado a partir de .env.example."; \ + fi + @echo "" + @echo "Preencha no .env antes de subir a stack:" + @grep -nE '^[A-Z_]+=$$' .env | sed 's/^/ linha /' || echo " (nada obrigatorio em branco)" + @echo "" + @echo "O painel usa DASHBOARD_USER e DASHBOARD_PASSWORD. Sem senha definida," + @echo "o primeiro acesso usa a credencial de recuperacao gerada no boot." + VENV ?= .venv PYTHON ?= $(shell which $(VENV)/bin/python3 2>/dev/null || which python3 2>/dev/null) diff --git a/README.md b/README.md index 555119c..a5c7c34 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,88 @@ request; a push to `master` republishes the wiki automatically. --- +--- + +## 🔑 Signing in to the dashboard + +| | | +| :--- | :--- | +| **Address** | `http://localhost:9091` | +| **User** | `admin` — or whatever you set in `DASHBOARD_USER` | +| **Password** | the value of `DASHBOARD_PASSWORD` in your `.env` | + +There is **no factory password**, and that is deliberate: a fixed password shipped +in an image is public the moment the image is. You choose it once, in one place: + +```bash +cp .env.example .env +# edit .env: +DASHBOARD_USER=admin +DASHBOARD_PASSWORD= +``` + +Then bring the stack up. That user and that password are what the panel accepts. + +### Did not set a password, and now cannot get in? + +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 +``` + +Sign in as `admin` with that value, then set a real password on the screen. The +recovery credential keeps working afterwards — it is break-glass, and one that +stopped working the moment you set a password would be useless exactly when you +need it. + +> **Português:** o painel pede usuário e senha. O usuário é `admin` (ou o que +> estiver em `DASHBOARD_USER`) e a senha é a que **você** definir em +> `DASHBOARD_PASSWORD` no `.env` — não existe senha de fábrica, porque um valor +> fixo publicado na imagem é uma credencial pública. Se subiu sem definir senha, +> use o comando acima para ler a credencial de recuperação e entre com ela. + ## Running with Docker +### Configuração: `.env` a partir do exemplo + +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. + +```bash +make setup # cria o .env a partir do .env.example, sem sobrescrever um existente +``` + +O alvo lista, ao final, exatamente quais variáveis ficaram em branco e precisam +ser preenchidas. Preencha e suba a stack. + +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. + +### Portas, e por que cada uma é diferente + +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. + +| Serviço | Porta interna | Publicada no host | +| :--- | :--- | :--- | +| 9Router | `20128` | `20128` | +| OmniRoute | `20128` | `20129` | +| LiteLLM | `4000` | `20130` | +| 9RTKSync (painel) | `9090` | `9091` | +| OminiRTkSync (painel) | `9090` | `9092` | +| LiteLlmRTKSync (painel) | `9090` | `9093` | + +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. + + Official multi-architecture Docker images (`linux/amd64` and `linux/arm64`) are published automatically to the GitHub Container Registry (GHCR): ```bash diff --git a/docs/Egress-And-Multi-Session.md b/docs/wiki/Egress-And-Multi-Session.md similarity index 100% rename from docs/Egress-And-Multi-Session.md rename to docs/wiki/Egress-And-Multi-Session.md diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md index 9308769..04f808b 100644 --- a/docs/wiki/Home.md +++ b/docs/wiki/Home.md @@ -21,6 +21,7 @@ republishes these pages automatically. Editing a page directly here will be over | [Authentication](Authentication) | Credentials, headless mode, break-glass recovery | | [Logging](Logging) | Persistent file log, rotation, 30-day retention | | [Architecture](Architecture) | How the sync engine talks to the 9Router database | +| [Egress and Multi-Session](Egress-And-Multi-Session) | Why several accounts sharing one outbound address is the risk, and what the gateway models | | [Troubleshooting](Troubleshooting) | Concrete symptoms and what they actually mean | | [Upstream Fixes](Upstream-Fixes) | Bugs found in the gateways and the patches sent upstream | @@ -63,6 +64,29 @@ differ so they can run side by side: `9091` for 9RTKSync, `9092` for OminiRTKSyn --- +--- + +## Signing in to the dashboard + +| | | +| :--- | :--- | +| **Address** | `http://localhost:9091` | +| **User** | `admin` — or whatever `DASHBOARD_USER` says | +| **Password** | the value you set in `DASHBOARD_PASSWORD` | + +There is **no factory password**: a fixed one shipped in an image is public the +moment the image is. Set yours in `.env` before bringing the stack up. + +Brought it up without setting one? The container generated a recovery +credential on first boot — read it and sign in as `admin`, then set a real +password on the screen: + +```bash +docker exec router-sync cat /app/data/db/.dashboard_recovery +``` + +Full detail in [Authentication](Authentication). + ## License MIT — see [LICENSE](https://github.com/pathbit/9RTKSync/blob/master/LICENSE). diff --git a/docs/wiki/Installation.md b/docs/wiki/Installation.md index dbdb38d..63e86ef 100644 --- a/docs/wiki/Installation.md +++ b/docs/wiki/Installation.md @@ -143,3 +143,38 @@ docker compose up -d 9rtksync State that survives upgrades lives in the data volume: `.dashboard_auth.json` (screen-set credentials), `.dashboard_recovery` (break-glass hash) and `ui_prefs.sqlite` (interface language). None of them are stored in the gateway's own database. + +## Configuration: `.env` from the example + +Everything is configured by environment variable, read from a `.env` next to the +compose file — Compose finds it on its own, with no flag. + +```bash +make setup # creates .env from .env.example, never overwriting an existing one +``` + +The target then lists exactly which variables were left blank. Fill them in and +bring the stack up. + +`.env` is never versioned, and `.env.example` carries no secret value — a value +published in an example file is a public credential by definition. A test +guarantees every variable a compose requires exists in the example, so +`cp .env.example .env` never produces an incomplete `.env`. + +## Ports + +The three synchronizers listen on the **same port inside the container** +(`9090`) and publish on different host ports, so all three can run side by side. +Same for the gateways. + +| Service | Inside | Published | +| :--- | :--- | :--- | +| 9Router | `20128` | `20128` | +| OmniRoute | `20128` | `20129` | +| LiteLLM | `4000` | `20130` | +| 9RTKSync panel | `9090` | `9091` | +| OminiRTkSync panel | `9090` | `9092` | +| LiteLlmRTKSync panel | `9090` | `9093` | + +All bound to `127.0.0.1`: the gateway holds real credentials and should not be +reachable from the local network. diff --git a/docs/wiki/_Sidebar.md b/docs/wiki/_Sidebar.md index 303a895..4a928b8 100644 --- a/docs/wiki/_Sidebar.md +++ b/docs/wiki/_Sidebar.md @@ -7,6 +7,7 @@ - [Authentication](Authentication) - [Logging](Logging) - [Architecture](Architecture) +- [Egress and Multi-Session](Egress-And-Multi-Session) - [Troubleshooting](Troubleshooting) - [Upstream Fixes](Upstream-Fixes) diff --git a/src/nine_rtksync/web/render.py b/src/nine_rtksync/web/render.py index f724518..782c90f 100644 --- a/src/nine_rtksync/web/render.py +++ b/src/nine_rtksync/web/render.py @@ -664,6 +664,16 @@ def render_dashboard( .accordion-button:not(.collapsed) {{ background: var(--surface-2); color: #fff; box-shadow: none; }} .cron-log {{ white-space: pre-wrap; word-break: break-word; font-size: .8rem; color: #b9c0cf; background: #0b0d12; border: 1px solid var(--line); border-radius: .35rem; padding: .6rem; }} + /* Barra de acoes do cabecalho: todos os controles com a MESMA altura. + O seletor de idioma carrega so a bandeira, um elemento com altura + propria; sem texto ao lado para definir a linha, ele esticava o botao e + ficava mais alto que os vizinhos. Fixar a altura em todos resolve na + origem, em vez de compensar caso a caso. */ + .barra-acoes {{ display: flex; align-items: stretch; gap: .5rem; }} + .barra-acoes > * {{ display: flex; align-items: center; }} + .barra-acoes .btn {{ height: 2rem; padding-top: 0; padding-bottom: 0; + display: inline-flex; align-items: center; line-height: 1; }} + .barra-acoes .fi {{ line-height: 1; }} @@ -682,7 +692,7 @@ def render_dashboard(

-
+
{render_language_switcher(lang)}