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:
- 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).
- Credit Payout — residente levanta crédito do till; aparece no EOD como cash-out.
channel=credit_payout, txn CP-AAAAMMDD-….
- 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
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
- 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.
- 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.
- 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.
- Unit file do frontend TEST foi alterado (wrapper
start.sh). Rever se deve passar para o formato padrão do projecto.
- 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
- Antes de destruir um ambiente: backup completo de
config/, ficheiros .env*, unit files systemd e DB (tar da pasta inteira).
- 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.
- Passwords: usar sempre
app.auth.hash_password() (bcrypt-12). Nunca outro formato.
- Rate limit de login: está em
auth_events, por IP. Restart não limpa.
- 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.
- 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
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)
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