commit 0db766cd18c714fc020048f6a15c22298de47b77
parent 5f03b36f0a64d53c38be79f5dc6fe31d3fea0423
Author: Pablo Murad <pablo@pablomurad.com>
Date: Sun, 22 Mar 2026 14:33:26 -0300
documentação: still
Diffstat:
58 files changed, 824 insertions(+), 3939 deletions(-)
diff --git a/DOCS_REBUILD_CHANGELOG.md b/DOCS_REBUILD_CHANGELOG.md
@@ -0,0 +1,102 @@
+# Changelog da reconstrução da documentação (runv-server)
+
+Documento em **pt-BR**. Data da passagem: conforme o commit em que este ficheiro foi adicionado.
+
+## Ficheiros criados (canónico `docs/`)
+
+| Ficheiro | Função |
+|----------|--------|
+| [docs/README.md](docs/README.md) | Porta de entrada, ordem de leitura, mapa rápido |
+| [docs/00-overview.md](docs/00-overview.md) | Visão geral, limites público/privado, fontes de verdade |
+| [docs/01-server-baseline-debian.md](docs/01-server-baseline-debian.md) | Debian, tempo, locale, pré-requisitos |
+| [docs/02-admin-access-and-ssh.md](docs/02-admin-access-and-ssh.md) | Modelo root/admin, SSH |
+| [docs/03-paths-files-and-state.md](docs/03-paths-files-and-state.md) | Caminhos `/var/lib/runv`, logs, email, web |
+| [docs/04-bootstrap-and-base-system.md](docs/04-bootstrap-and-base-system.md) | `starthere.py`, quotas ext4, Apache, UFW |
+| [docs/05-tools-and-system-experience.md](docs/05-tools-and-system-experience.md) | `tools.py`, MOTD, skel, jail SSH |
+| [docs/06-site-and-apache.md](docs/06-site-and-apache.md) | `genlanding.py`, DocumentRoot, TLS |
+| [docs/07-public-members-directory.md](docs/07-public-members-directory.md) | `build_directory.py`, `members.json`, privacidade |
+| [docs/08-email.md](docs/08-email.md) | Mailgun, legado msmtp, ficheiros de estado |
+| [docs/09-terminal-entre.md](docs/09-terminal-entre.md) | Conta `entre`, fila, limites (não provisiona Unix) |
+| [docs/10-user-provisioning-and-admin-ops.md](docs/10-user-provisioning-and-admin-ops.md) | `create_runv_user.py`, fluxo de aprovação |
+| [docs/11-daily-operations.md](docs/11-daily-operations.md) | Operação corrente |
+| [docs/12-security-and-privacy.md](docs/12-security-and-privacy.md) | Confiança, dados sensíveis |
+| [docs/13-troubleshooting.md](docs/13-troubleshooting.md) | Erros frequentes |
+| [docs/14-smoke-tests-and-validation.md](docs/14-smoke-tests-and-validation.md) | Verificações seguras |
+| [docs/15-glossary-and-reference.md](docs/15-glossary-and-reference.md) | Glossário, índice de scripts |
+| [docs/diagrams/architecture.mmd](docs/diagrams/architecture.mmd) | Sequência SSH entre → fila (Mermaid) |
+| [docs/diagrams/member-flow.mmd](docs/diagrams/member-flow.mmd) | Fluxo pedido → admin → dados públicos (Mermaid) |
+
+Alteração mínima **fora** de `docs/` para não quebrar referências em código ou templates:
+
+- [README.md](README.md) (raiz): ponteiro para `docs/README.md`.
+- `tools/tools.py`, `site/build_directory.py`, `email/configure_mailgun.py`, `email/configure_msmtp_legacy.py`: docstrings / mensagens apontam para `docs/…` em vez de `.md` removidos nos módulos.
+- `terminal/templates/admin_mail.txt`: linha de ajuda ao admin aponta para `docs/10-user-provisioning-and-admin-ops.md` (antes referia `terminal/docs/ADMIN.md`, removido).
+
+**Nota:** existiu cópia errónea em `dev-notes/DOCS_REBUILD_CHANGELOG.md`; foi removida — a versão canónica é **sempre** este ficheiro na raiz.
+
+## Fontes de evidência usadas
+
+- Código Python em `scripts/admin/`, `terminal/`, `site/`, `tools/`, `email/`, `patches/`.
+- `terminal/config.example.toml`, exemplos em `site/example-users.json`.
+- Documentação modular **antes da remoção**: `INSTALL.md` (raiz), `site/*.md`, `terminal/docs/*.md`, `tools/docs/*.md`, `email/docs/*.md`, `scripts/**/*.md`, `dev-notes/RUNV_CURRENT_STATE_AUDIT.md`.
+- `terminal/docs/ARCHITECTURE.md` (fluxo e componentes).
+- Diff e defaults nos scripts (caminhos predefinidos, flags).
+
+## Contradições identificadas e como foram tratadas
+
+1. **Cron vs refresh “sem cron”**
+ - `INSTALL.md` sugeria exemplo de cron para `build_directory.py`.
+ - `site/README.md` enfatizava refresh via `create_runv_user.py` / `genlanding.py` sem cron.
+ - **Resolução:** em `docs/07-public-members-directory.md` (e operações diárias) ficam explícitos **dois modos válidos**: regeneração automática nos fluxos de provisionamento/landing **ou** cron/manual — não são mutuamente exclusivos.
+
+2. **`USO.md` inexistente**
+ - `terminal/docs/ARCHITECTURE.md` referia `USO.md`, que não existia no repositório.
+ - **Resolução:** descrito em `docs/09-terminal-entre.md` e neste changelog; o fluxo cobre-se nos docs canónicos.
+
+3. **Múltiplos `INSTALL.md` por módulo**
+ - Conteúdo sobreposto entre raiz e `tools/`, `email/`, `terminal/`.
+ - **Resolução:** um único percurso numerado em `docs/01`–`docs/11`, com secções por componente.
+
+## O que ficou explicitamente **NÃO VERIFICADO** neste ambiente
+
+- Execução de `--help` e comportamento em runtime de `scripts/admin/create_runv_user.py`, `terminal/setup_entre.py` e `site/genlanding.py` em **Windows**: falham no import (`fcntl` / `grp` inexistentes). **Verificação plena:** correr em **Debian/Linux** alvo.
+- Estado real de um servidor de produção (Apache vhosts, TLS, quotas aplicadas, conteúdo de `/var/lib/runv/users.json`): apenas inferência a partir de defaults no código — qualquer deploy concreto deve ser confirmado no servidor.
+
+## Verificações executadas (2026-03-22, Windows / PowerShell)
+
+| Comando | Resultado |
+|---------|-----------|
+| `python -m compileall -q scripts terminal site tools email patches` | Exit 0 |
+| `cd email && python -m pytest tests/ -q` | 11 passed |
+| `python site/build_directory.py --users-json site/example-users.json --dry-run` | JSON válido no stdout (`username`, `since`, `path`) |
+| `python site/build_directory.py --help` | Exit 0 |
+| `python email/configure_mailgun.py --help` | Exit 0 |
+| `python scripts/admin/create_runv_user.py --help` | **Falha:** `ModuleNotFoundError: fcntl` |
+| `python terminal/setup_entre.py --help` | **Falha:** `ModuleNotFoundError: grp` |
+| `python site/genlanding.py --help` | **Falha:** `ModuleNotFoundError: grp` |
+| `git status -sb` | Registado no momento da passagem (working tree com `docs/` e alterações pendentes) |
+
+## Pressupostos dependentes do ambiente (operador)
+
+- **Um único host Debian** com paths tipo `/var/www/runv.club/html`, `/var/lib/runv/`, `/opt/runv/terminal/` — são defaults no código; outros caminhos exigem flags explícitas.
+- **Root/sudo** para bootstrap, `tools.py`, email, `entre`, provisionamento de utilizadores.
+- **Decisões de segurança** (firewall, TLS, política de passwords SSH) combinam o que o repo automatiza com o que o operador mantém — ver `docs/02` e `docs/12`.
+
+## Documentação `.md` removida nesta reconstrução
+
+Removidos de propósito para evitar duplicação e contradições com `docs/` (lista não exaustiva de paths relativos à raiz do repo):
+
+- `INSTALL.md`
+- `dev-notes/RUNV_MEMBER_BUBBLE_CHANGELOG.md` (changelog local; conteúdo operacional relevante está em `docs/07` e `docs/10`)
+- `dev-notes/RUNV_CURRENT_STATE_AUDIT.md`
+- `site/README.md`, `site/build_directory.md`, `site/genlanding.md`, `site/news/README.md`
+- `terminal/README.md`, `terminal/docs/INSTALL.md`, `terminal/docs/ARCHITECTURE.md`, `terminal/docs/ADMIN.md`
+- `tools/README.md`, `tools/skel/README.md`, `tools/docs/INSTALL.md`, `tools/docs/ADMIN.md`, `tools/docs/USER_EXPERIENCE.md`
+- `email/README.md`, `email/docs/INSTALL.md`, `email/docs/ADMIN.md`, `email/docs/INTEGRATION.md`, `email/docs/TROUBLESHOOTING.md`
+- `scripts/starthere.md`, `scripts/skel.md`, `scripts/create_runv_user.md`, `scripts/del-user.md`, `scripts/admin/perm1.md`, `scripts/doom/doom.md`, `scripts/docs/*.md`
+
+**Não removido:** `email/.pytest_cache/README.md` (artefacto gerado por pytest; não é documentação do produto).
+
+## Adequação para um operador novo
+
+A documentação é **utilizável** para alguém que não escreveu o código, desde que leia `docs/README.md` na ordem sugerida e execute verificações em **Debian** onde os scripts Unix são relevantes. Lacunas conhecidas estão marcadas como NÃO VERIFICADO ou recomendação, sem afirmar CI/deploy que não exista no repositório.
diff --git a/INSTALL.md b/INSTALL.md
@@ -1,184 +0,0 @@
-# Instalação do servidor runv.club
-
-Este documento descreve a **ordem recomendada** para preparar um servidor Debian (testado em Debian 13 “trixie”) com os scripts deste repositório, e onde aprofundar a configuração de cada módulo.
-
-**Pré-requisitos**
-
-- Acesso **root** (ou `sudo`) no servidor.
-- Repositório clonado num caminho fixo (ex.: `/root/runv-server` ou `/opt/runv/src`). Os exemplos abaixo assumem que o diretório raiz do clone é `REPO` — substitua pelo caminho real.
-
-**Convenções de caminhos no servidor**
-
-| Caminho | Função |
-|--------|--------|
-| `/var/lib/runv/users.json` | Metadados dos utilizadores (criado na primeira operação que o use) |
-| `/var/lib/runv/users.lock` | Lock para escrita segura em `users.json` |
-| `/var/lib/runv/entre-queue` | Fila de pedidos do SSH «entre» |
-| `/var/log/runv/` | Logs do terminal `entre` |
-| `/opt/runv/terminal/` | Cópia instalada do módulo `terminal/` |
-
----
-
-## Ordem geral (resumo)
-
-1. **Bootstrap do sistema** — `scripts/admin/starthere.py` (Apache, SSH, firewall, quotas, pacotes base).
-2. **Ferramentas e ficheiros globais** — `tools/tools.py` (MOTD, skel, binários, pacotes do manifest).
-3. **Site Apache / landing** — `site/genlanding.py`.
-4. **Dados públicos da landing** — `site/build_directory.py` (idealmente em **cron**).
-5. **Email de saída (Mailgun API, predefinido)** — `email/configure_mailgun.py` (+ documentação em `email/docs/`; legado SMTP: `configure_msmtp_legacy.py`).
-6. **SSH restrito «entre»** — `terminal/setup_entre.py`.
-7. **Operação** — `create_runv_user.py`, `update_user.py`, `del-user.py` (e só em cenários controlados: `scripts/doom/doom.py`).
-
-Os passos 2–6 podem ser ajustados conforme já tiveres Apache ou email configurados; a ordem acima minimiza dependências (Apache antes de publicar; `users.json` antes do cron que o lê).
-
----
-
-## 1. Bootstrap: `starthere.py`
-
-**Objetivo:** instalar e preparar Apache, OpenSSH, UFW, quotas, pacotes úteis e hardening básico.
-
-```bash
-cd REPO/scripts/admin
-sudo python3 starthere.py --help # rever opções
-sudo python3 starthere.py # ou com flags que precisares
-```
-
-**Nota:** este script **não** configura email (Mailgun/msmtp) nem o utilizador `entre`. Consulta também os Markdown em `scripts/docs/` se existirem no teu clone.
-
----
-
-## 2. Ferramentas globais: `tools/tools.py`
-
-**Objetivo:** aplicar manifest de pacotes APT, scripts em `/usr/local/bin`, MOTD, skel de utilizador, etc.
-
-```bash
-cd REPO/tools
-sudo python3 tools.py --help
-sudo python3 tools.py # execução real (requer root conforme o script)
-```
-
-Detalhes: `tools/docs/INSTALL.md` (se presente).
-
----
-
-## 3. Landing Apache: `genlanding.py`
-
-**Objetivo:** virtual host, site estático, opcionalmente Certbot.
-
-```bash
-cd REPO/site
-sudo python3 genlanding.py --help
-sudo python3 genlanding.py --domain runv.club # exemplo; ajustar domínio e flags
-```
-
-Garante que o DNS aponta para o servidor antes de TLS com Certbot.
-
----
-
-## 4. Dados públicos: `build_directory.py`
-
-**Objetivo:** gerar ficheiros consumidos pela landing a partir de `/var/lib/runv/users.json`.
-
-```bash
-cd REPO/site
-python3 build_directory.py --help
-```
-
-**Cron (exemplo)** — executar como utilizador com permissão de leitura a `users.json` e escrita no destino web:
-
-```cron
-*/15 * * * * cd /caminho/para/REPO/site && /usr/bin/python3 build_directory.py
-```
-
-Ajusta intervalo e caminhos conforme a política do servidor.
-
----
-
-## 5. Email (Mailgun API + opcional legado SMTP): `email/configure_mailgun.py`
-
-**Objetivo:** estado em `/etc/runv-email.json`, segredos em `/etc/runv-email.secrets.json`, envio via **API HTTP Mailgun** (sem msmtp no caminho predefinido). Modo **SMTP/msmtp** apenas com `--legacy-smtp` ou `configure_msmtp_legacy.py`.
-
-```bash
-cd REPO/email
-sudo python3 configure_mailgun.py --help
-sudo python3 configure_mailgun.py --dry-run # simular
-sudo python3 configure_mailgun.py # aplicar (root)
-```
-
-O ficheiro `configure_msmtp.py` apenas indica os comandos actualizados (Mailgun ou legado).
-
-Documentação completa:
-
-- `email/docs/INSTALL.md`
-- `email/docs/ADMIN.md`, `TROUBLESHOOTING.md`, `INTEGRATION.md`
-- `email/README.md`
-
-Scripts auxiliares (legado / diagnóstico): `email/scripts/send_test_mail.sh`, `email/scripts/diagnose_msmtp.sh`.
-
----
-
-## 6. Terminal SSH «entre»: `setup_entre.py`
-
-**Objetivo:** utilizador `entre`, cópia do módulo para `/opt/runv/terminal`, drop-in `sshd_config`, filas e logs.
-
-```bash
-cd REPO/terminal
-sudo python3 setup_entre.py --help
-sudo python3 setup_entre.py
-```
-
-- Coloca chaves em `~entre/.ssh/authorized_keys` antes de confiar em acessos.
-- Integração com email (avisos): `email/docs/INTEGRATION.md` e `terminal/docs/INSTALL.md`.
-
-Arranque do serviço Python (systemd ou manual) está descrito na documentação do terminal.
-
----
-
-## 7. Operação: contas runv
-
-Todos em `REPO/scripts/admin/` (executar como **root** salvo indicação em contrário).
-
-| Script | Uso |
-|--------|-----|
-| `create_runv_user.py` | Criar utilizador Unix + home + quota + entrada em `users.json` |
-| `update_user.py` | Atualizar metadados / quota / estado |
-| `del-user.py` | Remover utilizador e limpar metadados (com locks e confirmações) |
-
-**Ordem típica na vida real:** após infraestrutura (passos 1–6), usas `create_runv_user.py` para cada membro; `build_directory.py` (cron) mantém o site alinhado com `users.json`.
-
-### `scripts/doom/doom.py` (perigoso)
-
-Remove **todas** as contas listadas em `users.json` exceto a indicada por `--keep`. Só em ambientes de teste ou com backups e confirmação explícita.
-
-```bash
-cd REPO/scripts/doom
-sudo python3 doom.py --help
-```
-
----
-
-## Verificação rápida (checklist)
-
-- [ ] `sshd -t` sem erros após drop-in do `entre`.
-- [ ] `apache2ctl configtest` / `apachectl configtest` OK após `genlanding.py`.
-- [ ] `systemctl status apache2` e `ssh` ativos.
-- [ ] Email: `send_test_mail.sh` ou equivalente a partir da documentação do email.
-- [ ] Ficheiro `users.json` coerente e `build_directory.py` gera saída esperada.
-- [ ] Login SSH como `entre` executa apenas o menu esperado (ForceCommand).
-
----
-
-## Documentação por pasta
-
-| Pasta | Documentos |
-|-------|------------|
-| `email/` | `README.md`, `docs/INSTALL.md`, `ADMIN.md`, … |
-| `terminal/` | `docs/INSTALL.md` |
-| `tools/` | `docs/INSTALL.md` |
-| `scripts/` | `docs/*.md` (conforme o repositório) |
-
----
-
-## Nota sobre o código Python
-
-Os scripts usam `subprocess` com **listas de argumentos** (sem `shell=True` nas invocações analisadas), o que reduz risco de injeção de comando. Antes de atualizar em produção, convém correr `python3 -m compileall` na raiz do repositório e testar com `--dry-run` onde o script o suportar.
diff --git a/README.md b/README.md
@@ -1,3 +1,7 @@
# runv-server
-Repositório de automação e documentação para **runv.club** (pubnix Debian).
+Automação e conteúdo para **runv.club** (pubnix Debian): bootstrap, site estático Apache, email, SSH `entre`, provisionamento de contas.
+
+**Documentação canónica:** [docs/README.md](docs/README.md)
+
+**Changelog desta reconstrução:** [DOCS_REBUILD_CHANGELOG.md](DOCS_REBUILD_CHANGELOG.md)
diff --git a/RUNV_CURRENT_STATE_AUDIT.md b/RUNV_CURRENT_STATE_AUDIT.md
@@ -1,224 +0,0 @@
-# RUNV_CURRENT_STATE_AUDIT.md
-
-Auditoria do **estado actual** do repositório **runv-server**, tratado como linha de base nova.
-**Não** se inferem regressões a partir do histórico Git; apenas o que está presente **agora** no working tree e o que foi **verificado** por leitura de ficheiros e comandos indicados.
-
-**Comandos executados nesta passagem**
-
-| Comando | Resultado |
-|---------|-----------|
-| `python -m compileall -q scripts terminal site tools email patches` | Exit code **0** (sem erros reportados). |
-| `python -m pytest tests/ -q` em `email/` | **11 passed** em ~0,25 s. |
-| `python site/build_directory.py --users-json site/example-users.json --dry-run` | Emitiu JSON no stdout (amostra com `alice`); **não verificado** código de saída no shell (PowerShell reportou -1 sem indicar falha lógica do script). |
-| `git status -sb` | `## main...origin/main [ahead 1]` e ` D RUNV_SERVER_AUDIT.md`. |
-
-**Não executado:** `bandit`, `ruff`, `mypy`, testes fora de `email/tests/`, workflows CI (não existem em `.github/` na raiz do repo — **ver secção 10**).
-
----
-
-## 1. Resumo executivo
-
-O snapshot actual contém um conjunto **coerente** de módulos Python (stdlib) e documentação para operar um pubnix Debian: separação **explícita** entre o fluxo SSH `entre` (fila) e o provisionamento via `create_runv_user.py`; geração de dados públicos **filtrada** em `build_directory.py`; fila de pedidos com criação de ficheiro **exclusiva** (`O_EXCL`). Não há `shell=True` / `os.system` / `eval` nos `.py` verificados em `scripts/`, `terminal/`, `site/`, `tools/`, `email/`, `patches/` (além de **comentários** em `tools/tools.py` e `email/lib/mailer.py`). A qualidade de barreira automática global é **fraca** (sem CI na raiz, um único pacote de testes em `email/tests/`). A higiene Git no momento da verificação mostra **um ficheiro de auditoria anterior removido** do índice (`RUNV_SERVER_AUDIT.md`) e ramo **ahead 1** face a `origin/main`. A pasta `.cursor/skills/` existe no disco local e **não** está em `.gitignore` — risco de ruído se alguém fizer `git add .`.
-
----
-
-## 2. Veredito global
-
-**Coherent enough to continue** (pt: *coerente o suficiente para continuar*).
-
-**Justificativa:** invariantes arquitecturais principais estão **reflectidas no código actual** e a árvore **compila**; há testes **passando** onde foram corridos. Os problemas são sobretudo **manutenibilidade** (script de provisionamento muito grande, política duplicada), **ausência de CI**, e **higiene/ignore** — não contradições estruturais que impeçam evolução incremental segura com disciplina.
-
----
-
-## 3. O que está claramente correcto neste momento
-
-| Item | Evidência |
-|------|-----------|
-| `entre_app` declara não criar contas Linux | `terminal/entre_app.py` L5–L6. |
-| `entre_app` não invoca `adduser`/`useradd` | Grep sem matches em `terminal/entre_app.py`. |
-| Provisionamento de **membros** via `adduser` no script canónico | `scripts/admin/create_runv_user.py` L722–L741 (`run_adduser` com lista de argumentos). |
-| Fila: ficheiro novo não substitui existente | `terminal/entre_core.py` `save_request_json` L487–L490 (`O_CREAT\|O_EXCL`). |
-| Dataset público mínimo | `site/build_directory.py` L96–L105 (`username`, `since`, `path`, opcional `homepage_mtime`). |
-| Caminhos default alinhados entre example e docs de produto | `terminal/config.example.toml` L5–7 (`queue_dir`, `log_file`, `templates_dir`); `build_directory.py` default `--users-json` L31–33 (`/var/lib/runv/users.json`). |
-| Compilação Python dos pacotes listados | `compileall` exit 0 (**verificado**). |
-| Testes do submódulo email | `pytest` **11 passed** (**verificado**). |
-
----
-
-## 4. Problemas críticos
-
-**Nenhum bloqueador estrutural** identificado só com base no código e verificações acima: a separação fila vs provisionamento mantém-se; o output público analisado não inclui email nem fingerprint.
-
-**Atenção operacional (não é “bug” de código, mas risco de uso):** `create_runv_user.py` omite refresh da landing se `--landing-document-root` não existir (help L1565–L1568). Operador pode assumir lista actualizada sem verificar path — **documentação** deve deixar isso explícito em runbook (parcialmente já no help).
-
----
-
-## 5. Achados por severidade
-
-### Alta
-
-- **(Nenhum)** com evidência de falha de segurança directa no código revisto (subprocess inseguro, leak público de campos sensíveis no `build_directory`, overwrite de fila).
-
-### Média
-
-- **M1 — Política de validação duplicada:** `USERNAME_PATTERN`, `EMAIL_PATTERN`, listas de nomes reservados e tipos de chave aparecem em `terminal/entre_core.py` (ex. L33–L74) e em `scripts/admin/create_runv_user.py` (ex. L72–80); comentário em `entre_core.py` L31–L32 admite **não** importar em runtime — risco de **deriva**.
-
-- **M2 — Concentração de complexidade:** `create_runv_user.py` é ficheiro **muito grande** (ordem de ~2000 linhas com docstring inicial extensa) — **difícil** de rever e testar de ponta a ponta sem suite dedicada (**NOT VERIFIED:** contagem exacta de linhas nesta passagem).
-
-- **M3 — Ausência de CI na raiz do projecto:** glob `.github/*` na raiz devolveu **0** ficheiros; workflows encontram-se apenas sob `.cursor/skills/...` (não fazem parte do produto runv). **NOT VERIFIED:** existência de CI noutro remoto ou branch.
-
-### Baixa
-
-- **L1 — `.gitignore` não ignora `.cursor/`:** `.gitignore` actual L1–28 não menciona `.cursor`; se a pasta `skills` for grande, `git add .` pode poluir o índice.
-
-- **L2 — `site/README.md` vs `INSTALL.md` sobre cron:** `INSTALL.md` recomenda exemplo de cron para `build_directory.py` (L87–91); `site/README.md` L19 menciona regeneração via `create_runv_user` / `genlanding` “(sem cron)”. São **modos alternativos**, mas a redacção pode confundir quem procura uma única verdade operacional.
-
-- **L3 — Estado Git:** ` D RUNV_SERVER_AUDIT.md` — ficheiro marcado como removido no índice/working tree no momento do `git status`; resolver antes de push (**snapshot only**).
-
----
-
-## 6. Segurança
-
-| Tema | Avaliação |
-|------|-----------|
-| `shell=True` / `os.system` / `eval` em `.py` dos dirs de produto | **Não encontrado** (grep em `scripts`, `terminal`, `site`, `tools`, `email`, `patches`; excepção: linhas de **comentário** em `tools/tools.py` L5, `email/lib/mailer.py` L5). |
-| Fila — corrida / overwrite | **Mitigado** por `O_EXCL` (`entre_core.py` L487–L490). |
-| Dados sensíveis no JSON da fila | Presentes no **payload** (`entre_core.py` L512–L518: `email`, `public_key_fingerprint`) — **esperado** para revisão admin; **não** copiados para `members.json` pelo `build_directory.py` (L96–L105). |
-| `useradd` no terminal | Apenas para criar o utilizador de sistema **`entre`** em `setup_entre.py` L246–254 — **distinto** do provisionamento de membros. |
-| Temp files para `ssh-keygen` | `tempfile.mkstemp` + `unlink` em `finally` (`entre_core.py` L218–L242). |
-| Config example `entre` | `config.example.toml` L11–14: `admin_email` vazio, remetente `noreply@runv.club` — não expõe segredos; paths padrão sensatos. |
-| Análise estática de segurança (bandit) | **NOT VERIFIED** (não executado). |
-
----
-
-## 7. Operação
-
-- **Ordem em `INSTALL.md` L22–31** (bootstrap → tools → site → build público → email → terminal → operações) é **logicamente consistente** com dependências típicas (Apache antes de publicar; metadados antes de consumo no site).
-
-- **Paths hardcoded** (`/var/lib/runv/...`, `/opt/runv/terminal`, `/etc/runv-email.json`) são **consistentes** entre example TOML, defaults em scripts e `until.md` / `INSTALL.md` — adequados a um deploy Debian único; ambientes multi-tenant exigiriam overrides (**fora do escopo verificado**).
-
-- **Debian / ext4 / quotas:** `starthere.py` docstring L14–18 restringe automatismo de quota a **ext4** — assumido e documentado; **NOT VERIFIED** em VM.
-
----
-
-## 8. Documentação vs código
-
-| Tópico | Situação |
-|--------|----------|
-| `entre` não cria membros | **Alinhado** (`entre_app.py` L5–L6; sem `adduser` no `entre_app`). |
-| `create_runv_user` canónico | **Alinhado** (docstring L3–L37; `run_adduser` L722+). |
-| Membros públicos filtrados | **Alinhado** (`build_directory.py` L96–L105). |
-| Cron vs hooks de refresh | **Nuance:** dois discursos (`INSTALL` vs `site/README`); **não** contradição lógica, falta harmonização de linguagem. |
-| Refresh landing após user | **Código** exige DocumentRoot existente (`create_runv_user.py` L1565–L1568, fluxo ~L1872+); **operadores** devem ler o help — risco de suposição errada. |
-
----
-
-## 9. Qualidade de código / manutenibilidade
-
-- **Grande:** `scripts/admin/create_runv_user.py` concentra política, I/O, subprocess, jail, quota, metadata, refresh landing — **serviceable** mas **pesado** para onboarding de novos contribuidores.
-
-- **Duplicação:** regras de validação entre `entre_core.py` e `create_runv_user.py` (**médio** risco de deriva).
-
-- **Testes:** `email/tests/test_mailgun_client.py` cobre cliente Mailgun (**11** testes, **verificado**). **Sem** suite equivalente visível para `entre_core`, `build_directory`, ou locks de `users.json` (**NOT VERIFIED:** outros testes escondidos).
-
-- **“Feio mas seguro” vs “perigoso”:** o código analisado cai sobretudo em **feio mas seguro** no que toca a subprocess e dados públicos; **perigoso** seria `shell=True` com entrada do utilizador — **não observado** nos dirs de produto.
-
----
-
-## 10. Higiene do repositório (snapshot actual)
-
-- **`git status -sb`:** `main...origin/main [ahead 1]`; ` D RUNV_SERVER_AUDIT.md`.
-
-- **`.github/workflows` na raiz:** **ausente** (0 ficheiros em `z:/Code/runv-server/.github/*`).
-
-- **`.cursor/skills`:** pasta presente sob `.cursor/` no ambiente local (**NOT VERIFIED** tamanho total); **não** listada no `.gitignore`.
-
-- **`.gitignore`:** ignora `terminal/config.toml`, artefactos de news, segredos de email — **razoável** para deploy.
-
----
-
-## 11. Matriz: manter / corrigir leve / refactor / rebuild
-
-| Área | Classificação | Nota |
-|------|---------------|------|
-| **terminal/** | **Refactor** (leve) | Extrair política partilhada ou testes de contrato reduziriam deriva. |
-| **scripts/admin/** | **Fix lightly** + **Refactor** (faseado) | Manter como fonte de verdade; quebrar em módulos seria refactor **sem urgência** se houver testes primeiro. |
-| **site/** | **Manter** | `build_directory.py` claro. |
-| **tools/** | **Manter** | Manifest + cópias; alinhado a comentários de segurança. |
-| **email/** | **Manter** | Testes existentes passam. |
-| **docs/** | **Fix lightly** | Harmonizar cron vs “sem cron”; realçar pré-requisito do DocumentRoot. |
-| **patches/** | **Manter** | Auxiliar. |
-
-**Rebuild:** **não** justificado pelo estado actual verificado.
-
----
-
-## 12. Ficheiros que merecem atenção primeiro
-
-1. `scripts/admin/create_runv_user.py` — tamanho, refresh condicional da landing.
-2. `terminal/entre_core.py` + `scripts/admin/create_runv_user.py` — duplicação de regex/listas.
-3. `site/README.md` + `INSTALL.md` — narrativa cron / refresh.
-4. `.gitignore` — considerar `.cursor/` ou documentar “nunca adicionar skills ao repo runv”.
-5. Estado Git — `RUNV_SERVER_AUDIT.md` removido: decidir commit ou restauração.
-6. `until.md` — bom índice; manter coerente com defaults dos scripts.
-
----
-
-## 13. Próximas 10 acções (ordenadas)
-
-1. Resolver `git status` (commit ou descartar remoção de `RUNV_SERVER_AUDIT.md`; alinhar `ahead 1`).
-2. Garantir que `.cursor/` não entra no histórico do runv (ignore ou política de equipa).
-3. Adicionar CI mínimo na **raiz**: `python -m compileall -q …` + `pytest email/tests`.
-4. Checklist de release: comparar `USERNAME_PATTERN` / reservados entre `entre_core` e `create_runv_user`.
-5. Runbook: “DocumentRoot tem de existir para refresh automático de `members.json`”.
-6. Unificar parágrafo sobre cron em `site/README.md` / `INSTALL.md`.
-7. (Opcional) `bandit -r scripts terminal site tools email patches` e registar resultados.
-8. Smoke: `python3 site/build_directory.py --dry-run` com cópia de `users.json` de teste.
-9. Smoke: `python3 -m py_compile terminal/entre_app.py terminal/entre_core.py`.
-10. Documentar ausência de `.github/workflows` no projecto ou adicionar um workflow simples.
-
----
-
-## 14. Smoke tests manuais seguros (próximos)
-
-- `python -m compileall -q scripts terminal site tools email patches`
-- `cd email && python -m pytest tests/ -q`
-- `python site/build_directory.py --users-json site/example-users.json --dry-run`
-- `python scripts/admin/create_runv_user.py --help` (rever flags de landing/quota)
-- `python terminal/setup_entre.py --help` (em máquina de desenvolvimento, sem sudo se possível)
-
----
-
-## 15. Questões abertas / incertezas
-
-- **Conteúdo do commit “ahead 1”** local face a `origin/main` — **NOT VERIFIED** (`git show` não executado).
-- **Bandit / Ruff / mypy** — **NOT VERIFIED**.
-- **Páginas HTML em `public/`** cumprem regra de rodapé em 100% dos ficheiros — **NOT VERIFIED** (não revisto ficheiro a ficheiro).
-- **Configuração real em produção** (PAM, Mailgun, Apache) — **NOT VERIFIED**.
-
----
-
-## 16. Apêndice de evidências
-
-| ID | Afirmação | Evidência |
-|----|-----------|-----------|
-| E1 | `entre` não provisiona membro no app | `terminal/entre_app.py` L5–L6 |
-| E2 | Sem adduser no entre_app | grep vazio em `terminal/entre_app.py` |
-| E3 | Fila atómica | `terminal/entre_core.py` L487–L490 |
-| E4 | Payload fila com PII técnico | `terminal/entre_core.py` L512–L518 |
-| E5 | Público mínimo | `site/build_directory.py` L96–L105 |
-| E6 | adduser para membros | `scripts/admin/create_runv_user.py` L729 |
-| E7 | useradd só `entre` | `terminal/setup_entre.py` L246–254 |
-| E8 | Default users.json | `site/build_directory.py` L31–33 |
-| E9 | Defaults fila/log TOML | `terminal/config.example.toml` L5–7 |
-| E10 | Landing default path | `scripts/admin/create_runv_user.py` L1562–L1568 |
-| E11 | compileall OK | comando executado, exit 0 |
-| E12 | pytest email | 11 passed |
-| E13 | Sem shell=True (prod dirs) | grep nos seis caminhos de produto |
-| E14 | git status snapshot | `main...origin/main [ahead 1]`, ` D RUNV_SERVER_AUDIT.md` |
-| E15 | Sem workflows na raiz | glob `z:/Code/runv-server/.github/*` → 0 |
-| E16 | .gitignore actual | `.gitignore` L1–28 |
-
----
-
-*Fim do relatório — estado actual apenas, sem inferência de histórico.*
diff --git a/docs/00-overview.md b/docs/00-overview.md
@@ -0,0 +1,48 @@
+# Visão geral
+
+[← Índice](README.md)
+
+## O que é o runv-server
+
+Repositório de **scripts (principalmente Python 3, biblioteca padrão)**, conteúdo estático web e documentação para operar um servidor **pubnix** estilo tilde (**runv.club**) em **Debian**. Não é uma aplicação web monolítica (sem `package.json` na raiz do produto).
+
+## Âmbito do repositório
+
+- **Infraestrutura:** bootstrap (`starthere.py`), quotas ext4, Apache, UFW, ferramentas globais (`tools.py`).
+- **Site público:** landing estática em `site/public/`, geração de Apache (`genlanding.py`), dados públicos de membros (`build_directory.py`).
+- **Email transacional:** Mailgun HTTP por defeito; modo legado SMTP/msmtp (`email/`).
+- **Pedidos de conta:** fluxo SSH ao utilizador `entre` (`terminal/`) — **fila em JSON**, sem criar contas Unix automaticamente.
+- **Provisionamento canónico:** `scripts/admin/create_runv_user.py` cria utilizador Unix, home, jail, quota, metadados.
+
+## Resumo arquitetural
+
+| Componente | Responsabilidade |
+|------------|------------------|
+| `terminal/` | Recolher, validar, enfileirar pedidos; **não** faz `adduser` de membros. |
+| `create_runv_user.py` | **Única** fonte canónica do fluxo de criação de conta membro (ordem documentada na docstring). |
+| `users.json` | Metadados dos membros no servidor (`/var/lib/runv/users.json`). |
+| Fila `entre-queue/` | Pedidos pendentes de revisão humana antes do provisionamento. |
+| `build_directory.py` | Lê `users.json` → gera `members.json` **filtrado** para o site. |
+
+Diagrama: [diagrams/architecture.mmd](diagrams/architecture.mmd).
+
+## Ciclo de vida de um novo membro
+
+1. Visitante liga `ssh entre@…` e preenche o fluxo guiado.
+2. Gera-se um ficheiro JSON na fila (`/var/lib/runv/entre-queue/`).
+3. **Admin** revê o pedido e executa `create_runv_user.py` (root).
+4. Actualiza-se `users.json`; opcionalmente regera-se `DocumentRoot/data/members.json` (constelação na landing).
+5. O membro aparece na lista pública **só** com campos não sensíveis.
+
+Diagrama: [diagrams/member-flow.mmd](diagrams/member-flow.mmd).
+
+## Fronteira dados públicos / privados
+
+- **Público (`members.json`):** apenas o que `build_directory.py` escreve: `username`, `since`, `path`, opcionalmente `homepage_mtime` (com `--homes-root`). Ver [07-public-members-directory.md](07-public-members-directory.md).
+- **Privado:** email, fingerprint de chave, quotas detalhadas, campos internos de `users.json` **não** são copiados para o JSON público (garantido no código de `build_directory.py`).
+
+## Fontes de verdade
+
+1. **Código** dos scripts referidos neste índice.
+2. O **`INSTALL.md` da raiz** e a documentação `.md` nos módulos foram **substituídos** por esta árvore `docs/` (conteúdo absorvido e harmonizado; ver `DOCS_REBUILD_CHANGELOG.md`).
+3. Docstrings de `starthere.py`, `create_runv_user.py`, `setup_entre.py`, `genlanding.py`, etc.
diff --git a/docs/01-server-baseline-debian.md b/docs/01-server-baseline-debian.md
@@ -0,0 +1,28 @@
+# Baseline do servidor Debian
+
+[← Índice](README.md)
+
+## Obrigatório (implícito nos scripts)
+
+- **Sistema:** **Debian** (o projecto referencia Debian 13 “trixie” em vários README históricos e docstrings; **não verificado** em cada release).
+- **Acesso:** capacidade de executar comandos como **root** (`sudo` ou sessão root) para bootstrap, `tools.py`, `genlanding.py`, `setup_entre.py`, `create_runv_user.py`.
+- **Python 3** instalado (scripts usam shebang `python3`).
+
+## Recomendação operacional (não imposta pelo repo)
+
+- **Hostname** coerente com DNS público se for servir `runv.club` ou outro domínio.
+- **Hora:** NTP/chrony para timestamps correctos em logs e `created_at` (o repo **não** configura NTP por si).
+- **Locale UTF-8** para terminais e logs legíveis — padrão Debian moderno.
+
+## Sistema de ficheiros e quotas
+
+- **`starthere.py`** e a lógica de quota em `create_runv_user.py` assumem **ext4** com **usrquota** no mount que contém `/home` (ou path de sonda). Se o FS não for ext4, a automatização de quota em `starthere.py` **falha de propósito** (mensagem de erro no script). **Evidência:** docstring `scripts/admin/starthere.py` (filesystem ext4).
+
+## O que o repositório não faz
+
+- Não escolhe hostname por si.
+- Não configura NTP, locale ou timezone como passo dedicado (tratar como **pré-requisito de exploração** ou configuração manual Debian).
+
+## Próximo passo
+
+[04-bootstrap-and-base-system.md](04-bootstrap-and-base-system.md) após garantir Debian + root + Python 3.
diff --git a/docs/02-admin-access-and-ssh.md b/docs/02-admin-access-and-ssh.md
@@ -0,0 +1,31 @@
+# Acesso administrativo e SSH
+
+[← Índice](README.md)
+
+## Modelo operacional
+
+- A maioria dos scripts de infraestrutura exige **root** no servidor alvo.
+- O repositório **não** define um “utilizador admin” específico além do que o Debian/OpenSSH já permitem: tipicamente **root com chave SSH** ou utilizador em `sudo`.
+
+## Facto do repositório
+
+- **`starthere.py`** documenta que **não** reconfigura SSH além do contexto do bootstrap (ver docstring: não mexe em SSH).
+- **`setup_entre.py`** configura SSH **só** para o utilizador especial `entre` (drop-in, PAM opcional, modos de auth documentados no script).
+
+## Recomendação de segurança (genérica, não codificada no repo)
+
+- Preferir **autenticação por chave** para a conta que usa para administrar o servidor.
+- Desactivar login root por palavra-passe em produção se a política o exigir — **não** é alteração feita automaticamente por estes scripts.
+
+## Distinção
+
+| Tema | Origem |
+|------|--------|
+| Chaves do **admin** no servidor | Política do operador / Debian |
+| Chave pública no **pedido `entre`** | Recolhida pelo fluxo `entre`, instalada **só** quando `create_runv_user.py` cria o membro |
+
+## Relação com scripts
+
+Sem root/sudo não é possível: `starthere.py`, `tools.py`, `genlanding.py` (sem `--dry-run`), `setup_entre.py`, `create_runv_user.py`.
+
+Próximo: [03-paths-files-and-state.md](03-paths-files-and-state.md).
diff --git a/docs/03-paths-files-and-state.md b/docs/03-paths-files-and-state.md
@@ -0,0 +1,32 @@
+# Caminhos, ficheiros e estado
+
+[← Índice](README.md)
+
+## Caminhos canónicos no servidor
+
+| Caminho | Função | Gerado / versionado |
+|---------|--------|---------------------|
+| `/var/lib/runv/users.json` | Lista de metadados dos membros (fonte para `build_directory.py`) | **Gerado** na primeira operação que use; **nunca** commitar com dados reais |
+| `/var/lib/runv/users.lock` | Lock `flock` para escrita segura em `users.json` | Gerado em uso |
+| `/var/lib/runv/entre-queue/` | Fila de pedidos JSON do SSH `entre` | Gerado; ficheiros por `request_id` |
+| `/var/log/runv/entre.log` | Log do fluxo `entre` (configurável via TOML) | Gerado |
+| `/opt/runv/terminal/` | Instalação do módulo `terminal/` (`setup_entre.py`) | Cópia a partir do repo; `config.toml` gerado localmente |
+| `/etc/runv-email.json` | Estado público de configuração de email | Gerado por `configure_mailgun.py` |
+| `/etc/runv-email.secrets.json` | Segredos (API keys, etc.) | Gerado; **0600**, root; **nunca** commitar |
+| `/var/www/runv.club/html` | DocumentRoot **predefinido** em produção (`genlanding.py`, default `--landing-document-root` em `create_runv_user.py`) | Gerado no servidor; não é o mesmo que `site/public/` no clone |
+
+**Evidência:** `docs/04-bootstrap-and-base-system.md`, `terminal/config.example.toml`, defaults em `site/genlanding.py`, `site/build_directory.py`, `scripts/admin/create_runv_user.py`.
+
+## No clone do Git
+
+- **`site/public/data/members.json`:** no repositório deve permanecer lista vazia `[]` (placeholder); dados reais vêm de `build_directory.py` no deploy.
+- **`terminal/config.toml`:** no `.gitignore`; usar `config.example.toml` + `gen_config_toml.py` / `setup_entre.py`.
+
+## O que nunca commitar
+
+- `runv-email.secrets.json` (qualquer cópia)
+- `users.json` com dados reais
+- Ficheiros JSON da fila com PII
+- Chaves privadas SSH
+
+Próximo: [04-bootstrap-and-base-system.md](04-bootstrap-and-base-system.md).
diff --git a/docs/04-bootstrap-and-base-system.md b/docs/04-bootstrap-and-base-system.md
@@ -0,0 +1,27 @@
+# Bootstrap e sistema base
+
+[← Índice](README.md)
+
+## Script: `scripts/admin/starthere.py`
+
+**O que faz** (docstring do script): actualiza APT; instala pacotes úteis; limpeza `autoremove`/`autoclean`; activa Apache2; se UFW inactivo, permite SSH/80/443 e activa UFW; descobre o filesystem que contém `/home`; adiciona `usrquota` ao `fstab` em ext4; remount / quotacheck / quotaon; activa quotas de utilizador.
+
+**O que não faz** (mesma docstring): não purga pacotes arbitrariamente; **não** configura email; **não** cria utilizadores; **não** mexe no SSH para além do contexto descrito; não instala stack de email.
+
+## Execução
+
+```bash
+cd REPO/scripts/admin
+sudo python3 starthere.py --help
+sudo python3 starthere.py
+```
+
+## Ordem sugerida
+
+O bootstrap é o **primeiro** passo lógico antes de `tools.py`, site, email e `entre` (ver [00-overview.md](00-overview.md) e ordem em documentação histórica absorvida).
+
+## Pressupostos
+
+- **ext4** no volume onde `/home` (ou path de sonda) reside — caso contrário o script aborta a parte de quotas automáticas.
+
+Próximo: [05-tools-and-system-experience.md](05-tools-and-system-experience.md).
diff --git a/docs/05-tools-and-system-experience.md b/docs/05-tools-and-system-experience.md
@@ -0,0 +1,32 @@
+# Ferramentas e experiência de sistema
+
+[← Índice](README.md)
+
+## Script: `tools/tools.py`
+
+**Função:** orquestrar no servidor Debian:
+
+1. Pacotes APT listados em `tools/manifests/apt_packages.txt` (alias `chat` → pacote `weechat`).
+2. Cópia de `tools/bin/` para `/usr/local/bin` (`runv-help`, `runv-links`, `runv-status`, `chat`, …).
+3. MOTD dinâmico: `tools/motd/60-runv` → `/etc/update-motd.d/60-runv`.
+4. Modelos para novas contas: `tools/skel/` → `/etc/skel/`.
+5. Drop-in SSH para utilizadores jailed: `tools/sshd/90-runv-jailed.conf` → `/etc/ssh/sshd_config.d/`.
+
+**Princípios declarados no código:** Python stdlib; **sem `shell=True`** em subprocess.
+
+## Execução
+
+```bash
+cd REPO/tools
+sudo python3 tools.py --help
+sudo python3 tools.py --dry-run --verbose # simular
+sudo python3 tools.py
+```
+
+Flags úteis: `--force`, `--skip-apt` (ver `--help`).
+
+## IRC / patches
+
+A rede IRC “da casa” e o comando `chat` ligam-se a `patches/patch_irc.py` conforme documentação histórica do módulo (código em `patches/`).
+
+Próximo: [06-site-and-apache.md](06-site-and-apache.md).
diff --git a/docs/06-site-and-apache.md b/docs/06-site-and-apache.md
@@ -0,0 +1,28 @@
+# Site público e Apache
+
+[← Índice](README.md)
+
+## Conteúdo estático
+
+- **`site/public/`:** HTML, CSS, JS servidos como DocumentRoot após `genlanding.py`.
+- A landing faz `fetch("data/members.json")` **relativo à URL** — o ficheiro efectivo é **`DocumentRoot/data/members.json`** (ver `site/public/assets/app.js`).
+
+## Script: `site/genlanding.py`
+
+- Configura VirtualHost Apache, `mod_userdir`, `mod_rewrite`, copia `site/public` → DocumentRoot.
+- Modo produção: domínio predefinido `runv.club`, DocumentRoot predefinido `/var/www/runv.club/html`.
+- Modo `--dev`: `runv.local`, `/var/www/runv-dev/html`.
+- Opcional: `--certbot` (incompatível com `--dev`).
+- Após cópia, por omissão chama `build_directory.py` para gravar `data/members.json` no DocumentRoot (`--no-refresh-members` para omitir).
+- Versão actual do script: constante `VERSION` no ficheiro (ex.: `0.04`).
+
+## TLS e DNS
+
+- **Recomendação:** DNS a apontar para o servidor antes de Certbot (documentado historicamente).
+
+## Constelação (bolhas)
+
+- Depende de `members.json` no DocumentRoot.
+- Após **`create_runv_user.py`:** se `--landing-document-root` existir como directório, o script tenta regerar `data/members.json` e imprime linha **`constelação (bolhas)`** ou **AVISO** se faltar path ou falhar o refresh (**evidência:** código actual em `create_runv_user.py`).
+
+Próximo: [07-public-members-directory.md](07-public-members-directory.md).
diff --git a/docs/07-public-members-directory.md b/docs/07-public-members-directory.md
@@ -0,0 +1,44 @@
+# Directório público de membros
+
+[← Índice](README.md)
+
+## Script: `site/build_directory.py`
+
+- **Entrada:** `--users-json` (default `/var/lib/runv/users.json`) — deve ser uma **lista** JSON de objectos.
+- **Saída:** `-o` / `--output` (default no repo: `site/public/data/members.json`; em produção típico: `DocumentRoot/data/members.json`).
+
+## Schema público (campos escritos)
+
+Cada elemento do array gerado contém:
+
+| Campo | Origem / notas |
+|-------|------------------|
+| `username` | De `users.json` |
+| `since` | `created_at` se for string; senão `""` |
+| `path` | `"/~username/"` |
+| `homepage_mtime` | Opcional; só com `--homes-root` (ex. `/home`) |
+
+**Privacidade:** o script **não** copia email, fingerprint SSH, quotas nem outros campos internos (**evidência:** lógica em `build_directory.py`, função `main`).
+
+## Consumo no browser
+
+- `site/public/assets/app.js`: `validMembers()` exige `username` e `path` (strings); `since` opcional para brilho visual.
+
+## Quando regenerar
+
+1. **Hooks:** `create_runv_user.py` (se DocumentRoot existir e refresh activo); `genlanding.py` após cópia (por omissão).
+2. **Cron (opcional):** exemplo histórico em `INSTALL` — adequado se quiser actualização periódica sem depender só de criar utilizadores.
+3. **Manual:**
+
+```bash
+python3 REPO/site/build_directory.py \
+ --users-json /var/lib/runv/users.json \
+ -o /var/www/runv.club/html/data/members.json
+```
+
+## Cron vs hooks (sem contradição)
+
+- **Hooks** actualizam quando corres `create_runv_user` ou `genlanding`.
+- **Cron** é **opcional** para alinhar site com `users.json` mesmo sem novos provisionamentos.
+
+Próximo: [08-email.md](08-email.md).
diff --git a/docs/08-email.md b/docs/08-email.md
@@ -0,0 +1,32 @@
+# Email (saída)
+
+[← Índice](README.md)
+
+## Arquitectura actual
+
+- **Predefinição:** envio via **Mailgun HTTP API** (`email/configure_mailgun.py`).
+- **Estado:** `/etc/runv-email.json`
+- **Segredos:** `/etc/runv-email.secrets.json` (permissões restritas; não versionar).
+
+## Modo legado
+
+- SMTP via `msmtp` / `sendmail`: flags `--legacy-smtp` ou `configure_msmtp_legacy.py` (detalhes nas docstrings e `--help` dos scripts em `email/`).
+
+## Biblioteca
+
+- `email/lib/mailer.py` — envio reutilizável; templates em `email/templates/`.
+- Variável `RUNV_EMAIL_ROOT` ou `email_package_root` no JSON para o fluxo `entre` localizar templates.
+
+## Integração com `entre`
+
+- Notificações ao admin usam `admin_email` no `config.toml` do terminal **ou** fallback em `/etc/runv-email.json` (comportamento verificado no código de `terminal/` + `email/lib`).
+
+## O que o repo não é
+
+- **Não** é MTA completo (não recebe correio para caixas locais de membros como produto deste repositório).
+
+## Testes
+
+- Existem testes em `email/tests/` (ex.: `test_mailgun_client.py`). Ver [14-smoke-tests-and-validation.md](14-smoke-tests-and-validation.md).
+
+Próximo: [09-terminal-entre.md](09-terminal-entre.md).
diff --git a/docs/09-terminal-entre.md b/docs/09-terminal-entre.md
@@ -0,0 +1,41 @@
+# Terminal SSH «entre»
+
+[← Índice](README.md)
+
+## Papel
+
+- Utilizador Unix especial **`entre`**: ao ligar por SSH, o OpenSSH executa **`ForceCommand`** → `entre_app.py`.
+- **Recolhe** dados (username, email, presença online, chave pública), **valida** (`entre_core.py`), **grava** JSON na fila com criação exclusiva (`O_EXCL`), **regista** log, **opcionalmente** notifica admin por email.
+
+## Limite explícito (facto de código)
+
+- **`entre_app.py` / `entre_core.py` não criam contas Linux de membros.** O utilizador `entre` em si é criado por **`setup_entre.py`** com `useradd` — isso é **bootstrap do sistema**, não provisionamento de membro.
+
+## Ficheiros principais
+
+| Ficheiro | Função |
+|----------|--------|
+| `entre_app.py` | UI terminal, passos |
+| `entre_core.py` | Config TOML, validação, fila, log, sendmail/Mailgun |
+| `setup_entre.py` | Instalação: `entre`, `/opt/runv/terminal`, fila, logs, drop-in sshd, modos de auth |
+| `config.example.toml` | Modelo; `config.toml` gerado, não versionado no mesmo sítio |
+| `templates/*.txt` | Textos editáveis |
+| `systemd/*.path`, `*.service` | Opcional (notificações) |
+
+## Configuração
+
+- `queue_dir` default `/var/lib/runv/entre-queue`
+- `log_file` default `/var/log/runv/entre.log`
+- Ver `terminal/config.example.toml`
+
+## Modos de autenticação (`setup_entre.py`)
+
+- Documentados na docstring: `shared-password`, `key-only`, `empty-password` (estilo tilde.town), com avisos de segurança explícitos no código.
+
+## Documentação histórica
+
+- O antigo `terminal/docs/ARCHITECTURE.md` referia `USO.md`, que **não existia** neste snapshot. O fluxo operacional está consolidado neste documento e em [10-user-provisioning-and-admin-ops.md](10-user-provisioning-and-admin-ops.md) (a documentação modular em `terminal/docs/` foi removida em favor de `docs/` — ver `DOCS_REBUILD_CHANGELOG.md` na raiz).
+
+Diagrama de sequência: [diagrams/architecture.mmd](diagrams/architecture.mmd).
+
+Próximo: [10-user-provisioning-and-admin-ops.md](10-user-provisioning-and-admin-ops.md).
diff --git a/docs/10-user-provisioning-and-admin-ops.md b/docs/10-user-provisioning-and-admin-ops.md
@@ -0,0 +1,31 @@
+# Provisionamento de utilizadores e operações admin
+
+[← Índice](README.md)
+
+## Fonte canónica: `scripts/admin/create_runv_user.py`
+
+- **Único** script de criação de **membros** com a política completa (docstring longa no ficheiro): `adduser`, chaves, `public_html` / gopher / gemini, permissões, jail (Jailkit), quota, metadados em `users.json`.
+- Executar como **root** no servidor Debian.
+
+## Pós-criação: constelação
+
+- Flag `--landing-document-root` (default `/var/www/runv.club/html`): se o directório **existir**, corre `build_directory.py` para `data/members.json` (salvo `--no-refresh-landing-members`).
+- Saída explícita para o operador: linha de **sucesso** com contagem ou **AVISO** com comando sugerido se path em falta ou falha (**código actual**).
+
+## Outros scripts admin
+
+| Script | Uso |
+|--------|-----|
+| `update_user.py` | Actualizar metadados / quota / estado (`users.json` com lock) |
+| `del-user.py` | Remover utilizador e metadados |
+| `setup_alt_protocols.py` | Reparar protocolos para contas criadas fora do fluxo |
+| `scripts/doom/doom.py` | **Perigoso:** remove contas em massa; só testes / com backup |
+
+## Fluxo de aprovação
+
+1. JSON na fila `entre-queue/`.
+2. Admin valida manualmente.
+3. `create_runv_user.py` com dados aprovados.
+4. Refresh público conforme [07](07-public-members-directory.md).
+
+Próximo: [11-daily-operations.md](11-daily-operations.md).
diff --git a/docs/11-daily-operations.md b/docs/11-daily-operations.md
@@ -0,0 +1,36 @@
+# Operação diária (dia 2+)
+
+[← Índice](README.md)
+
+## Adicionar membro
+
+1. Pedido via `entre` ou processo interno.
+2. `sudo python3 scripts/admin/create_runv_user.py …` (ver `--help` no servidor).
+3. Confirmar linha **constelação (bolhas)** ou corrigir com `build_directory.py` manual.
+
+## Actualizar lista pública sem novo membro
+
+```bash
+sudo python3 REPO/site/build_directory.py \
+ --users-json /var/lib/runv/users.json \
+ -o /var/www/runv.club/html/data/members.json
+```
+(Ajustar paths ao teu DocumentRoot.)
+
+## Após `git pull` no servidor
+
+- `sudo python3 tools/tools.py` para MOTD/skel/bin conforme alterações.
+
+## Notícias
+
+- Colocar `.md` em `site/news/`, executar `site/news/publish_news.py`; depois voltar a copiar `public/` ou correr `genlanding.py` se aplicável.
+
+## Wiki
+
+- Fontes em `site/wiki/` com gerador `build_wiki.py` (estrutura no repo).
+
+## Email
+
+- Testes documentados no módulo `email/` (`send_test_mail.sh`, etc., se presentes).
+
+Próximo: [12-security-and-privacy.md](12-security-and-privacy.md).
diff --git a/docs/12-security-and-privacy.md b/docs/12-security-and-privacy.md
@@ -0,0 +1,25 @@
+# Segurança e privacidade
+
+[← Índice](README.md)
+
+## Factos do código (produto)
+
+- Uso de `subprocess` com **listas de argumentos** — sem `shell=True` nos módulos Python principais verificados em auditorias recentes (`scripts`, `terminal`, `site`, `tools`, `email`, `patches`).
+- Fila `entre`: ficheiros criados com **`O_CREAT|O_EXCL`** para evitar sobrescrever pedidos (`entre_core.py`).
+- **`members.json` público:** apenas campos acordados em `build_directory.py` — ver [07-public-members-directory.md](07-public-members-directory.md).
+
+## Fila vs site público
+
+- JSONs em `entre-queue/` contêm dados para **revisão admin** (incl. email, chave pública, fingerprint no payload).
+- Esses campos **não** devem aparecer no `members.json` servido pelo HTTP — o gerador público não os copia.
+
+## Segredos
+
+- `/etc/runv-email.secrets.json`, chaves SSH privadas, tokens: **nunca** em Git; seguir `.gitignore`.
+
+## Recomendações gerais (não automatizadas pelo repo)
+
+- Firewall, `sshd_config` global, desactivar root login, etc. — política do operador.
+- Modo `empty-password` do `entre` é **deliberadamente fraco** para onboarding; docstring de `setup_entre.py` descreve riscos.
+
+Próximo: [13-troubleshooting.md](13-troubleshooting.md).
diff --git a/docs/13-troubleshooting.md b/docs/13-troubleshooting.md
@@ -0,0 +1,33 @@
+# Resolução de problemas
+
+[← Índice](README.md)
+
+## Bolhas / constelação não aparecem
+
+1. Confirmar que existe **`DocumentRoot/data/members.json`** (não só `site/public/data/members.json` no clone).
+2. Ver mensagem de **`create_runv_user.py`**: AVISO se DocumentRoot inexistente.
+3. Browser: em viewport ≤768px o JS **omitido** de propósito (`app.js`).
+
+## `members.json` vazio
+
+- `users.json` inexistente → `build_directory.py` assume `[]` com aviso em stderr.
+- JSON inválido → script termina com erro.
+
+## Email não envia (entre / Mailgun)
+
+- Verificar `/etc/runv-email.json`, segredos, `admin_email`, `email_package_root` / `RUNV_EMAIL_ROOT`.
+
+## Apache
+
+- `apache2ctl configtest` após alterações de vhost.
+- `genlanding.py` imprime erros se `build_directory` falhar.
+
+## Quotas
+
+- FS não ext4 → automatização de `starthere.py` pode recusar; configurar manualmente ou usar volume ext4.
+
+## SSH `entre`
+
+- Sessão fecha de imediato: rever PAM / modo `empty-password` / logs em `/var/log/runv/entre.log`.
+
+Próximo: [14-smoke-tests-and-validation.md](14-smoke-tests-and-validation.md).
diff --git a/docs/14-smoke-tests-and-validation.md b/docs/14-smoke-tests-and-validation.md
@@ -0,0 +1,46 @@
+# Smoke tests e validação
+
+[← Índice](README.md)
+
+## Sintaxe Python (todo o produto)
+
+```bash
+cd REPO
+python3 -m compileall -q scripts terminal site tools email patches
+```
+
+**Esperado:** código de saída `0`.
+
+## Submódulo email
+
+```bash
+cd REPO/email
+python3 -m pytest tests/ -q
+```
+
+**Esperado:** testes passam (há `test_mailgun_client.py`).
+
+## `build_directory.py`
+
+```bash
+python3 site/build_directory.py --users-json site/example-users.json --dry-run
+```
+
+**Esperado:** JSON no stdout com `username`, `since`, `path`.
+
+## `--help` (requer Unix)
+
+Vários scripts importam `fcntl` ou `grp` — **não executáveis** em Windows típico:
+
+- `scripts/admin/create_runv_user.py --help`
+- `terminal/setup_entre.py --help`
+- `site/genlanding.py --help`
+
+Em **Debian:** correr os `--help` acima e guardar a saída para operadores.
+
+## O que **não** existe no repo (facto)
+
+- **Sem** workflows `.github/workflows` na raiz do projecto runv (verificado por ausência de `.github/` no clone típico).
+- **Sem** suite de testes para `entre_core` ou `build_directory` além do que está em `email/tests/`.
+
+Próximo: [15-glossary-and-reference.md](15-glossary-and-reference.md).
diff --git a/docs/15-glossary-and-reference.md b/docs/15-glossary-and-reference.md
@@ -0,0 +1,44 @@
+# Glossário e referência rápida
+
+[← Índice](README.md)
+
+## Glossário
+
+| Termo | Significado |
+|-------|-------------|
+| **entre** | Utilizador SSH especial para pedidos de entrada; não cria membros. |
+| **Fila** | Directório `/var/lib/runv/entre-queue/` com JSON por pedido. |
+| **members.json** | Dataset **público** para a constelação na landing. |
+| **users.json** | Metadados **internos** dos membros no servidor. |
+| **DocumentRoot** | Raiz Apache onde `genlanding.py` copia `site/public/`. |
+| **REPO** | Caminho do clone (ex. `/opt/runv/src`). |
+
+## Índice de scripts (principal)
+
+| Caminho | Descrição curta |
+|---------|-----------------|
+| `scripts/admin/starthere.py` | Bootstrap APT, Apache, UFW, quotas ext4 |
+| `scripts/admin/create_runv_user.py` | Provisionamento canónico de membro |
+| `scripts/admin/update_user.py` | Actualizar membro / metadados |
+| `scripts/admin/del-user.py` | Remover membro |
+| `tools/tools.py` | APT, MOTD, skel, binários locais |
+| `site/genlanding.py` | Apache + cópia landing + refresh members |
+| `site/build_directory.py` | users.json → members.json público |
+| `email/configure_mailgun.py` | Config email Mailgun / legado |
+| `terminal/setup_entre.py` | Instalar fluxo `entre` |
+| `terminal/entre_app.py` | App ForceCommand |
+| `terminal/entre_core.py` | Núcleo validação/fila |
+
+## Módulos (pastas)
+
+- `scripts/admin/` — administração
+- `site/` — web estático + geradores
+- `tools/` — experiência global Debian
+- `email/` — envio
+- `terminal/` — SSH entre
+- `patches/` — patches auxiliares (ex. IRC)
+
+## Mapa de documentação
+
+- **Canónico:** esta pasta `docs/`.
+- **Changelog da reconstrução:** `DOCS_REBUILD_CHANGELOG.md` na raiz.
diff --git a/docs/README.md b/docs/README.md
@@ -0,0 +1,43 @@
+# Documentação runv-server
+
+Índice canónico do repositório **runv-server** (automação para pubnix Debian / runv.club). Tudo em **pt-BR**. Esta pasta é a **porta de entrada**; não dependa de ficheiros `.md` antigos nos módulos (foram removidos nesta reconstrução — ver `DOCS_REBUILD_CHANGELOG.md` na raiz).
+
+## Ordem de leitura sugerida
+
+1. [Visão geral](00-overview.md)
+2. [Baseline Debian](01-server-baseline-debian.md)
+3. [Acesso admin e SSH](02-admin-access-and-ssh.md)
+4. [Caminhos e estado](03-paths-files-and-state.md)
+5. [Bootstrap](04-bootstrap-and-base-system.md)
+6. [Ferramentas globais](05-tools-and-system-experience.md)
+7. [Site e Apache](06-site-and-apache.md)
+8. [Membros públicos](07-public-members-directory.md)
+9. [Email](08-email.md)
+10. [Terminal entre](09-terminal-entre.md)
+11. [Provisionamento e admin](10-user-provisioning-and-admin-ops.md)
+12. [Operação diária](11-daily-operations.md)
+13. [Segurança e privacidade](12-security-and-privacy.md)
+14. [Resolução de problemas](13-troubleshooting.md)
+15. [Smoke tests](14-smoke-tests-and-validation.md)
+16. [Glossário e referência](15-glossary-and-reference.md)
+
+## Mapa rápido
+
+| Quero… | Documento |
+|--------|-----------|
+| Entender o que é o projeto | [00-overview.md](00-overview.md) |
+| Preparar o servidor Debian | [01](01-server-baseline-debian.md), [04](04-bootstrap-and-base-system.md) |
+| Instalar landing Apache | [06-site-and-apache.md](06-site-and-apache.md) |
+| Lista de bolhas / `members.json` | [07-public-members-directory.md](07-public-members-directory.md) |
+| Pedidos SSH `entre` | [09-terminal-entre.md](09-terminal-entre.md) |
+| Criar conta membro | [10-user-provisioning-and-admin-ops.md](10-user-provisioning-and-admin-ops.md) |
+| Email Mailgun / legado | [08-email.md](08-email.md) |
+
+## Diagramas (Mermaid)
+
+- [diagrams/architecture.mmd](diagrams/architecture.mmd)
+- [diagrams/member-flow.mmd](diagrams/member-flow.mmd)
+
+## Código-fonte
+
+A documentação descreve scripts em `scripts/`, `terminal/`, `site/`, `tools/`, `email/`. As docstrings e `--help` dos scripts são fonte de verdade complementar (ver [14-smoke-tests-and-validation.md](14-smoke-tests-and-validation.md)).
diff --git a/docs/diagrams/architecture.mmd b/docs/diagrams/architecture.mmd
@@ -0,0 +1,21 @@
+%% Fluxo SSH entre + fila (alinhado ao código actual; espelha o antigo terminal/docs/ARCHITECTURE.md, removido)
+%% Renderizar com qualquer visualizador Mermaid
+sequenceDiagram
+ participant C as Cliente_SSH
+ participant S as sshd
+ participant A as entre_app_py
+ participant Q as entre_queue
+ participant L as entre_log
+ participant M as sendmail_ou_Mailgun
+
+ C->>S: autentica_como_entre
+ S->>A: ForceCommand
+ A->>C: formulario_guiado
+ C->>A: respostas
+ A->>A: validacao_entre_core
+ A->>Q: JSON_O_EXCL
+ A->>L: eventos
+ opt admin_email_configurado
+ A->>M: notificacao
+ end
+ A->>C: despedida
diff --git a/docs/diagrams/member-flow.mmd b/docs/diagrams/member-flow.mmd
@@ -0,0 +1,20 @@
+%% Ciclo membro: pedido até presença pública (sem automatizar aprovação)
+flowchart LR
+ subgraph pedido [Pedido]
+ E[SSH_entre]
+ F[fila_JSON]
+ E --> F
+ end
+ subgraph admin [Admin]
+ R[revisao_humana]
+ C[create_runv_user_py]
+ R --> C
+ end
+ subgraph dados [Dados]
+ U[users_json]
+ M[members_json_publico]
+ U --> M
+ end
+ F --> R
+ C --> U
+ M --> W[HTTP_landing]
diff --git a/email/README.md b/email/README.md
@@ -1,68 +0,0 @@
-# Email runv.club — envio via Mailgun HTTP API (predefinido)
-
-**Aviso: o configurador predefinido foi feito para Mailgun.** Não pré-configura credenciais.
-
-Subsistema **só de envio** para Debian 13: por omissão grava estado em `/etc/runv-email.json` e segredos em `/etc/runv-email.secrets.json`, e envia mensagens pela **API HTTP Mailgun** (Basic Auth `api` + API key). Opcionalmente mantém um modo **legado** com `msmtp` + `sendmail`.
-
-## O que faz (predefinido)
-
-- Configura envio **sem** Postfix/Exim/Dovecot — **não** é um MTA completo.
-- **Não** recebe email (sem IMAP, sem caixa local).
-- Biblioteca Python reutilizável (`lib/mailer.py`) com templates em texto puro; suporte opcional a corpo **HTML** no `send_mail`.
-
-## O que instala (APT) — só modo legado SMTP
-
-| Pacote | Papel |
-|--------|-------|
-| `msmtp` | Cliente SMTP. |
-| `msmtp-mta` | Fornece `/usr/sbin/sendmail`. |
-| `ca-certificates` | Confiança TLS. |
-| `bsd-mailx` | Comando `mail` para testes em CLI. |
-
-**Mailgun API (predefinido)** não exige estes pacotes.
-
-## Execução rápida
-
-```bash
-cd /caminho/runv-server/email
-sudo python3 configure_mailgun.py
-```
-
-Legado SMTP:
-
-```bash
-sudo python3 configure_mailgun.py --legacy-smtp
-# ou: sudo python3 configure_msmtp_legacy.py
-```
-
-O ficheiro `configure_msmtp.py` apenas **indica** estes comandos (substituição do antigo fluxo).
-
-Flags: `--dry-run`, `--verbose`, `--force`, `--test`, `--legacy-smtp`. Detalhes: [docs/INSTALL.md](docs/INSTALL.md).
-
-## Documentação
-
-| Ficheiro | Conteúdo |
-|----------|-----------|
-| [docs/INSTALL.md](docs/INSTALL.md) | Mailgun vs legado, ficheiros, flags, testes, variáveis de ambiente. |
-| [docs/ADMIN.md](docs/ADMIN.md) | Alterar remetente, admin, segredos. |
-| [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) | Falhas comuns. |
-| [docs/INTEGRATION.md](docs/INTEGRATION.md) | `lib/mailer.py`, eventos, `entre`. |
-
-## Biblioteca
-
-Defina `RUNV_EMAIL_ROOT` para a pasta `email/` do repositório (onde estão `lib/` e `templates/`) e importe `lib.mailer`. O configurador grava também `email_package_root` em `/etc/runv-email.json` para o serviço `entre` encontrar o módulo sem variável de ambiente.
-
-## Checklist manual de verificação (Mailgun)
-
-- [ ] `sudo ls -l /etc/runv-email.json /etc/runv-email.secrets.json` — **0600**, root.
-- [ ] `sudo python3 configure_mailgun.py --test` — email de teste recebido.
-- [ ] `email_package_root` no JSON aponta para a pasta `email/` do deploy (para notificações `entre`).
-- [ ] Fluxo `entre` com `admin_email` no `config.toml` — notificação ao admin (Mailgun ou sendmail de fallback).
-
-## Scripts auxiliares (legado / diagnóstico)
-
-- `scripts/diagnose_msmtp.sh` — diagnóstico msmtp (modo SMTP).
-- `scripts/send_test_mail.sh` — teste via `mail`.
-- `scripts/netrc_password.py` — usado por `passwordeval` no msmtp (só legado).
-
-Versão do módulo alinhada ao repositório runv-server.
diff --git a/email/configure_mailgun.py b/email/configure_mailgun.py
@@ -4,7 +4,7 @@ Configurador de email runv — Mailgun HTTP API (predefinido).
Aviso: este script foi feito para Mailgun. Não pré-configura nenhuma credencial.
-Executar como root. Ver email/docs/INSTALL.md.
+Executar como root. Ver docs/08-email.md no repositório.
"""
from __future__ import annotations
@@ -219,7 +219,7 @@ def print_summary(public: dict[str, Any], *, dry_run: bool) -> None:
if dry_run:
print(" (dry-run — ficheiros não gravados)")
print()
- print("Documentação: email/docs/INSTALL.md")
+ print("Documentação: docs/08-email.md (repositório)")
def main() -> int:
diff --git a/email/configure_msmtp_legacy.py b/email/configure_msmtp_legacy.py
@@ -5,7 +5,7 @@ LEGADO — Instalador/configurador runv.club: envio via msmtp + sendmail (Debian
O caminho predefinido do projeto é Mailgun API (`configure_mailgun.py`).
Use este script apenas se precisar de SMTP local/msmtp.
-Executar como root. Ver email/docs/INSTALL.md.
+Executar como root. Ver docs/08-email.md no repositório.
"""
from __future__ import annotations
@@ -460,7 +460,7 @@ def main() -> int:
print(f" netrc: {NETRC_PATH} (credenciais — não partilhar)")
print(f" estado: {STATE_PATH}")
print(f" sendmail: /usr/sbin/sendmail (msmtp-mta)")
- print("\nDocumentação: email/docs/INSTALL.md")
+ print("\nDocumentação: docs/08-email.md (repositório)")
print("Teste posterior: sudo python3 email/configure_msmtp_legacy.py --test")
print("Mailgun (recomendado): sudo python3 email/configure_mailgun.py")
return 0
diff --git a/email/docs/ADMIN.md b/email/docs/ADMIN.md
@@ -1,52 +0,0 @@
-# Administração — email runv.club
-
-**Predefinição:** Mailgun HTTP API (`configure_mailgun.py`). Secção final: **legado SMTP/msmtp**.
-
-## Mailgun — alterar remetente (From)
-
-1. Edite `/etc/runv-email.json` — campo `default_from`.
-2. O endereço deve estar autorizado no domínio Mailgun configurado.
-3. Valide: `sudo python3 configure_mailgun.py --test`.
-
-**Não** coloque a API key neste ficheiro.
-
-## Mailgun — alterar email do administrador
-
-1. Edite `admin_email` em `/etc/runv-email.json`.
-2. Fluxo **entre:** com `admin_email` vazio no `/opt/runv/terminal/config.toml`, o `entre_app` usa o mesmo `admin_email` do JSON — não precisa duplicar. Os avisos do entre usam *From* **`noreply@runv.club`** por omissão (ou `mail_from` no TOML).
-
-## Mailgun — rodar API key ou região
-
-1. Para nova key: edite `/etc/runv-email.secrets.json` (0600) **ou** defina `RUNV_MAILGUN_API_KEY` no ambiente do processo.
-2. Para mudar domínio/região: edite `/etc/runv-email.json` (`mailgun_domain`, `mailgun_region`, `api_base_url` coerente: `https://api.mailgun.net` vs `https://api.eu.mailgun.net`).
-3. Recomendado: voltar a correr `sudo python3 configure_mailgun.py --force` para prompts guiados.
-
-## Mailgun — reenviar teste
-
-```bash
-sudo python3 /caminho/runv-server/email/configure_mailgun.py --test
-```
-
-## Legado SMTP — alterar remetente (From)
-
-1. Edite `/etc/msmtprc` na conta `runv`: linha `from ...`.
-2. Actualize `/etc/runv-email.json` campo `default_from`.
-3. Valide com `sudo python3 configure_msmtp_legacy.py --test` ou envio via `mail`.
-
-## Legado SMTP — credenciais
-
-- Senha/token **só** em `/root/.netrc` (ou `configure_msmtp_legacy.py` com `--force`).
-- **Nunca** coloque senhas em `/etc/runv-email.json` em claro.
-
-## Integrar outros scripts
-
-Ver [INTEGRATION.md](INTEGRATION.md). Resumo: `RUNV_EMAIL_ROOT` ou `email_package_root` no JSON; usar `lib.mailer.send_mail`.
-
-## Aliases msmtp (só legado)
-
-- **msmtp** expande aliases — útil para `mail root` → admin.
-- **`newaliases`** (estilo Sendmail) **não** actualiza `/etc/msmtp_aliases`.
-
-## Log (legado)
-
-- `/var/log/msmtp.log` quando usa msmtp.
diff --git a/email/docs/INSTALL.md b/email/docs/INSTALL.md
@@ -1,134 +0,0 @@
-# Instalação — módulo email runv.club
-
-**Aviso: o configurador predefinido foi feito para Mailgun.** Não embute credenciais, domínios nem chaves — tudo é pedido em tempo de configuração.
-
-Debian 13 (ou próximo). **Apenas envio** — caminho predefinido **Mailgun HTTP API** (Basic Auth: utilizador `api`, palavra-passe = API key). O modo **SMTP/msmtp + sendmail** permanece disponível como **legado**, desativado por predefinição.
-
-## O que o predefinido faz (Mailgun)
-
-- Grava metadados em **`/etc/runv-email.json`** (0600, root): domínio Mailgun, região da API (o configurador fixa **`us`** e `https://api.mailgun.net/`), remetente padrão, email do admin, tipo de chave, caminho da pasta `email/` do repositório (`email_package_root`), etc. **Sem API key neste ficheiro.**
-- Grava segredos em **`/etc/runv-email.secrets.json`** (0600, root): apenas `mailgun_api_key`. **Não partilhar nem fazer backup deste ficheiro para repositórios públicos.**
-
-### API key em variável de ambiente (opcional)
-
-Em tempo de execução, **`RUNV_MAILGUN_API_KEY`** (se definida) **tem prioridade** sobre o ficheiro de segredos. Útil para systemd ou contentores; o estado público pode continuar a referir `api_key_source: file` — o runtime usa na mesma a env quando presente.
-
-### Mailgun: SMTP vs HTTP API
-
-- **Credenciais SMTP** do painel Mailgun são para clientes SMTP (ex.: msmtp); **não** são o mesmo fluxo que a API HTTP.
-- A **HTTP API** usa autenticação **HTTP Basic**: username fixo **`api`**, password = **API key** (primary ou domain sending key).
-- **US:** `https://api.mailgun.net/v3/<domínio>/messages` (o configurador usa sempre este endpoint; é o mesmo eixo que o SMTP **`smtp.mailgun.org`** nas credenciais SMTP do painel.)
-- **EU:** `https://api.eu.mailgun.net/v3/<domínio>/messages` — só para contas/domínios alojados na região UE; nesse caso **edite** `mailgun_region` (`eu`) e `api_base_url` em `/etc/runv-email.json` após correr o script, ou a API devolverá erros de autenticação/domínio.
-
-### IP allowlist (API)
-
-Se no painel Mailgun estiver activa a **restrição por IP** para a API, qualquer servidor que chame `api.mailgun.net` tem de ter o **seu IP público** na lista. Sem isso, a API pode responder **401** / «Invalid private key» / **Forbidden** mesmo com chave e domínio correctos. Inclua o IP da VPS (ou desactive a allowlist para testes).
-
-### Obter uma API key
-
-1. Painel Mailgun → domínio → **Domain settings** / **Sending API keys**.
-2. Preferir **domain sending key** (menor privilégio) se só precisar de enviar desse domínio; **primary API key** também funciona se tiver permissão de envio.
-
-Para validar a **primary** no painel ou com `curl`, a listagem de domínios usa **`GET /v4/domains`** (US ou EU). A **domain sending key** não serve para esse endpoint; o envio do runv usa **`POST /v3/<domínio>/messages`** (já implementado em `lib/mailgun_client.py`).
-
-## Executar o configurador (predefinido)
-
-```bash
-cd /caminho/para/runv-server/email
-sudo python3 configure_mailgun.py
-```
-
-No arranque é mostrado o aviso de que o script foi feito para Mailgun e **não** pré-configura credenciais.
-
-O script pergunta:
-
-- tipo de chave (domain sending vs primary);
-- domínio de envio Mailgun (ex.: `mg.exemplo.com`);
-- API key (**ecoada** ao digitar; deve ser introduzida **duas vezes iguais** para continuar — útil para validar cópia/colar; evite terminais partilhados);
-- remetente padrão (From);
-- email do administrador (notificações / teste);
-- caminho da pasta **`email/`** do repositório (para importações, ex. fluxo `entre` — por omissão é a pasta onde está o script).
-
-A região da API HTTP **não é perguntada**: fica **`us`** (`api.mailgun.net`). Conta só UE: ajuste manualmente o JSON (ver secção «SMTP vs HTTP API» acima).
-
-## Ficheiros criados (Mailgun)
-
-| Ficheiro | Descrição |
-|----------|-----------|
-| `/etc/runv-email.json` | Metadados **sem** API key. **0600** root. |
-| `/etc/runv-email.secrets.json` | `mailgun_api_key`. **0600** root. **World-readable proibido.** |
-
-## Flags (`configure_mailgun.py`)
-
-| Flag | Efeito |
-|------|--------|
-| `--dry-run` | Não grava ficheiros; mostra acções. |
-| `--verbose` / `-v` | Log DEBUG (nunca inclui a API key). |
-| `--force` / `-f` | Sobrescreve estado/segredos sem confirmar. |
-| `--test` | Só envia `templates/system_test.txt` via **Mailgun API** (requer estado existente). |
-| `--legacy-smtp` | Delega no configurador **SMTP/msmtp** (`configure_msmtp_legacy.py`). |
-
-## Teste de envio (API)
-
-```bash
-sudo python3 configure_mailgun.py --test
-```
-
-Em caso de falha, mensagens típicas:
-
-- **401 / 403** — Chave incorrecta (não é API HTTP / não é do domínio), região errada (US vs EU), ou **IP allowlist** no painel a bloquear o servidor; confira também se o domínio na URL coincide com o domínio verificado.
-- **400** — payload inválido; From não autorizado no domínio; campos em falta.
-- **404** — domínio errado ou URL/região incorreta (US vs EU).
-- **Timeout / erro de rede** — DNS, firewall ou TLS.
-
-## Modo legado: SMTP + msmtp + sendmail
-
-Apenas se precisar de relay SMTP clássico:
-
-```bash
-sudo python3 configure_mailgun.py --legacy-smtp
-# ou directamente:
-sudo python3 configure_msmtp_legacy.py
-```
-
-Instala `msmtp`, `msmtp-mta`, `ca-certificates`, `bsd-mailx`, gera `/etc/msmtprc`, `/root/.netrc`, `/etc/msmtp_aliases`, e grava `/etc/runv-email.json` com **`backend: sendmail`**.
-
-**`configure_msmtp.py`** (sem `_legacy`) é apenas um **encaminhamento** com mensagem a indicar os comandos correctos.
-
-## Verificação rápida (Mailgun)
-
-```bash
-sudo ls -l /etc/runv-email.json /etc/runv-email.secrets.json
-# Ambos devem ser -rw------- root root
-sudo python3 configure_mailgun.py --test
-```
-
-Nunca imprima o conteúdo de `runv-email.secrets.json` em chats ou logs públicos.
-
-## Biblioteca Python (`lib/mailer.py`)
-
-Com **`backend: mailgun`** no estado, `send_mail` usa a API Mailgun (urllib, stdlib). Com **`backend: sendmail`** ou estado antigo só com `smtp_host`, usa `sendmail -t -i`.
-
-Defina **`RUNV_EMAIL_ROOT`** para a pasta `email/` ao importar em scripts (ou use `email_package_root` em `/etc/runv-email.json` — o fluxo `entre` tenta ambos).
-
-Exemplo:
-
-```bash
-sudo RUNV_EMAIL_ROOT=/caminho/runv-server/email python3 -c "
-import sys
-sys.path.insert(0, '/caminho/runv-server/email')
-from lib.mailer import send_mail
-send_mail('voce@exemplo.com', 'Teste', 'Corpo.', from_addr='noreply@exemplo.com')
-"
-```
-
-## Variáveis de ambiente úteis
-
-| Variável | Uso |
-|----------|-----|
-| `RUNV_EMAIL_ROOT` | Caminho da pasta `email/` (import `lib.*`). |
-| `RUNV_EMAIL_STATE_PATH` | Alternativa a `/etc/runv-email.json` (testes). |
-| `RUNV_EMAIL_SECRETS_PATH` | Alternativa ao caminho de segredos indicado no estado. |
-| `RUNV_MAILGUN_API_KEY` | API key em memória/ambiente (sobrepor ficheiro de segredos). |
-
-Próximo: [ADMIN.md](ADMIN.md) para operação corrente.
diff --git a/email/docs/INTEGRATION.md b/email/docs/INTEGRATION.md
@@ -1,92 +0,0 @@
-# Integração — email com o resto do runv-server
-
-**Predefinição:** envio via **Mailgun HTTP API** quando `/etc/runv-email.json` indica `backend: mailgun` (ou contém `mailgun_domain` + `mailgun_region` sem `backend: sendmail`). Caso contrário, `lib.mailer` usa **sendmail** (msmtp legado).
-
-## Variável de ambiente
-
-Defina **`RUNV_EMAIL_ROOT`** como caminho absoluto para a pasta **`email/`** do repositório (a que contém `lib/` e `templates/`).
-
-```bash
-export RUNV_EMAIL_ROOT=/srv/runv-server/email
-```
-
-O configurador Mailgun grava também **`email_package_root`** em `/etc/runv-email.json`. O fluxo **`entre`** usa esse campo (ou `RUNV_EMAIL_ROOT`) para importar `lib.mailer` e enviar via API quando Mailgun está activo.
-
-Em Python, antes de importar:
-
-```python
-import os
-import sys
-ROOT = "/srv/runv-server/email"
-os.environ.setdefault("RUNV_EMAIL_ROOT", ROOT)
-sys.path.insert(0, ROOT)
-from lib.mailer import (
- send_mail,
- send_admin_notice,
- send_user_notice,
- render_template,
-)
-from lib import templates as T
-```
-
-**Nunca** use `shell=True` em `subprocess` para envio; a biblioteca usa urllib (Mailgun) ou `sendmail` com lista de argumentos.
-
-## API resumida (`lib/mailer.py`)
-
-| Função | Uso |
-|--------|-----|
-| `render_template(nome, **kwargs)` | Lê `templates/<nome>.txt` e substitui `{placeholders}`. |
-| `send_mail(to, subject, body, from_addr=..., html=..., sendmail=..., headers=..., _state=...)` | Texto; `html` opcional (Mailgun). `to` string ou lista. `_state` evita reler disco (testes / entre). |
-| `send_admin_notice(..., html_body=...)` | Template → admin. |
-| `send_user_notice(..., html_body=...)` | Template → utilizador. |
-
-Com **Mailgun**, `sendmail` é ignorado para o transporte (usa API). Com **legado**, `sendmail` por defeito: `/usr/sbin/sendmail`.
-
-## Mapa evento → template → script
-
-| Evento | Template(s) | Onde disparar |
-|--------|-------------|----------------|
-| Novo pedido na fila `entre` | Corpo do email: [`terminal/templates/admin_mail.txt`](../../terminal/templates/admin_mail.txt) (não `email/templates/admin_new_request.txt`). Opcional: `user_request_received` existe em `email/templates/` mas **não** está ligado ao `entre`. | Após `save_request_json` em [`terminal/entre_core.py`](../../terminal/entre_core.py) / [`entre_app.py`](../../terminal/entre_app.py). Email admin via `sendmail_notify`; com Mailgun, tenta **primeiro** `lib.mailer.send_mail` se `/etc/runv-email.json` e `email_package_root` / `RUNV_EMAIL_ROOT` forem válidos. |
-| Pedido aprovado (manual) | `user_approved` | Processo admin (manual / futuro). |
-| Pedido rejeitado | `user_rejected` (+ `reason`) | Idem. |
-| Conta criada | `admin_user_created` → admin; `user_account_created` → utilizador | [`scripts/admin/create_runv_user.py`](../../scripts/admin/create_runv_user.py): `--no-welcome-email` / `--no-admin-create-email` para desactivar cada ramo. |
-| Conta removida / banimento | `user_account_community_deactivated` → utilizador | [`scripts/admin/del-user.py`](../../scripts/admin/del-user.py): envia por omissão se existir email em `users.json` e `/etc/runv-email.json` válido; `--no-ban-notify-email` desactiva. Templates `admin_user_deleted` / `user_account_removed` existem mas **não** estão ligados a este script. |
-| Erro operacional | `admin_error` | Scripts admin / cron. |
-| Quota | `user_quota_warning` | Monitorização / quotas. |
-| Teste | `system_test` | `configure_mailgun.py --test` (API) ou legado. |
-
-## Fluxo **entre** (terminal)
-
-- **`entre_core.sendmail_notify`** tenta primeiro envio **Mailgun** se `/etc/runv-email.json` for compatível e se `email_package_root` ou `RUNV_EMAIL_ROOT` permitir importar `lib.mailer`.
-- Se Mailgun não aplicável ou falhar o ramo API, usa **`sendmail -t -i`** como antes (requer msmtp-mta no modo legado).
-
-### Coerência de configuração
-
-| Ficheiro | Campos |
-|----------|--------|
-| `/etc/runv-email.json` | `backend`, `admin_email`, `default_from`, Mailgun (`mailgun_domain`, …) ou SMTP (`smtp_host`, …), `email_package_root`. |
-| `/opt/runv/terminal/config.toml` | `admin_email` (opcional se o JSON já tiver), `mail_from` (omissão **noreply@runv.club**), `sendmail_path`. |
-
-O destinatário dos avisos do **entre** pode vir só do JSON (`admin_email` vazio no TOML). O *From* dos mesmos avisos é **noreply@runv.club** por omissão (não `entre@runv.club`). Garanta esse endereço autorizado no Mailgun se usar API.
-
-## `create_runv_user.py` / `del-user.py`
-
-O **`create_runv_user.py`** envia por omissão:
-
-1. **Boas-vindas** ao utilizador (`user_account_created`), com instruções SSH; `--no-welcome-email` desactiva.
-2. **Aviso ao admin** (`admin_user_created` para `admin_email` no JSON); `--no-admin-create-email` desactiva.
-
-Requer `/etc/runv-email.json` (com `default_from`, `admin_email` para o ramo admin), segredos Mailgun se aplicável, e pasta `email/` acessível (`email_package_root` ou `RUNV_EMAIL_ROOT`). Para o texto de boas-vindas, `--welcome-ssh-host` ou `RUNV_WELCOME_SSH_HOST` define o hostname SSH sugerido.
-
-Obtenha `admin_email` / `default_from` de `/etc/runv-email.json` — **não** hardcodar.
-
-O **`del-user.py`** envia **`user_account_community_deactivated`** ao endereço no campo `email` do registo em `/var/lib/runv/users.json` (lido **antes** de apagar o registo), com texto de desativação por descumprimento das normas da comunidade. Requer `default_from` e pasta `email/` acessível (`RUNV_EMAIL_ROOT` ou `email_package_root`). Com `--skip-metadata` ainda tenta ler o ficheiro de metadados para obter o email.
-
-## Checklist de integração
-
-- [ ] `RUNV_EMAIL_ROOT` ou `email_package_root` correcto para serviços Python e **entre**.
-- [ ] `sudo python3 configure_mailgun.py --test` (ou legado) com sucesso.
-- [ ] Templates revistos (português, placeholders).
-- [ ] Nenhum segredo em logs ou `print()` (API key só em ficheiro 0600 ou env).
-
-Roteiro passo a passo no servidor: [VERIFICATION_CHECKLIST.md](VERIFICATION_CHECKLIST.md).
diff --git a/email/docs/TROUBLESHOOTING.md b/email/docs/TROUBLESHOOTING.md
@@ -1,62 +0,0 @@
-# Resolução de problemas — email runv.club
-
-## Mailgun API — 401 / 403
-
-- API key errada ou revogada; ou key sem permissão para o **domínio** indicado.
-- Confirme região **US vs EU** (URL base no JSON deve coincidir com a conta).
-
-## Mailgun API — 400
-
-- `From` não autorizado para o domínio; campos obrigatórios em falta; domínio não verificado no Mailgun.
-
-## Mailgun API — 404
-
-- Domínio incorrecto no path `/v3/.../messages` ou região trocada (US/EU).
-
-## Mailgun — timeout / rede
-
-- Firewall de saída, DNS, ou problemas TLS. Teste conectividade HTTPS ao host `api.mailgun.net` ou `api.eu.mailgun.net`.
-
-## Mailgun — `entre` não envia
-
-- Confirme `email_package_root` em `/etc/runv-email.json` aponta para a pasta `email/` do deploy **ou** defina `RUNV_EMAIL_ROOT` no ambiente do serviço.
-- Confirme `backend` é `mailgun` (ou domínio+região presentes sem `backend: sendmail`).
-
-## Legado — autenticação SMTP falha
-
-- Confirme `user` no `msmtprc` e `login` no `.netrc` para o mesmo `machine <host>` que o **host** SMTP.
-- Ver `/var/log/msmtp.log` (sem publicar dados sensíveis).
-
-## Legado — TLS / STARTTLS
-
-- Porta **587** + `tls on` + `tls_starttls on`; **465** muitas vezes `tls on` + `tls_starttls off`.
-- Confirme `ca-certificates` instalado.
-
-## `sendmail` não encontrado (modo legado)
-
-- Instale `msmtp-mta`: `apt-get install -y msmtp-mta`.
-- Em modo **Mailgun**, `sendmail` não é necessário para `lib.mailer.send_mail`.
-
-## `mail` não funciona (legado)
-
-- Instale `bsd-mailx`.
-
-## Template ausente (`lib/mailer.py`)
-
-- Defina `RUNV_EMAIL_ROOT` para a pasta **`email/`** do repositório.
-
-## Permissões em `/root/.netrc` (legado)
-
-- **600**, root. `sudo chmod 600 /root/.netrc && sudo chown root:root /root/.netrc`.
-
-## Permissões em segredos Mailgun
-
-- `/etc/runv-email.secrets.json` deve ser **0600** root. Nunca world-readable.
-
-## `passwordeval` / `netrc_password.py` (legado)
-
-- `/usr/local/lib/runv-email/netrc_password.py` — reinstalar com `configure_msmtp_legacy.py`.
-
-## `--test` diz que falta estado
-
-- Corra primeiro `configure_mailgun.py` (ou `configure_msmtp_legacy.py` no modo SMTP) **sem** `--test` para criar `/etc/runv-email.json`.
diff --git a/scripts/admin/create_runv_user.py b/scripts/admin/create_runv_user.py
@@ -1103,10 +1103,11 @@ def try_refresh_landing_members_json(
users_json: Path,
homes_root: Path | None,
log: logging.Logger,
-) -> bool:
+) -> tuple[bool, int | None]:
"""
Regenera public/data/members.json no DocumentRoot da landing (build_directory.py).
Falhas são apenas registadas — não aborta o provisionamento.
+ Devolve (sucesso, número de membros no JSON público ou None se não foi possível contar).
"""
script = _REPO_ROOT / "site" / "build_directory.py"
if not script.is_file():
@@ -1114,7 +1115,7 @@ def try_refresh_landing_members_json(
"build_directory.py não encontrado em %s; members.json da landing não atualizado",
script,
)
- return False
+ return False, None
out = document_root / "data" / "members.json"
cmd = [
sys.executable,
@@ -1135,14 +1136,23 @@ def try_refresh_landing_members_json(
r.returncode,
err_tail[:2000] if err_tail else "(sem saída)",
)
- return False
+ return False, None
log.info("members.json da landing actualizado em %s", out)
if r.stderr and r.stderr.strip():
log.debug("build_directory stderr: %s", r.stderr.strip()[:1500])
- return True
+ n_public: int | None = None
+ try:
+ raw = out.read_text(encoding="utf-8")
+ parsed = json.loads(raw)
+ if isinstance(parsed, list):
+ n_public = len(parsed)
+ log.info("constelação: %s membro(s) no dataset público (%s)", n_public, out)
+ except (OSError, json.JSONDecodeError, TypeError) as ex:
+ log.warning("members.json escrito mas não foi possível validar a lista: %s", ex)
+ return True, n_public
except (OSError, subprocess.TimeoutExpired) as e:
log.warning("falha ao executar build_directory: %s", e)
- return False
+ return False, None
def print_banner() -> None:
@@ -1563,8 +1573,9 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
type=Path,
default=Path("/var/www/runv.club/html"),
help=(
- "DocumentRoot da landing Apache; após criar o utilizador, executa site/build_directory.py "
- "para gravar data/members.json (bolinhas no site). Se a pasta não existir, o passo é ignorado."
+ "DocumentRoot da landing Apache (directório existente para actualizar a constelação); "
+ "após criar o utilizador, executa site/build_directory.py para gravar data/members.json. "
+ "Se não existir, o refresh é omitido e é impresso um AVISO com o comando sugerido."
),
)
p.add_argument(
@@ -1869,11 +1880,12 @@ def main(argv: list[str] | None = None) -> int:
append_user_metadata(args.metadata_file, args.lock_file, record, log)
members_refreshed = False
+ members_public_count: int | None = None
if not args.no_refresh_landing_members and args.landing_document_root:
root = args.landing_document_root.resolve()
if root.is_dir():
log.info("=== fase: actualizar members.json da landing (%s)", root)
- members_refreshed = try_refresh_landing_members_json(
+ members_refreshed, members_public_count = try_refresh_landing_members_json(
document_root=root,
users_json=args.metadata_file,
homes_root=args.members_homes_root.resolve()
@@ -1882,8 +1894,9 @@ def main(argv: list[str] | None = None) -> int:
log=log,
)
else:
- log.info(
- "landing document root inexistente (%s); omitindo build_directory.py",
+ log.warning(
+ "DocumentRoot da landing inexistente (%s); constelação/bolhas não actualizadas "
+ "(corra site/genlanding.py antes ou aponte --landing-document-root para o DocumentRoot real).",
root,
)
@@ -1910,15 +1923,33 @@ def main(argv: list[str] | None = None) -> int:
print(f" URL prevista: {args.base_url.rstrip('/')}/~{user}/")
print(f" fingerprint: {fingerprint}")
print(f" metadados: {args.metadata_file}")
- if members_refreshed:
- print(
- f" landing members: {args.landing_document_root.resolve() / 'data' / 'members.json'}",
- )
- elif not args.no_refresh_landing_members and args.landing_document_root:
- dr = args.landing_document_root.resolve()
- if dr.is_dir():
+ dr_resolved = (
+ args.landing_document_root.resolve() if args.landing_document_root else None
+ )
+ out_members = (dr_resolved / "data" / "members.json") if dr_resolved else None
+ if args.no_refresh_landing_members:
+ print(" constelação (bolhas): omitida (--no-refresh-landing-members)")
+ elif dr_resolved is not None:
+ if not dr_resolved.is_dir():
+ print(
+ f" AVISO constelação: DocumentRoot inexistente ({dr_resolved}) — "
+ "bolhas não actualizadas. Depois de criar o site: "
+ f"python3 {_REPO_ROOT / 'site' / 'build_directory.py'} "
+ f"--users-json {args.metadata_file} -o {out_members}",
+ file=sys.stderr,
+ )
+ elif members_refreshed:
+ cnt = (
+ f", {members_public_count} membro(s) público(s)"
+ if members_public_count is not None
+ else ""
+ )
+ print(f" constelação (bolhas): actualizado{cnt} → {out_members}")
+ else:
print(
- " landing members: (falha ao regenerar; ver log — corra build_directory.py manualmente)",
+ f" AVISO constelação: falha ao regenerar members.json (ver log). "
+ f"Manual: python3 {_REPO_ROOT / 'site' / 'build_directory.py'} "
+ f"--users-json {args.metadata_file} -o {out_members}",
file=sys.stderr,
)
if args.no_quota:
diff --git a/scripts/admin/perm1.md b/scripts/admin/perm1.md
@@ -1,34 +0,0 @@
-# perm1 — jail para contas existentes
-
-Script **`scripts/admin/perm1.py`** (root): adiciona utilizadores **uid ≥ 1000** ao grupo **`runv-jailed`** e cria o layout Jailkit em **`/srv/jail/<user>`** com **bind mount** da home real, mais linha em **`/etc/fstab`** (idempotente).
-
-**Excluídos:** `nobody`, `pmurad-admin`, `entre`.
-
-**Pré-requisitos:** `tools/tools.py` já aplicado (pacote **jailkit**, drop-in **`90-runv-jailed.conf`**, grupo `runv-jailed`).
-
-## Opções Jailkit
-
-- **`--jk-profile`** — perfil passado a `jk_init` quando o jail **ainda não tem** `bin/` (default: **`extendedshell`**, mais completo que `basicshell`). Valores: `extendedshell`, `basicshell`.
-- **`--no-jk-init`** — **não** executa `jk_init`; só adiciona ao grupo, garante `home/<user>` no jail, bind e fstab. Exige que **`/srv/jail/<user>/bin`** já exista (jail pré-provisionado); caso contrário o script falha com mensagem explícita.
-
-Se `bin/` já existir, `jk_init` **não** é voltado a correr (idempotente).
-
-## Reverter (undo)
-
-O script **`patches/undoperm.py`** (na raiz do repositório) remove o utilizador de `runv-jailed`, desmonta o bind, apaga a linha em `/etc/fstab` e, só com **`--purge-jail-dir`**, remove `/srv/jail/<user>`. **Não** restaura ficheiros alterados por `jk_init`.
-
-```bash
-sudo python3 patches/undoperm.py --verbose --dry-run
-sudo python3 patches/undoperm.py --only-user maria
-```
-
-## Exemplos perm1
-
-```bash
-sudo python3 scripts/admin/perm1.py --verbose
-sudo python3 scripts/admin/perm1.py --only-user maria --dry-run
-sudo python3 scripts/admin/perm1.py --jk-profile basicshell
-sudo python3 scripts/admin/perm1.py --no-jk-init --only-user maria
-```
-
-Após aplicar, teste SSH com um utilizador antes de confiar em produção.
diff --git a/scripts/create_runv_user.md b/scripts/create_runv_user.md
@@ -1,281 +0,0 @@
-# create_runv_user — provisionamento interno (runv.club)
-
-**Versão 0.02** · **Desenvolvido por pmurad — 2026**
-
-Ferramenta de linha de comando para **administradores** criarem contas Unix no servidor **Debian/Linux** (runv.club). Não é cadastro público.
-
-É a **fonte principal** da política de provisionamento: usa `adduser`, mas o fluxo completo (SSH, `public_html`, jail `runv-jailed`, `README.md` opcional, permissões, quota, metadados, log) está centralizado aqui — sem depender de `adduser.local`, `QUOTAUSER` ou regras em `/etc/adduser.conf`.
-
-**Ambiente:** execute apenas no servidor (ou VM Debian). O script usa `pwd`, `fcntl`, `adduser`, `ssh-keygen`, `findmnt`/`setquota` — **não é suportado no Windows.**
-
-### O que o script garante (ordem de execução)
-
-1. **Criar o usuário** — `adduser --disabled-password` (conta Unix).
-2. **Instalar a chave** — `~/.ssh/authorized_keys` com chave validada e modos `700` / `600`.
-3. **Preparar `public_html`** — diretório `755` e `~/public_html/index.html` estático (sem JavaScript, sem CDN); não sobrescreve sem `--force-index`.
-4. **Preparar Gopher e Gemini** — `~/public_gopher/` com `gophermap` e `~/public_gemini/` com `index.gmi` (modelos em português); não sobrescreve sem **`--force-gopher`** / **`--force-gemini`**. Se existir **`/var/gemini/users`**, aplica **bind mount** **`/var/gemini/users/<user>`** ← **`~/public_gemini`** (via `setup_alt_protocols`; **`--force-gemini`** migra symlink legado). Se essa pasta global não existir, regista **aviso** no log — corra **[`setup_alt_protocols.py`](docs/alt_protocols.md)** no servidor.
-5. **Skel** — o Debian **copia `/etc/skel` no passo 1**. O skel instalado por **`tools/tools.py`** **não** inclui `README.md`. Só é criado `~/README.md` com **`--with-readme`**; **`--force-readme`** só faz sentido em conjunto (substituir se já existir).
-6. **Permissões** — `apply_runv_permissions` reforça home `755`, `.ssh`, sites públicos e, se existir, `README.md`.
-7. **Jail SSH** — por omissão: grupo **`runv-jailed`**, **`/srv/jail/<user>`**, `jk_init extendedshell` (perfil Jailkit; idempotente se `bin/` já existir), **bind** de `/home/<user>` em `/srv/jail/<user>/home/<user>`, linha em **`/etc/fstab`** (idempotente). **Não** aplica a **`entre`** nem **`pmurad-admin`**. **`--no-jail`** desliga. Requer **`tools/tools.py`** já aplicado (jailkit + drop-in sshd). Contas **já existentes**: **[`admin/perm1.py`](admin/perm1.md)**; reversão: **`patches/undoperm.py`**.
-8. **Quota** (se ativa), verificação final e metadados JSON.
-
-**Log** em arquivo (e stderr com `--verbose`) com estas fases numeradas, quota, metadados e verificação final.
-
-### Email de boas-vindas ao utilizador
-
-Após criar a conta com sucesso (e antes do aviso de quota parcial, se aplicável), o script tenta enviar o template **`user_account_created`** para o endereço de metadado (`--email`), usando `lib.mailer` e o estado global em **`/etc/runv-email.json`** (Mailgun ou SMTP legado — ver `email/docs/INSTALL.md`).
-
-- Requisitos: configurador de email já executado; pasta `email/` encontrável (`RUNV_EMAIL_ROOT`, `email_package_root` no JSON, ou repositório ao lado de `scripts/`).
-- **`--no-welcome-email`** — não envia.
-- **`--welcome-ssh-host HOST`** ou **`RUNV_WELCOME_SSH_HOST`** — inclui no corpo um comando `ssh usuario@HOST` concreto; sem isso, o texto usa um placeholder `<hostname>` e pede ao utilizador que confirme o endereço com o administrador.
-- Falhas de envio **não** revertem a criação da conta; ficam só no log.
-
-## Quota ext4
-
-O `create_runv_user.py` descobre **automaticamente** o mount que contém `/home/username` (`findmnt` / `admin/runv_mount.py` no repositório) e aplica `setquota` nesse ponto — tanto se a home está na **raiz `/`** como se **`/home`** é um volume **ext4** separado. O filesystem tem de ser **ext4** com **`usrquota`** (ou **`usrjquota=`**) ativo nesse mount.
-
-- **Não** usa `xfs_quota` nem assume XFS.
-- **Não** altera `/etc/fstab`, **não** remonta, **não** reinicia a máquina, **não** executa `quotaon` por si.
-- **Apenas verifica** se o ambiente está pronto e, em caso afirmativo, aplica limites ao utilizador recém-criado.
-
-### Preparar o sistema (Debian 13)
-
-**Recomendado:** no servidor novo, correr **`admin/starthere.py`** (ver **[starthere.md](starthere.md)**) como root — instala pacotes, pode ativar Apache/UFW e configura **usrquota** no mount detetado a partir de `/home` (o mesmo critério que este script).
-
-**Alternativa manual** (se não usar `starthere.py`):
-
-1. Instalar ferramentas: `sudo apt install quota`
-2. Em **`/etc/fstab`**, na linha do mount onde está a home (no caso típico, `/`), acrescentar **`usrquota`** (e opcionalmente `grpquota`) nas opções de mount, por exemplo:
- `UUID=... / ext4 defaults,usrquota 0 1`
-3. Remontar read-write com nova opção ou reiniciar:
- `sudo mount -o remount /`
-4. Inicializar ficheiros de quota (ajuste o mountpoint se não for `/`):
- `sudo quotacheck -cum /`
- (pode demorar; em sistemas com quota já ativa use os flags que a sua política recomendar.)
-5. Ativar quotas:
- `sudo quotaon -v /`
-6. Confirmar:
- `findmnt -n -o OPTIONS /` deve mostrar `usrquota` ou `usrjquota=...`
-
-Só depois disto o `create_runv_user.py` conseguirá aplicar limites automaticamente.
-
-### Política padrão runv (ajustável por flags)
-
-| Limite | Padrão |
-|--------|--------|
-| Blocos soft | 450 MiB |
-| Blocos hard | 500 MiB |
-| Inodes soft | 10000 |
-| Inodes hard | 12000 |
-
-**Unidades:** os flags `--quota-soft-mb` e `--quota-hard-mb` usam o sufixo histórico `-mb`, mas os valores são **MiB** (mebibytes, 1024² bytes), **não** megabytes decimais (10⁶). Internamente convertem para as unidades de **1 KiB** que o `setquota` usa em ext4 (vfsv0): `kib = mib * 1024`.
-
-### Comportamento e política de falhas (v1)
-
-| Situação | Comportamento |
-|----------|----------------|
-| **Padrão** (sem `--no-quota`) | Após criar o utilizador, tenta aplicar quota. Se o sistema **não** estiver preparado ou `setquota` falhar, a conta **permanece**, metadados gravados com `status: partial_quota` e `quota_status: failed` ou `not_configured`, **saída 3** (`EXIT_INCONSISTENT`), mensagem de aviso forte no stderr. |
-| **`--require-quota`** | Antes de `adduser`, verifica ext4 + `usrquota`/usrjquota + `setquota`. Se falhar, **aborta sem criar** utilizador (saída 1). |
-| **`--no-quota`** | Não chama `setquota`; metadados com `quota_enabled: false`, `quota_status: skipped`. |
-
-Não há remoção automática da conta quando só a quota falha; o admin decide (ex.: `del-user.py` ou `deluser` manual).
-
-## Modo interativo (recomendado)
-
-Sem argumentos, o script entra em **modo interativo**: mostra o cabeçalho (versão e crédito), faz perguntas e você responde no terminal.
-
-```bash
-sudo python3 /usr/local/bin/create_runv_user.py
-```
-
-Ou explicitamente:
-
-```bash
-sudo python3 /usr/local/bin/create_runv_user.py --interactive
-# ou
-sudo python3 /usr/local/bin/create_runv_user.py -i
-```
-
-Fluxo típico:
-
-1. Nome de usuário Unix
-2. Email administrativo (metadado)
-3. Chave SSH: colar **uma linha** OpenSSH ou indicar **caminho** de um arquivo `.pub`
-4. Dry-run (só validar, sem criar usuário) — sim/não
-5. Se for criar de verdade: sobrescrever `index.html` existente — sim/não
-6. Se for criar de verdade: sobrescrever `gophermap` existente (`--force-gopher`) — sim/não
-7. Se for criar de verdade: sobrescrever `index.gmi` existente (`--force-gemini`) — sim/não
-8. Se for criar de verdade: criar `~/README.md` (`--with-readme`) — sim/não (padrão não)
-9. Se criar README: sobrescrever se já existir (`--force-readme`) — sim/não
-10. Se for criar de verdade: omitir jail SSH (`--no-jail`) — sim/não (padrão não = aplicar jail)
-11. Log verboso — sim/não
-12. Criar **sem** quota (`--no-quota`) — sim/não (padrão não)
-13. Se for com quota: exigir sistema pronto **antes** de criar (`--require-quota`) — sim/não (padrão não)
-14. Confirmação final antes de executar
-
-`Ctrl+C` cancela. Se responder “não” na confirmação final, o script encerra sem alterar o sistema.
-
-## Modo não interativo (CLI)
-
-Nos exemplos com caminho **`admin/create_runv_user.py`**, execute a partir do diretório **`scripts/`** do repositório (ou ajuste o caminho). Em produção, use normalmente **`/usr/local/bin/create_runv_user.py`** após `install`.
-
-### Criação normal com quota (padrão)
-
-```bash
-sudo python3 admin/create_runv_user.py \
- --username alice \
- --email alice@example.com \
- --public-key "ssh-ed25519 AAAA... comentario"
-```
-
-Opcional: **`--with-readme`**, **`--no-jail`** (conta sem chroot SSH).
-
-### Sem quota
-
-```bash
-sudo python3 admin/create_runv_user.py \
- --username alice \
- --email alice@example.com \
- --public-key-file /root/alice.pub \
- --no-quota
-```
-
-### Exigir quota configurada antes de criar
-
-```bash
-sudo python3 admin/create_runv_user.py \
- -u alice \
- --email alice@example.com \
- --public-key "ssh-ed25519 AAAA..." \
- --require-quota
-```
-
-Se `usrquota` não estiver ativo em `/`, o script termina **sem** chamar `adduser`.
-
-### Dry-run
-
-```bash
-python3 admin/create_runv_user.py \
- --username alice \
- --email alice@example.com \
- --public-key "ssh-ed25519 AAAA..." \
- --dry-run
-```
-
-(Não exige root.)
-
-### Limites personalizados
-
-```bash
-sudo python3 admin/create_runv_user.py \
- -u bob \
- --email bob@example.com \
- --public-key "..." \
- --quota-soft-mb 400 \
- --quota-hard-mb 450 \
- --quota-inode-soft 8000 \
- --quota-inode-hard 9000
-```
-
-### Exemplo: falha por quota não habilitada
-
-Com utilizador criado com sucesso mas mount **sem** `usrquota`:
-
-- Stderr: aviso forte de conta criada sem quota aplicada.
-- Exit code **3**.
-- Em `/var/lib/runv/users.json`: `status: partial_quota`, `quota_status: not_configured` (ou `failed` se `setquota` falhou).
-
-Versão e crédito:
-
-```bash
-python3 admin/create_runv_user.py --version
-```
-
-## Pré-requisitos no servidor
-
-- Debian 13 (ou outro Linux com `adduser` e `deluser`)
-- Python 3 (`python3`)
-- Pacotes: `openssh-client` (`ssh-keygen`), `adduser`, **`quota`** (para `setquota`), **`util-linux`** (`findmnt`)
-- Para quota: ext4 com **`usrquota`** (ou **`usrjquota=`**) no mount que contém `/home`
-- Apache com `mod_userdir` já configurado (o script não altera o Apache)
-- SSH com chaves (o script não altera `sshd_config`)
-
-## Instalação
-
-```bash
-sudo install -m 755 admin/create_runv_user.py /usr/local/bin/create_runv_user.py
-sudo mkdir -p /var/lib/runv
-```
-
-Log padrão: `/var/log/runv-user-provision.log`
-Metadados: `/var/lib/runv/users.json`
-
-### Landing (`members.json`)
-
-Após gravar metadados, o script executa por omissão [`site/build_directory.py`](../site/build_directory.md) (via `python3` no repositório ao lado de `site/`) para actualizar **`data/members.json`** no DocumentRoot da landing (padrão **`/var/www/runv.club/html`**), desde que essa pasta exista. Assim a constelação na página reflecte a nova conta **sem cron**.
-
-- **`--no-refresh-landing-members`** — não chama `build_directory`.
-- **`--landing-document-root PATH`** — outro DocumentRoot (default: `/var/www/runv.club/html`).
-- **`--members-homes-root PATH`** — passa `--homes-root` ao `build_directory` (ex. `/home` para `homepage_mtime`).
-
-## Opções úteis (CLI)
-
-- `--dry-run` — valida tudo e mostra o plano sem criar usuário
-- `--verbose` — mais detalhes no stderr
-- `--force-index` — sobrescreve `~/public_html/index.html` se já existir
-- `--force-gopher` — sobrescreve `~/public_gopher/gophermap` se já existir
-- `--force-gemini` — sobrescreve `~/public_gemini/index.gmi` se já existir e corrige o **bind mount** em `/var/gemini/users/<user>` (migra symlink legado) se necessário
-- `--force-readme` — sobrescreve `~/README.md` se já existir (útil se o skel do sistema já criou um README)
-- `--no-quota` — não aplica `setquota`
-- `--require-quota` — falha antes de `adduser` se quota não estiver disponível
-- `--quota-soft-mb`, `--quota-hard-mb`, `--quota-inode-soft`, `--quota-inode-hard` — limites (MiB para blocos)
-- `--metadata-file`, `--lock-file`, `--log-file` — caminhos alternativos (ex.: testes em VM)
-- `--base-url` — URL base no resumo (padrão `http://runv.club`)
-- `--landing-document-root`, `--no-refresh-landing-members`, `--members-homes-root` — ver secção *Landing* acima
-
-## Metadados JSON (campos de quota)
-
-Cada registo pode incluir:
-
-- `quota_enabled` (bool)
-- `quota_soft_mb`, `quota_hard_mb` (int ou null)
-- `quota_inode_soft`, `quota_inode_hard` (int ou null)
-- `quota_filesystem`, `quota_mountpoint` (string ou null)
-- `quota_applied_at` (ISO 8601 ou null)
-- `quota_status`: `skipped` | `applied` | `failed` | `not_configured`
-
-## Códigos de saída
-
-| Código | Significado |
-|--------|-------------|
-| 0 | Sucesso (utilizador criado e, se aplicável, quota aplicada) |
-| 1 | Erro de validação ou argumentos (incl. `--require-quota` com sistema não pronto) |
-| 2 | Falha de sistema (subprocess, permissões) antes/desde rollback completo |
-| 3 | Estado inconsistente: utilizador criado mas quota não aplicada / não configurada; ou rollback falhou |
-
-## Como testar no Debian 13 (resumo)
-
-1. Configure quota no `/` conforme a secção “Preparar o sistema”.
-2. `sudo python3 admin/create_runv_user.py --username testquota ... --verbose`
-3. `sudo quota -u testquota` ou `repquota /` para ver limites.
-4. Teste `--dry-run` sem root.
-5. Teste `--require-quota` com fstab **sem** usrquota: deve sair **1** sem criar utilizador.
-6. Remova o utilizador de teste com a sua ferramenta de banimento (`admin/del-user.py`) quando terminar.
-
-## Segurança (resumo)
-
-- Sem `shell=True`; subprocess só com lista de argumentos.
-- Username e caminhos validados; sem path traversal.
-- Chave pública validada com `ssh-keygen`; fingerprint SHA256 em metadados.
-- Email é só metadado administrativo, não conta Unix.
-
-## Limitações
-
-- Quota suportada: **ext4** com quota de utilizador tradicional; outros filesystems recusados com mensagem clara.
-- Sem remoção de utilizador por este script (use `admin/del-user.py` a partir de `scripts/` no repositório, ou a cópia em `/usr/local/bin` se instalou).
-- O script **não** configura automaticamente fstab nem `quotaon`.
-- Backup de `/var/lib/runv/users.json` é manual.
-
-## Dependências Python
-
-Nenhuma biblioteca PyPI — apenas a biblioteca padrão (ver `requirements.txt`).
diff --git a/scripts/del-user.md b/scripts/del-user.md
@@ -1,123 +0,0 @@
-# del-user.py — banimento / remoção de conta (runv.club)
-
-**Versão 0.02** · runv.club
-
-Ferramenta para **administradores** removerem **permanentemente** um utilizador Unix no Debian (banimento no runv.club): apaga a conta e, por defeito, a home com `deluser --remove-home`.
-
-- **Não** remove nem altera configuração do Apache ou SSH globalmente.
-- Opcionalmente remove a entrada correspondente em `/var/lib/runv/users.json` (mesmo formato que `create_runv_user.py`).
-- **Gemini:** **umount** do bind em **`/var/gemini/users/<user>`** se estiver montado, remove a linha correspondente em **`/etc/fstab`**, remove symlink legado ou directório vazio.
-- Se a home estiver num **ext4** com **usrquota** ativo, tenta **`setquota`** para repor limites a zero **antes** de `deluser` (mount detetado automaticamente, mesma lógica que `create_runv_user.py` / `runv_mount.py`). Se `setquota` falhar, a remoção da conta continua com aviso em stderr.
-
-**Ambiente:** servidor **Linux** (Debian). Executar como **root** ou `sudo`. No Windows use só para revisão do código.
-
-## Objetivo
-
-- Eliminar o utilizador do sistema (`deluser`).
-- Remover a pasta home (`--remove-home`) ou, se pedido, todos os ficheiros detidos pelo UID (`--purge-all-files`).
-- Manter o registo interno coerente ao apagar o username do JSON de metadados runv (opcional).
-
-## Segurança
-
-- **Nunca** remove `root`.
-- Recusa contas **reservadas** (ex.: `www-data`, `nobody`) salvo `--force`.
-- Recusa UID **< 1000** (contas de sistema típicas) salvo `--force`.
-- Confirmação interativa: tem de **digitar o username** à letra (salvo `-y`/`--yes`).
-- Sem `shell=True`; usa `subprocess` com lista de argumentos.
-
-## Requisitos
-
-- Pacote Debian `adduser` (fornece o comando `deluser`).
-- Python 3 (stdlib: `pathlib`, `fcntl`, `json`, etc.).
-
-## Uso
-
-Nos exemplos com **`admin/del-user.py`**, execute a partir do diretório **`scripts/`** do repositório.
-
-### Simular (sem root)
-
-```bash
-python3 admin/del-user.py -u alguem --dry-run
-python3 admin/del-user.py -u alguem --dry-run --verbose
-```
-
-### Remover (interativo)
-
-```bash
-sudo python3 admin/del-user.py --username spammer
-```
-
-O script pede que escreva de novo o username para confirmar.
-
-### Remover sem pergunta (automação / scripts)
-
-```bash
-sudo python3 admin/del-user.py -u spammer --yes
-```
-
-### Remover também ficheiros do utilizador fora da home
-
-Cuidado: apaga **todos** os ficheiros detidos por esse UID no sistema.
-
-```bash
-sudo python3 admin/del-user.py -u spammer --yes --purge-all-files
-```
-
-### Não tocar no `users.json`
-
-```bash
-sudo python3 admin/del-user.py -u spammer --yes --skip-metadata
-```
-
-### Forçar remoção de conta “de sistema” (perigoso)
-
-```bash
-sudo python3 admin/del-user.py -u algum --yes --force
-```
-
-## Opções
-
-| Opção | Significado |
-|--------|-------------|
-| `-u`, `--username` | Utilizador a remover (obrigatório). |
-| `--dry-run` | Só mostra o plano; não exige root. |
-| `-v`, `--verbose` | Mais saída (comando `deluser`, etc.). |
-| `-y`, `--yes` | Não pede confirmação interativa. |
-| `--force` | Ignora bloqueio a contas reservadas / UID < 1000. |
-| `--purge-all-files` | Usa `deluser --remove-all-files` em vez de `--remove-home`. |
-| `--skip-metadata` | Não altera `/var/lib/runv/users.json`. |
-| `--metadata-file` | Caminho alternativo ao JSON de metadados. |
-| `--lock-file` | Lock `flock` para escrita do JSON (default runv). |
-
-## Códigos de saída
-
-- `0` — sucesso.
-- `1` — validação / utilizador inexistente / confirmação cancelada.
-- `2` — falha de `deluser` ou erro ao gravar metadados.
-
-## Limitações
-
-- Se o utilizador tiver sessões ativas ou processos a correr, `deluser` pode falhar ou comportar-se de forma estranha — termine sessões antes, se necessário.
-- `--purge-all-files` pode afetar ficheiros em diretórios partilhados se o UID tiver dono em mais sítios; use com consciência.
-- O script **não** revoga tokens ou chaves noutros serviços (só o que o SO e os teus processos fizerem com a conta removida).
-
-## Exemplo de saída (trecho)
-
-```
-del-user.py — removendo 'spammer' (UID 1005)
-
- [exec] deluser --remove-home spammer
- [ok] deluser concluído para 'spammer'
- [metadata] removido registo de 'spammer' em /var/lib/runv/users.json
-
---- Resumo ---
- Conta removida: 'spammer'
- Próximo passo: verificar se não restam processos desse UID ...
-```
-
-## Relação com outros scripts
-
-- **`create_runv_user.py`**: cria conta e acrescenta linha ao JSON.
-- **`del-user.py`**: remove conta e remove a linha com o mesmo `username` no JSON (salvo `--skip-metadata`).
-
-— runv.club
diff --git a/scripts/docs/1 - begining.md b/scripts/docs/1 - begining.md
@@ -1,184 +0,0 @@
-# runv.club – Initial Server Setup (SSH Hardening Guide)
-
-## Configurações básicas do servidor (Debian)
-
-Antes do hardening SSH e dos scripts em `scripts/admin/` (`starthere.py`, `create_runv_user.py`, …), convém deixar o sistema **identificável**, com **hora fiável** e **locale** coerente. Executar como **root** ou `sudo` onde indicado.
-
-### Nome do host (hostname)
-
-Escolha um nome estável (ex.: `runv-debian`, `runv-prod`). Evite espaços e caracteres estranhos.
-
-```bash
-sudo hostnamectl set-hostname runv-debian
-hostnamectl status
-```
-
-Garanta que o FQDN local resolve (muitas stacks Debian esperam uma linha `127.0.1.1`):
-
-```bash
-grep -E '^127\.0\.1\.1' /etc/hosts || \
- echo "127.0.1.1 runv-debian" | sudo tee -a /etc/hosts
-```
-
-(Ajuste `runv-debian` ao hostname que definiu.)
-
-### Fuso horário e relógio (NTP)
-
-Para servidores é comum **UTC**; se preferir hora local (ex. Portugal):
-
-```bash
-sudo timedatectl set-timezone Europe/Lisbon
-# ou: sudo timedatectl set-timezone UTC
-sudo timedatectl set-ntp true
-timedatectl status
-```
-
-Confirme que **NTP sync: yes** e que a data/hora estão corretas. Logs e metadados (`create_runv_user` grava timestamps) ficam alinhados.
-
-### Locale e teclado (opcional mas útil)
-
-```bash
-sudo dpkg-reconfigure locales
-```
-
-Selecione pelo menos `en_US.UTF-8` ou `pt_PT.UTF-8` (UTF-8). Para consola:
-
-```bash
-sudo dpkg-reconfigure keyboard-configuration
-```
-
-### Pacotes e índices APT
-
-Após timezone/locale:
-
-```bash
-sudo apt update
-sudo apt full-upgrade -y
-```
-
-### Notas para o projeto runv-server
-
-- **Debian recente** (ex. 13): alinhado a `scripts/requirements.txt` e aos guias em `scripts/*.md`.
-- **Quotas ext4:** não edite à mão `fstab`/quotas se for usar **`starthere.py`** — ele deteta o mount de `/home` e prepara `usrquota` de forma coerente com **`create_runv_user.py`**.
-- **Documentação interna:** anote hostname, IP público/privado e timezone num sítio da equipa (evita confusão entre VPS X / VPS Y).
-
----
-
-## Overview
-This document describes the initial secure setup of the runv.club server on Debian 13.
-The goal is to establish a safe baseline before installing pubnix / shared-hosting services for runv.club.
-
----
-
-## 1. Create Admin User
-
-```bash
-adduser pmurad
-adduser pmurad sudo
-```
-
-Verify:
-```bash
-id pmurad
-```
-
-Switch:
-```bash
-su - pmurad
-```
-
-Test:
-```bash
-sudo whoami
-```
-
----
-
-## 2. Generate SSH Key (Client)
-
-```powershell
-ssh-keygen -t ed25519 -C "runv-sandbox" -f "$env:USERPROFILE\.ssh\runv-sandbox"
-```
-
----
-
-## 3. Install Public Key
-
-```bash
-mkdir -p /home/pmurad/.ssh
-chmod 700 /home/pmurad/.ssh
-
-cat > /home/pmurad/.ssh/authorized_keys <<'EOF'
-<YOUR PUBLIC KEY>
-EOF
-
-chmod 600 /home/pmurad/.ssh/authorized_keys
-chown -R pmurad:pmurad /home/pmurad/.ssh
-```
-
----
-
-## 4. Test SSH Login
-
-```powershell
-ssh -i "$env:USERPROFILE\.ssh\runv-sandbox" pmurad@SERVER_IP
-```
-
----
-
-## 5. Check SSH Config
-
-```bash
-sudo sshd -T | grep -E 'passwordauthentication|pubkeyauthentication|permitrootlogin'
-```
-
----
-
-## 6. Disable Root Login
-
-```bash
-sudo mkdir -p /etc/ssh/sshd_config.d
-
-sudo tee /etc/ssh/sshd_config.d/99-runv-hardening.conf > /dev/null <<'EOF'
-PermitRootLogin no
-EOF
-```
-
-Validate:
-```bash
-sudo sshd -t
-sudo systemctl reload ssh
-```
-
----
-
-## 7. Disable Password Auth
-
-```bash
-sudo tee /etc/ssh/sshd_config.d/99-runv-hardening.conf > /dev/null <<'EOF'
-PermitRootLogin no
-PasswordAuthentication no
-PubkeyAuthentication yes
-EOF
-```
-
-Reload:
-```bash
-sudo sshd -t
-sudo systemctl reload ssh
-```
-
-Verify:
-```bash
-sudo sshd -T | grep -E 'passwordauthentication|pubkeyauthentication|permitrootlogin'
-```
-
----
-
-## Final State
-
-- Root login: disabled
-- Password login: disabled
-- Key authentication: enabled
-
-Secure SSH baseline achieved.
diff --git a/scripts/docs/2 - server setup.md b/scripts/docs/2 - server setup.md
@@ -1,625 +0,0 @@
-# runv.club – Fase 2: Apache, UserDir e o primeiro utilizador com site pessoal
-
-## Objetivo
-
-Este documento continua a configuração inicial do servidor **runv.club** em **Debian 13**.
-
-Neste ponto, o servidor já tem:
-
-- um usuário administrador sem root (`pmurad`)
-- login por chave SSH funcionando
-- login como root desativado no SSH
-- autenticação por senha desativada no SSH
-
-Agora o objetivo é montar o **primeiro caminho real de publicação web por utilizador** (`~username`):
-
-- instalar o Apache
-- habilitar páginas `~username`
-- criar um usuário de teste
-- fazer `~/public_html` funcionar
-- confirmar que `http://SERVIDOR/~testuser/` carrega
-
-Esta é a primeira prova concreta de que a máquina serve sites pessoais por conta Unix no runv.club.
-
----
-
-## O que estamos construindo
-
-Neste desenho, cada utilizador publica um site a partir do diretório home.
-
-O padrão clássico é:
-
-- existe conta de usuário
-- o usuário tem uma pasta chamada `public_html`
-- o Apache serve essa pasta em:
-
-```text
-http://seu-dominio/~username/
-```
-
-Por exemplo, no runv.club, o alvo futuro é:
-
-```text
-http://runv.club/~testuser/
-```
-
-Para testes em VM local antes do DNS estar pronto, pode ser algo como:
-
-```text
-http://192.168.x.x/~testuser/
-```
-
-ou
-
-```text
-http://runv-debian/~testuser/
-```
-
-conforme sua rede e resolução de nomes.
-
----
-
-## Aviso importante antes de começar
-
-**Não** instale pacotes extras aleatórios ainda.
-
-Você **não** precisa agora de:
-
-- PHP
-- MariaDB
-- PostgreSQL
-- Node.js
-- Docker
-- Certbot
-- servidor de e-mail
-- BBJ
-- ttbp
-- botany
-
-Isso seria prematuro e desnecessário.
-
-Por enquanto você só precisa do mínimo para provar:
-
-1. Apache funciona
-2. `mod_userdir` funciona
-3. permissões estão corretas
-4. publicação a partir do home do usuário funciona
-
-Até isso funcionar, o resto é ruído.
-
----
-
-## Passo 1 – Atualizar listas de pacotes
-
-Entre como `pmurad` e execute:
-
-```bash
-sudo apt update
-```
-
-Em seguida, atualize os pacotes instalados:
-
-```bash
-sudo apt upgrade -y
-```
-
-Assim a máquina fica atualizada antes de instalar o Apache.
-
----
-
-## Passo 2 – Instalar o Apache
-
-Instale o Apache com:
-
-```bash
-sudo apt install -y apache2
-```
-
-Após a instalação, verifique se o serviço está em execução:
-
-```bash
-sudo systemctl status apache2
-```
-
-Você deve ver algo como:
-
-```text
-active (running)
-```
-
-Se quiser uma verificação mais curta:
-
-```bash
-systemctl is-active apache2
-```
-
-Resultado esperado:
-
-```text
-active
-```
-
-Se o Apache não estiver em execução, inicie-o:
-
-```bash
-sudo systemctl start apache2
-```
-
-E habilite na inicialização:
-
-```bash
-sudo systemctl enable apache2
-```
-
----
-
-## Passo 3 – Testar o Apache a partir da própria VM
-
-Teste localmente primeiro, de dentro do Debian:
-
-```bash
-curl http://localhost
-```
-
-Você deve receber HTML da página padrão do Apache.
-
-Se o `curl` não estiver instalado:
-
-```bash
-sudo apt install -y curl
-```
-
-Se o Apache estiver funcionando, isso confirma que o servidor web está ativo antes de mexer no `UserDir`.
-
----
-
-## Passo 4 – Liberar HTTP no firewall
-
-Se você habilitou o UFW antes, é preciso liberar o tráfego web.
-
-Verifique o status do firewall:
-
-```bash
-sudo ufw status
-```
-
-Libere o Apache:
-
-```bash
-sudo ufw allow 'Apache'
-```
-
-Verifique de novo:
-
-```bash
-sudo ufw status
-```
-
-Você deve ver a porta 80 liberada.
-
-Se o UFW ainda não estiver habilitado, não é fatal em um sandbox de VM. Mesmo assim, se você pretende usá-lo, este é o momento certo para abrir o HTTP.
-
----
-
-## Passo 5 – Testar o Apache de outra máquina
-
-No seu Windows, abra o navegador e tente:
-
-```text
-http://IP_DA_VM/
-```
-
-Exemplo:
-
-```text
-http://192.168.50.120/
-```
-
-Você deve ver a página padrão do Apache.
-
-Se **não** vir, o problema é um destes:
-
-- Apache não está em execução
-- firewall bloqueando a porta 80
-- IP da VM errado
-- problema de bridge/rede no Proxmox
-- o navegador está batendo na máquina errada
-
-**Não** continue até a página padrão do Apache funcionar.
-
----
-
-## Passo 6 – Habilitar o módulo UserDir
-
-É o recurso que permite:
-
-```text
-/~username/
-```
-
-Habilite com:
-
-```bash
-sudo a2enmod userdir
-```
-
-Verifique a sintaxe da configuração do Apache:
-
-```bash
-sudo apache2ctl configtest
-```
-
-Resultado esperado:
-
-```text
-Syntax OK
-```
-
-Em seguida, recarregue o Apache:
-
-```bash
-sudo systemctl reload apache2
-```
-
-Neste ponto o Apache já conhece `~username`, mas ainda não há conteúdo de usuário para servir.
-
----
-
-## Passo 7 – Inspecionar a configuração do UserDir
-
-O pacote Apache do Debian costuma colocar a configuração do módulo em:
-
-```text
-/etc/apache2/mods-available/userdir.conf
-```
-
-Leia o arquivo:
-
-```bash
-cat /etc/apache2/mods-available/userdir.conf
-```
-
-Provavelmente você verá algo como:
-
-```apache
-UserDir public_html
-<Directory /home/*/public_html>
- AllowOverride FileInfo AuthConfig Limit Indexes
- Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec
- Require method GET POST OPTIONS
-</Directory>
-```
-
-O ponto principal é este:
-
-```apache
-UserDir public_html
-```
-
-Isso significa que o Apache procurará conteúdo em:
-
-```text
-/home/NOME_DO_USUARIO/public_html
-```
-
-Ótimo. É o que queremos.
-
----
-
-## Passo 8 – Criar um usuário de teste
-
-Crie uma conta de teste não administrativa para o teste de publicação via UserDir.
-
-**Não** use `pmurad` nesse teste.
-Mantenha papéis de admin e usuário comum separados.
-
-Crie o usuário:
-
-```bash
-sudo adduser testuser
-```
-
-Você pode dar uma senha temporária simples para uso em laboratório.
-
-Confirme que o home existe:
-
-```bash
-ls -ld /home/testuser
-```
-
----
-
-## Passo 9 – Criar public_html e uma página de teste
-
-Crie a pasta de publicação:
-
-```bash
-sudo -u testuser mkdir -p /home/testuser/public_html
-```
-
-Crie um arquivo HTML básico:
-
-```bash
-sudo -u testuser tee /home/testuser/public_html/index.html > /dev/null <<'EOF'
-<!doctype html>
-<html lang="pt-BR">
-<head>
- <meta charset="utf-8">
- <title>testuser no runv.club</title>
-</head>
-<body>
- <h1>Funcionou.</h1>
- <p>Esta é a primeira página pessoal no runv.club.</p>
-</body>
-</html>
-EOF
-```
-
-Verifique o arquivo:
-
-```bash
-ls -l /home/testuser/public_html/index.html
-```
-
----
-
-## Passo 10 – Ajustar permissões do jeito certo
-
-É aqui que iniciantes costumam errar.
-
-O Apache precisa conseguir:
-
-1. entrar em `/home/testuser`
-2. entrar em `/home/testuser/public_html`
-3. ler `/home/testuser/public_html/index.html`
-
-Defina as permissões assim:
-
-```bash
-sudo chmod 755 /home/testuser
-sudo chmod 755 /home/testuser/public_html
-sudo chmod 644 /home/testuser/public_html/index.html
-```
-
-Confira:
-
-```bash
-namei -l /home/testuser/public_html/index.html
-```
-
-Se o `namei` não for encontrado, instale o pacote que o fornece:
-
-```bash
-sudo apt install -y util-linux
-```
-
-O importante é que o usuário do Apache (`www-data`) consiga percorrer e ler o que precisa.
-
----
-
-## Passo 11 – Testar a página do usuário localmente
-
-De dentro do Debian:
-
-```bash
-curl http://localhost/~testuser/
-```
-
-A saída esperada deve incluir o seu HTML.
-
-Você também pode testar só o cabeçalho:
-
-```bash
-curl -I http://localhost/~testuser/
-```
-
-Um resultado saudável parece com:
-
-```text
-HTTP/1.1 200 OK
-```
-
-Se aparecer:
-
-```text
-403 Forbidden
-```
-
-o problema quase certamente são permissões.
-
-Se aparecer:
-
-```text
-404 Not Found
-```
-
-o caminho, o nome de usuário ou a configuração do módulo está errado.
-
----
-
-## Passo 12 – Testar no navegador
-
-Agora, no Windows, abra:
-
-```text
-http://IP_DA_VM/~testuser/
-```
-
-Se a página carregar, você tem o primeiro caminho real de publicação por utilizador.
-
-Esse é o marco.
-
----
-
-## Passo 13 – Entender os três modos de falha mais comuns
-
-### Falha 1 – 403 Forbidden
-
-Causa:
-- `/home/testuser` está muito restritivo
-- permissões de `public_html` erradas
-- permissões do arquivo erradas
-
-Correção:
-```bash
-sudo chmod 755 /home/testuser
-sudo chmod 755 /home/testuser/public_html
-sudo chmod 644 /home/testuser/public_html/index.html
-```
-
-### Falha 2 – 404 Not Found
-
-Causa:
-- módulo `userdir` não habilitado
-- nome da pasta errado
-- nome do arquivo ausente
-- erro de digitação no nome de usuário
-
-Verifique:
-```bash
-sudo a2query -m userdir
-ls -l /home/testuser/public_html
-```
-
-### Falha 3 – Página do Apache funciona, mas `~testuser` não
-
-Causa:
-- módulo carregado, mas permissões quebradas
-- `userdir.conf` alterado incorretamente
-- Apache não recarregado após habilitar o módulo
-
-Correção:
-```bash
-sudo apache2ctl configtest
-sudo systemctl reload apache2
-```
-
----
-
-## Passo 14 – Comandos úteis para diagnóstico
-
-Se algo falhar, estes são os primeiros comandos a usar.
-
-### Status do Apache
-```bash
-sudo systemctl status apache2
-```
-
-### Verificar sintaxe da configuração
-```bash
-sudo apache2ctl configtest
-```
-
-### Confirmar que o módulo está habilitado
-```bash
-sudo a2query -m userdir
-```
-
-### Ler o log de erro do Apache
-```bash
-sudo tail -n 50 /var/log/apache2/error.log
-```
-
-### Ler o log de acesso
-```bash
-sudo tail -n 50 /var/log/apache2/access.log
-```
-
-### Testar localmente
-```bash
-curl -I http://localhost/~testuser/
-```
-
-Esses logs importam. Não adivinhe quando o Apache já está dizendo o que está quebrado.
-
----
-
-## Passo 15 – Como fica o sucesso
-
-Você termina esta fase quando **tudo** isto for verdade:
-
-- Apache instalado
-- Apache inicia automaticamente
-- página padrão acessível
-- módulo `userdir` habilitado
-- `testuser` existe
-- `/home/testuser/public_html/index.html` existe
-- `http://localhost/~testuser/` retorna `200 OK`
-- `http://IP_DA_VM/~testuser/` abre no navegador
-
-Se algum item for falso, a fase não terminou.
-
----
-
-## Passo 16 – O que vem depois
-
-Só depois disso funcionando você deve ir para a camada seguinte:
-
-1. preparar `/etc/skel`
-2. definir os arquivos padrão que novos usuários recebem
-3. criar um modelo de homepage inicial mais limpo
-4. documentar como novos usuários publicam páginas
-5. mais tarde: adicionar ttbp, botany e outras ferramentas sociais
-
-**Não** pule para software de comunidade antes de provar que o caminho de publicação web funciona.
-
----
-
-## Resumo rápido de comandos
-
-Para conveniência, o fluxo principal de novo:
-
-```bash
-sudo apt update
-sudo apt upgrade -y
-sudo apt install -y apache2 curl
-sudo systemctl enable apache2
-sudo systemctl start apache2
-
-curl http://localhost
-
-sudo ufw allow 'Apache'
-
-sudo a2enmod userdir
-sudo apache2ctl configtest
-sudo systemctl reload apache2
-
-sudo adduser testuser
-
-sudo -u testuser mkdir -p /home/testuser/public_html
-
-sudo -u testuser tee /home/testuser/public_html/index.html > /dev/null <<'EOF'
-<!doctype html>
-<html lang="pt-BR">
-<head>
- <meta charset="utf-8">
- <title>testuser no runv.club</title>
-</head>
-<body>
- <h1>Funcionou.</h1>
- <p>Esta é a primeira página pessoal no runv.club.</p>
-</body>
-</html>
-EOF
-
-sudo chmod 755 /home/testuser
-sudo chmod 755 /home/testuser/public_html
-sudo chmod 644 /home/testuser/public_html/index.html
-
-curl -I http://localhost/~testuser/
-```
-
----
-
-## Nota final
-
-Este documento é propositalmente detalhado porque iniciantes costumam falhar aqui por motivos chatos:
-
-- permissões erradas
-- esquecer de recarregar o serviço
-- IP errado
-- firewall não aberto
-- testar na ordem errada
-
-Faça na ordem e funciona.
-Faça aleatoriamente e você perde tempo.
diff --git a/scripts/docs/alt_protocols.md b/scripts/docs/alt_protocols.md
@@ -1,190 +0,0 @@
-# Gopher e Gemini — `setup_alt_protocols.py`
-
-Script em **`scripts/admin/setup_alt_protocols.py`**: instala e configura **gophernicus** (Gopher, porta **70**) e **molly-brown** (Gemini, TLS, porta **1965**) no Debian, alinhado ao runv.club.
-
-## Modelo de conteúdo
-
-| Protocolo | Pasta na home | Ficheiro inicial | URL típica |
-|-----------|---------------|------------------|------------|
-| **HTTP** (já existente) | `~/public_html/` | `index.html` | `http://runv.club/~user/` |
-| **Gopher** | `~/public_gopher/` | `gophermap` | `gopher://runv.club/1/~user` |
-| **Gemini** | `~/public_gemini/` | `index.gmi` | `gemini://runv.club/~user/` (canónico: path **`/~user/`**, tilde colado ao nome); `gemini://runv.club/~/user/` redirecciona (**v0.11+**) |
-
-**Gemini (molly-brown):** `DocBase = /var/gemini`, `HomeDocBase = users`. O conteúdo de **`~/public_gemini`** expõe-se em **`/var/gemini/users/<user>`** com **`mount --bind`** e linha em **`/etc/fstab`** (**v0.13+**). O pacote **Molly Debian** recusa **symlinks** cujo destino fica fora do DocBase (`Refusing to follow symlink … outside of DocBase!` no error log); por isso **não** se usam symlinks para `~/public_gemini`.
-
-### Gopher vs Gemini: formato do endereço
-
-- **Gopher (gophernicus):** selectors **`~username/…`** (tilde **colado** ao nome), alinhado com URLs como **`gopher://runv.club/1/~user`**. Não há o mesmo «split» de path que no Molly.
-- **Gemini (Molly Brown):** o código Go do Molly trata o primeiro segmento do path como **`~username`** (tilde **colado** ao nome), p.ex. **`/~pmurad/`** → `DocBase/users/pmurad/`. O formato **`/~/pmurad/`** (slash entre `~` e o nome) faz o utilizador ficar **vazio** e devolve **51 Not found**. O `.conf` gerado (**v0.11+**) inclui **`[TempRedirects]`** que redirecciona **`/~/user…` → `/~user…`** para compatibilidade com links antigos.
-
-### URLs Gemini que *não* são capsules de utilizador
-
-O Molly **não** espelha o HTTP `mod_userdir` no mesmo path: **`gemini://runv.club/pmurad`** (path **`/pmurad`**) **não** aponta para a home. O capsule correcto é **`gemini://runv.club/~pmurad/`** (path **`/~pmurad/`**). **`gemini://runv.club/~/pmurad/`** (slash extra) **não** é o formato que o `resolvePath` entende sozinho; com **v0.11+**, o `.conf` redirecciona (**30**) esse pedido para **`gemini://runv.club/~pmurad/`** (o cliente deve seguir o redirect).
-
-## Travessia da home (`755` na política runv)
-
-Apache (`mod_userdir`), **gophernicus** e **molly-brown** precisam de **execução para «others»** (`o+x`, mínimo) em **cada** componente do caminho até a pasta pública (`~/public_html`, `~/public_gopher`, `~/public_gemini`). O utilizador de runtime **não é o mesmo** em todos: no Debian o pacote **molly-brown** usa **`User=molly-brown`** com **`DynamicUser=yes`** (não `www-data`); o **gophernicus** usa o **`User=`** do unit (tipicamente `gophernicus`) — veja `/lib/systemd/system/gophernicus@.service`. Uma home em **`700`** impede a travessia: **HTTP, Gopher e Gemini** deixam de servir conteúdo (p.ex. Gemini **«Not found»** com `index.gmi` presente).
-
-- **ACL (POSIX):** se `ls -l` mostrar **`+`** nos modos, há entradas **`getfacl`** além do `chmod`. Uma **mask** restritiva ou ausência de leitura efectiva para «other» / utilizador do serviço pode bloquear o Apache, gophernicus ou Molly mesmo com **`644`/`755`** aparentes. Diagnóstico: `getfacl ~/public_gemini/index.gmi` (e directórios no caminho).
-
-- **Novas contas:** [`create_runv_user.py`](../admin/create_runv_user.py) aplica **`755`** na home em `apply_runv_permissions`.
-- **Backfill:** a partir do **v0.07**, [`setup_alt_protocols.py`](../admin/setup_alt_protocols.py) repõe a home do utilizador para **`755`** quando o modo actual é outro (com registo em log). O **v0.08** corrige a detecção de caminhos Let's Encrypt quando `live`/`archive` são **symlinks**. O **v0.09** introduziu redirects Molly baseados numa leitura incorrecta do README upstream. O **v0.11** corrige **`[TempRedirects]`** para **`/~/user…` → `/~user…`** (alinhado ao `resolvePath` em Go). O **v0.12** documenta **ACL POSIX** na travessia e alarga o **WARNING** do `test -r` do `index.gmi` com indicação a `getfacl` quando `ls` mostra `+`. O **v0.13** substitui **symlinks** Gemini por **bind mounts** + **`fstab`** (compatível com o Molly Debian). Validação **`test -r`** do `gophermap` com o utilizador do serviço gophernicus mantém-se. O **v0.14** gera **`/var/gopher/gophermap`** (menu com **`1~user`**) e **`/var/gemini/index.gmi`** na raiz do DocBase para utilizadores com **`~/public_gopher`**, alinhando **`IRC_PATCH_SKIP_USERS`** (incl. **`entre`**, **`pmurad-admin`**) ao bind Gemini e à limpeza com **`--force`**.
-- **Conflito:** [`patches/patch_permissions.py`](../../patches/patch_permissions.py) pode aplicar **`chmod 700`** em cada `/home/<user>` por política de privacidade — isso **quebra** a hospedagem em `public_*` até voltar a alinhar permissões (provisionamento ou `chmod` manual).
-
-## Let's Encrypt e chave TLS (v0.07+; symlinks v0.08+)
-
-Quando o certificado Gemini está sob a árvore Let's Encrypt (por defeito **`/etc/letsencrypt/live/<domínio>/fullchain.pem`**), o script aplica **antes** de gravar o `.conf` do molly-brown. A partir do **v0.08**, as raízes `live` e `archive` são **resolvidas** (`resolve(strict=False)`): se `/etc/letsencrypt/live` for um **symlink**, o ajuste de `chmod` / `ssl-cert` nos `privkey` continua a aplicar-se ao caminho canónico correcto (deixa de aparecer no log um salto falso «cert não está sob …/live»).
-
-| Alvo | Acção |
-|------|--------|
-| `/etc/letsencrypt/live` | `chmod 755` |
-| `/etc/letsencrypt/archive` | `chmod 755` |
-| `/etc/letsencrypt/live/<domínio>` | `chmod 755` |
-| `/etc/letsencrypt/archive/<domínio>` | `chmod 755` (se existir) |
-| `archive/<domínio>/privkey*.pem` | `chgrp ssl-cert`, `chmod 640` |
-
-O `<domínio>` é o nome do directório pai de `fullchain.pem` (igual ao de `--gemini-cert` quando aponta para LE). Caminhos **fora** de `/etc/letsencrypt/live/` **não** são alterados.
-
-Se o grupo **`ssl-cert`** não existir no sistema, o script regista **WARNING** e não altera os `privkey*.pem` (instale o pacote que fornece esse grupo, p.ex. em Debian).
-
-**`certbot renew`** pode repor modos mais restritos nos directórios e chaves. Recomenda-se um script em **`/etc/letsencrypt/renewal-hooks/deploy/`** que volte a aplicar a mesma política, ou reexecutar `setup_alt_protocols.py` após renovações (com as flags que fizer sentido: p.ex. `--skip-install --skip-gopher --skip-backfill` se só quiser TLS + Gemini).
-
-## Validação final (v0.09+)
-
-No fim da execução, além de verificar ficheiros e **bind mount** Gemini **como root**:
-
-- Se **`gophernicus.socket`** estiver **`active`**, o script tenta **`runuser -u <User=do_unit> -- test -r`** no **`gophermap`** da primeira conta da lista **fora** de **`IRC_PATCH_SKIP_USERS`** (o `User=` lê-se de `/lib/systemd/system/gophernicus@.service`; fallback **`gophernicus`**). Falha → **WARNING** (home `755`/`o+x`, `public_gopher` `755`, `gophermap` `644`).
-- Se **`molly-brown@`** estiver **`active`**, tenta **`runuser -u www-data -- test -r`** no **`index.gmi`** da mesma amostra (heurística: o unit Debian usa **`molly-brown`** dinâmico; ficheiros **`644`** e pastas **`755`** devem permitir leitura a «others» — ou **ACL** compatível; ver nota **ACL** na secção de travessia). Falha → **WARNING** (`public_gemini` `755`, `index.gmi` `644`, bind `/var/gemini/users/<user>`). Se a amostra ainda for **symlink**, regista **WARNING** de migração (**`--force`**).
-
-Em **`--dry-run`**, só regista os comandos. Sem **`runuser`** (util-linux), estes passos são omitidos.
-
-## Utilizadores antigos vs novos
-
-- **Política:** permissões correctas para **HTTP**, **Gopher** e **Gemini** devem existir **à criação** (fluxo [`create_runv_user.py`](../admin/create_runv_user.py): `apply_runv_permissions`) e ser **reaplicadas** no backfill ([`setup_alt_protocols.py`](../admin/setup_alt_protocols.py): home `755`, `public_gopher` / `public_gemini`, bind mounts Gemini).
-- **Novos:** modelos via **`/etc/skel`** (após `tools/tools.py`) e **`create_runv_user.py`** quando o provisionador corre.
-- **Antigos / contas só `adduser`:** correr **`setup_alt_protocols.py`** (backfill completo) ou pastas/bind Gemini com **`patches/yetgg.py`** (mesma lista que `patch_irc.py`: união JSON + `/home`) se a infraestrutura de sistema já existir; ou reparar com `create_runv_user` e flags `--force-*` onde fizer sentido.
-
-## Requisitos Gemini
-
-- **TLS obrigatório** (certificado + chave PEM). Por defeito o script tenta Let's Encrypt em `/etc/letsencrypt/live/runv.club/`; use **`--gemini-cert`** e **`--gemini-key`** se forem noutro sítio.
-- Sem certificados válidos, o script **não** ativa o serviço `molly-brown@`, mas pode criar `/var/gemini` e aplicar bind mounts (e linhas `fstab` quando o mount corre).
-
-## Erro `Error opening error log file: open /-` (read-only file system)
-
-O **molly-brown** trata `AccessLog` e `ErrorLog` como **caminhos de ficheiro**. Valores como `"-"` (estilo «stdout» noutros programas) são interpretados de forma errada e o processo tenta abrir `/-`, falhando de imediato.
-
-- **Comportamento actual do script (v0.06+):** grava `AccessLog` / `ErrorLog` em **`/var/lib/molly-brown/`** (v0.07+ ajuste LE; v0.08+ LE com symlinks + teste `www-data`; ver secções acima). (`runv.club-access.log`, `runv.club-error.log`). Esse caminho coincide com **`StateDirectory=molly-brown`** do unit Debian: o systemd cria o directório com o dono correcto (**`DynamicUser=yes`**) **antes** do `ExecStart`, sem `chown` manual. **Não** pré-cria pastas nem ficheiros de log (evita conflitos com `LogsDirectory` em `/var/log`).
-- **Versões antigas (v0.05):** usavam o drop-in `50-runv-logs.conf` com `LogsDirectory=molly-brown`. Se `/var/log/molly-brown` já existia como root, o systemd podia **migrar** para `/var/log/private/molly-brown` e o serviço falhava. O **v0.06+** **remove** esse drop-in e muda os caminhos no `.conf` para `/var/lib/molly-brown/`.
-- **Servidor já provisionado:** correr com **`--force`** para regravar o `.conf` e remover o drop-in obsoleto (com backup do drop-in se usar `--force`). Exemplo:
- `sudo python3 scripts/admin/setup_alt_protocols.py --verbose --force`
-- **Correcção manual rápida (só `.conf`):** `AccessLog` / `ErrorLog` com caminhos absolutos sob **`/var/lib/molly-brown/`**; **sem** `LogsDirectory` extra em drop-in; `sudo systemctl daemon-reload`; `sudo systemctl reset-failed molly-brown@runv.club.service` e `start`.
-
-## Erro `permission denied` em `/var/log/molly-brown/…` ou migração para `/var/log/private/molly-brown`
-
-No Debian, **`DynamicUser=yes`** faz o UID de runtime ser dinâmico; `chown` estático não bate. Se viu **`migrating to /var/log/private/molly-brown`** ou **`permission denied`** em `/var/log/molly-brown`, actualize para **v0.06+** com **`--force`**.
-
-**Limpeza recomendada (serviço parado):**
-
-```bash
-sudo systemctl stop 'molly-brown@runv.club.service'
-sudo rm -f /etc/systemd/system/molly-brown@.service.d/50-runv-logs.conf
-sudo rm -rf /var/log/molly-brown /var/log/private/molly-brown
-sudo systemctl daemon-reload
-sudo python3 /opt/runv/src/scripts/admin/setup_alt_protocols.py --verbose --force
-```
-
-(ajuste o caminho do script ao seu clone). Os logs passam a ficar só em **`/var/lib/molly-brown/`** (legível com `sudo`).
-
-## Erro `permission denied` em `/var/lib/molly-brown/…`
-
-Raro se o `.conf` aponta para `/var/lib/molly-brown/` e não há override que desactive `StateDirectory`. Confirme `grep StateDirectory /lib/systemd/system/molly-brown@.service` e caminhos no `.conf`; veja também **TLS** (`privkey` legível pelo grupo `ssl-cert`).
-
-## Checklist rápido (conf antiga, UFW, «activating»)
-
-1. **Ainda vê `open /-` no journal?** O `/etc/molly-brown/runv.club.conf` no servidor pode continuar com `ErrorLog = "-"` até correr o script com **`--force`** (ou editar à mão). Confirme: `grep -E 'AccessLog|ErrorLog' /etc/molly-brown/runv.club.conf`.
-2. **UFW:** o script só executa `ufw allow` automaticamente quando **`ufw status`** mostra o firewall **activo** na altura da execução. Se activou o UFW **depois**, ou usa outro firewall, abra **70/tcp** (Gopher) e **1965/tcp** (Gemini) manualmente:
- ```bash
- sudo ufw allow 70/tcp comment 'gopher'
- sudo ufw allow 1965/tcp comment 'gemini'
- sudo ufw reload
- ```
- Com `--skip-firewall` ou UFW inactivo, o script regista no log os mesmos comandos sugeridos para copiar.
-3. **`molly-brown@runv.club: activating` no fim do script:** **não** indica sucesso — só que o unit ainda não estava `active` nesse instante (p.ex. crash loop). Use `systemctl is-active molly-brown@runv.club.service`, `systemctl is-failed …` e `sudo ss -tlnp | grep 1965`. Se `is-failed` for positivo, veja `journalctl` como abaixo.
-
-## Molly não sobe ou fica em «activating»
-
-- **`journalctl` sem mensagens:** os logs do serviço do sistema exigem **root** — use `sudo journalctl -u molly-brown@runv.club.service -b --no-pager -n 80`.
-- **Estado e porta:** `sudo systemctl status molly-brown@runv.club.service --no-pager` e `sudo ss -tlnp | grep 1965` (deve haver um processo a escutar em **1965/tcp**).
-- **Permissões TLS (frequente):** o Molly corre como utilizador não-root; se `privkey.pem` for só `root:root` `0600`, o arranque falha. Verifique `sudo namei -l /etc/letsencrypt/live/runv.club/privkey.pem` e compare com o utilizador do unit (`systemctl cat molly-brown@runv.club.service`). Soluções típicas: grupo `ssl-cert`, ACL, ou certificados num path legível pelo utilizador do serviço (mantendo segurança).
-- **Teste local:** `openssl s_client -connect 127.0.0.1:1965 -servername runv.club </dev/null 2>/dev/null | head -20`
-- **Cliente (Lagrange, etc.):** teste **`gemini://runv.club/~user/`** (canónico); `gemini://runv.club/~/user/` deve redireccionar (**v0.11+** no `.conf`) **depois** de `systemctl is-active molly-brown@runv.club.service` devolver `active`.
-
-## Gemini **51 Not found** e log «Refusing to follow symlink … outside of DocBase!»
-
-O **Molly Debian** não segue symlinks de `/var/gemini/users/<user>` para fora de `DocBase`. Com **symlink** antigo, o `index.gmi` pode existir e o `test -r` no script passar, mas o servidor devolve **51**.
-
-- **Correcção:** `sudo python3 scripts/admin/setup_alt_protocols.py --verbose --force` (**v0.13+**) migra para **`mount --bind`** e persiste em **`/etc/fstab`**.
-- **Verificar:** `findmnt /var/gemini/users/<user>` e `grep gemini/users /etc/fstab`.
-
-## Execução (root)
-
-Use a **raiz do repositório** clonada; o script carrega `patches/patch_irc.py` para a lista de utilizadores (união JSON + `/home`). Sem esse ficheiro, o comando falha com mensagem explícita.
-
-```bash
-cd /caminho/para/runv-server
-sudo python3 scripts/admin/setup_alt_protocols.py --dry-run --verbose
-sudo python3 scripts/admin/setup_alt_protocols.py --verbose
-```
-
-### Flags úteis
-
-| Flag | Efeito |
-|------|--------|
-| `--dry-run` | Simula; não grava (validação de root ignorada em alguns passos só se documentado). |
-| `--verbose` | Log detalhado. |
-| `--force` | Sobrescreve configs de sistema (com backup com timestamp) e ficheiros modelo no backfill (exceto **`~/public_gemini/index.gmi`** se já existir). Necessário para **regravar** `/etc/molly-brown/runv.club.conf` (incl. **`[TempRedirects]`** v0.11: **`/~/…` → `/~…`**) e remover o drop-in obsoleto **`50-runv-logs.conf`** (v0.05) ao migrar logs para `/var/lib/molly-brown/`. **v0.14+:** também **regrava** o **`gophermap` raiz** e **`/var/gemini/index.gmi`**; para utilizadores em **`IRC_PATCH_SKIP_USERS`**, remove **bind mount** + linha **`fstab`** + legado em **`/var/gemini/users/<user>`** se ainda existirem. |
-| `--skip-install` | Não corre `apt-get`. |
-| `--skip-gopher` / `--skip-gemini` | Ignora pacote, config e serviço desse protocolo. |
-| `--skip-firewall` | Não altera UFW. |
-| `--skip-backfill` | Não cria pastas nem bind mounts Gemini por utilizador. |
-| `--skip-services` | Não `systemctl enable --now`. |
-| `--skip-system-config` | Não escreve `/etc/default/gophernicus`, nem `molly-brown`, nem gophermap raiz. |
-| `--users-json PATH` | Parte da fonte de usernames (lista JSON com `username`). Predefinido: `/var/lib/runv/users.json`. |
-| `--homes-root PATH` | Parte da fonte de usernames (directórios em `/home` com UID ≥ 1000). O backfill usa a **união** JSON + homes (igual a `patches/patch_irc.py`). |
-| `--gemini-hostname HOST` | Predefinido: `runv.club`. |
-| `--gemini-cert` / `--gemini-key` | Caminhos PEM para molly-brown. |
-
-## Descoberta de utilizadores (backfill)
-
-A lista de contas para criar `~/public_gopher`, `~/public_gemini` e **bind mounts** em `/var/gemini/users/` é a **união** de:
-
-1. Usernames em **`users.json`** (lista de objetos com campo `username`), quando o ficheiro existe e o JSON é válido; e
-2. Nomes em **`--homes-root`** com UID ≥ 1000 e entrada em `passwd`.
-
-Depois aplicam-se as mesmas exclusões que em **`patches/patch_irc.py`** (**`IRC_PATCH_SKIP_USERS`** — contas de sistema, **`entre`**, **`pmurad-admin`**, etc.; **v0.14+**). Esse conjunto governa também a **lista pública** Gopher/Gemini (menu raiz e **`index.gmi`** DocBase) e impede **bind mount** para esses nomes. Para só pastas/bind Gemini sem reinstalar serviços, pode usar **`patches/yetgg.py`**.
-
-## Índice raiz Gopher e Gemini (**v0.14+**)
-
-- **`/var/gopher/gophermap`:** menu explícito (linhas **`i`** de boas-vindas + **`1~username ~username/ host 70`**) só para utilizadores do backfill que tenham **`~/public_gopher`** como directório. Não usa a directiva global **`~`** do gophernicus, para poder **respeitar** **`IRC_PATCH_SKIP_USERS`**.
-- **`/var/gemini/index.gmi`:** página Gemtext na raiz do DocBase com links **`=> gemini://<hostname>/~user/`** para o **mesmo** conjunto (backfill + `public_gopher`). Quem está em **`IRC_PATCH_SKIP_USERS`** **não** recebe bind em **`/var/gemini/users/`** e **não** aparece nesse menu.
-- **Limpeza:** após remover um nome de **`IRC_PATCH_SKIP_USERS`** ou corrigir mounts antigos, correr **`setup_alt_protocols.py --verbose --force`**. Verificar: `findmnt /var/gemini/users/<user>` vazio para contas excluídas.
-
-## Relação com outros scripts
-
-- **`create_runv_user.py`**: após `public_html`, cria `public_gopher`, `public_gemini` e aplica **bind mount** em `/var/gemini/users/<user>` (via `setup_alt_protocols`) **excepto** se o username estiver em **`IRC_PATCH_SKIP_USERS`**.
-- **`del-user.py`**: **umount**, remove linha **`fstab`** de bind e remove symlink legado ou directório vazio em `/var/gemini/users/<user>`.
-- **`tools/tools.py`**: copia modelos para `/etc/skel` (só contas futuras).
-
-## Testes manuais sugeridos
-
-1. `sudo python3 scripts/admin/setup_alt_protocols.py --dry-run --verbose`
-2. `sudo python3 scripts/admin/setup_alt_protocols.py --verbose`
-3. `dpkg -l gophernicus molly-brown`
-4. `systemctl is-active gophernicus.socket` e `systemctl is-active molly-brown@runv.club.service`
-5. `ufw status` (se ativo, confirmar 70/tcp e 1965/tcp permitidos)
-6. Verificar `/etc/skel/public_gopher` e `public_gemini` após `tools.py`
-7. Criar utilizador de teste com `create_runv_user.py`
-8. `ls -la /home/teste/public_gopher/gophermap /home/teste/public_gemini/index.gmi` e `findmnt /var/gemini/users/teste` (deve mostrar bind a `~/public_gemini`)
-9. Cliente Gopher/Gemini: `gopher://runv.club/1/~teste` e **`gemini://runv.club/~teste/`** (canónico); opcionalmente `gemini://runv.club/~/teste/` → redirect **30** para o canónico (**v0.11+**)
-
-Versão do script: ver `python3 scripts/admin/setup_alt_protocols.py --version`.
diff --git a/scripts/docs/irc_patch.md b/scripts/docs/irc_patch.md
@@ -1,69 +0,0 @@
-# IRC no runv.club — comando **`chat`**
-
-Estilo [tilde.club](https://tilde.club): o utilizador corre só **`chat`** e liga ao IRC da casa com auto-ligação já preparada.
-
-## Alinhamento (plano / produto)
-
-- **MOTD** (`tools/motd/60-runv`) e **`runv-help`** referem **apenas o comando `chat`** — sem citar outros nomes de binário ao utilizador.
-- **Provisionamento** (`patches/patch_irc.py`) usa **sempre** `weechat-headless` (`-a`, `-r`, `--stdout`): é o fluxo suportado para automatizar `/server add` e `/set` sem editar ficheiros à mão.
-- O **cliente interactivo** no terminal é instalado pelos **pacotes globais** em `tools/manifests/apt_packages.txt` (o launcher `chat` escolhe o primeiro binário adequado no PATH); utilizadores continuam a ver só **`chat`**.
-
-## O que o admin faz
-
-```bash
-cd /caminho/runv-server
-sudo python3 patches/patch_irc.py --all-users --verbose
-# ou um utilizador:
-sudo python3 patches/patch_irc.py --user alice --verbose
-```
-
-- Instala **`/usr/local/bin/chat`** (salvo `--skip-launcher`).
-- Por utilizador: `~/.config/weechat/`, servidor interno **`runv`** (por defeito), nick = **username Unix**, nicks alternativos `user_`, `user__`, `user|away`, autojoin por defeito no canal **`#runv`**.
-- O patch aplica também **definições globais** WeeChat (na mesma sessão `weechat-headless`): `irc.look.buffer_switch_join` = `on`, `irc.look.server_buffer` = `independent`, e `buflist.look.display_conditions` = ``${buffer.plugin} == irc && ${type} == channel`` (na buflist aparecem **só canais**, ex. `#runv` — sem a linha do servidor `runv` nem `core.weechat`). Mensagens do servidor continuam acessíveis noutros modos; para voltar a listar tudo, redefine o `display_conditions` na mão. WeeChat antigo ou sem plugin buflist: o `/set buflist.*` pode falhar.
-- Com **`--all-users`**, a lista de contas é a **união** de: usernames em `users.json` **e** utilizadores com diretório em `--homes-root` (por omissão `/home`), UID ≥ 1000 e fora da lista interna de contas de sistema — assim contas de administração (ex.: `pmurad-admin`) que não estão no JSON também são provisionadas.
-- Exige **`weechat-headless`** no sistema para aplicar o patch; sem esse binário o script falha com mensagem clara (`apt install weechat-headless`).
-- Se a config **já existe** mas **não coincide** com o alvo (host, TLS, nicks, autojoin, etc.), o patch **realinha** sozinho (`/server del` + voltar a criar). Não é obrigatório **`--force`** para isso. **`--force`** serve para **reaplicar mesmo quando já estava alinhada** (útil para repor estado conhecido).
-
-## O que o utilizador faz
-
-```bash
-chat
-```
-
-Opcional: variável de ambiente **`WEECHAT_HOME`** para outro directório de dados (convénio do cliente IRC).
-
-## Defaults (ajustáveis por flags)
-
-| Parâmetro | Default |
-|-----------|---------|
-| Host IRC | `irc.portalidea.com.br` |
-| TLS | ligado (`--tls`; omitir `--no-tls`) |
-| Porta | `6697` com TLS, `6667` sem TLS (ou `--port`) |
-| Nome do servidor na config | `runv` |
-| Autojoin | `#runv`; `--autojoin ""` para nenhum; ou `--autojoin '#canal1,#canal2'` |
-
-Não há SASL/NickServ automático; no código há comentários para extensão futura com **dados seguros** (sem senhas em texto plano).
-
-## Flags úteis
-
-- `--dry-run`, `--verbose`, `--force` (reaplica mesmo com config já igual ao alvo)
-- `--skip-launcher`, `--skip-backfill`
-- `--users-json`, `--homes-root`
-- `--user` **ou** `--all-users` (obrigatório um dos dois)
-
-## Integração `tools.py`
-
-Copia **`tools/bin/chat`** → `/usr/local/bin`. O `patches/patch_irc.py` pode reinstalar o mesmo ficheiro se correres o patch sem rerodar `tools.py`.
-
-## Testes rápidos (Debian 13)
-
-```bash
-sudo python3 patches/patch_irc.py --dry-run --all-users --verbose
-sudo python3 patches/patch_irc.py --user "$(logname)" --verbose
-command -v chat && ls -l "$(command -v chat)"
-command -v weechat-headless
-sudo -u USER test -f /home/USER/.config/weechat/irc.conf && grep '^runv\.' /home/USER/.config/weechat/irc.conf
-sudo -u USER grep '^runv\.autojoin' /home/USER/.config/weechat/irc.conf
-```
-
-Substitui `USER` por um utilizador real.
diff --git a/scripts/doom/doom.md b/scripts/doom/doom.md
@@ -1,88 +0,0 @@
-# `doom.py` — reset em massa de utilizadores runv
-
-Script de administração que **remove todas as contas runv** registadas em `users.json`, **excepto** um conjunto **protegido**. A remoção real (Unix, quotas, metadados) é feita pelo mesmo fluxo que o banimento manual: chama [`../admin/del-user.py`](../admin/del-user.py) com `-y` para cada utilizador a apagar.
-
-**Aviso:** operação **irreversível**. Use primeiro `--dry-run` se tiver dúvidas.
-
-## O que é “protegido”
-
-O script **nunca** passa ao `del-user.py` ninguém que pertença ao conjunto **protegido**:
-
-```
-protegido = { conta de referência (--keep / omissão) } ∪ { quem está ligado ao processo }
-```
-
-### Conta de referência (`keeper`)
-
-- Com **`--keep USER`**: essa conta é a referência explícita (e entra no protegido).
-- Sem `--keep`, com **EUID root** e **`SUDO_USER`** definido (caso típico `sudo python3 doom.py`): a referência é o utilizador em `SUDO_USER`.
-- **Root sem `SUDO_USER`**: é **obrigatório** `--keep USER` (evita ambiguidade).
-- **Sem ser root** (ex.: só inspeção): a referência é o utilizador do **real UID** (útil em cenários limitados; a remoção real ainda exige root).
-
-### Quem está ligado ao processo (nunca apagado)
-
-Mesmo que `--keep` aponte para **outra** conta, o script **não apaga**:
-
-| Origem | Motivo |
-|--------|--------|
-| `SUDO_USER` | Quem executou `sudo`. |
-| Real UID e **effective** UID | Cobre `sudo -u bob`: protege quem invocou e o utilizador efectivo. |
-| `root` | Se o processo corre como root **e** não há `SUDO_USER`, `root` fica protegido se existir no JSON. |
-
-Os nomes são normalizados para bater com entradas típicas de `users.json` (ex.: minúsculas, regra de username runv).
-
-## Fonte da lista de utilizadores
-
-- Lê **apenas** os `username` presentes no ficheiro JSON de metadados (por omissão `/var/lib/runv/users.json`).
-- **Não** enumera o `/etc/passwd` por conta própria: o que não está no JSON **não** é alvo do doom (mas contas órfãs no sistema continuam fora deste script).
-
-## Fluxo de execução
-
-1. Determina `keeper` e o conjunto **protegido** (referência + quem rodou).
-2. Lista todos os utilizadores no JSON **fora** do protegido → **vítimas**.
-3. Se não houver vítimas, termina sem chamar `del-user.py`.
-4. **Confirmação:** a menos que use `--yes`, é preciso escrever **`DOOM`** em maiúsculas.
-5. Em modo real, exige **root** (EUID 0).
-6. Para cada vítima, invoca `del-user.py` com `--yes` e os mesmos caminhos de metadata/lock que indicar.
-
-## Modo dry-run
-
-`--dry-run` **não exige root**. Mostra quem seria removido e corre o `del-user.py` em dry-run por cada vítima (sem alterações reais).
-
-## Opções principais
-
-| Opção | Função |
-|--------|--------|
-| `--keep USER` | Referência explícita; obrigatório em root puro sem `SUDO_USER`. |
-| `--yes` / `-y` | Sem prompt `DOOM` (automação; perigoso). |
-| `--dry-run` | Simulação. |
-| `--metadata-file`, `--lock-file` | Sobrescreve caminhos do JSON e do lock (útil em testes). |
-| `--purge-all-files` | Repassa ao `del-user.py` (além de remover home). |
-| `-v` / `--verbose` | Mais saída no `del-user.py`. |
-
-## Exemplos
-
-```bash
-# Conta a manter = quem fez sudo (ex.: alice); apaga todos os outros no JSON
-sudo python3 /caminho/scripts/doom/doom.py
-
-# Root em consola sem SUDO_USER: dizer explicitamente quem fica como referência
-sudo python3 /caminho/scripts/doom/doom.py --keep alice
-
-# Simular sem apagar nada
-python3 /caminho/scripts/doom/doom.py --dry-run
-
-# Automação (sem confirmação DOOM)
-sudo python3 /caminho/scripts/doom/doom.py --yes
-```
-
-## Nota sobre “ficar só um utilizador”
-
-O objectivo habitual é deixar **uma** conta runv no JSON (a referência). Se `--keep` for diferente de quem invocou o script, **várias** contas podem permanecer no ficheiro (referência + todas as que o doom é obrigado a proteger). Isto é intencional: **nunca** apagar quem está ligado ao processo.
-
-## Dependências
-
-- Python 3, acesso root para execução real.
-- `scripts/admin/del-user.py` no repositório, relativo a `doom.py` (`../admin/del-user.py`).
-
-Versão do script documentada aqui: alinhada a `doom.py` (ver `--version`).
diff --git a/scripts/skel.md b/scripts/skel.md
@@ -1,145 +0,0 @@
-# skel.py — preparar `/etc/skel` (runv.club)
-
-**Versão 0.02** · runv.club
-
-Script para **administradores** prepararem o diretório `/etc/skel` no **Debian** (ex.: Debian 13), de modo que `adduser` copie automaticamente uma home inicial com `public_html`, página de boas-vindas e README para novos usuários.
-
-- **Não** cria usuários.
-- **Não** altera Apache, SSH, chaves ou pacotes.
-- Só cria/atualiza ficheiros sob `/etc/skel` (com segurança e idempotência).
-
-**Ambiente:** execute no servidor Linux como **root** (ou `sudo`). No Windows, use apenas para revisão do código; a execução real é no Debian.
-
-## Objetivo
-
-- Garantir `/etc/skel/public_html/` e `/etc/skel/public_html/index.html`.
-- Garantir `/etc/skel/README.md` (texto de ajuda para quem entra na shell).
-- Aplicar permissões: diretório `755`, ficheiros `644`.
-- Ser **idempotente**: voltar a correr não apaga nada; sem `--force`, ficheiros existentes são **preservados**.
-
-## Requisitos
-
-- Python 3 (stdlib apenas; usa `pathlib`).
-- Permissões de root para escrita em `/etc/skel`.
-
-## Como executar
-
-Nos exemplos com **`admin/skel.py`**, execute a partir do diretório **`scripts/`** do repositório.
-
-Torne o script executável (opcional):
-
-```bash
-sudo chmod +x admin/skel.py
-```
-
-(a partir do diretório `scripts/` do repositório; ou use o caminho absoluto até `scripts/admin/skel.py`.)
-
-### Simular (sem root, sem alterar disco)
-
-```bash
-python3 admin/skel.py --dry-run
-python3 admin/skel.py --dry-run --verbose
-```
-
-### Aplicar de verdade
-
-```bash
-sudo python3 admin/skel.py
-sudo python3 admin/skel.py --verbose
-```
-
-### Regenerar templates (sobrescrever)
-
-Se `index.html` ou `README.md` já existirem em `/etc/skel` e quiser substituir pelo conteúdo embutido no script:
-
-```bash
-sudo python3 admin/skel.py --force
-```
-
-## Opções
-
-| Opção | Efeito |
-|--------|--------|
-| `--dry-run` | Mostra o que seria criado/atualizado; **não** exige root; não escreve ficheiros. |
-| `--verbose` | Mais detalhe (ex.: `chmod` explícito). |
-| `--force` | Sobrescreve `index.html` e `README.md` se já existirem. Sem `--force`, são preservados. |
-| `--version` | Mostra versão do script. |
-
-## Exemplos de saída
-
-### Dry-run (trecho)
-
-```
-Modo dry-run — nenhuma alteração em disco.
-
- [dry-run] criaria diretório: /etc/skel/public_html
- [dry-run] criaria arquivo: /etc/skel/public_html/index.html
- [dry-run] criaria arquivo: /etc/skel/README.md
-
-Resumo: nada foi gravado. Execute sem --dry-run como root para aplicar.
-```
-
-### Execução real (trecho)
-
-```
-skel.py — preparando /etc/skel para runv.club
-
- [dir] criado: /etc/skel/public_html
- [file] criado: /etc/skel/public_html/index.html
- [file] criado: /etc/skel/README.md
-
-Aplicando permissões...
-
---- Resumo ---
- ...
-```
-
-Segunda execução **sem** `--force`: deve aparecer `preservado` para os ficheiros já existentes.
-
-## Limitações
-
-- Não altera `/etc/skel` fora do que o script define (outros ficheiros que o admin adicionar manualmente ficam).
-- Não remove ficheiros.
-- Não cria `public_html` na home de utilizadores **já** existentes — só para **novos** utilizadores criados **depois** de preparar o skel; utilizadores antigos precisam copiar à mão ou de outro procedimento.
-
-## Como testar no Debian 13
-
-1. **Preparar o skel**
-
- ```bash
- sudo python3 ./admin/skel.py --verbose
- ```
-
-2. **Criar um utilizador de teste**
-
- ```bash
- sudo adduser testuser
- ```
-
- (ou `adduser --disabled-password` se o seu fluxo não precisar de password interativa.)
-
-3. **Verificar cópia a partir de `/etc/skel`**
-
- Como `testuser` (ou inspecionando a home):
-
- ```bash
- sudo ls -la /home/testuser/
- sudo ls -la /home/testuser/public_html/
- sudo cat /home/testuser/public_html/index.html | head
- sudo cat /home/testuser/README.md | head
- ```
-
-4. **Permissões típicas para publicação web** (o README em `/etc/skel` explica; após `adduser`, confirme se a sua política exige `chmod` na home — muitas instalações já ficam com home `755` e `public_html` herdado de `755`).
-
-5. **Prova no browser** (se DNS e Apache estiverem corretos): `http://runv.club/~testuser/`
-
-## Ficheiros geridos pelo script
-
-| Caminho | Conteúdo |
-|---------|-----------|
-| `/etc/skel/public_html/index.html` | Página inicial estática em português (CSS embutido, visual simples). |
-| `/etc/skel/README.md` | Instruções para o utilizador na shell. |
-
-## Créditos
-
-runv.club.
diff --git a/scripts/starthere.md b/scripts/starthere.md
@@ -1,94 +0,0 @@
-# starthere.py
-
-Bootstrap **conservador** para preparar um servidor **Debian**: pacotes úteis ao runv.club e **quotas de utilizador** (`usrquota`) no **mesmo filesystem ext4 onde vivem as homes** (detetado automaticamente — tipicamente `/` se `/home` está na raiz, ou `/home` se é um volume dedicado). A lógica de descoberta é a de [`runv_mount.py`](admin/runv_mount.py), alinhada a [`create_runv_user.py`](create_runv_user.md).
-
-Versão do script: **0.02** (use `python3 admin/starthere.py --version` a partir do diretório `scripts/` do repositório).
-
-### Comportamento de `--dry-run`
-
-O script **consulta sempre o `findmnt` real** (só leitura) para mostrar o estado do mount detetado. Como o `fstab` não é gravado em dry-run, o kernel **não** passa a mostrar `usrquota` até uma execução real; por isso, em dry-run o fluxo **assume** quotas ativas só para completar o plano de quotas, incluindo `quotacheck` / `quotaon` simulados.
-
-## Quando usar
-
-- Máquina nova ou recém-instalada, antes de criar utilizadores com [`create_runv_user.py`](create_runv_user.md).
-- Quando ainda não existem quotas ativas no filesystem das homes e pretende alinhar ao fluxo de `create_runv_user.md` (ext4 + `setquota`).
-
-## Requisitos
-
-- **root** (`sudo` ou sessão root).
-- **Debian** (ou derivado com `apt-get`).
-- O path de sonda (predefinido **`/home`**) tem de residir num filesystem **`ext4`**. Se `/home` estiver noutro tipo (btrfs, xfs, …), o script aborta — configure quotas manualmente ou use layout ext4 para as homes.
-- Acesso à rede para `apt-get update` / `install` (exceto em `--dry-run`, que não executa APT mas ainda pode ler `/etc/fstab` nos passos de quota).
-
-## O que o script faz
-
-1. **`apt-get update`** (a menos que `--no-install`).
-2. **`apt-get install -y`** de um conjunto fixo de pacotes (personalizável com `--packages`; ver lista em `BASE_PACKAGES` no código).
-3. **Limpeza segura**: `apt-get autoremove` e `autoclean` (desligável com `--no-cleanup`).
-4. **Serviços** (desligável com `--no-services`):
- - `systemctl enable --now apache2`
- - Se o UFW estiver **inativo**: `ufw allow OpenSSH`, `80/tcp`, `443/tcp`, depois `ufw --force enable` (não altera regras se o UFW já estiver ativo).
-5. **Quotas** (desligável com `--no-quota`):
- - **Deteta** o mountpoint com `find_mount_triple` sobre `--quota-probe` (predefinido `/home`).
- - Garante `usrquota` na linha **ext4** correspondente a esse mountpoint em `/etc/fstab`.
- - Backup de `/etc/fstab` em `/root/runv-fstab-backups/fstab.<timestamp>.bak`.
- - **`mount -o remount,usrquota <mount>`** e, se preciso, **`mount -o remount <mount>`** (saltável com `--skip-remount`).
- - **`quotacheck`** / **`quotaon`** nesse mesmo `<mount>`.
-
-## O que o script não faz
-
-- Não remove pacotes em massa nem faz `purge` agressivo.
-- Não altera vhosts Apache nem cria utilizadores (só enable/start do serviço `apache2`).
-- Não altera configuração SSH além do que o pacote `openssh-server` já traz.
-- Não instala stack de correio.
-- Não define limites por utilizador — isso fica para `create_runv_user.py` / `setquota` manual.
-
-## Opções de linha de comandos
-
-| Opção | Efeito |
-|--------|--------|
-| `--dry-run` | Mostra comandos e plano; não executa subprocessos reais (saídas simuladas onde aplicável). |
-| `--verbose` | Ecoa comandos e saída de stderr/stdout dos programas. |
-| `--packages` | Lista explícita de pacotes a instalar (substitui o padrão). |
-| `--no-install` | Não corre APT; apenas lógica de quotas (útil se os pacotes já estiverem instalados). |
-| `--no-cleanup` | Não corre `autoremove` / `autoclean`. |
-| `--no-quota` | Só instala/limpa; não mexe em `fstab` nem quotas. |
-| `--quota-probe PATH` | Caminho para descobrir o FS de quotas (predefinido `/home`; deve bater com onde o `create_runv_user` cria homes). |
-| `--skip-remount` | Não tenta `remount` após editar `fstab`. |
-| `--allow-live-scan` | Usa apenas **`quotacheck -cuM`** (não tenta antes `-cu`). |
-| `--no-services` | Não ativa Apache nem configura/ativa UFW. |
-| `--version` | Mostra versão e sai. |
-
-Códigos de saída: **0** sucesso; **2** erro operacional (`BootstrapError`); **130** interrupção (Ctrl+C).
-
-## Pacotes base (padrão)
-
-Incluem, entre outros: `apache2`, `openssh-server`, `sudo`, `ufw`, `quota`, ferramentas de rede e ficheiros (`curl`, `wget`, `git`, `rsync`), consola (`tmux`, `htop`, `vim`, …), `jq`, `acl`, `build-essential`, `python3-venv`, `python3-pip`, `ripgrep`, `shellcheck`. A lista completa está em `BASE_PACKAGES` em [`starthere.py`](admin/starthere.py).
-
-## Quotas: comportamento esperado e problemas comuns
-
-- Depois de editar `fstab`, o **remount** pode falhar em alguns ambientes (cloud-init, segurança do kernel). O script sugere **reiniciar a VM** e voltar a executar o script.
-- **`quotacheck` com filesystem montado**: o script tenta **`-cu`**, depois **`-cuM`**, e se ainda falhar (p.ex. quotas já ativas após remount — mensagem «use -f»), **`-cuM -f`** e **`-cu -f`**. **`--allow-live-scan`** começa por **`-cuM`** e, se preciso, **`-cuM -f`**.
-- **`quotaon -vu`**: se o kernel já tiver quotas de utilizador activas neste mount (comum após `remount,usrquota` + `quotacheck`), o `quotaon` pode devolver **Device or resource busy**. O script trata isso como **sucesso** e imprime um aviso — confirme com **`quota -vs`** ou **`sudo repquota -s <mount>`** (`repquota` costuma estar em `/usr/sbin/`).
-- Confirme com `mount | grep ' on <mount> '` (o `<mount>` é o indicado no resumo do script, ex. `/` ou `/home`) e `quota -vs`.
-- **Reinício:** em muitas contas normais o comando `reboot` não está no `PATH`; use `sudo reboot` ou `/sbin/reboot`.
-
-### Avisos «external quota files» e `tune2fs -O quota`
-
-O `quotacheck` e o `quotaon` podem imprimir avisos dizendo que o kernel prefere a **feature interna `quota` do ext4** e que os ficheiros clássicos (`aquota.user` / «external quota files») estão **deprecated**.
-
-- **Isto não invalida o que o script fez:** com `usrquota` no mount e `quota -vs` a mostrar o filesystem, as quotas estão ativas; `setquota` (como em `create_runv_user.py`) funciona neste modo.
-- O script usa o caminho suportado em **/** montado: `usrquota` + ficheiros de quota geridos pelas ferramentas `quota` — é o esperado quando a feature `quota` do ext4 **não** foi ligada no superbloco.
-- **Migrar** para quotas «só no ext4» (sem aquele aviso) implica, em geral, **desmontar** o volume (para `/` isso significa **modo rescue/live** ou VM parada), correr algo como `tune2fs -O quota <dispositivo>`, voltar a montar e rever opções de mount/documentação do seu Debian — fora do âmbito automático deste bootstrap.
-- O pacote **`e2fsprogs`** (inclui `tune2fs`, `dumpe2fs`) está na lista base para inspeção manual; após um run bem-sucedido, o script pode imprimir uma **nota** em stderr se detectar que a feature interna ainda não está ativa.
-
-## Relação com outros scripts
-
-- Após este bootstrap, use **[create_runv_user.md](create_runv_user.md)** para criar contas com quota e `public_html`.
-- **[skel.md](skel.md)** e **[del-user.md](del-user.md)** cobrem esqueleto de ficheiros e remoção de utilizadores.
-
-## Segurança e operações
-
-- Alterações em **`/etc/fstab`** são precedidas de backup em **`/root/runv-fstab-backups/`**.
-- O script foi pensado para **ext4 no volume das homes**; não extrapola para outros filesystems.
-- Revise sempre `--dry-run` / `--verbose` num ambiente de teste antes de produção.
diff --git a/site/README.md b/site/README.md
@@ -1,104 +0,0 @@
-# Site público (landing runv.club)
-
-Conteúdo estático inspirado em [tilde.town](https://tilde.town) e [tilde.club](https://tilde.club): landing com constelação de links por membro (`members.json`), **`/news/`** (lista dinâmica via `data/news.json` + RSS), **`/wiki/`**, e **`/junte-se/`** — guia de chave SSH (Linux, macOS, Windows) e acesso a **`entre@runv.club`**.
-
-## O que significa “membro” na página
-
-- **Membro listado** = conta presente em `/var/lib/runv/users.json` (criada por `create_runv_user.py`).
-- **Não** é “sessão SSH ativa neste momento” nem “logged in”; isso exigiria outra fonte de dados (ex. `lastlog`).
-
-## Privacidade
-
-- `build_directory.py` **filtra** o JSON interno: só escreve `username`, `since` (data de criação), `path` (`/~user/`) e, opcionalmente, `homepage_mtime` se você usar `--homes-root`.
-- **Nunca** copia email, fingerprint de chave nem quotas para `members.json`.
-
-## Stack
-
-- **HTML/CSS/JS** estáticos em `public/`.
-- **Rodapé:** em todas as páginas HTML em `public/` deve constar o **contato** da administração — `admin@runv.club` (bloco `<footer class="site-footer">` como em `index.html`).
-- **Geração de dados**: Python 3 (stdlib); `members.json` é regenerado por `create_runv_user.py` e por `genlanding.py` (sem cron); sem CGI.
-
-## Gerar `public/data/members.json`
-
-**No Git**, `public/data/members.json` fica **`[]`**: a landing não deve mostrar utilizadores fictícios. Quem aparece na constelação vem **só** de `build_directory.py` a ler **`/var/lib/runv/users.json`**, invocado automaticamente após **`create_runv_user.py`** e após **`genlanding.py`** (podes ainda correr o script à mão). Em desenvolvimento, usa uma cópia de teste com **`--users-json site/example-users.json`**. Se **`users.json` ainda não existir** no servidor, o `build_directory.py` assume **zero membros** (aviso em stderr) em vez de falhar.
-
-Manual detalhado do script: **[`build_directory.md`](build_directory.md)**.
-
-No servidor (como root), após provisionar contas:
-
-```bash
-sudo python3 /caminho/ao/repo/site/build_directory.py \
- --users-json /var/lib/runv/users.json \
- --homes-root /home \
- -o /caminho/deploy/public/data/members.json
-```
-
-Sem acesso a `/home` (ex.: build na **sua** máquina só para pré-visualizar):
-
-```bash
-python3 site/build_directory.py \
- --users-json site/example-users.json \
- -o site/public/data/members.json
-```
-
-Dry-run:
-
-```bash
-python3 site/build_directory.py --users-json site/example-users.json --dry-run
-```
-
-## Publicar notícias (`news/publish_news.py`)
-
-Coloque um `.md` em **`site/news/`** (linha 1 = título; resto = corpo), execute `python3 site/news/publish_news.py`. Isto gera **`public/news/data/news.json`**, **`public/news/feed.rss`** e actualiza **`lastmod`** de `/news/` no `sitemap.xml`. O `.md` é removido após publicar.
-
-`news.json` **não** é versionado (`.gitignore`) para não conflitar com `git pull` no servidor. Manual: **[`news/README.md`](news/README.md)**.
-
-Depois, volte a copiar `public/` para o `DocumentRoot` (`genlanding.py` ou deploy manual).
-
-## Configurar Apache (`genlanding.py`)
-
-Para **gerar o VirtualHost**, **ativar** `mod_userdir` / `mod_rewrite`, copiar **`public/`** para o `DocumentRoot` e (opcional) rodar **Certbot**, use o script **[`genlanding.py`](genlanding.py)**. Manual completo: **[`genlanding.md`](genlanding.md)**.
-
-Exemplos:
-
-```bash
-# Produção (runv.club, /var/www/runv.club/html)
-sudo python3 site/genlanding.py
-
-# Pré-visualização
-python3 site/genlanding.py --dry-run
-
-# VM / teste local (runv.local; por padrão desativa 000-default para IP servir a landing)
-sudo python3 site/genlanding.py --dev
-# Manter página Debian em paralelo: --dev --keep-default-site
-
-# TLS após HTTP correto (não combinar com --dev)
-sudo python3 site/genlanding.py --certbot
-```
-
-## Deploy no Apache (manual)
-
-Alternativa ao genlanding: copiar o conteúdo de **`public/`** para o `DocumentRoot` do VirtualHost do domínio (ex. `/var/www/runv.club/html/`), ou configurar `DocumentRoot` para apontar diretamente para esta pasta.
-
-**Certifique-se** de que `mod_userdir` continua a servir `~/public_html` para cada **usuário**; a landing é só a **raiz** do site.
-
-**`members.json`:** não é necessário cron. Com **`genlanding.py`**, a cópia de `public/` é seguida por `build_directory.py` no `DocumentRoot` (use `--no-refresh-members` para omitir). Com **`create_runv_user.py`**, após criar a conta o mesmo ficheiro é regenerado por omissão (`--no-refresh-landing-members` para omitir).
-
-## Arquivos
-
-| Caminho | Função |
-|---------|--------|
-| `genlanding.py` | Configura Apache (vhost, cópia de `public/`, opcional Certbot); ver `genlanding.md` |
-| `build_directory.py` | Gera `members.json` público; ver **`build_directory.md`** |
-| `build_directory.md` | Como usar `build_directory.py` (flags, integração com genlanding/create_runv_user) |
-| `public/index.html` | Landing |
-| `public/faq/index.html` | FAQ (texto estático; link discreto no rodapé) |
-| `public/junte-se/index.html` | Pedir entrada: gerar chave SSH e `ssh entre@runv.club` |
-| `public/assets/style.css` | Estilos |
-| `public/assets/app.js` | Constelação, lista, filtro, shuffle |
-| `public/assets/news-page.js` | Lista de notícias a partir de `news/data/news.json` |
-| `news/publish_news.py` | Ingere `.md` e gera `news.json`, RSS e `sitemap` |
-| `public/news/data/news.json` | Gerado localmente / no servidor (ignorado pelo git) |
-| `public/news/feed.rss` | Feed RSS (stub no repo; regerado pelo script) |
-| `public/data/members.json` | Dados públicos (regenerado; exemplo no repo) |
-| `example-users.json` | Amostra para testes locais |
diff --git a/site/build_directory.md b/site/build_directory.md
@@ -1,126 +0,0 @@
-# build_directory.py — gerar `members.json` para a landing
-
-O script [`build_directory.py`](build_directory.py) lê o ficheiro interno **`users.json`** (criado pelo [`create_runv_user.py`](../scripts/create_runv_user.md)) e gera um JSON **público** consumido pelo JavaScript da landing (`public/assets/app.js`): posiciona os **pontos** (links para `/~utilizador/`) com base em `username`, `since` e `path`.
-
-- **Python 3**, só biblioteca padrão (sem PyPI).
-- **Não** é um servidor web: corre na linha de comando. Em produção é invocado automaticamente por [`create_runv_user.py`](../scripts/create_runv_user.md) (após criar conta) e por [`genlanding.py`](genlanding.md) (após copiar `public/` para o DocumentRoot), salvo flags para desactivar.
-
-Visão geral do `site/`: [README.md](README.md).
-
-## O que entra e o que sai
-
-### Entrada (`--users-json`)
-
-Caminho para o JSON do provisionador (por omissão no servidor: `/var/lib/runv/users.json`). O ficheiro deve ser uma **lista** de objectos; cada entrada com `username` (string) é considerada.
-
-Se o caminho **ainda não existir** (bootstrap antes do primeiro `create_runv_user.py`), o script **não falha**: emite um aviso em stderr, assume **lista vazia** e gera `members.json` com `[]`. Podes também criar manualmente `/var/lib/runv/users.json` com conteúdo `[]` se preferires.
-
-O script **ignora** linhas que não sejam dicionários ou sem `username` válido. Se o ficheiro **existir** mas o JSON for inválido ou não for uma lista, o script termina com erro.
-
-### Saída (`-o` / `--output`)
-
-Um único ficheiro JSON (por omissão: **`site/public/data/members.json`**, relativo à pasta onde está o script).
-
-Cada elemento do array público tem:
-
-| Campo | Significado |
-|--------|-------------|
-| `username` | Nome Unix do membro |
-| `since` | Valor de `created_at` no `users.json`, se existir e for string (senão `""`) |
-| `path` | URL do site pessoal, ex. `"/~alice/"` |
-| `homepage_mtime` | *(Opcional)* Só se usares `--homes-root`: ISO UTC da última modificação de `public_html/index.html` desse utilizador |
-
-### Privacidade
-
-**Nunca** são copiados para o ficheiro público: email, fingerprint SSH, quotas, nem outros campos internos do `users.json`.
-
-## Opções da linha de comando
-
-| Opção | Curto | Por omissão | Descrição |
-|--------|------|-------------|-----------|
-| `--users-json` | — | `/var/lib/runv/users.json` | Ficheiro fonte (lista JSON) |
-| `--output` | `-o` | `site/public/data/members.json`* | Onde gravar o JSON público |
-| `--homes-root` | — | *(não definido)* | Se definires (ex. `/home`), tenta acrescentar `homepage_mtime` por utilizador |
-| `--dry-run` | — | — | Imprime o JSON no **stdout**; não grava ficheiro |
-
-\*O caminho por omissão é relativo ao directório do script: `<pasta_do_build_directory.py>/public/data/members.json`.
-
-## Como executar (a partir da raiz do repositório)
-
-### 1. Servidor em produção
-
-Com acesso a `users.json` e, de preferência, a `/home` para `homepage_mtime`:
-
-```bash
-cd /caminho/ao/runv-server
-sudo python3 site/build_directory.py \
- --users-json /var/lib/runv/users.json \
- --homes-root /home \
- -o /var/www/runv.club/html/data/members.json
-```
-
-Ajusta `-o` ao **DocumentRoot** real (o mesmo que usaste com [`genlanding.py`](genlanding.md), ex. `/var/www/runv.club/html/data/members.json`).
-
-### 2. Máquina local (sem `/var/lib/runv`)
-
-Usa o exemplo do repo ou uma cópia sanitizada do `users.json`:
-
-```bash
-cd /caminho/ao/runv-server
-python3 site/build_directory.py \
- --users-json site/example-users.json \
- -o site/public/data/members.json
-```
-
-Assim podes editar a landing e recarregar o browser sem tocar no servidor.
-
-### 3. Pré-visualizar no terminal (dry-run)
-
-```bash
-python3 site/build_directory.py \
- --users-json site/example-users.json \
- --dry-run
-```
-
-Útil para validar o JSON sem sobrescrever ficheiros.
-
-### 4. Sem `--homes-root`
-
-Se não quiseres (ou não puderes) ler as homes:
-
-```bash
-sudo python3 site/build_directory.py \
- --users-json /var/lib/runv/users.json \
- -o /var/www/runv.club/html/data/members.json
-```
-
-A lista aparece na landing; não haverá `homepage_mtime` (o JS deve tolerar campo em falta).
-
-## Erros comuns
-
-| Mensagem / situação | Causa provável |
-|---------------------|----------------|
-| `Ficheiro inexistente` | `--users-json` aponta para um path errado ou ficheiro ainda não criado |
-| `Formato inválido: esperada lista JSON` | O ficheiro não é um array JSON no topo |
-| Permissão negada ao gravar `-o` | Corre com `sudo` ou escolhe um `-o` onde o teu utilizador possa escrever |
-| `homepage_mtime` nunca aparece | Falta `--homes-root` ou não existe `~/public_html/index.html` legível para esse user |
-| «Escritos N membros» mas a página não mostra pontos | Gravaste em `site/public/data/` no repo; o **site público** usa o **DocumentRoot** do Apache (ex. `/var/www/runv.club/html/`). Usa `-o` para esse path, ou corre `genlanding.py` (regenera `members.json` por omissão após a cópia). |
-
-## Fluxo em produção (sem cron)
-
-Não é necessário agendar `build_directory.py` no cron. A lista pública actualiza-se quando:
-
-1. **`create_runv_user.py`** cria uma conta — por omissão chama `build_directory.py` com `-o <DocumentRoot>/data/members.json` se `--landing-document-root` existir no disco (padrão `/var/www/runv.club/html`). Use `--no-refresh-landing-members` para omitir.
-2. **`genlanding.py`** copia a landing — por omissão volta a executar `build_directory.py` no mesmo DocumentRoot. Use `--no-refresh-members` para omitir.
-
-Podes continuar a correr o script **manualmente** com os exemplos desta página (útil para reparos ou ambientes sem esses passos).
-
-## Relação com outros ficheiros
-
-| Ferramenta | Papel |
-|------------|--------|
-| [`create_runv_user.py`](../scripts/create_runv_user.md) | Mantém `/var/lib/runv/users.json`; opcionalmente regenera `members.json` na landing |
-| [`genlanding.py`](genlanding.md) | Copia `public/` para o Apache; por omissão regenera `data/members.json` a partir de `users.json` |
-| `public/assets/app.js` | Faz `fetch` a `data/members.json` (caminho relativo à página) |
-
-Depois de alterar `members.json` no servidor, não é obrigatório recarregar o Apache — é ficheiro estático servido como qualquer outro.
diff --git a/site/build_directory.py b/site/build_directory.py
@@ -122,7 +122,7 @@ def main() -> None:
"Nota: com membros > 0, confirme que este path é o servido pelo HTTP "
"(<DocumentRoot>/data/members.json). Se a landing em produção não mostrar os pontos, "
"use -o ex.: /var/www/runv.club/html/data/members.json ou copie o ficheiro para lá "
- "(ou genlanding.py). Ver site/build_directory.md.",
+ "(ou genlanding.py). Ver docs/07-public-members-directory.md no repositório.",
file=sys.stderr,
)
diff --git a/site/genlanding.md b/site/genlanding.md
@@ -1,115 +0,0 @@
-# genlanding.py — Apache para a landing runv
-
-Script em [`genlanding.py`](genlanding.py) (Python 3, stdlib) que configura o **Apache** em Debian para:
-
-- servir a landing estática (`site/public/`) num **VirtualHost** dedicado;
-- activar **`mod_userdir`** e **`mod_rewrite`** (redirect **www → apex** em HTTP);
-- em **produção** e em **`--dev`**: por omissão desactiva `000-default.conf` (salvo `--keep-default-site`) para pedidos por IP servirem a landing; opcional **Certbot** em produção (`--certbot`).
-
-Não substitui o manual de [`scripts/docs/2 - server setup.md`](../scripts/docs/2%20-%20server%20setup.md) para aprender permissões e diagnóstico; automatiza o caminho habitual após DNS e pacotes base.
-
-**SEO:** canonical, Open Graph, Twitter Card, JSON-LD, `robots.txt` e `sitemap.xml` vivem em [`public/`](public/) (sobretudo [`public/index.html`](public/index.html)); o `genlanding.py` apenas copia essa árvore para o `DocumentRoot`.
-
-**Notícias:** após correr [`news/publish_news.py`](news/publish_news.py) (gera `public/news/data/news.json` e `feed.rss`), execute de novo o `genlanding.py` (ou copie `public/`) para o servidor servir os ficheiros actualizados.
-
-**FAQ:** conteúdo em [`public/faq/index.html`](public/faq/index.html); o deploy copia `public/` inteiro, logo o FAQ segue automaticamente. Link discreto no rodapé das páginas.
-
-**Wiki:** ficheiros-fonte em [`wiki/*.txt`](wiki/) (`NN_slug.txt`). Em **local**, antes do deploy, gere o HTML em [`public/wiki/`](public/wiki/) com `python3 site/wiki/build_wiki.py` (actualiza também as entradas da wiki em [`public/sitemap.xml`](public/sitemap.xml) entre os comentários `<!-- wiki:gerado -->`). O `genlanding.py` **só copia** `site/public/` — **não** executa este gerador no servidor; ficheiros em `site/wiki/` (excepto o que estiver dentro de `public/`) **não** entram no `DocumentRoot`.
-
-## Pré-requisitos
-
-- **Debian** com `apache2` instalado (recomendado: [`scripts/admin/starthere.py`](../scripts/admin/starthere.py) antes).
-- **Produção:** DNS de `runv.club` e `www.runv.club` a apontar para o servidor; porta **80** acessível se fores usar **Certbot**.
-- Executar como **root** (`sudo`), excepto `--dry-run` (permite pré-visualizar noutra máquina).
-
-## Uso rápido
-
-```bash
-cd /caminho/ao/runv-server
-sudo python3 site/genlanding.py
-```
-
-Produção (valores por omissão: `ServerName` **runv.club**, `DocumentRoot` **`/var/www/runv.club/html`**, ficheiro **`/etc/apache2/sites-available/runv.club.conf`**).
-
-Pré-visualizar sem alterar nada:
-
-```bash
-python3 site/genlanding.py --dry-run
-```
-
-## Flags principais
-
-| Flag | Descrição |
-|------|-----------|
-| `--dev` | Modo **teste local**: `runv.local`, `DocumentRoot` `/var/www/runv-dev/html`, ficheiro `runv-dev.conf`; por omissão **desactiva** `000-default` (igual à produção). |
-| `--domain NAME` | Substitui o `ServerName` (e `www.NAME` como alias). |
-| `--document-root PATH` | Substitui o `DocumentRoot`. |
-| `--source PATH` | Origem da landing (default: `site/public` relativo ao script). |
-| `--keep-default-site` | Mantém `000-default.conf` activo (**produção** e **`--dev`**). Com `000-default` activo, pedidos por **IP** não casam com `ServerName` e continuam a mostrar a página Debian; ver secção abaixo. |
-| `--certbot` | Depois de configurar HTTP, executa `certbot --apache -d <domínio> -d www.<domínio>`. **Incompatível com `--dev`.** |
-| `--dry-run` | Mostra o VirtualHost e comandos; não exige root. |
-| `--no-refresh-members` | Não executar `build_directory.py` após copiar `public/` (omitir `data/members.json`). |
-| `--members-users-json PATH` | Fonte para `build_directory` (default: `/var/lib/runv/users.json`). |
-| `--members-homes-root PATH` | Opcional: `--homes-root` para `build_directory` (ex. `/home`). |
-
-## Pedidos por IP vs `ServerName`
-
-Com **vários** `VirtualHost *:80`, o Apache escolhe o vhost pelo cabeçalho **`Host`**. Se pedires `http://192.168.50.85/`, o `Host` é o IP (ou não coincide com `runv.local`) → o servidor usa o vhost **por defeito** na porta 80, que no Debian costuma ser **`000-default`** (`/var/www/html`, página “It works!”).
-
-- **Por omissão** (`--dev` ou produção **sem** `--keep-default-site`): o script desactiva `000-default`; o vhost runv fica como único (ou primeiro) em `:80` e **pedidos por IP** passam a servir a **landing** no `DocumentRoot` configurado.
-- Com **`--keep-default-site`**: mantém-se `000-default`; para ver a landing usa **`http://runv.local/`** (com `/etc/hosts`) ou força o host no cliente:
-
- ```bash
- curl -sI -H 'Host: runv.local' http://192.168.50.85/
- ```
-
-Se `curl http://runv.local/` não devolver nada na VM, confirma que **`runv.local`** está em **`/etc/hosts`** a apontar para o IP correcto (ex. `127.0.0.1` ou o IP da interface).
-
-## Modo `--dev` (VM ou laptop)
-
-1. Correr: `sudo python3 site/genlanding.py --dev` (por omissão desactiva `000-default`; usa `--keep-default-site` se quiseres manter a página Debian em paralelo).
-2. Opcional: no **cliente** ou na VM, editar `/etc/hosts` para nome bonito:
-
- ```text
- 127.0.0.1 runv.local www.runv.local
- ```
-
- (Se o Apache estiver noutra máquina, usa o IP dessa máquina em vez de `127.0.0.1`.)
-
-3. Abrir `http://runv.local/` ou `http://IP_DA_VM/` no browser (sem `--keep-default-site`). O redirect **www → apex** usa **HTTP** (não uses Certbot em `--dev`).
-
-## Ordem sugerida (produção)
-
-1. `starthere.py` — pacotes, Apache a correr, quotas, etc.
-2. `genlanding.py` — VirtualHost + cópia da landing.
-3. Opcional: `genlanding.py --certbot` **numa segunda execução** (ou a primeira já com `--certbot` se tudo estiver pronto), **depois** de confirmar HTTP no domínio.
-4. Lista de membros: após o passo 2, o script **já** corre [`build_directory.py`](build_directory.py) por omissão (salvo `--no-refresh-members`). Novas contas também disparam o mesmo via [`create_runv_user.py`](../scripts/create_runv_user.md).
-
-## Relação com `build_directory.py`
-
-- `genlanding.py` **copia** o conteúdo actual de `public/` e, por omissão, executa `site/build_directory.py` com `-o` em `<DocumentRoot>/data/members.json` e `--users-json` em `/var/lib/runv/users.json`, para a constelação reflectir contas reais **sem** depender de cron.
-- **`--no-refresh-members`** omite esse passo (útil se `users.json` ainda não existir e quiseres evitar o aviso, ou fluxos especiais).
-
-### Lista pública (só utilizadores reais)
-
-- **`public/data/members.json`** no repositório deve ser **`[]`** (placeholder). **Não** versionar nomes fictícios como membros da comunidade; a única fonte de verdade para quem aparece no site é **`/var/lib/runv/users.json`**, filtrada por `build_directory.py`.
-- **`site/example-users.json`** existe só para desenvolvimento / testes locais com `build_directory.py --users-json`, não para ship em produção como se fossem contas reais.
-- **Deploy:** cada `genlanding.py` substitui o `DocumentRoot`; o passo integrado de `build_directory` **repõe** `members.json` a partir de `users.json`, evitando ficar preso ao `[]` do repo.
-
-## O que o script não faz
-
-- Não cria utilizadores Unix nem mexe em `users.json`.
-- Não configura **firewall** nem **DNS**.
-- Não valida certificados além do que o **Certbot** fizer se invocares `--certbot`.
-
-## Ficheiros tocados
-
-| Caminho | Acção |
-|---------|--------|
-| `/etc/apache2/sites-available/runv.club.conf` ou `runv-dev.conf` | Criado / sobrescrito |
-| `DocumentRoot` (ex. `/var/www/runv.club/html`) | Conteúdo substituído pela cópia de `public/` |
-| `a2enmod userdir`, `rewrite` | Activados |
-| `a2dissite 000-default` | Sem `--keep-default-site` (produção ou `--dev`); falha silenciosa se já desactivado |
-| `a2ensite` | Activa o site runv |
-
-Versão do script: ver `python3 site/genlanding.py --version`.
diff --git a/site/genlanding.py b/site/genlanding.py
@@ -9,13 +9,14 @@ depois volte a correr este script para copiar.
Executar como root (excepto --dry-run). Apenas biblioteca padrão Python 3.
-Versão 0.03 — runv.club
+Versão 0.04 — runv.club
"""
from __future__ import annotations
import argparse
import grp
+import json
import os
import pwd
import re
@@ -25,7 +26,7 @@ import sys
from pathlib import Path
from typing import Final
-VERSION: Final[str] = "0.03"
+VERSION: Final[str] = "0.04"
EXIT_OK: Final[int] = 0
EXIT_USAGE: Final[int] = 1
EXIT_ERROR: Final[int] = 2
@@ -159,6 +160,11 @@ def refresh_members_json_in_document_root(
f"({users_json} → {document_root / 'data' / 'members.json'})",
)
return
+ if not document_root.is_dir():
+ eprint(
+ f"Erro: DocumentRoot inexistente ({document_root}); não é possível gravar data/members.json."
+ )
+ return
script = SCRIPT_DIR / "build_directory.py"
if not script.is_file():
eprint(f"Aviso: {script} não encontrado; members.json não regenerado.")
@@ -187,6 +193,17 @@ def refresh_members_json_in_document_root(
if r.stderr.strip():
for line in r.stderr.strip().splitlines()[:5]:
print(f" {line}")
+ try:
+ data = json.loads(out.read_text(encoding="utf-8"))
+ if isinstance(data, list):
+ print(
+ f" [ok] constelação (bolhas): {len(data)} membro(s) — "
+ "o index.html faz fetch a data/members.json (relativo ao DocumentRoot)."
+ )
+ else:
+ eprint("Aviso: members.json não é uma lista JSON; verifique build_directory.py.")
+ except (OSError, json.JSONDecodeError, TypeError) as e:
+ eprint(f"Aviso: não foi possível confirmar o conteúdo de members.json: {e}")
def chown_www_data(path: Path, *, dry_run: bool) -> None:
diff --git a/site/news/README.md b/site/news/README.md
@@ -1,25 +0,0 @@
-# Publicar notícias (`publish_news.py`)
-
-1. Crie um ficheiro **`.md`** nesta pasta (`site/news/`) com **qualquer nome** (excepto `_*`, que são ignorados). O ficheiro **`README.md`** nunca é publicado.
-2. **Linha 1:** título da notícia.
-3. **Linhas seguintes:** corpo em Markdown leve:
- - `**negrito**`
- - `*itálico*` ou `_itálico_`
- - `++sublinhado++`
-4. No servidor (ou no clone), a partir da raiz do repositório:
-
- ```bash
- python3 site/news/publish_news.py --verbose
- ```
-
-5. O script:
- - acrescenta a notícia a `site/public/news/data/news.json` (data **DD-MM-AAAA**, fuso `America/Sao_Paulo` quando o pacote **tzdata** está disponível; caso contrário usa **UTC−3** fixo);
- - regera `site/public/news/feed.rss`;
- - actualiza `lastmod` da URL `/news/` em `site/public/sitemap.xml`;
- - **apaga** o `.md` processado.
-
-**Git:** `news.json` está em `.gitignore` para evitar conflitos em `git pull` no servidor. No repositório há só `site/public/news/data/news.json.example` (lista vazia). Em produção, após o primeiro `publish_news.py`, copie o `DocumentRoot` com `genlanding.py` ou mantenha `news.json` só no servidor.
-
-**Windows:** instale `tzdata` (`pip install tzdata`) para o fuso `America/Sao_Paulo` exacto.
-
-**Modelo:** veja `_exemplo.md` (não é publicado).
diff --git a/terminal/README.md b/terminal/README.md
@@ -1,65 +0,0 @@
-# terminal — pedido de entrada SSH (`entre@runv.club`)
-
-Módulo **runv.club** para quem se liga por SSH ao utilizador Unix **`entre`**: em vez de shell normal, corre uma experiência **textual guiada** que recolhe nome de utilizador, email, **sítios ou perfis online** (onde te possamos ver) e chave pública SSH, grava um JSON na **fila local** e (opcionalmente) notifica o administrador por **sendmail**.
-
-**Não cria contas Linux.** O provisionamento continua a ser manual (ou via [`scripts/admin/create_runv_user.py`](../scripts/admin/create_runv_user.md)).
-
-## Ficheiros principais
-
-| Ficheiro | Função |
-|----------|--------|
-| `entre_app.py` | Programa principal (ForceCommand SSH). |
-| `entre_core.py` | Validação, fila JSON, log, email. |
-| `setup_entre.py` | Instalação no servidor (root): utilizador `entre`, shell `/bin/sh`, `--auth-mode` (`shared-password` \| `key-only` \| `empty-password` estilo tilde.town), PAM opcional, drop-in SSH, `sshd -t` + `sshd -T -C`, reload. |
-| `config.example.toml` | Modelo versionado; **não** editar como `config.toml` no git. |
-| `gen_config_toml.py` | Gera `config.toml` a partir do example (evita conflitos em `git pull`). |
-| `templates/*.txt` | Textos da experiência e do email ao admin. |
-| `docs/USO.md` | **Instalação + uso** (admin, visitante, testes, checklist). |
-| `docs/INSTALL.md` | Guia de instalação detalhado (Debian 13). |
-| `docs/ADMIN.md` | Operação e aprovação de pedidos. |
-| `docs/ARCHITECTURE.md` | Desenho e segurança. |
-
-## Instalação e uso (resumo)
-
-Guia unificado: **[`docs/USO.md`](docs/USO.md)**.
-
-Em linhas:
-
-1. Como root: `python3 setup_entre.py` (ou `scripts/install.sh`) — por omissão `--auth-mode shared-password`, shell `/bin/sh`, validação `sshd -T -C …`.
-2. **Onboarding sem senha (estilo tilde.town):** `sudo python3 setup_entre.py --auth-mode empty-password` — **PAM** por omissão; SSH por omissão **keyboard-interactive** (melhor no **OpenSSH do Windows**). Teste: `ssh entre@runv.club`. Se a sessão fechar, veja PAM e logs em [INSTALL.md](docs/INSTALL.md). Para o modo README tilde (**password** + senha vazia), use **`--empty-password-tilde-password-auth`** (Linux/Git Bash).
-3. Gerar ou ajustar `/opt/runv/terminal/config.toml` com `python3 gen_config_toml.py --install-root /opt/runv/terminal` (ou `--force` para repor o example). O ficheiro está em `.gitignore` no clone; só o **example** é versionado.
-4. Modo default: `sudo passwd entre`. Modo `key-only`: `authorized_keys`.
-5. Visitante: `ssh entre@runv.club` e seguir o fluxo até à despedida.
-
-Opcional: `--skip-sshd` para aplicar o bloco `Match User entre` à mão (`INSTALL.md`).
-
-## Teste local (sem SSH)
-
-```bash
-chmod +x scripts/test_local.sh
-./scripts/test_local.sh
-```
-
-Usa `terminal/data/queue` e `config.example.toml`. Exige **`ssh-keygen`** no PATH (validação da chave).
-
-## Variáveis de ambiente (opcional)
-
-| Variável | Efeito |
-|----------|--------|
-| `RUNV_ENTRE_ROOT` | Raiz do módulo (default: pasta do `entre_core.py`). |
-| `RUNV_ENTRE_CONFIG` | Caminho absoluto do `config.toml`. |
-| `RUNV_ENTRE_QUEUE_DIR` | Sobrepõe `queue_dir` do TOML. |
-| `RUNV_ENTRE_LOG_FILE` | Sobrepõe `log_file` do TOML. |
-| `RUNV_ENTRE_TEMPLATES_DIR` | Sobrepõe `templates_dir`. |
-
-## Checklist manual de teste
-
-- [ ] `python3 -m py_compile entre_app.py entre_core.py setup_entre.py`
-- [ ] `./scripts/test_local.sh` — percorrer fluxo até gravar JSON em `data/queue/`
-- [ ] Confirmar que **não** sobrescreve se repetir o mesmo `request_id` (colisão improvável; o código regera UUID)
-- [ ] Com `admin_email` preenchido e `mailutils`/`sendmail`: pedido gera tentativa de email (ver log)
-- [ ] No servidor: após `setup_entre.py`, `sshd -t` OK e ficheiro `runv-entre.conf` (ou equivalente manual com `--skip-sshd`)
-- [ ] `ssh entre@servidor` — fluxo completo e ficheiro em `/var/lib/runv/entre-queue/`
-- [ ] Após aprovação: correr `create_runv_user.py` com dados do JSON (ver `docs/ADMIN.md`)
-
-Versão da app: ver `python3 entre_app.py --version`.
diff --git a/terminal/docs/ADMIN.md b/terminal/docs/ADMIN.md
@@ -1,103 +0,0 @@
-# Operação — fila de pedidos `entre` (runv.club)
-
-Fluxo geral de instalação e utilização: **[USO.md](USO.md)**.
-
-## Onde ficam os pedidos
-
-- Directório: **`/var/lib/runv/entre-queue/`**
-- Um ficheiro **`{request_id}.json`** por pedido (UUID v4).
-- Permissões: directório `0700`, dono **`entre`**; ficheiros `0640` na criação.
-
-## Conteúdo típico do JSON
-
-| Campo | Descrição |
-|-------|-----------|
-| `request_id` | Identificador único. |
-| `username` | Nome Unix desejado pelo candidato. |
-| `email` | Contacto. |
-| `online_presence` | Texto livre com sítios/perfis indicados pelo candidato. |
-| `public_key` | Linha OpenSSH normalizada. |
-| `public_key_fingerprint` | SHA256 (formato OpenSSH). |
-| `submitted_at` | ISO 8601 UTC. |
-| `remote_addr` | Endereço remoto, se `SSH_CONNECTION`/`SSH_CLIENT` existir. |
-| `tty` | `SSH_TTY`, se existir. |
-| `source` | `entre-ssh`. |
-| `status` | Inicialmente `pending`. |
-| `app_version` | Versão do `entre_app`. |
-
-## Ler e filtrar
-
-```bash
-sudo ls -1 /var/lib/runv/entre-queue/
-sudo jq -r '"\(.submitted_at) \(.username) \(.email) \(.status)"' /var/lib/runv/entre-queue/*.json
-```
-
-## Revisão manual
-
-1. Abrir o JSON e confirmar que username, email, `online_presence` e chave são plausíveis.
-2. Procurar duplicados (mesmo email ou mesma fingerprint com pedidos `pending`).
-3. Decidir: aprovar, rejeitar ou pedir mais informação por email **fora** deste sistema.
-
-## Aprovar e criar a conta real
-
-Use o provisionador interno **[`scripts/admin/create_runv_user.py`](../../scripts/admin/create_runv_user.py)** (no servidor, como root):
-
-```bash
-sudo python3 /caminho/create_runv_user.py \
- --username "NOME_DO_JSON" \
- --email "EMAIL_DO_JSON" \
- --public-key 'LINHA_EXACTA_DO_JSON'
-```
-
-Ou modo interactivo sem flags e colar os dados. O script valida de novo (regex, chave, utilizador ainda inexistente, etc.).
-
-**Importante:** os dados do JSON são **proposta**; a última palavra é sempre o operador e o `create_runv_user.py`.
-
-## Marcar pedidos no JSON
-
-Não há base de dados: o operador pode:
-
-- Acrescentar campos manualmente, por exemplo:
- - `"reviewed_at": "2026-03-20T12:00:00+00:00"`
- - `"status": "approved"` | `"rejected"` | `"archived"`
- - `"reviewer": "admin"`
-- Ou mover ficheiros para subpastas (`approved/`, `rejected/`) se criar essa convenção localmente.
-
-Sugestão mínima: manter o ficheiro no sítio e só alterar `status` para auditoria simples.
-
-## Notificação ao administrador
-
-1. **Obrigatória:** novo ficheiro na fila.
-2. **Log:** `/var/log/runv/entre.log` (ou o caminho em `config.toml`); também um resumo curto (`admin_console_notice`) na mesma sessão.
-3. **Email:** o `entre_app.py` envia o corpo definido em `templates/admin_mail.txt` quando há destinatário válido:
- - **Prioridade:** `admin_email` em `config.toml`.
- - **Fallback:** se `admin_email` no TOML estiver vazio, usa `admin_email` de `/etc/runv-email.json` (o mesmo ficheiro do Mailgun / `configure_mailgun.py`).
- - **Transporte:** [`entre_core.sendmail_notify`](../entre_core.py) tenta **primeiro** a API **Mailgun** via `lib.mailer.send_mail` quando o JSON global indica Mailgun; caso contrário usa `sendmail_path` (por omissão `/usr/sbin/sendmail`). Requisitos Mailgun: `email_package_root` ou variável `RUNV_EMAIL_ROOT` a apontar para a pasta `email/` do repositório.
- - **Remetente:** por omissão **`noreply@runv.club`** (`mail_from` no TOML ou constante no código). Não usar `entre@runv.club` no *From* (conta SSH). Outro endereço: defina `mail_from` no `config.toml`.
-
-### Reenviar notificação
-
-Não há botão. Opções:
-
-- Copiar o JSON e enviar email manualmente.
-- Script local que relê o JSON e chama `sendmail` com o mesmo formato que `templates/admin_mail.txt`.
-
-### Depuração de email
-
-- Ver log: `grep -E 'notificação|Mailgun|sendmail' /var/log/runv/entre.log`.
-- **Mailgun:** confirmar `/etc/runv-email.json` + chave em `/etc/runv-email.secrets.json`; IP allowlist no painel Mailgun; `email_package_root` ou `RUNV_EMAIL_ROOT`.
-- **Legado (MTA):** testar `echo test | mail -s test root` (conforme o servidor); `ls -l /usr/sbin/sendmail`.
-
-## Pedidos inválidos ou spam
-
-- Marcar `status` como `rejected` ou arquivar.
-- Não apagar de imediato se quiseres trilho de auditoria; podes mover para `archive/` depois de um tempo.
-- **Rate limiting** avançado está fora de âmbito deste módulo; pode ser feito à frente (fail2ban, firewall, etc.).
-
-## Logs e privacidade
-
-Os JSONs contêm dados pessoais e chave pública. Restringe acesso ao directório da fila e rotações de log conforme a política da runv.
-
-## Ligação com o site / documentação pública
-
-Se existir página “Junte-se a nós” no site estático, deve apontar para **`ssh entre@runv.club`** e explicar geração de chaves — mantém coerência com este fluxo.
diff --git a/terminal/docs/ARCHITECTURE.md b/terminal/docs/ARCHITECTURE.md
@@ -1,65 +0,0 @@
-# Arquitectura — módulo `terminal` (entre SSH)
-
-Instalação e uso operacional: **[USO.md](USO.md)**.
-
-## Fluxo ponta a ponta
-
-```mermaid
-sequenceDiagram
- participant C as Cliente_SSH
- participant S as sshd
- participant A as entre_app.py
- participant Q as entre_queue
- participant L as entre.log
- participant M as sendmail
-
- C->>S: autentica como entre
- S->>A: ForceCommand
- A->>C: splash, intro curta + aviso chave
- A->>C: formulário (4 passos: user, email, presença online, pubkey)
- C->>A: respostas por ecrã (presença online: várias linhas até .)
- A->>A: validação (entre_core)
- A->>Q: JSON O_EXCL
- A->>L: eventos
- opt admin_email configurado
- A->>M: email resumo
- end
- A->>C: despedida
-```
-
-## Componentes
-
-| Peça | Papel |
-|------|--------|
-| `entre_app.py` | Orquestra etapas, I/O terminal, confirmação. |
-| `entre_core.py` | Config TOML, validação (alinhada ao `create_runv_user.py`), escrita atómica do JSON, logging, `sendmail`. |
-| `setup_entre.py` | Bootstrap: utilizador `entre`, árvore em `/opt/runv/terminal`, permissões, snippet SSH impresso. |
-| `templates/*.txt` | Conteúdo editável sem alterar código. |
-| `systemd/*.path` + `*.service` | Gatilho opcional em alterações da fila. |
-
-## Decisões de segurança
-
-- **Sem `shell=True`:** `subprocess.run([...], ...)` apenas com listas literais.
-- **Sem criação de utilizadores** no `entre_app.py` / `entre_core.py`.
-- **Sem alteração de Apache ou sshd** pelo código de aplicação.
-- **Fila:** criação com `O_CREAT|O_EXCL` para não sobrescrever ficheiros existentes.
-- **Entrada:** limites de tamanho; chave numa linha; rejeição de marcadores de chave privada.
-- **SSH:** `DisableForwarding` e afins recomendados no `Match User entre` para limitar túneis e agent forwarding.
-- **Utilizador `entre`:** shell `nologin` reduz superfície se alguma configuração falhar (ainda assim, o essencial é o `ForceCommand` correcto).
-
-## Por que a conta não é criada na hora
-
-- **Revisão humana:** pubnix/tilde costuma evitar contas automáticas abertas a abuso.
-- **Coerência com o projeto:** o provisionamento oficial e quotas/metadata estão centralizados em `create_runv_user.py`.
-- **Auditoria:** JSONs imutáveis na entrada (novo `request_id` por tentativa gravada) facilitam rastrear o que foi pedido.
-
-## Pontos de extensão futura
-
-- Campo opcional “mensagem ao admin” no JSON.
-- Script que promove `pending` → `approved` e chama `create_runv_user.py`.
-- Notificação via webhook ou Matrix no `runv-entre-notify.service`.
-- Base de dados: substituir fila por tabela **mudaria** este módulo; hoje é deliberadamente ficheiro-only.
-
-## Alinhamento com `create_runv_user.py`
-
-Regex de username/email (inclui checagem explícita de um único `@` antes do regex), tipos de chave e normalização da linha pública seguem a mesma filosofia que [`scripts/admin/create_runv_user.py`](../../scripts/admin/create_runv_user.py). O código **não** importa esse ficheiro em runtime (evita dependência de path do repositório em `/opt/runv/terminal`); comentários no código referem a necessidade de manter políticas sincronizadas.
diff --git a/terminal/docs/INSTALL.md b/terminal/docs/INSTALL.md
@@ -1,241 +0,0 @@
-# Instalação — fluxo SSH `entre` (runv.club)
-
-Guia para **Debian 13** (ou derivado próximo). Por defeito, `setup_entre.py` instala **`/etc/ssh/sshd_config.d/runv-entre.conf`**, corre **`sshd -t`** e **`systemctl reload ssh`** (com backup do drop-in anterior se existir). Use **`--skip-sshd`** se preferir aplicar o bloco à mão.
-
-Para um único documento com **instalação + uso** (visitante e admin), ver também **[USO.md](USO.md)**.
-
-## 1. Dependências
-
-```bash
-sudo apt update
-sudo apt install -y python3 openssh-server openssh-client mailutils
-```
-
-- **python3** — interpretador (stdlib: `tomllib`, `email`, etc.).
-- **openssh-client** — binário `ssh-keygen` usado para validar fingerprint da chave pública.
-- **openssh-server** — serviço SSH.
-- **mailutils** (ou outro MTA com **sendmail** em `/usr/sbin/sendmail`) — **opcional**, só para email ao admin.
-
-**Recomendado para o servidor runv.club:** configurar envio **sem Postfix/Exim** com o módulo do repositório **[`email/`](../email/README.md)** (`msmtp` + `msmtp-mta` + `bsd-mailx`). Depois disso, `/usr/sbin/sendmail` encaminha para o seu SMTP externo e o `entre` continua a usar `sendmail_path = "/usr/sbin/sendmail"` no `config.toml`.
-
-## 2. Obter o código
-
-A partir do repositório `runv-server`, a pasta relevante é `terminal/`.
-
-## 3. Executar o setup (root)
-
-```bash
-cd /caminho/do/repositório/terminal
-sudo python3 setup_entre.py
-```
-
-Ou:
-
-```bash
-sudo sh scripts/install.sh
-```
-
-O script:
-
-- cria o utilizador **`entre`** (se não existir), com home por omissão `/home/entre` e shell **`/bin/sh`** (o OpenSSH precisa de shell funcional para o contexto do **ForceCommand**; `nologin` impede o fluxo);
-- alinha o shell com **`chsh`** se `entre` já existir com outro shell;
-- garante **`~entre/.ssh`** e **`authorized_keys`** (vazio; útil sobretudo em `--auth-mode key-only`);
-- cria **`/var/lib/runv/entre-queue`** (dono `entre`, modo `0700`);
-- garante **`/var/log/runv/`** e o ficheiro **`entre.log`** (dono `entre`, leitura/escrita para append);
-- copia o módulo para **`/opt/runv/terminal`** e, se não existir `config.toml`, gera-o a partir de `config.example.toml`;
-- **OpenSSH (por defeito):** escreve o drop-in conforme **`--auth-mode`** (omissão: **`shared-password`**); **`sshd -t`**, validação **`sshd -T -C …`**, **`systemctl reload ssh`** (em falha, reverte o drop-in).
-
-Opções úteis:
-
-- `--auth-mode shared-password` | `key-only` | `empty-password` — método para `entre`.
-- **`empty-password` (onboarding estilo [tilde.town](https://tilde.town) / `join@tilde.town`):** cria grupo **`entre-open`**, mete `entre` no grupo, **`passwd -d`**, valida **NP**. **Por omissão** o drop-in usa **`AuthenticationMethods keyboard-interactive`** + **`KbdInteractiveAuthentication yes`** (PAM **`pam_succeed_if`** sem prompts) — melhor com **OpenSSH do Windows**, que em geral não envia palavra-passe vazia no método **`password`**. **`--empty-password-tilde-password-auth`** volta ao esquema README tilde (**`password`** + **`PermitEmptyPasswords yes`**). **Por omissão** altera **`/etc/pam.d/sshd`**: backup e linha **`pam_succeed_if … user ingroup …`** antes de **`@include common-auth`**. Sem isto, no Debian o **PAM** pode recusar o fluxo → **«Connection closed»**. **Não** é ausência total de autenticação: é política explícita só para `entre`.
-- `--empty-password-group` — nome do grupo suplementar (default: `entre-open`).
-- **`--empty-password-tilde-password-auth`** — só com **`empty-password`**: drop-in estilo README tilde (**`password`** + **`PermitEmptyPasswords yes`**); omissão = keyboard-interactive (recomendado para Windows).
-- **`--skip-pam-empty-password-rule`** — não mexer no PAM (só para quem configura à mão; em geral **não** use em `empty-password` em Debian).
-- `--sshd-test-connection` — argumento `-C` para `sshd -T` (deve bater com o `Match`, ex.: `user=entre,host=runv.club,addr=127.0.0.1`).
-- `--dry-run` — apenas mensagens, sem alterações.
-- **Reexecução:** se já existir **`/opt/runv/terminal/entre_app.py`**, em terminal interactivo o script pergunta se deseja continuar (actualiza `entre_app.py`, `entre_core.py`, `templates/`, etc.). Responder **não** cancela tudo. Em seguida, se **`config.toml`** já existir, pergunta se deve **substituí-lo** pelo example (omissão: **não**, para não perder `admin_email`).
-- `-y` / `--yes` — não mostrar esses prompts (útil em scripts); **`config.toml`** continua preservado salvo **`--force-config`**.
-- `--force-config` — repõe `config.toml` a partir do example (sem segundo prompt).
-- `--skip-copy` — só directórios/utilizador (sem copiar ficheiros).
-- `--skip-sshd` — não toca no SSH; imprime o bloco `Match User entre` para cópia manual.
-- `--no-reload` — grava o drop-in e corre `sshd -t` + validação `-T`, mas não recarrega o serviço (útil para rever antes).
-
-## 4. Configuração (`config.toml`)
-
-O **`config.toml`** não deve ser versionado no repositório (está em **`.gitignore`** em `terminal/config.toml` no clone). Gere-o a partir do modelo:
-
-```bash
-sudo python3 /opt/runv/src/terminal/gen_config_toml.py --install-root /opt/runv/terminal
-```
-
-- **`--force`** — sobrescreve um `config.toml` já existente (perde edições locais nesse ficheiro).
-- O **`setup_entre.py`** chama a mesma lógica na primeira instalação (ou com **`--force-config`**).
-
-Edite **`/opt/runv/terminal/config.toml`** quando precisar de valores que não vêm do example:
-
-- **`admin_email`** — endereço para notificações. Pode ficar vazio no TOML se **`admin_email`** estiver definido em **`/etc/runv-email.json`** (fallback usado pelo `entre_app.py`). Se ambos estiverem vazios, só fila + log.
-- **`mail_from`** — remetente do email (cabeçalho `From`); por omissão **`noreply@runv.club`** (não use **`entre@runv.club`**: essa conta é só SSH). Valores antigos `entre@runv.club` no TOML são normalizados para noreply. Para outro remetente verificado no Mailgun, defina explicitamente no TOML.
-- **`sendmail_path`** — normalmente `/usr/sbin/sendmail` (ramo legado; com Mailgun configurado, o envio pode ser pela API sem precisar de MTA).
-
-## 5. Autenticação SSH para o utilizador `entre`
-
-O OpenSSH **exige** sempre **alguma** credencial; **não** existe “`ssh` e entrou” sem palavra-passe nem chave no protocolo.
-
-**Modo recomendado (`--auth-mode shared-password`):** palavra-passe Unix **partilhada**, definida **só pelo root** (`sudo passwd entre`, etc.), com **`AuthenticationMethods password`**, **`PubkeyAuthentication no`** e **`KbdInteractiveAuthentication no`** no `Match User entre`, para acesso sem chave pré-registada.
-
-**`key-only`:** só chave pública em **`authorized_keys`**; sem palavra-passe.
-
-**`empty-password`:** `passwd -d entre`, grupo **`entre-open`**, regra PAM **`pam_succeed_if user ingroup entre-open`** (recomendado no Debian). **Por omissão** o SSH usa **`keyboard-interactive`** (PAM resolve sem prompts; compatível com Windows). Com **`--empty-password-tilde-password-auth`**, **`AuthenticationMethods password`** + **`PermitEmptyPasswords yes`** (como muitos README tilde). **Menos seguro** que palavra-passe ou chave; usar só para onboarding do utilizador especial `entre`, não para contas normais.
-
-O fluxo **`entre_app`** (historinha + formulário) **não** altera a senha Unix; só recolhe o pedido de conta.
-
-O visitante **não** obtém shell interactivo normal: o **`ForceCommand`** substitui o comando remoto (o shell em passwd é apenas o contexto mínimo exigido pelo OpenSSH).
-
-## 6. OpenSSH (`runv-entre.conf`)
-
-O setup coloca o ficheiro **`/etc/ssh/sshd_config.d/runv-entre.conf`** com o mesmo conteúdo lógico que abaixo (o caminho de `python3` vem de `which python3` no servidor). Confirme que **`/etc/ssh/sshd_config`** inclui algo como `Include /etc/ssh/sshd_config.d/*.conf` (comum no Debian).
-
-Exemplo equivalente:
-
-```
-Match User entre
- AuthenticationMethods password
- PasswordAuthentication yes
- KbdInteractiveAuthentication no
- PubkeyAuthentication no
- PermitEmptyPasswords no
- ForceCommand /usr/bin/python3 /opt/runv/terminal/entre_app.py
- PermitTTY yes
- PermitUserRC no
- X11Forwarding no
- AllowAgentForwarding no
- AllowTcpForwarding no
- PermitTunnel no
- DisableForwarding yes
-```
-
-Exemplo **`--auth-mode empty-password`** (omissão; keyboard-interactive + PAM — recomendado para Windows):
-
-```
-Match User entre
- AuthenticationMethods keyboard-interactive
- PasswordAuthentication no
- KbdInteractiveAuthentication yes
- PubkeyAuthentication no
- PermitEmptyPasswords no
- ForceCommand /usr/bin/python3 /opt/runv/terminal/entre_app.py
- PermitTTY yes
- PermitUserRC no
- X11Forwarding no
- AllowAgentForwarding no
- AllowTcpForwarding no
- PermitTunnel no
- DisableForwarding yes
-```
-
-Com **`--empty-password-tilde-password-auth`** (README tilde; `PermitEmptyPasswords yes`):
-
-```
-Match User entre
- AuthenticationMethods password
- PasswordAuthentication yes
- KbdInteractiveAuthentication no
- PubkeyAuthentication no
- PermitEmptyPasswords yes
- ForceCommand /usr/bin/python3 /opt/runv/terminal/entre_app.py
- PermitTTY yes
- PermitUserRC no
- X11Forwarding no
- AllowAgentForwarding no
- AllowTcpForwarding no
- PermitTunnel no
- DisableForwarding yes
-```
-
-Em **`empty-password`**, o script faz backup de **`/etc/pam.d/sshd`** e insere antes de `@include common-auth` (ou primeira linha `auth`), salvo **`--skip-pam-empty-password-rule`**:
-
-```
-auth [success=done default=ignore] pam_succeed_if.so user ingroup entre-open
-```
-
-(Ajuste `entre-open` com **`--empty-password-group`** se mudar o nome do grupo.)
-
-Se usou **`--skip-sshd`**, crie o ficheiro à mão e depois:
-
-```bash
-sudo sshd -t
-sudo systemctl reload ssh
-```
-
-Confirme que o caminho de **`python3`** e de **`entre_app.py`** coincidem com o servidor (`which python3`).
-
-## 7. Teste local do programa (sem SSH)
-
-Na máquina de desenvolvimento (com `ssh-keygen` disponível):
-
-```bash
-cd terminal
-chmod +x scripts/test_local.sh
-./scripts/test_local.sh
-```
-
-Os pedidos ficam em `terminal/data/queue/`.
-
-## 8. Teste via SSH
-
-A partir de um cliente:
-
-```bash
-ssh entre@runv.club
-```
-
-(Substitua o host.) Percorra o fluxo até ao fim e verifique:
-
-```bash
-sudo ls -la /var/lib/runv/entre-queue/
-sudo jq . /var/lib/runv/entre-queue/<request_id>.json
-```
-
-## 9. Teste de notificação por email
-
-1. Defina o destinatário: **`admin_email`** no `config.toml` **ou** (se o TOML estiver vazio) **`admin_email`** em **`/etc/runv-email.json`**.
-2. **Mailgun:** estado e segredos correctos; `email_package_root` ou `RUNV_EMAIL_ROOT`; teste com `email/configure_mailgun.py --test` no servidor.
-3. **Legado:** **`sendmail`** e MTA a aceitar relay ou mail local.
-4. Opcional: inspeccionar o formato com:
-
-```bash
-sh scripts/test_mail.sh
-```
-
-Se o email falhar, o pedido **mantém-se** na fila e o log regista o aviso (`notificação Mailgun falhou`, `sendmail falhou`, etc.).
-
-## 10. systemd.path (opcional)
-
-Para reagir a alterações na fila (log extra, hook próprio):
-
-```bash
-sudo cp systemd/runv-entre-notify.path /etc/systemd/system/
-sudo cp systemd/runv-entre-notify.service /etc/systemd/system/
-sudo systemctl daemon-reload
-sudo systemctl enable --now runv-entre-notify.path
-```
-
-Edite **`runv-entre-notify.service`** se quiser outro `ExecStart` (sem depender do Python do módulo para notificações simples).
-
-## 11. Segurança e reversão do drop-in
-
-A instalação automática faz **backup** do ficheiro anterior (`runv-entre.conf.bak.<timestamp>`), valida com **`sshd -t`** e só então recarrega o serviço. Se o teste falhar, o script **reverte** (ou remove o ficheiro numa primeira instalação). Para ambientes onde qualquer alteração ao SSH exige revisão prévia, use **`--no-reload`** ou **`--skip-sshd`**.
-
-## Problemas frequentes
-
-| Sintoma | Verificação |
-|---------|-------------|
-| `entre_app.py` não arranca | Permissões em `/opt/runv/terminal`, dono `entre`, `python3` no caminho. |
-| Erro ao gravar fila | Dono e modo de `/var/lib/runv/entre-queue`. |
-| Log vazio / permissão | Dono de `/var/log/runv/entre.log`. |
-| Chave rejeitada | `ssh-keygen` instalado; chave numa linha; tipo permitido. |
-| Sessão SSH fecha logo | Autenticação de `entre` falhou antes do ForceCommand. |
-| Email do novo pedido não chega | `admin_email` no TOML ou no `/etc/runv-email.json`; Mailgun: allowlist de IP, chave HTTP, `email_package_root` / `RUNV_EMAIL_ROOT`; legado: `sendmail_path` e MTA. Ver log `entre`. |
-
-Documentação de operação: **[ADMIN.md](ADMIN.md)**. Desenho: **[ARCHITECTURE.md](ARCHITECTURE.md)**.
diff --git a/terminal/templates/admin_mail.txt b/terminal/templates/admin_mail.txt
@@ -18,4 +18,4 @@ Chave pública (uma linha):
---
Fila: /var/lib/runv/entre-queue/{request_id}.json
-Aprovar com create_runv_user.py (ver terminal/docs/ADMIN.md).
+Aprovar com create_runv_user.py (ver docs/10-user-provisioning-and-admin-ops.md no repositório runv-server).
diff --git a/tools/README.md b/tools/README.md
@@ -1,63 +0,0 @@
-# tools — experiência base runv.club (Debian)
-
-Módulo para **automatizar** no servidor Debian 13 (ou compatível):
-
-1. **Pacotes globais** via `apt` (lista em `manifests/apt_packages.txt`) — para todos os usuários, **sem** passar pelo `/etc/skel`.
-2. **Comandos locais** em `/usr/local/bin`: `runv-help`, `runv-links`, `runv-status`, **`chat`** (IRC; rede da casa provisionada com **`patches/patch_irc.py`** — utilizadores usam só `chat`).
-3. **MOTD dinâmico** em `/etc/update-motd.d/60-runv` (arte ASCII verde, texto em português).
-4. **Arquivos padrão** copiados para `/etc/skel/` (README, `.bash_aliases`, `public_html/index.html`, `public_gopher/gophermap`, `public_gemini/index.gmi`) — **somente modelos de home**, nunca instaladores de sistema.
-
-## Regras
-
-- **`/etc/skel`** = apenas arquivos que **novas contas** recebem na home (via `adduser`). **Não** instala programas.
-- **Programas** = sempre **`apt`** (globais).
-- **Scripts do projeto** = **`/usr/local/bin`**.
-- **MOTD** = script executável em **`/etc/update-motd.d/`**.
-- Python **stdlib** apenas; **`subprocess` sem `shell=True`**; sem Docker, sem web, sem DB.
-
-## Execução rápida
-
-No servidor, a partir da raiz do repositório (ou com caminho absoluto):
-
-```bash
-sudo python3 tools/tools.py
-```
-
-Simular sem alterar nada:
-
-```bash
-sudo python3 tools/tools.py --dry-run --verbose
-```
-
-Sem `--force`, o script **atualiza** MOTD, `bin/` e skel quando o ficheiro no repositório **mudou** em relação ao destino. Para sobrescrever **sempre** (mesmo idêntico):
-
-```bash
-sudo python3 tools/tools.py --force
-```
-
-Reaplicar só scripts/MOTD/skel **sem** rodar `apt`:
-
-```bash
-sudo python3 tools/tools.py --skip-apt
-```
-
-## Conteúdo
-
-| Caminho | Função |
-|---------|--------|
-| `tools.py` | Orquestra apt, cópias e permissões |
-| `manifests/apt_packages.txt` | Um pacote Debian por linha |
-| `bin/` | Scripts shell instalados em `/usr/local/bin` |
-| `motd/60-runv` | Fragmento MOTD (verde, pubnix) |
-| `skel/` | Modelos copiados para `/etc/skel/` |
-| `docs/` | Instalação, administração, experiência do usuário |
-
-## Byobu
-
-O pacote **byobu** é instalado **globalmente** com os demais, mas **não** é ativado automaticamente para todos os usuários. Quem quiser pode habilitar depois com **`byobu-enable`** na própria conta. Integrar isso ao fluxo de **provisionamento** (`create_runv_user` / onboarding) fica para uma etapa futura — não é papel deste módulo forçar Byobu no login.
-
-## Documentação
-
-- **[docs/INSTALL.md](docs/INSTALL.md)** — dependências, flags, verificação.
-- **[docs/ADMIN.md](docs/ADMIN.md)** — operação e manutenção.
-- **[docs/USER_EXPERIENCE.md](docs/USER_EXPERIENCE.md)** — o que o usuário vê e recebe.
diff --git a/tools/docs/ADMIN.md b/tools/docs/ADMIN.md
@@ -1,107 +0,0 @@
-# Administração — módulo `tools/`
-
-Operação contínua do runv.club em **Debian**.
-
-## Atualizar a lista de pacotes
-
-1. Edite **`tools/manifests/apt_packages.txt`** (um pacote por linha; comentários com `#`).
-2. No servidor:
-
-```bash
-sudo python3 tools/tools.py --verbose
-```
-
-Use **`--skip-apt`** se quiser **não** rodar o apt nesta passada (por exemplo, durante janela de manutenção em que só atualiza arquivos).
-
-## Trocar textos do MOTD
-
-- Edite **`tools/motd/60-runv`** no repositório (shell `sh`, sem `figlet`). O logótipo **RUNV** usa as mesmas linhas UTF-8 que a landing e o `entre_app.py`; só esse bloco leva ANSI verde (`%b` + literais `\033`, não `echo -e`).
-- Reaplique:
-
-```bash
-sudo python3 tools/tools.py --force --skip-apt
-```
-
-(`--force` força cópia mesmo sem mudança no conteúdo; sem ele, basta alterar o ficheiro no repo e rodar `tools.py`.)
-
-**Boas práticas:** mantenha fallbacks (`command -v` / redirecionar stderr) para não quebrar o login se algum binário sumir.
-
-## Editar `runv-help`, `runv-links`, `runv-status`
-
-- **`runv-status`** verifica o nome de login (`id -un`); por omissão só **`pmurad-admin`** passa. Para outro admin, edite a variável **`RUNV_STATUS_USER`** no script em **`tools/bin/runv-status`** antes de reaplicar.
-
-1. Altere os arquivos em **`tools/bin/`**.
-2. Instale de novo:
-
-```bash
-sudo python3 tools/tools.py --force --skip-apt
-```
-
-Confirme permissões **755** em `/usr/local/bin/`.
-
-## Reaplicar tudo com `tools.py`
-
-Instalação completa (apt + arquivos):
-
-```bash
-sudo python3 tools/tools.py --force --verbose
-```
-
-Só arquivos (sem apt):
-
-```bash
-sudo python3 tools/tools.py --force --skip-apt
-```
-
-## Remover um script
-
-O `tools.py` **não remove** arquivos do sistema. Para retirar, por exemplo, `runv-help`:
-
-```bash
-sudo rm -f /usr/local/bin/runv-help
-```
-
-Para o MOTD:
-
-```bash
-sudo rm -f /etc/update-motd.d/60-runv
-```
-
-Para modelos no skel (cuidado — afeta **novas** contas, não apaga homes existentes):
-
-```bash
-sudo rm -f /etc/skel/README.md
-# etc.
-```
-
-Depois, se quiser reinstalar só a partir do repositório:
-
-```bash
-sudo python3 tools/tools.py --force --skip-apt
-```
-
-## Ajustar permissões manualmente
-
-Se algo ficou com modo errado:
-
-```bash
-sudo chmod 755 /usr/local/bin/runv-help /usr/local/bin/runv-links /usr/local/bin/runv-status
-sudo chmod 755 /etc/update-motd.d/60-runv
-sudo chmod 644 /etc/skel/README.md /etc/skel/.bash_aliases /etc/skel/public_html/index.html
-sudo chmod 755 /etc/skel/public_html
-```
-
-Dono típico: **root:root** (o script tenta `chown` após copiar).
-
-## Byobu
-
-- **Instalado** globalmente com o apt deste módulo.
-- **Não** habilitado automaticamente para todos (evita surpresas no login).
-- Usuários podem usar **`byobu-enable`** quando quiserem.
-- Documentar ou automatizar no **onboarding** / `create_runv_user` é decisão futura — ver **`tools/README.md`**.
-
-## Idempotência
-
-- Rodar **`tools.py`** **sem `--force`** compara origem e destino: se forem **idênticos**, pula; se o repo tiver **versão nova**, copia e atualiza.
-- **`apt-get install`** já é idempotente para pacotes instalados.
-- Use **`--force`** para sobrescrever **sempre** (mesmo conteúdo igual), por exemplo para repor dono/permissões.
diff --git a/tools/docs/INSTALL.md b/tools/docs/INSTALL.md
@@ -1,133 +0,0 @@
-# Instalação — módulo `tools/` (runv.club)
-
-Guia em **português** para administradores. Ambiente alvo: **Debian 13** (ou Debian estável recente).
-
-## Dependências
-
-- **root** no servidor (sudo).
-- **Python 3** do Debian (sem PyPI obrigatório).
-- **`apt`** funcional (`apt-get`).
-- Rede para `apt-get update` / `install` (ou espelho local configurado).
-
-Não é necessário Docker, banco de dados nem painel web.
-
-## O que o `tools.py` faz
-
-1. Valida execução como **root** (exceto em `--dry-run`, que só simula).
-2. Lê **`manifests/apt_packages.txt`** (ignora linhas vazias e `#`).
-3. Executa **`apt-get update -qq`** e **`apt-get install -y --no-install-recommends`** com esses pacotes.
-4. Copia **`bin/runv-help`**, **`runv-links`**, **`runv-status`**, **`bin/chat`** → **`/usr/local/bin/`** com modo **755** (`chat` abre o IRC com config em `~/.config/weechat`; ver **`scripts/docs/irc_patch.md`**).
-5. Copia **`motd/60-runv`** → **`/etc/update-motd.d/60-runv`** com modo **755**.
-6. Garante **Jailkit + SSH chroot** (idempotente): grupo **`runv-jailed`**, remove **`pmurad-admin`** desse grupo se estiver, instala **`/etc/ssh/sshd_config.d/90-runv-jailed.conf`** a partir do repo, **`sshd -t`** e **`systemctl reload ssh`** (ou `sshd`).
-7. Copia o **`skel/`** do repositório para **`/etc/skel/`**:
- - `.bash_aliases` → **644**
- - `public_html/index.html` → diretório **`public_html` 755**, arquivo **644**
- - `public_gopher/gophermap` → diretório **`public_gopher` 755**, arquivo **644**
- - `public_gemini/index.gmi` → diretório **`public_gemini` 755**, arquivo **644**
-
- O **`README.md` não** é copiado para `/etc/skel` (política runv: contas novas sem README na home por defeito). Se existir **`/etc/skel/README.md`** de uma instalação antiga, o `tools.py` **remove** esse ficheiro ao sincronizar o skel.
-
-O **`/etc/skel`** só afeta **contas novas** criadas depois da cópia (o Debian copia o skel no `adduser`). Utilizadores **já existentes** não recebem automaticamente estes ficheiros: use **[`scripts/admin/setup_alt_protocols.py`](../../scripts/docs/alt_protocols.md)** (backfill) ou crie `~/public_gopher` e `~/public_gemini` manualmente.
-
-Se o destino **já existir** e for **idêntico** (conteúdo byte-a-byte) à origem no repositório, a cópia é **ignorada**. Se o ficheiro no repo **mudou**, o `tools.py` **atualiza** o destino mesmo sem **`--force`**. Use **`--force`** para sobrescrever sempre (útil para repor permissões/mtime ou forçar cópia igual).
-
-## Execução
-
-```bash
-cd /caminho/para/runv-server
-sudo python3 tools/tools.py
-```
-
-### Flags
-
-| Flag | Efeito |
-|------|--------|
-| `--dry-run` | Não grava nem chama apt de verdade; mostra o que seria feito. |
-| `--verbose` | Log detalhado no stderr. |
-| `--force` | Sobrescreve sempre, mesmo quando origem e destino são idênticos. |
-| `--skip-apt` | Pula `apt-get` (útil para atualizar só MOTD/bin/skel). |
-
-Exemplo seguro antes da primeira aplicação:
-
-```bash
-sudo python3 tools/tools.py --dry-run --verbose
-```
-
-## Verificar pacotes instalados
-
-```bash
-dpkg -l wtmpdb jailkit byobu tmux lynx weechat weechat-headless mutt bsdgames tree less curl wget git
-```
-
-Ou:
-
-```bash
-apt list --installed 2>/dev/null | grep -E 'wtmpdb|jailkit|byobu|tmux|lynx|weechat|mutt|bsdgames|tree|less|curl|wget|git'
-```
-
-**Importante:** esses programas são **globais**. **Não** dependem do `/etc/skel`. Qualquer usuário com shell pode usá-los após a instalação (e após login, se o pacote estiver no `PATH`).
-
-## Verificar comandos em `/usr/local/bin`
-
-```bash
-ls -l /usr/local/bin/runv-help /usr/local/bin/runv-links /usr/local/bin/runv-status /usr/local/bin/chat
-/usr/local/bin/runv-help
-```
-
-Devem ser executáveis (**`-rwxr-xr-x`**) e imprimir texto em português com cores.
-
-## Verificar MOTD
-
-O Debian monta o MOTD com scripts em `/etc/update-motd.d/`. Para testar **só** o fragmento runv:
-
-```bash
-sudo chmod +x /etc/update-motd.d/60-runv # se ainda não estiver
-/etc/update-motd.d/60-runv
-```
-
-Para ver a sequência completa (pode ser longa):
-
-```bash
-run-parts /etc/update-motd.d/
-```
-
-Em novo login SSH você deve ver o bloco **verde** com arte **RUNV**, a tagline, a lista de comandos úteis e a dica **“digite runv-help para começar”**. Estatísticas do servidor (**`runv-status`**) não aparecem no MOTD nem em `runv-help`; só o utilizador **`pmurad-admin`** pode executar `runv-status`.
-
-## Verificar `/etc/skel`
-
-```bash
-ls -la /etc/skel/
-ls -la /etc/skel/public_html/
-```
-
-Esperado:
-
-- `.bash_aliases` com permissões **644** (arquivo).
-- **Sem** `README.md` em `/etc/skel` após sincronização.
-- `public_html` como diretório **755**.
-- `public_html/index.html` **644**.
-
-Novas contas criadas com `adduser` **depois** desta instalação recebem esses arquivos na home (junto com o restante do skel padrão do Debian, como `.bashrc`, se existir no sistema).
-
-## SSH: grupo `runv-jailed` (chroot)
-
-O drop-in **`90-runv-jailed.conf`** aplica `ChrootDirectory /srv/jail/%u` a utilizadores no grupo **`runv-jailed`**. A conta **`pmurad-admin`** **não** deve estar nesse grupo (administração fora do chroot). Novas contas normais recebem jail e grupo via **`scripts/admin/create_runv_user.py`**; contas já existentes via **`scripts/admin/perm1.py`**.
-
-Após alterar o sshd, confirme **`sshd -t`** e teste login com um utilizador de staging antes de aplicar em produção.
-
-## Instruções de teste (checklist)
-
-1. **Dry-run:** `sudo python3 tools/tools.py --dry-run --verbose` — revisar saída.
-2. **Aplicar:** `sudo python3 tools/tools.py --verbose`.
-3. **Segunda execução** sem `--force` com repo **inalterado** — deve **pular** ficheiros já iguais; após **editar** MOTD/bin/skel no repo, a mesma execução deve **copiar de novo**.
-4. **`runv-help` / `runv-links`** — qualquer utilizador; **`runv-status`** — apenas como **`pmurad-admin`**.
-5. **MOTD:** rodar `/etc/update-motd.d/60-runv` ou novo login SSH.
-6. **Skel:** criar usuário de teste com `adduser` e conferir **ausência** de `~usuario/README.md` e presença de `~/public_html/index.html` (se o skel runv tiver sido aplicado).
-
-## Problemas comuns
-
-- **apt-get update falha:** corrija espelhos/rede; o script registra erro e ainda pode copiar bin/MOTD/skel.
-- **Permissão negada:** execute com `sudo` / root.
-- **MOTD não aparece:** em alguns setups o display do MOTD depende de `pam_motd` e SSH; confira configuração do `sshd` e PAM no Debian.
-- **MOTD sem grelha `last`:** em **Debian 13+**, o comando **`last`** vem do pacote **`wtmpdb`**, não de `util-linux`. Instale com `apt install wtmpdb`. O fragmento `60-runv` tenta `last` em PATH, `/usr/bin/last` e `/bin/last`. A mensagem *sem registos recentes em wtmp* indica wtmp vazio, não falta do binário.
-- **Gemini (`molly-brown`) inactivo ou «activating»:** guia de diagnóstico (journalctl com `sudo`, porta 1965, permissões da chave TLS) em **`scripts/docs/alt_protocols.md`** — secção *Molly não sobe ou fica em «activating»*.
diff --git a/tools/docs/USER_EXPERIENCE.md b/tools/docs/USER_EXPERIENCE.md
@@ -1,57 +0,0 @@
-# Experiência do usuário — runv.club (`tools/`)
-
-Visão para **quem entra no servidor** pela primeira vez (e para quem documenta suporte).
-
-## O que aparece no login
-
-1. **MOTD** — O Debian executa os scripts em `/etc/update-motd.d/`. O fragmento **`60-runv`** mostra:
- - logótipo **RUNV** (mesmo desenho UTF-8 da landing) **só nesse bloco** em verde;
- - tagline `.club — um computador para compartilhar` (sem estatísticas no MOTD; o comando **`runv-status`** existe mas **não** é listado aqui e só o utilizador **`pmurad-admin`** pode executá-lo);
- - **Comandos úteis** em lista, com nome a verde e descrição a cinza (ANSI), alinhada ao texto do `runv-help`;
- - secção **Últimos usuários online**: grelha **3×3** com até **9 nomes únicos** (fonte: **`last`** / wtmp; ordem = atividade recente; cada utilizador só aparece **uma** vez; ignora linhas `reboot` / `wtmp` e os utilizadores **`entre`** e **`root`**). Em **Debian 13+**, o binário **`last`** vem do pacote **`wtmpdb`** (o `tools.py` instala-o). O fragmento tenta **`/usr/bin/last`** se o PATH de `update-motd.d` não incluir `last`. Se aparecer *sem registos recentes em wtmp*, o ficheiro de logins ainda não tem entradas (ex.: sem logins SSH registados).
- - linha final: **digite `runv-help` para começar**.
-
-2. **Prompt da shell** — Depende do shell padrão (geralmente Bash no Debian). O que o usuário **herda** da home vem do **`/etc/skel`** no momento em que a conta foi criada.
-
-## Comandos locais do runv
-
-| Comando | Função |
-|---------|--------|
-| **`runv-help`** | Texto de ajuda: o que é o runv, comandos úteis, dicas, link do site. |
-| **`runv-links`** | Links: runv.club, Portal IDEA, etc. |
-| **`runv-status`** | (Só **`pmurad-admin`**) hostname, uptime, memória, disco, `who`. Não aparece no MOTD nem em `runv-help`. |
-
-Todos são **shell scripts** em **`/usr/local/bin`**, com cores ANSI simples, texto em **português**. Não dependem de Python na sessão do usuário.
-
-## O que o usuário recebe na home (contas novas)
-
-Quando um administrador cria a conta com **`adduser`**, o Debian copia **`/etc/skel`** para a home. Depois de rodar o módulo **`tools/`**, o skel inclui (entre o que o Debian já traz, como `.bashrc` quando aplicável):
-
-- **`.bash_aliases`** — atalhos (`ll`, `la`, `l`, `help-runv`).
-- **`public_html/index.html`** — página inicial mínima em HTML estático (sem JS, sem CDN), em português.
-
-**Não** há **`README.md`** no skel runv: orientação inicial está no **MOTD** e no comando **`runv-help`**. Quem quiser um README na home pode criar manualmente ou o admin pode usar **`create_runv_user.py --with-readme`** ao provisionar.
-
-**Observação:** no Bash do Debian, o arquivo **`~/.bashrc`** costuma ter (por padrão) um bloco que carrega **`~/.bash_aliases`** se existir. Se o usuário remover esse trecho do `.bashrc`, os aliases deixam de carregar — isso é comportamento padrão do Debian, não do runv.
-
-## Programas globais (apt)
-
-Pacotes listados em **`manifests/apt_packages.txt`** (incluindo ferramentas de terminal e IRC) ficam **instalados no sistema**. O comando global **`chat`** em `/usr/local/bin` é o único nome que o utilizador precisa para IRC na rede da casa; a config é aplicada pelo admin com **`patches/patch_irc.py`**. O usuário **não** precisa de nada no skel para **executá-los**: após o admin rodar `tools.py`, eles passam a existir no `PATH`. Ou seja:
-
-- **Skel** ≠ instalar programas.
-- **Skel** = arquivos iniciais na home.
-- **apt** = programas para todos.
-
-## Byobu
-
-- Está **disponível** após a instalação dos pacotes.
-- **Não** abre sozinho para todos no login.
-- Quem quiser pode rodar **`byobu-enable`** na própria conta, quando fizer sentido.
-
-## Como isso ajuda iniciantes
-
-- **MOTD** orienta na hora do login (**`runv-help`**).
-- **`runv-help`** e **`runv-links`** explicam o pubnix, permissões e URLs oficiais.
-- Administradores com a conta **`pmurad-admin`** podem usar **`runv-status`** para contexto do servidor (outros utilizadores recebem recusa explícita).
-
-Juntos, reduzem fricção para quem nunca usou pubnix ou SSH no dia a dia.
diff --git a/tools/skel/README.md b/tools/skel/README.md
@@ -1,58 +0,0 @@
-# Bem-vindo(a) ao runv.club
-
-O **runv.club** é um servidor compartilhado (pubnix) pensado para a comunidade brasileira: você acessa por **SSH**, usa a **shell** e pode publicar uma **página web** simples.
-
-## Sua página na internet
-
-- Os arquivos públicos do site ficam em **`~/public_html/`**.
-- A página principal é **`~/public_html/index.html`** (HTML estático).
-- A URL pública será algo como **`https://runv.club/~seu_usuario/`** (o nome após `~` é o seu login).
-
-Edite o HTML com um editor no terminal, por exemplo:
-
-```bash
-nano ~/public_html/index.html
-```
-
-## Permissões (importante)
-
-Para o servidor web enxergar seu site:
-
-| Local | Modo sugerido |
-|-------|----------------|
-| Sua home (`~`) | `755` |
-| `~/public_html` | `755` |
-| Arquivos dentro de `public_html` | `644` |
-
-Exemplo:
-
-```bash
-chmod 755 ~ ~/public_html
-chmod 644 ~/public_html/index.html
-```
-
-## Ajuda rápida no servidor
-
-Digite no terminal:
-
-```bash
-runv-help
-```
-
-Você verá uma lista de **comandos úteis** (navegação no terminal, e-mail, IRC, jogos, etc.) e dicas para quem está começando.
-
-Outros comandos locais:
-
-- **`runv-links`** — links do projeto e do mantenedor.
-
-## Arquivos públicos
-
-Tudo o que você colocar em **`public_html`** pode ser lido pelo mundo via HTTP. **Não coloque** chaves privadas, senhas ou dados sensíveis nessa pasta.
-
-## Aliases
-
-Este diretório pode incluir um arquivo **`.bash_aliases`** (já sugerido no skel) com atalhos como `ll` e `help-runv`. Se o seu shell for Bash, ele costuma carregar aliases desse arquivo se a linha correspondente existir no `~/.bashrc` (no Debian isso costuma vir comentado — você pode descomentar).
-
----
-
-Seja gentil com a máquina e com a comunidade. Bom uso do runv.club.
diff --git a/tools/tools.py b/tools/tools.py
@@ -3,7 +3,7 @@
runv.club — ferramentas globais, MOTD, Jailkit/SSH runv-jailed, comandos em /usr/local/bin e /etc/skel.
Debian 13 · Python 3 stdlib apenas · sem shell=True.
-Execute como root. Ver tools/README.md e tools/docs/INSTALL.md.
+Execute como root. Ver docs/05-tools-and-system-experience.md no repositório.
"""
from __future__ import annotations