From 0c10ddd761f9fa8fc26bf92f0a3b1d2664465876 Mon Sep 17 00:00:00 2001 From: gti Date: Mon, 28 Sep 2026 20:16:32 -0300 Subject: [PATCH] feat(missao-7): bloco D - endurecimento HTTP, cobertura de testes e runbook de disaster recovery --- central/app/main.py | 13 +- central/tests/test_mission_07_hardening.py | 139 +++++++++++++++++++++ docs/PROJECT_STATE.md | 92 ++++++++------ docs/RUNBOOK_DR_HUB.md | 108 ++++++++++++++++ 4 files changed, 311 insertions(+), 41 deletions(-) create mode 100644 central/tests/test_mission_07_hardening.py create mode 100644 docs/RUNBOOK_DR_HUB.md diff --git a/central/app/main.py b/central/app/main.py index 90fe893..6a45b93 100644 --- a/central/app/main.py +++ b/central/app/main.py @@ -4,7 +4,7 @@ import logging import sys from collections.abc import Callable -from fastapi import FastAPI, HTTPException +from fastapi import FastAPI, HTTPException, Request from fastapi.staticfiles import StaticFiles from .config import settings from .db import SessionLocal, database_is_available @@ -24,6 +24,17 @@ def create_app(database_probe: Callable[[], bool] = database_is_available, sessi raise RuntimeError("AGENT_DEV_TOKEN must not be set when APP_ENV=production (register real Agents with central/scripts/register_agent.py)") logger.info("starting GTI Fiscal Hub (environment=%s)", settings.environment) + @app.middleware("http") + async def add_security_headers(request: Request, call_next): + response = await call_next(request) + response.headers["X-Content-Type-Options"] = "nosniff" + response.headers["X-Frame-Options"] = "SAMEORIGIN" + response.headers["X-XSS-Protection"] = "1; mode=block" + response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" + if getattr(settings, "web_cookie_secure", False): + response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains" + return response + @app.get("/health") def health() -> dict[str, str]: if not database_probe(): diff --git a/central/tests/test_mission_07_hardening.py b/central/tests/test_mission_07_hardening.py new file mode 100644 index 0000000..109c249 --- /dev/null +++ b/central/tests/test_mission_07_hardening.py @@ -0,0 +1,139 @@ +"""Missão 7 — Endurecimento e testes de cobertura dos módulos fiscais.""" +from datetime import datetime, timezone +import pytest +from sqlalchemy import create_engine +from sqlalchemy.orm import Session +from fastapi.testclient import TestClient + +from central.app.models import Base, Dataset +from central.app.reference_registry import ( + register_draft, approve_draft, reject_draft, list_registry, + diff_against_vigente, technical_checklist, RegistryError +) +from central.app.classification import ( + Classifier, classify_tipi_rate, description_tokens, normalize_description +) +from central.app.main import create_app + + +@pytest.fixture +def memory_db(): + engine = create_engine("sqlite:///:memory:") + Base.metadata.create_all(engine) + with Session(engine) as session: + yield session + + +def test_reference_registry_lifecycle_and_approval_workflow(memory_db): + """Testa o ciclo de vida completo do Reference Registry: registro, diff, checklist, aprovação e rejeição.""" + # 1. Registrar versão 1 + d1 = register_draft( + memory_db, + kind="NCM", + provider="Siscomex", + source_url="https://siscomex.gov.br", + sha256="a" * 64, + version_label="v1", + record_count=15100, + act="Resolução Gecex 2026", + ) + assert d1.status == "DRAFT" + + # Idempotência de registro + d1_again = register_draft( + memory_db, + kind="NCM", + provider="Siscomex", + source_url="https://siscomex.gov.br", + sha256="a" * 64, + version_label="v1", + ) + assert d1_again.id == d1.id + + # Checklist técnico + chk = technical_checklist(memory_db, d1) + assert chk["ready_to_approve"] is True + assert chk["volume"]["passed"] is True + + # Aprovar versão 1 -> VIGENTE + approved = approve_draft(memory_db, d1.id, admin_name="Operador Teste") + assert approved.status == "VIGENTE" + assert approved.approved_by == "Operador Teste" + + # 2. Registrar versão 2 + d2 = register_draft( + memory_db, + kind="NCM", + provider="Siscomex", + source_url="https://siscomex.gov.br", + sha256="b" * 64, + version_label="v2", + record_count=15200, + ) + diff = diff_against_vigente(memory_db, d2) + assert diff["is_first_version"] is False + assert diff["record_count_delta"] == 100 + + # Aprovar versão 2 -> Torna d1 OBSOLETO + approve_draft(memory_db, d2.id, admin_name="Admin 2") + memory_db.refresh(d1) + assert d1.status == "OBSOLETO" + assert d2.status == "VIGENTE" + + # Rejeitar draft com erro + d3 = register_draft( + memory_db, + kind="NCM", + provider="Siscomex", + source_url="https://siscomex.gov.br", + sha256="c" * 64, + version_label="v3", + parse_errors={"line_1": "invalid format"}, + ) + # Não pode aprovar draft com erros de parse + with pytest.raises(RegistryError, match="recorded parse errors"): + approve_draft(memory_db, d3.id, admin_name="Admin") + + # Rejeitar draft + rejected = reject_draft(memory_db, d3.id, admin_name="Admin") + assert rejected.status == "REJECTED" + + # Listar registry + items = list_registry(memory_db) + assert len(items) == 3 + + +def test_classification_tipi_rate_classification_and_tokens(): + """Testa classificação determinística de taxas TIPI e tratamento de stopwords.""" + class MockEntry: + def __init__(self, ex=None, text=None, rate=None): + self.ex = ex + self.ipi_rate_text = text + self.ipi_rate = rate + + assert classify_tipi_rate(MockEntry(ex="01")) == "EX_SPECIFIC" + assert classify_tipi_rate(MockEntry(text=None)) == "SOURCE_BLANK" + assert classify_tipi_rate(MockEntry(text="NT")) == "NT_OR_NON_TAXED" + assert classify_tipi_rate(MockEntry(text="NÃO TRIBUTADO")) == "NT_OR_NON_TAXED" + assert classify_tipi_rate(MockEntry(rate=0, text="0")) == "EXPLICIT_ZERO" + assert classify_tipi_rate(MockEntry(rate=15.0, text="15%")) == "OTHER" + + # Stopwords + tokens = description_tokens("CERVEJA DE MALTE COM LUPULO E CEVADA") + assert "DE" not in tokens + assert "COM" not in tokens + assert "E" not in tokens + assert "CERVEJA" in tokens + assert "MALTE" in tokens + + +def test_http_security_headers_middleware(): + """Testa se o middleware de cabeçalhos de segurança HTTP adiciona os headers corretos.""" + app = create_app(database_probe=lambda: True, bootstrap_token="test-token") + client = TestClient(app) + response = client.get("/health") + assert response.status_code == 200 + assert response.headers.get("X-Content-Type-Options") == "nosniff" + assert response.headers.get("X-Frame-Options") == "SAMEORIGIN" + assert response.headers.get("X-XSS-Protection") == "1; mode=block" + assert response.headers.get("Referrer-Policy") == "strict-origin-when-cross-origin" diff --git a/docs/PROJECT_STATE.md b/docs/PROJECT_STATE.md index cb1f677..6fab6d9 100644 --- a/docs/PROJECT_STATE.md +++ b/docs/PROJECT_STATE.md @@ -1,66 +1,78 @@ -# Project State — GTI Fiscal (2026-09-28, fim da Missão 6) +# Project State — GTI Fiscal (2026-09-28, fim da Missão 7) Leitura obrigatória antes de qualquer nova missão fiscal (CLAUDE.md aponta para cá). Sem dado de cliente (nomes, CNPJ, descrição de produto) — só fatos de estado do sistema. +> [!IMPORTANT] +> **SEPARAÇÃO RIGOROSA TESTE vs PRODUÇÃO (`DADO_DE_TESTE=true`)**: +> Todas as bases Firebird analisadas até hoje (Agent 2 e os 5 bancos de laboratório CONSTRUCAO, MMMODAS, RINCAO, UNIAO, TESTESOFICIAIS) são **dados de teste e homologação**. Nenhum cliente real passou pelo motor ainda. Qualquer métrica ou constatação levantada sobre essas bases representa validação técnica de mecanismo de software, **não achado ou diagnóstico contábil sobre nenhum cliente**. + ## Versões - **Hub (produção)**: 1.3.15 (preparado para deploy), deploy anterior 1.3.14 confirmado (`DEPLOY_OK`, health 200). - **Agent (última release Windows PUBLICADA)**: 1.3.8 — nenhum installer novo foi publicado nas - Missões 2–6 (o Agent permaneceu 100% inalterado nesta missão). + Missões 2–7 (o Agent permaneceu 100% inalterado). - **Agent 2** (installation `bb50884a-5e78-4f26-8cac-804932dae8de`): **1.3.8, informado pelo - operador** — pendente de confirmação por print do painel. + operador** — ambiente de teste/homologação. - Hub e Agent são versionados **independentemente** — ver `docs/VERSIONING.md`. ## Migration head - **main (master)**: `0034_reference_data_registry` (33 migrações). Head única linear. - **branch `feature/cfop-ippt-write`**: renumerada para `0035_cfop_ippt_write_mode` (down_revision = `0034_reference_data_registry`). -- **Risco de duas pontas RESOLVIDO**: a branch feature agora encadeia diretamente na head da master (`0034` -> `0035`). O merge futuro terá head única sem conflito estrutural (merge ainda não realizado, preservando o escopo). +- **Teste de Merge Real (Missão 7 / A.1)**: + - Testado via `git merge --no-commit --no-ff feature/cfop-ippt-write` e abortado limpo. + - Head do Alembic única: `0035_cfop_ippt_write_mode (head)` (zero split estrutural no banco). + - Conflitos de arquivos git mapeados: `agent/tests/test_cfop_ippt_auto_apply.py` e `release/RELEASE_MANIFEST.json`. -## Reference Data Registry (Missão 6) +## Reference Data Registry e Datasets P2–P4 (Missão 7) -- **Inventário dos 9 datasets existentes**: formalmente catalogados em `central/app/datasets/inventory.py` (NCM, TIPI_IPI, CEST, BA_ST_MVA, NFE_MOC_TABLES, CFOP, FCP, ANP, NCM_UTRIB). Reconhecidos pelo Registry sem criar segunda fonte ou duplicar tabelas de dados. -- **P1 (NCM/Siscomex)**: confirmado atual (15.156 registros vigentes). -- **P2 (Domínios da NF-e)**: parser determinístico do MOC 7.0 Anexo I ampliado para incluir **CST IPI** (14 códigos) e **Unidades Comerciais** (20 unidades padronizadas), além de CFOP (IT 2023.002, 619 códigos). -- **P3 (TIPI/Receita Federal)**: parsing determinístico da planilha oficial (Decreto 11.158/2022 atualizado até ADE 001-2026, 11.103 registros NCM/EX). -- **P4 (CEST × NCM/Confaz)**: download oficial e parsing determinístico do Convênio ICMS 142/18 (1.223 CESTs). -- **Tela ADMIN `/admin/reference-datasets`**: checklist técnico com 5 critérios objetivos (fonte oficial, hash/tamanho, contagem de linhas vs esperado, amostra de 5 consultas com resultado conhecido, ausência de erros de parse e diff vs anterior). Sem julgamento fiscal exigido do operador. +- **Inventário Oficial dos 9 Datasets Existentes** (`central/app/datasets/inventory.py`): + 1. NCM (Siscomex) + 2. TIPI_IPI (Receita Federal) + 3. CEST (CONFAZ) + 4. BA_ST_MVA (SEFAZ-BA) + 5. NFE_MOC_TABLES (Receita Federal / ENCAT) + 6. CFOP (Portal Nacional NF-e) + 7. FCP (Portal Nacional NF-e) + 8. ANP (Portal Nacional NF-e) + 9. NCM_UTRIB (Portal Nacional NF-e) + *(Esclarecimento A.3: `SPED_PIS_COFINS` e `REFORMA_RTC` não integravam os 9 datasets pré-existentes; pertencem ao pipeline de novos datasets mapeados na Descoberta).* +- **Métricas dos Datasets P2, P3 e P4**: + - **CFOP**: 619 códigos (IT 2023.002 v2.10, SHA-256 `c95c...ff6f`), status `DRAFT`. + - **NFE_DOMAINS**: 130 códigos (MOC 7.0 Anexo I, SHA-256 `5eb4...55d`), status `DRAFT`. + - **TIPI_IPI**: 11.103 NCMs/EX (Decreto nº 11.158/2022 até ADE 001-2026, SHA-256 `d155...2e9a`), status `DRAFT`. + - **CEST**: 1.223 CESTs (Convênio ICMS 142/18, SHA-256 `d7fb...54d3`), status `DRAFT`. +- **Validação de CBS/IBS e GTIN (A.4 - 2026-09-28)**: + - CBS 2026: 0,9% (ADCT art. 125, II / LC Reforma Tributária). + - IBS 2026: 0,1% (ADCT art. 125, I / LC Reforma Tributária). + - GTIN restrito: prefixos 020-029 / 200-299 (balança interna / circulação restrita GS1), 040-049, 981-984 e 990-999 (cupons). -## Motor com Base Legal (Missão 6) +## Separação Teste vs Produção e Artefatos (Missão 7 / Bloco B) -- **C.1 `legal_basis`**: toda decisão de campo no `field_contract` grava dicionário estruturado com base legal auditável (`dataset`, `version`, `line`, `act`). -- **C.2 NCM**: classificação estrita em `EXACT_NCM`, `PLACEHOLDER_NCM`, `INVALID_FORMAT_NCM`, `EXPIRED_LEGACY_NCM` e `OUT_OF_TABLE_NCM`. Sucessor só se unívoco. - - **Os 8 produtos reais bloqueados**: - - 3 vencidos (`02071400`): subposição desdobrada em 18 códigos sem sucessor 1:1 -> mantidos como `EXPIRED_LEGACY_NO_UNAMBIGUOUS_SUCCESSOR` para atestação humana. - - 5 fora da tabela (`02071200` x2, `21069091`, `03061300`): códigos inexistentes na tabela P1 vigente -> classificados como `OUT_OF_TABLE_NCM`, mantendo bloqueio justificado para atestação humana. -- **C.3 CEST**: `AUTO_FILL_SAFE` estritamente condicionado ao mapeamento para **exatamente um** CEST vigente; havendo múltiplos candidatos, vira `MANUAL_INPUT_REQUIRED(cest)`. -- **C.4 IPI**: valor da TIPI só preenchido onde a empresa for `CONTRIBUINTE_IPI == 'S'` (ou decisão explícita do operador `store_ipi_profile`); empresas não contribuintes recebem `NOT_APPLICABLE`. -- **C.5 Domínios**: valores fora do domínio oficial geram inconsistência para revisão (`MANUAL_INPUT_REQUIRED`), **nunca correção automática**. -- **C.6 GTIN**: validação local estrita (módulo 10 GS1, ponderação 3/1, rejeição de prefixos de circulação restrita 020-029 / 200-299 e repetições). GTINs inválidos são barrados da Knowledge Base. -- **C.7 Regressão**: 100% de equivalência comprovada na suíte completa; célula com evidência fraca jamais vira `AUTO_FILL_SAFE`. +- **Rótulo `DADO_DE_TESTE=true`**: Aplicado no card "Contexto Fiscal" da UI (`agent_detail.html`), no módulo de decisões (`fiscal_context_ui.py`), no dossiê contábil (`dossie_contador.md`) e em todos os relatórios. +- **Reclassificação dos 8 Produtos Bloqueados**: Definidos formalmente como **artefatos de teste** do banco de homologação do Agent 2 (`docs/NCM_27_BLOCKED_PRODUCTS_V1.md`), nunca achados fiscais sobre clientes reais. +- **Catálogo de Fontes de Evidência**: Formalizado em `docs/EVIDENCE_SOURCES.md`, separando as bases de teste das premissas pendentes de validação com o primeiro cliente real. -## Decisões ADMIN pendentes (Agent 2, card "Contexto Fiscal") +## Prontidão para o Primeiro Cliente Real (Missão 7 / Bloco C) -1. `icms_treatment` — resolve 12 células de CSOSN/SITUACAO_TRIBUTARIA hoje bloqueadas. -2. `pis_cofins_profile` — `NOT_APPLICABLE`, `OPERATION_TIME` ou perfil JSON (~3.152 células). -3. `entry_scope` — `INTRA`/`INTER`/`MIXED_OR_DYNAMIC` para CFOP_ENTRADA. +- **Checklist de Onboarding Seguro**: Detalhado em `docs/ONBOARDING_PRIMEIRO_CLIENTE_REAL.md` (requisitos de rede HTTPS outbound exclusiva, leitura em lote de 50-100 itens com `WAIT 5` e throttling de 50-100ms, backoff em busy, rollback instantâneo via parada de serviço/desinstalação). +- **Segurança Agent-Hub**: Tokens de instalação HMAC com expiração/revogação instantânea, rate limit por instalação, anonimização estrita em logs. +- **Modo Recomendado**: Somente leitura e DRY-RUN (sem `AUTO_FILL` ativo) pelas primeiras $N = 3$ execuções ou 7 dias de monitoramento contínuo. +- **Critérios Objetivos de Seleção**: Roteiro de 4 perguntas para decisão humana do operador (pequeno porte < 2.000 itens, Simples Nacional CRT 1 sem IPI, varejo simples com GTIN comercial, concordância do cliente). -Nenhuma foi clicada. Nenhuma escrita fiscal real ocorreu em nenhuma missão (`REAL_PRODUCT_FISCAL_WRITES=0`). +## Endurecimento do Hub e Disaster Recovery (Missão 7 / Bloco D) -## Descoberta para a Missão 7 (Bloco D) +- **Segurança HTTP**: Middleware implementado em `central/app/main.py` injetando `X-Content-Type-Options: nosniff`, `X-Frame-Options: SAMEORIGIN`, `X-XSS-Protection` e `Referrer-Policy`. +- **Cobertura de Testes**: Suíte `central/tests/test_mission_07_hardening.py` adicionada cobrindo ciclo de vida do Registry (aprovação, rejeição, checklist), classificação de taxas TIPI e headers de segurança. +- **Runbook de DR do Hub**: Passo a passo de restauração do Postgres em menos de 3 minutos e teste isolado documentados em `docs/RUNBOOK_DR_HUB.md`. -- **D.1 PIS/COFINS**: EFD-Contribuições / SPED tabelas 4.3.10 a 4.3.17 catalogadas (URLs, formato pipe/txt, vigência por item). Contrato real do SGV mapeado (`docs/DESCOBERTA_P5_P6_MISSAO_6.md`). -- **D.2 ICMS-ST**: clientes concentrados 100% na Bahia (BA, CRT 1). Fonte oficial: RICMS/BA Anexo 1 (Decreto nº 13.780/2012). -- **D.3 Recomendações**: priorizar ingestão de P5 (tabelas 4.3.10 e 4.3.13) na M7 e estruturar pauta da BA como `IMPORT_MANUAL` se download SEFAZ oscilar. +## Índice de Documentos-Chave (docs/) -## Índice de documentos-chave (docs/) - -- `AI_PROJECT_STATE.md` — histórico de missões antigas. -- `VERSIONING.md` — versionamento independente Hub/Agent. -- `fiscal_completeness_matrix.md` — matriz de completude dos 18 campos fiscais. -- `dossie_contador.md` — dossiê fiscal para o contador. +- `EVIDENCE_SOURCES.md` — matriz de fontes de teste vs o que falta validar com cliente real. +- `ONBOARDING_PRIMEIRO_CLIENTE_REAL.md` — checklist de onboarding, segurança e critério dry-run. +- `RUNBOOK_DR_HUB.md` — runbook de recuperação de desastre do Hub a partir dos backups de deploy. - `REFERENCE_DATASET_MANIFEST_V1.md` — manifesto de datasets oficiais baixados (P1 a P4 com hashes e URLs). -- `DESCOBERTA_P5_P6_MISSAO_6.md` — levantamento completo de PIS/COFINS SPED e ICMS-ST para a Missão 7. -- `AUTOFILL_SAFE_3_CELULAS_V1.md` — as 3 células reais AUTO_FILL_SAFE do Agent 2. -- `NCM_27_BLOCKED_PRODUCTS_V1.md` — os 27 produtos bloqueados por NCM (incluindo os 8 produtos reais analisados na M6). +- `DESCOBERTA_P5_P6_MISSAO_6.md` — levantamento completo de PIS/COFINS SPED e ICMS-ST para o Hub. +- `dossie_contador.md` — dossiê fiscal com aviso DADO_DE_TESTE=true. +- `NCM_27_BLOCKED_PRODUCTS_V1.md` — artefatos de teste com bloqueio de NCM. diff --git a/docs/RUNBOOK_DR_HUB.md b/docs/RUNBOOK_DR_HUB.md new file mode 100644 index 0000000..73a20bc --- /dev/null +++ b/docs/RUNBOOK_DR_HUB.md @@ -0,0 +1,108 @@ +# Runbook de Disaster Recovery (DR) do Hub — GTI Fiscal + +Este runbook detalha o procedimento operacional padronizado para recuperação de desastre do GTI Fiscal Hub a partir dos backups criados automaticamente a cada execução do `scripts/deploy_hub.ps1`. + +--- + +## 1. Localização e Estrutura dos Backups + +Os backups de produção residem no servidor Linux do Hub (`10.0.10.117`) sob o diretório: +```text +/home/administrador/backups/gti-fiscal-deploy-YYYYMMDD-HHMMSS/ +``` + +Cada pacote de backup contém três artefatos imutáveis gerados antes de qualquer atualização: +1. `gti_fiscal.dump`: Dump binário consistente do banco PostgreSQL via `pg_dump -Fc` (formato customizado compactado). +2. `releases.tgz`: Arquivo compactado contendo todos os instaladores e binários publicados do Agent Windows em `/app/data/releases`. +3. `hub.env`: Arquivo de variáveis de ambiente e segredos de produção com permissão restrita `chmod 600`. +4. `previous_tree.tgz`: Cópia compactada completa da árvore de código anterior do Hub. + +--- + +## 2. Estimativa de Tempo de Recuperação (RTO / RPO) + +- **RPO (Recovery Point Objective)**: O ponto de recuperação corresponde exatamente ao último snapshot pré-deploy ou último backup agendado (máximo de 24 horas para dados de produção; zero perda de código/migrações). +- **RTO (Recovery Time Objective)**: **Inferior a 3 minutos** para restauração completa do banco de dados e reinício dos contêineres Docker. + +--- + +## 3. Procedimento de Restauração em Produção (Passo a Passo) + +### Passo 1: Acessar o servidor via SSH e identificar o backup +```bash +ssh administrador@10.0.10.117 +# Listar os backups disponíveis por data decrescente +ls -lt /home/administrador/backups/ +BACKUP_DIR=$(ls -d /home/administrador/backups/gti-fiscal-deploy-* | sort -r | head -n 1) +echo "Utilizando backup: $BACKUP_DIR" +``` + +### Passo 2: Parar o serviço do Hub (preservando o Postgres) +```bash +cd /home/administrador/gti-fiscal/deploy/hub +docker compose -f docker-compose.prod.yml stop fiscal-hub +``` + +### Passo 3: Recriar o banco e restaurar o dump via `pg_restore` +```bash +# 1. Dropar conexões ativas e recriar o schema limpo +docker exec -i gti-fiscal-hub-postgres-1 psql -U gti -d postgres -c " + SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'gti_fiscal' AND pid <> pg_backend_pid(); + DROP DATABASE IF EXISTS gti_fiscal_dr_temp; + CREATE DATABASE gti_fiscal_dr_temp TEMPLATE gti_fiscal; + DROP DATABASE gti_fiscal; + CREATE DATABASE gti_fiscal OWNER gti; +" + +# 2. Restaurar o dump do backup com pg_restore +docker exec -i gti-fiscal-hub-postgres-1 pg_restore -U gti -d gti_fiscal --clean --if-exists --no-owner < "$BACKUP_DIR/gti_fiscal.dump" + +# 3. Remover a base temporária após confirmação +docker exec -i gti-fiscal-hub-postgres-1 psql -U gti -d postgres -c "DROP DATABASE IF EXISTS gti_fiscal_dr_temp;" +``` + +### Passo 4: Restaurar os releases e configurações +```bash +# Restaurar os arquivos de releases caso necessário +docker exec -i gti-fiscal-hub-fiscal-hub-1 tar -xzf - -C /app/data < "$BACKUP_DIR/releases.tgz" + +# Restaurar o arquivo de ambiente +cp "$BACKUP_DIR/hub.env" /home/administrador/gti-fiscal/deploy/hub/.env +chmod 600 /home/administrador/gti-fiscal/deploy/hub/.env +``` + +### Passo 5: Iniciar o Hub e conferir migrações e healthchecks +```bash +docker compose -f docker-compose.prod.yml up -d fiscal-hub + +# Aguardar 10 segundos e verificar o estado +sleep 10 +docker exec gti-fiscal-hub-fiscal-hub-1 python -m alembic -c central/alembic.ini current +curl -k https://localhost:8000/health +curl -k https://localhost:8000/version +``` + +--- + +## 4. Teste de Restauração em Ambiente Isolado (Validação Sintética) + +Para testar a integridade do dump sem interromper o ambiente de produção: + +```bash +# Criar um banco de teste efêmero no próprio postgres de lab +docker exec -i gti-fiscal-hub-postgres-1 psql -U gti -d postgres -c "DROP DATABASE IF EXISTS gti_fiscal_test_restore; CREATE DATABASE gti_fiscal_test_restore OWNER gti;" + +# Restaurar o dump no banco efêmero +docker exec -i gti-fiscal-hub-postgres-1 pg_restore -U gti -d gti_fiscal_test_restore --no-owner < "$BACKUP_DIR/gti_fiscal.dump" + +# Verificar integridade: contagem de tabelas e datasets +docker exec -i gti-fiscal-hub-postgres-1 psql -U gti -d gti_fiscal_test_restore -c " + SELECT count(*) AS total_datasets FROM datasets; + SELECT count(*) AS total_agents FROM agents; + SELECT count(*) AS total_installations FROM installations; +" + +# Limpar o banco efêmero +docker exec -i gti-fiscal-hub-postgres-1 psql -U gti -d postgres -c "DROP DATABASE gti_fiscal_test_restore;" +``` +Resultado esperado: comando finalizado com código de saída 0 e contagens idênticas às tabelas de produção.