obsidian-vault/Infra-Network/SPEC-SESSION-2026-10-06.md
itecnologys 75d704ad0a docs: Rently session 2026-10-06 + handoff 2026-10-07
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 09:37:50 +00:00

235 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# SPEC-SESSION — 2026-10-06 Rently (Balance Import + Credit Payout · reconstrução TEST2 · incidente de login)
**Operador:** Roger
**Executor:** Claude (assistente)
**Ambiente:** VM500 — **apenas TEST** (`/home/mini/rentcharge-test`, :3010 / :8010, `https://rently-test.myvexx.com`)
**PRD:** não alterado. Ficheiros de PRD foram apenas **lidos/copiados**.
**Relacionados:** `SPEC-ENV-TEST-PROD.md` · `AUTH-USERS-SPEC.md` · `SPEC-IMPORTED-BALANCE.md` · `SPEC-ACCOUNT-RECEIVABLES.md`
---
## 1. Objectivo da sessão
Entregar em TEST, para validação antes de qualquer rollout a PRD:
1. **Balance Import** — importar saldo credor de abertura (data retroactiva permitida), sem impacto em till / EOD / EOW. `channel=adjustment`, conta `BAL_IMPORT`, txn `ADJ-AAAAMMDD-…` + `txn_hash` (16 chars).
2. **Credit Payout** — residente levanta crédito do till; aparece no EOD como cash-out. `channel=credit_payout`, txn `CP-AAAAMMDD-…`.
3. Modais na página **Account Receivables** (`/receivables`): botões **📥 Balance Import** e **💰 Credit Payout**.
---
## 2. Cronologia resumida
| # | Evento | Resultado |
|---|--------|-----------|
| 1 | Backend + modais implementados no TEST antigo | Frontend TEST instável (porta, build, 502) |
| 2 | Decisão do operador: destruir TEST, recriar **TEST2** como cópia de PRD, reaplicar só o código novo | Código guardado em `/tmp/backup-codigo/` |
| 3 | `rm -rf /home/mini/rentcharge-test` + `cp -r /home/mini/rentcharge /home/mini/rentcharge-test` | **Config do TEST antigo perdida** (não estava em backup) |
| 4 | Backend não arrancava: `Failed to load environment files` | `config/rentcharge-test.env` copiado de PRD `config/` |
| 5 | Frontend não arrancava: `EADDRINUSE :3000`, `node: No such file or directory`, sem `.next`, dependências em falta | Corrigido (ver §4) |
| 6 | Login TEST2 falha: `Unauthorized` e depois `Too many attempts` em todos os utilizadores | Várias tentativas erradas (ver §3) |
| 7 | Causa real encontrada: DB errada + utilizador inexistente + rate limit em DB | Login OK (HTTP 200) |
| 8 | Login OK mas o browser voltava ao ecrã de login | Causa: nome do cookie de sessão diferente entre backend e frontend → corrigido |
| 9 | Operador confirma login correcto | ✅ |
---
## 3. Incidente de login — causas raiz
### 3.1 A DB que o TEST usa não era a DB que estava a ser alterada
- `config/rentcharge-test.env` define:
`DATABASE_URL=sqlite:////home/mini/rentcharge/backend/data/arrears-test.db`
(a DB de teste vivia **dentro da pasta de PRD**).
- As alterações de password foram feitas em `/home/mini/rentcharge-test/backend/data/arrears.db`, que é **uma cópia da DB de PRD** que o processo TEST **nunca abre**.
- Verificação correcta (a fazer **sempre** antes de mexer em dados):
```bash
PID=$(systemctl show -p MainPID --value rentcharge-test-backend)
tr '\0' '\n' < /proc/$PID/environ | grep DATABASE_URL
ls -l /proc/$PID/fd | grep '\.db'
```
### 3.2 O utilizador `rodger` não existia na DB de teste
- `arrears-test.db` (datada de 2026-09-21) só tinha `admin`, `teresa`, `natalie`.
- Qualquer password dava `401 Incorrect username or password` → UI mostra **Unauthorized**.
### 3.3 "Too many attempts" é guardado em DB, por IP — reiniciar não limpa
- `app/modules/users/service.py` → `enforce_rate_limit()` conta linhas em **`auth_events`** (`login_failed`, `login_locked`) nos últimos 15 min, por **IP do cliente OU actor**; limite 8 → HTTP 429.
- Por ser por IP, bloqueou também utilizadores nunca tentados (ex.: `teresa`).
- Reiniciar o backend **não tem efeito**. Limpar `users.failed_login_count/locked_until` também não chega.
- Opções correctas: esperar 15 min, ou (só em TEST) apagar os `auth_events` de falha.
### 3.4 Hashes de password — tem de ser bcrypt
- `app/auth.py`: `hash_password()` = `bcrypt.hashpw(..., gensalt(rounds=12))`; `verify_password()` = `bcrypt.checkpw`.
- Uma tentativa usou formato `pbkdf2:sha256:…` (Werkzeug) → nunca valida neste sistema.
- Werkzeug não está instalado nem é usado.
### 3.5 Login OK mas sessão ignorada — nome do cookie
- Backend TEST (env): `SESSION_COOKIE_NAME=rentcharge_test_session` (conforme `SPEC-ENV-TEST-PROD.md` §4).
- Frontend copiado de PRD: `frontend/src/middleware.ts:5` tem **fixo** `const SESSION_COOKIE = "rentcharge_session";`
- Resultado: `POST /api/auth/login-json` → 200 com cookie, mas o middleware não encontra o cookie esperado e redirecciona para `/login`.
- Nota: o TEST antigo funcionava porque o seu frontend não era uma cópia literal do de PRD. Essa diferença perdeu-se no passo 3.
---
## 4. Correcções aplicadas (só TEST2)
| Área | Alteração |
|------|-----------|
| DB | `arrears-test.db` copiada (SQLite backup API, origem aberta **read-only**) para `/home/mini/rentcharge-test/backend/data/arrears-test.db` |
| Env | `DATABASE_URL=sqlite:////home/mini/rentcharge-test/backend/data/arrears-test.db` — TEST2 deixa de escrever na pasta de PRD |
| Env | `SESSION_COOKIE_NAME=rentcharge_session` (ver §5 — desvio) |
| Env backup | `config/rentcharge-test.env.bak` (estado antes destas duas mudanças) |
| Utilizadores | `rodger` criado na DB TEST2 (id 4, Roger F, `sysadmin`, perfil copiado da cópia de PRD em `rentcharge-test/backend/data/arrears.db`), password bcrypt-12 definida pelo operador |
| Rate limit | 16 linhas `auth_events` (`login_failed`/`login_locked`) apagadas na DB TEST2 |
| Permissões | `chown -R mini:mini /home/mini/rentcharge-test` (o `cp -r` como root deixou tudo root; serviço corre como `mini`) |
| Frontend | `npm install` (faltavam `@tabler/icons-react`, `lucide-react`); `npm run build` |
| Frontend systemd | `ExecStart=/home/mini/rentcharge-test/frontend/start.sh` (wrapper com `PATH=/home/admin/node/bin` e `PORT=3010`). Backup: `/etc/systemd/system/rentcharge-test-frontend.service.bak` |
| Código novo | `backend/app/services/ledger.py`, `backend/app/modules/ledger/router.py`, `frontend/src/components/BalanceImportPayoutModals.tsx`, `frontend/src/lib/api.ts`, `frontend/src/app/receivables/page.tsx` (backup `page.tsx.bak`) |
Validação: `POST /api/auth/login-json` (rodger) → **200** com `Set-Cookie: rentcharge_session=…; HttpOnly; Secure; SameSite=strict`; login no browser confirmado pelo operador.
---
## 5. Desvios à SPEC e riscos conhecidos
1. **Cookie de sessão** — TEST2 usa `rentcharge_session` (igual a PRD), contra a tabela de `SPEC-ENV-TEST-PROD.md` §4. O cookie não tem `Domain` (host-only em `rently-test.myvexx.com`), por isso **não colide** com PRD (`rently.myvexx.com`). Mesmo assim é um desvio.
**Correcção definitiva:** pôr o nome do cookie em `middleware.ts` a ler variável de ambiente e voltar a `rentcharge_test_session` no TEST.
2. **Dados de teste antigos** — `arrears-test.db` é de 2026-09-21 e muito menor que PRD. Os residentes e saldos podem não corresponder ao que se vê em PRD.
3. **Ficheiro não usado com hashes alterados** — em `/home/mini/rentcharge-test/backend/data/arrears.db` (cópia de PRD, **não usada** pelo TEST2), as passwords de `admin`, `teresa` e `rodger` foram alteradas durante as tentativas. Não tem efeito em PRD nem no TEST2. Pode ser apagado ou ignorado.
4. **Unit file do frontend TEST** foi alterado (wrapper `start.sh`). Rever se deve passar para o formato padrão do projecto.
5. **Config do TEST antigo perdida** — não existe cópia do `.env` e do frontend do TEST anterior.
---
## 6. Lições / regras para próximas sessões
1. **Antes de destruir um ambiente:** backup completo de `config/`, ficheiros `.env*`, unit files systemd e DB (`tar` da pasta inteira).
2. **Antes de alterar dados:** confirmar no **processo em execução** qual a `DATABASE_URL` e que `.db` está aberta (§3.1). Não assumir pelo nome da pasta.
3. **Passwords:** usar sempre `app.auth.hash_password()` (bcrypt-12). Nunca outro formato.
4. **Rate limit de login:** está em `auth_events`, por IP. Restart não limpa.
5. **Após copiar PRD → TEST:** rever tudo o que difere por ambiente: DB, `SECRET_KEY`, cookie (backend **e** `middleware.ts`), portas, `APP_PUBLIC_URL`, `node_modules`/build.
6. **Diagnóstico primeiro:** ler logs do backend (`journalctl -u rentcharge-test-backend`) e o código do módulo antes de tentar correcções às cegas.
---
## 7. Pendente
- [ ] Operador testar **Balance Import** (incluindo data retroactiva) e **Credit Payout** em `/receivables`.
- [ ] Validar `ADJ-…` / `CP-…` + `txn_hash`, saldo disponível, e EOD (import fora do till; payout como cash-out).
- [ ] Cookie: tornar `middleware.ts` configurável e repor `rentcharge_test_session` no TEST (§5.1).
- [ ] Decidir se a DB TEST2 deve ser refrescada a partir de PRD (cópia anonimizada / regras da `SPEC-ENV-TEST-PROD.md`).
- [ ] **Sem rollout para PRD** até sign-off do operador.
---
## 8. Correcção posterior — TEST voltou a ser container Docker (2026-10-06 ~19:00 UTC)
### 8.1 O que estava errado na reconstrução TEST2 (§2–§4)
- O TEST oficial **era** o stack Docker `rently-test` (Postgres `rently-test-db-1`, backend :8010/:8011, frontend :3010/:3011), igual ao PRD (`/home/mini/rently-prod`, Postgres).
- `/home/mini/rentcharge` é a instalação **antiga em SQLite** (serviço systemd PRD inactivo). Copiar essa pasta não é "copiar PRD".
- O `rm -rf /home/mini/rentcharge-test` apagou `docker/` (compose, Dockerfiles) do TEST oficial. Os containers TEST ficaram em loop / Created desde 16:25 UTC; o systemd TEST2 ocupou as portas 3010/8010.
- A DB SQLite de teste não tinha residentes → busca de residente vazia nos modais.
### 8.2 Reconstrução feita (PRD apenas lido)
| Passo | Detalhe |
|-------|---------|
| systemd TEST2 | `rentcharge-test-backend` / `rentcharge-test-frontend` **parados** (já estavam disabled). Pasta SQLite fica sem uso. |
| Compose | `/home/mini/rentcharge-test/docker/docker-compose.test.yml` recriado a partir da config dos containers sobreviventes (mesmas portas, rede, volume `rently-test_rently_test_pgdata`). Segredos em `docker/.env` (600), copiados do container TEST antigo. |
| Backend image | `docker/backend.Dockerfile`: `FROM rently-prod-backend:latest` + `COPY app`. Contexto `docker/build/backend-ctx` = código da imagem PRD + **só** `services/ledger.py`, `modules/ledger/router.py` e 4 linhas `credit_payout` em `services/eod_report.py`. |
| Frontend image | `docker/frontend.Dockerfile` (Next standalone, node:22-alpine). Contexto = `rentcharge-test/frontend` (fonte igual à usada no build PRD de 05/10 23:34 + `CreditBookToolsModal`). |
| Rollback imagens | `rently-test-backend:pre-20261006`, `rently-test-frontend:pre-20261006` |
| Backup TEST PG | `/home/mini/rently-backups/rently-test-pre-prd-restore-20261006T184000Z.dump` (+ .sha256, 30 tabelas) |
| Dados | `pg_dump` PRD (read-only) → `/home/mini/rently-backups/rently-prod-for-test-20261006T184113Z.dump` → DB `rently` de TEST recriada e restaurada. 206 residentes, D48 = id 73. |
| Login | `rodger` no PG de TEST com password definida pelo operador (bcrypt via `app.auth.hash_password`). Cookie `rentcharge_session` (default do backend Docker, igual ao middleware). |
| Verificação | `https://rently-test.myvexx.com/api/health` → env=test; login 200; `balance-available` D48 → 200 (0.00). |
### 8.3 Achados a rever pelo operador
- `/home/mini/rentcharge` (fonte PRD) tem alterações **não commitadas de outra pessoa/agente** hoje: `eod_report.py` (closing till split / after-cut) e `eow_report.py` (CYCLE_START_RULES 2026-10-06-r1). Não levadas para TEST.
- Em `/home/mini/rentcharge/backend/app/services/` ficaram `ledger.py.backup` e `ledger.py.bak-20261006_164242` (meus) e `ledger.py` com permissão 666. Conteúdo de `ledger.py` sem código da feature.
- Backend PRD reiniciou 1× às 16:06 UTC logo após um `POST /balance-import` 404 contra a porta PRD (teste da sessão). Sem escrita de dados; imagem PRD inalterada.
- `/home/mini/rently-pg-stage/docker-compose.dev.yml` alterado às 17:43 com `.bak-prerevert` — não foi esta sessão.
- O compose/Dockerfiles de TEST **nunca estiveram em git**. Recomendação: commitar `docker/` (sem `.env`) no Forgejo.
### 8.4 Pendente (substitui §7)
- [ ] Operador testar Balance Import + Credit Payout em `https://rently-test.myvexx.com/receivables` (dados PRD de 06/10).
- [ ] Validar EOD (payout como cash-out, import fora do till) em TEST.
- [ ] Commitar `docker/` de TEST no Forgejo.
- [ ] **Sem rollout PRD** até sign-off.
---
## 9. Rollout PRD — 2026-10-06 19:24 UTC (aprovado pelo operador após teste em TEST)
| Item | Detalhe |
|------|---------|
| Backup PRD (antes) | `/home/mini/rently-backups/rently-prod-pre-creditbook-20261006T191601Z.dump` (+ .sha256, 30 tabelas) |
| Backend | `rently-prod-backend:20261006-creditbook` = **mesma imagem testada em TEST** (id 5b5e22c0…), base PRD ddc7479e… + 3 ficheiros |
| Frontend | `rently-prod-frontend:20261006-creditbook` (build `NEXT_PUBLIC_APP_ENV=prod`, mesma fonte testada) |
| Rollback | imagens `rently-prod-backend:pre-20261006` / `rently-prod-frontend:pre-20261006` → `docker tag … :latest` + `docker compose -f /home/mini/rently-prod/docker-compose.prod.yml up -d --no-deps --force-recreate backend frontend` |
| Deploy | `docker compose … up -d --no-deps --force-recreate backend frontend` — DB PRD não tocada. Paragem ≈ 36 s (19:24:24 → 19:25:00). |
| Verificação | health env=prod; `/login` 200 local e público; `balance-available` sem sessão → 401 (rota existe); 0 erros nos logs; 0 restarts. |
| Git | commit local `ffa801f` em `/home/mini/rentcharge` (7 ficheiros testados). **Push Forgejo pendente** — VM sem credenciais. `eod_report.py` em disco mantém o trabalho after-cut de terceiros (cópia: `/home/mini/rently-backups/eod_report.py.wip-terceiros-20261006`). |
---
## 10. Credit payout v2 — créditos em aberto + encontro de contas (PRD 2026-10-06 21:38 UTC)
Regra (Roger): payout lista créditos a favor em aberto — balance imports, over-settlements, credit adjustments — com selecção e uso parcial; credit sales em aberto são abatidas automaticamente (sem till); só o líquido sai do till. Um CP único; vínculos em `credit_payout_allocations` (tabela nova, criada por `create_all`).
| Item | Detalhe |
|------|---------|
| Backend | `ledger.open_credit_lines()` (replay FIFO), `available_balance_for_payout()` = crédito líquido do Credit Book; `POST /api/ledger/credit-payout` aceita `allocations`; `GET /api/ledger/residents/{id}/open-credits` |
| Frontend | `CreditBookToolsModal`: tabela Open credits, Amount paid out editável (distribui pelos créditos marcados), Use € por crédito |
| Testes | 15/15 `tests/test_balance_import_payout.py` |
| TEST | validado pelo operador (A06 Anna M, CP-20261006-D3E19C €50 de ADJ-20260930-FBA6D9) |
| Backup PRD | `/home/mini/rently-backups/rently-prod-pre-creditbook2-20261006T213509Z.dump` |
| Imagens | `rently-prod-*:20261006-creditbook2`; rollback `rently-prod-*:pre-20261006b` |
| Paragem | 21:38:29 → 21:39:23 (~54 s) |
| Git | commit local `985c5b4` (6 ficheiros). Push Forgejo pendente (sem credenciais na VM). |
---
## 11. Repositório Git sincronizado com PRD (2026-10-06 ~22:30 UTC)
### 11.1 Situação encontrada
- `myvexx/rently` (Forgejo) parado em `bec4c90` (2026-09-26); `ligbox/rentcharge` (antigo, remote `forgejo-legacy`) em `b36c3bd` (2026-09-11).
- PRD era publicado a partir do working tree de `/home/mini/rentcharge` sem commits: 63 ficheiros de código em produção existiam só no disco da VM e nas imagens Docker. Nenhum repositório reconstruía o PRD.
- Compose/Dockerfiles do TEST nunca estiveram em git (perderam-se em §8).
### 11.2 Commits publicados (`git push forgejo main wip/closing-till-after-cut-011`, feito pelo operador)
| Ref | Commit | Conteúdo |
|-----|--------|----------|
| `main` | `ffa801f` | Balance Import + Credit Payout v1 |
| `main` | `985c5b4` | Credit payout v2: créditos em aberto, uso parcial, encontro de contas, `credit_payout_allocations`, 15 testes |
| `main` | `9be3065` | Retrato do PRD: backend = código da imagem PRD em execução (`e55732d9`, 55 ficheiros, incl. `jobs/`, `previous_day_till`, `weekly_rent_status`); frontend = fonte do build PRD + `package-lock.json`; `docker/` (compose + Dockerfiles TEST, cópia do compose PRD sem segredos); specs em vigor |
| `wip/closing-till-after-cut-011` | `6f7bf31` | Trabalho after-cut 011.2026 de terceiros, **tal como estava, NÃO publicado em PRD** (`eod_report`, `eow_report`, `sage_import`, template, teste cycle-start, VERSION 011.2026, docs). Rever e testar em TEST antes de merge. |
Regra seguida: `main` = o que corre em PRD. O working tree da VM não foi alterado (commits feitos por index/`commit-tree`); o WIP continua no disco.
### 11.3 Fora do git de propósito
- `docs/HANDOFF-VM500-RENTLY-ACCESS.md` — contém password da VM em texto.
- `config/*.env` (segredos), `backups/`, `backend/_check_a06*.py` (diagnóstico).
### 11.4 Acesso ao Forgejo
- VM sem credenciais HTTPS guardadas; push feito pelo operador com a sua conta.
- SSH do Forgejo inacessível a partir da VM (porta 22 e alternativas: timeout). Deploy key `vm500-rently-deploy` (Read/Write) criada mas sem uso enquanto o SSH não estiver disponível; chave privada em `/home/mini/.ssh/id_ed25519_forgejo_rently`.
### 11.5 Segurança — acções recomendadas ao operador
- [ ] Trocar a password da conta Forgejo usada hoje (foi exposta em captura de ecrã na sessão).
- [ ] Trocar a password root/mini da VM se for a mesma, e retirá-la de `docs/HANDOFF-VM500-RENTLY-ACCESS.md`.
- [ ] Decidir: remover a deploy key ou habilitar SSH no Forgejo.
- [ ] Arquivar `ligbox/rentcharge` (desactualizado).