# Segunor Intel Grid — Project Status

_Última atualização: 2026-07-26 Europe/Lisbon (Fase 9: prazos só de clientes, clientes visíveis)_

## Visão geral

Plataforma interna da Segunor para monitorizar concorrência e clientes: processos
judiciais, anúncios de insolvência com prazos de reclamação, publicações do
registo comercial e contratos públicos. LAN-only em http://10.0.30.108.

- **Fase 1**: scrapers CITIUS (Distribuição + CIRE), dashboard, RBAC, ptdata auto-fill. ✅
- **Fase 2**: alertas (email + webhook), DRE scaffold, backup + cron. ✅ (DRE source pendente)
- **Fase 3**: intelligence layer — segmentação, coverage, competitor ranking, global search, on-demand analysis, risk score. ✅
- **Fase 3.1**: separação UI completa entre `internal`, `competitor` e `analysis`. ✅
- **Fase 3.2**: NIF lookup acelerado de 2-4s → 300-500ms. VIES UE como source primária, ptdata movido para background enrichment, cache LRU em-memória, skip-on-create quando o cliente já enviou o nome. ✅
- **Fase 3.3**: **DRE integration funcional** via RSS oficiais (Série I + II). Sem Playwright, sem reverse-engineering. Match local por nome de empresa contra ~150-370 publicações/dia. ✅
- **Fase 3.4**: scheduling split (internal diário, competitor semanal), DRE movido para 07:00, UI exibe próximas execuções, durações em minutos/horas, orphan-row reaper na startup. ✅

## Sidebar atual

```
Dashboard                                (só dados internal)
─── As minhas empresas ───
  Empresas        /companies              (internal)
  Processos       /processes              (internal)
─── Concorrentes ───
  Empresas        /competitors            (competitor)
  Processos       /competitors/processes  (competitor)
  Ranking         /competitors/ranking    (competitor)
─── Análise ───
  Nova análise    /analysis               (formulário)
  Empresas        /analysis/companies     (analysis)
  Processos       /analysis/processes     (analysis)
─── Geral ───
  Pesquisa        /search                 (cross-cutting: cobre todos os tipos)
─── Admin ───
  Utilizadores    /admin/users
  Scrapes         /admin/scraping
```

## Stack em produção

- Debian 12 LXC, 2GB RAM / 2 vCPU / 63GB
- Docker 29.4 + Compose v5.1
- 3 containers: `postgres:16-alpine`, `backend` (FastAPI+APScheduler), `frontend` (nginx+SPA em :80)
- Cron host: `/etc/cron.d/judicial-backup` corre `backup.sh` diariamente 02:00 UTC

## Estado por componente

| Componente | Estado | Notas |
|---|---|---|
| Infra + DB + auth | OK | |
| Scrapers CIRE + Distribuição | OK | Tribunal-sweep, filtro server-side |
| **Anúncios de insolvência (sweep nacional)** | OK | Fase 7. Diário 01:30, ~110/dia, prazo extraído do PDF |
| **Watchlist + prazos de reclamação** | OK | Fase 7. Alimentada pelo Sabichão ao criar cliente |
| **Publicações societárias (portal IRN)** | OK | Fase 7. Substitui o MJ dormente e a extensão Chrome |
| **Lookup de NIF (VIES + SICAE)** | OK | Fase 7. Substitui o ptdata, que devolve 503 |
| **Grafo de relações societárias** | OK | Fase 8. Nós e arestas dirigidas do registo comercial, com quota numérica e acto de origem |
| **Varrimento nacional do registo** | Em curso | Fase 8. Backfill de 5 anos (1818 dias); a listagem é barata, o conteúdo enche em contínuo |
| **Painel de notificações** | OK | Fase 8. Interruptor geral, matriz por tipo, excepção por empresa, teste e histórico |
| **Clientes no portal** | OK | Fase 9. Página própria, pesquisa por nome/NIF, e nos scrapers judiciais |
| **Prazos de reclamação** | OK | Fase 9. Só clientes insolventes — é o único caso com créditos a reclamar |
| Alertas email + webhook | OK | SSL implícito na 465; cadeia interruptor geral → empresa → tipo → destinatários |
| DRE integration | OK | RSS oficiais Série I + II |
| Backup + cron | OK | |
| Segmentação de empresas | OK | internal / competitor / analysis / related / **client** |
| Coverage tracking | OK | |
| Risk score 0–100 | OK | |
| Global search (trigram) | OK | |
| Competitor ranking | OK | |
| On-demand analysis | OK | |
| Contratos públicos (IMPIC) | OK | |
| **Tab Intel no Sabichão** | OK | Fase 7. `GET /api/v1/dossier?nif=` |
| Extensão Chrome MJ | Obsoleta | Substituída pelo scraper IRN; mantida como fallback |
| `mj_publications.py` + `recaptcha_audio.py` | Removidos | O portal legado tem reCAPTCHA + NoBot; não insistir |

## Sistemas ligados

- **Sabichão** — produção em `10.0.30.253` (`/var/www/sabichao`, chave
  `id_ed25519`, BD `segunor`). Dev é `192.168.69.20` (`/var/www/develop`,
  chave `id_ed25519_claude`) e **não** tem `INTELGRID_*` no `.env`, portanto
  nunca empurra clientes de teste para cá.
  - `funcoes/intelgrid_api.php` — screening de candidatos, push da watchlist,
    dossiê da tab Intel
  - `scripts/seed_watchlist.php` — carga inicial da carteira, em lotes
  - Os NIFs estão guardados com espaços; normalizar sempre antes de comparar

## Feito (Phase 3 changelog)

- **Migração 0003**: companies+= `monitoring_type` / `data_coverage_start` / `data_coverage_end` / `risk_score`. Nova tabela `process_parties` + gin trigram index em `name`. Backfill 3520 parties existentes.
- **`services/risk.py`**: `compute_risk(company_id)` + `update_company_risk`. Heurística cap 100.
- **`services/coverage.py`**: `update_coverage` recomputa start/end.
- **`services/analysis.py`**: `run_analysis(nif, name)` garante empresa, corre CIRE+Distribuição single-company (5y lookback), devolve metrics + risk.
- **`services/ingest.py`**: em cada processo novo — insere parties, atualiza coverage+risk, só dispara alertas se `monitoring_type='internal'`.
- **`jobs/scheduler.py`**: `_monitored_companies` passa a filtrar `monitoring_type IN ('internal','competitor')`.
- **API**:
  - `GET /api/search?query=...` — trigram fuzzy em `process_parties.name`
  - `GET /api/competitors?monitoring_type=[all|internal|competitor|analysis]`
  - `POST /api/analysis/run` `{nif, name?}`
  - `GET /api/companies?monitoring_type=...` + `POST/PATCH` aceitam `monitoring_type`
- **Frontend**: 3 páginas novas (`Competitors`, `Search`, `Analysis`) + sidebar atualizada + badges (`RiskBadge`, `CoverageBadge`, `MonitoringTypeBadge`) em Companies list + CompanyDetail + Competitors.

## Dados atuais (2026-04-17 09:00)

- **3 empresas** total, repartidas:
  - **internal (2)**: SEGUNOR (risk=0), IGPS PROTEK (risk=65 — devedor em PER)
  - **competitor (0)**: ainda nenhum adicionado pelo user
  - **analysis (1)**: MARIO MARTINS BRITO UNIPESSOAL LDA (NIF 517894769) — corrida pelo user em 2026-04-17 08:21 UTC, 58 processos em Distribuição
- **112 processos** total: 54 internal + 0 competitor + 58 analysis
  - Antes da Fase 3.1, os 58 da análise apareciam misturados em `/processes` (bug que o user reportou); agora vivem isolados em `/analysis/processes`
- 3520 parties extraídas em `process_parties`
- 2 alerts configurados; PATCH toggle on/off funcional

## Payload de alerta standard (testado)

```json
{
  "company": "SEGUNOR - SEGURANÇA PRIVADA, LDA",
  "nif": "514944617",
  "process_number": "3799/25.8T8STS",
  "court": "Santo Tirso - Tribunal Judicial da Comarca do Porto",
  "date": "2025-12-04",
  "type": "Processo Especial de Revitalização (CIRE)",
  "source": "distribuicao",
  "created_at": "2026-04-16T17:45:32.456602+00:00"
}
```

## Variáveis de ambiente

```
# DB (já configurado)
POSTGRES_DB / POSTGRES_USER / POSTGRES_PASSWORD

# Auth
SECRET_KEY
ADMIN_EMAIL

# Email alerts (opt-in; deixar SMTP_HOST vazio desativa)
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM=
SMTP_STARTTLS=true

# Webhook alerts
WEBHOOK_TIMEOUT_S=8.0
WEBHOOK_RETRIES=3

# DRE
DRE_ENABLED=false

# Scraper
SCRAPE_MIN_DELAY_S=0.3
SCRAPE_MAX_DELAY_S=1.2
SCRAPE_CONCURRENCY=8
FUZZY_MATCH_THRESHOLD=0.82
```

## Como operar

```bash
cd /opt/judicial

# logs
sudo docker compose logs -f backend

# aplicar mudanças de código
sudo docker compose up -d --build backend

# scrapes manuais
sudo docker compose exec backend python -m app.cli run-scrape cire --days todos
sudo docker compose exec backend python -m app.cli run-scrape distribuicao --date-from 2020-01-01 --date-to 2026-12-31

# análise on-demand via API (POST /api/analysis/run)
curl -b /tmp/c.txt -X POST http://localhost/api/analysis/run \
  -H 'Content-Type: application/json' \
  -d '{"nif": "501234567", "name": "Alguma Empresa SA"}'

# global search
curl -b /tmp/c.txt 'http://localhost/api/search?query=Ricardo%20Jorge'

# competitor ranking
curl -b /tmp/c.txt 'http://localhost/api/competitors?monitoring_type=competitor'

# backup
sudo /opt/judicial/infra/backup.sh

# DB shell
sudo docker compose exec postgres psql -U judicial -d judicial
```

## Gotchas / bugs conhecidos

- **DRE sem fonte de dados**: `dre.pt/api/v2/*` devolve SPA shell (2346 bytes). Scraping puro via HTTP não funciona. Alternativas: (A) Playwright — pesado; (B) reverse-engineer OutSystems screen services; (C) third-party.
- **`docker compose restart` NÃO recarrega código Python** — usar `up -d --build backend`
- **CITIUS POSTs precisam de TODOS os hidden inputs** (`__EVENTTARGET`, `__EVENTARGUMENT`, `__VIEWSTATEENCRYPTED`)
- **Dedup hash inclui `company_id`** — uma publicação que envolva N empresas monitorizadas cria N linhas, uma por empresa
- **`short_search_term` usa first 1–2 tokens** para robustez contra typos (ex: "Gardiennage" ↔ "Gradiennage")
- **Distribuição pagination**: tribunais com >10 resultados por busca só lêem 1ª página. Não afeta as empresas atuais (cada uma <10 por tribunal)
- **Risk score** só conta "insolvência própria" (role='devedor'/'insolvente'). Ser Credor em PER alheio não contribui — é counterparty risk, não own risk
- **`passlib` incompatível com bcrypt≥4.1** — arrancado, não reintroduzir
- **Alertas**: dispararam só para `monitoring_type='internal'`. Competitors/analysis têm scraping mas sem alertas (por spec)

## Phase 3.4 changelog (2026-04-17)

Resposta a feedback do utilizador: DRE cedo demais, durações ilegíveis para corridas longas, ter botões separados para varrer só minhas vs só concorrentes.

### Schedule final

| Job | Quando | Notas |
|---|---|---|
| `distribuicao_daily` | 03:00 diário | cobre internal + competitor (internal primeiro) |
| `cire_daily` | 03:30 diário | cobre internal + competitor, days=30 |
| `refresh_ptdata` | 05:15 diário | qualquer empresa com cache > 7 dias |
| `fetch_dre_daily` | 07:00 diário | DRE Série I+II (RSS já publicado a esta hora) |
| `vacuum_logs` | Domingo 02:00 | + reapa orphans `running` >2h |
| `infra/backup.sh` (cron host) | 02:00 diário | independente do APScheduler |

- **Ordering**: `_monitored_companies` ordena `internal` primeiro (`ORDER BY (monitoring_type='internal') DESC`) — falha parcial ainda cobre internal.
- **DRE 04:30 → 07:00**: o RSS oficial pode não estar atualizado às 04:30. 07:00 é margem segura.

### Trigger manual com filtro de tipo

UI `/admin/scraping` agora tem **2 botões por fonte** (Distribuição e CIRE):
- "Minhas" → `types: ["internal"]`
- "Concorrentes" → `types: ["competitor"]`

Backend `POST /api/admin/scrape/{source}` aceita `{"types": ["internal" | "competitor" | "analysis"]}` no body. Default = `["internal","competitor"]`. Inclui `types` em `scraping_logs.params` para rastreamento.

### Outras melhorias

- `_monitored_companies(types=...)`: aceita filtro de tipos e ordena `internal` primeiro (se ocorrer falha parcial, internal está coberto).
- `run_distribuicao(date_from, date_to, types)` e `run_cire(days, types)`: novos params permitem reuso pelas duas variantes do scheduler.
- **`reap_stale_running_on_startup()`**: corre no FastAPI lifespan; marca como `error: 'killed by backend restart'` qualquer linha que ficou em status `running` quando o processo morre. Resolve UI presa em "a correr" eternamente.
- **`vacuum_logs`** estendida: também reapa rows `running` com >2h sem terminar (defesa contra processos zombie).
- **`GET /api/admin/scheduled-jobs`**: novo endpoint, devolve lista de jobs APScheduler com `next_run_time`, `trigger`, `kwargs`.
- **UI `/admin/scraping`**: nova secção "Próximas execuções automáticas" mostra todos os jobs ordenados por next-run. Botão DRE agora também presente.
- **Duração**: novo helper `formatDurationBetween(start, end)` em `lib/utils.ts`. <60s mostra `Xs`, <1h mostra `Xm Ys`, ≥1h mostra `Xh Ym`. Substituído o cálculo inline em `Scraping.tsx`.

Ficheiros tocados:
`backend/app/jobs/scheduler.py`, `backend/app/main.py`, `backend/app/api/admin.py`,
`frontend/src/lib/utils.ts`, `frontend/src/pages/admin/Scraping.tsx`.

## Phase 3.3 changelog (2026-04-17)

DRE integration finalmente desbloqueada. Não foi via API (não existe), nem via Playwright, mas via **RSS oficiais publicados pelo próprio diariodarepublica.pt**:

- `https://files.diariodarepublica.pt/rss/serie1-html.xml` (Série I — leis)
- `https://files.diariodarepublica.pt/rss/serie2-html.xml` (Série II — anúncios, avisos, contratos)

Cada feed tem o "Diário do Dia" com 5-370 items. Buscamos os dois feeds uma vez por scrape, depois fazemos matching local de cada item contra todas as empresas monitorizadas.

Implementação:
- `app/services/dre.py` reescrito: parser regex de RSS XML, `fetch_dre_for_companies(companies)` em vez do antigo `fetch_dre_publications(company)`. Faz 2 HTTP calls (um por série) e mapeia por id de empresa em memória.
- Matching: `short_search_term` (já existe, robusto contra grafias) + word-boundary regex no `title` + `description` do item. Também faz match por NIF se aparecer no texto. Threshold mínimo de 4 chars no termo distintivo para reduzir falsos positivos.
- `DRE_ENABLED` default agora `true`. Job APScheduler `fetch_dre_daily` (04:30 Europe/Lisbon) já tinha lugar reservado e agora produz dados.
- CLI `python -m app.cli run-scrape dre` adicionado.
- `app/api/dre.py` (`GET /api/dre/company/{id}`) e `DrePublicationsSection` no frontend já existiam — agora populam dados reais.

**Limitação**: RSS só contém o dia atual; histórico inicial pré-deploy não é recuperável sem outra fonte. Para monitorização contínua (alerta de novas publicações) é exatamente o que se quer.

**Smoke test**: corrida em 861ms, encontrou 4 publicações de hoje para "ESPECIAL 1 SEGURANÇA PRIVADA S A" (3 avisos + 1 despacho).

Ficheiros tocados: `backend/app/services/dre.py` (rewrite), `backend/app/jobs/scheduler.py` (chamar bulk fn), `backend/app/cli.py` (branch dre), `backend/app/config.py` (DRE_ENABLED=true).

## Phase 3.2 changelog (2026-04-17)

Resposta à queixa "ptdata offline e sistema mais lento ao adicionar concorrentes".

- **VIES UE adicionado como fonte primária** — `https://ec.europa.eu/taxation_customs/vies/rest-api/ms/PT/vat/{nif}`. Devolve nome legal + morada, oficial, gratuito, sem rate-limit prático. Latência típica 300-500ms.
- **ptdata movido para background** — `app/services/enrichment.py` corre fire-and-forget após criação da empresa para adicionar CAE quando disponível. Já não bloqueia o user. O job diário `refresh_ptdata` (04:15) também recolhe casos em falta.
- **Cache LRU em-memória** no `lookup_nif` (256 entradas) — chamadas repetidas para o mesmo NIF dentro do mesmo processo: ~10ms. Útil porque o flow de adicionar empresa faz 2 lookups (Procurar + potencial relookup).
- **POST /api/companies skip lookup** — quando o cliente já envia `legal_name` (do passo "Procurar"), o backend confia e não chama `lookup_nif` novamente. Gravar passa a ser instantâneo.
- **Timeouts**: VIES 4s read / 2s connect (apanha NIFs de não-residentes lentos), ptdata 3s read / 1.5s connect (best-effort em background).
- **Latência medida 6 NIFs**: 330-490ms (era 2-4s antes). Cached 9-10ms.
- **Source label**: `"vies"` quando só VIES, `"local"` como último recurso (mod-11 só).

Ficheiros tocados: `backend/app/scrapers/ptdata.py`, `backend/app/services/enrichment.py` (novo), `backend/app/api/companies.py`.

## Phase 3.1 changelog (2026-04-17)

- **Backend**: query param opcional `monitoring_type` em `/api/processes` e `/api/dashboard/metrics`. JOIN com `companies` aplicado dinamicamente.
- **Frontend refactor**: extraído `CompaniesTable` e `ProcessesTable` como componentes reutilizáveis. Cada um aceita prop `monitoringType`. As 6 páginas (3 tipos × 2 entidades) ficam reduzidas a wrappers de 5 linhas.
- **Páginas movidas**: `Competitors.tsx` → `competitors/Ranking.tsx` (hardcoded competitor); `Analysis.tsx` → `analysis/NewAnalysis.tsx` (sem mudanças funcionais).
- **Páginas novas**: `competitors/CompetitorCompanies.tsx`, `competitors/CompetitorProcesses.tsx`, `analysis/AnalysisCompanies.tsx` (sem botão "Nova"), `analysis/AnalysisProcesses.tsx`.
- **Sidebar**: 4 secções agrupadas (As minhas empresas / Concorrentes / Análise / Geral / Admin) com section headers reutilizando o padrão existente do "Admin".
- **Dashboard**: agora só mostra métricas de `monitoring_type='internal'`. Header passou a "Visão geral das minhas empresas".
- **NewCompanyDialog**: aceita prop `monitoringType` — não há dropdown de tipo na criação. A criação a partir de cada página cria empresas do tipo correspondente. `/analysis/companies` não tem botão de criar (`canCreate=false`); criação faz-se em `/analysis` via formulário.
- **Detalhes** (`/companies/:id`, `/processes/:id`) continuam a funcionar para qualquer tipo — não filtrados, mantêm links válidos a partir de qualquer secção.
- **Pesquisa** (`/search`) intencionalmente cross-cutting — devolve hits de todos os tipos.

## Falta fazer

1. Configurar SMTP no `.env` se quiseres receber alertas por email
2. Decidir fonte DRE (Playwright, OutSystems scraping, terceiro) e implementar em `fetch_dre_publications`
3. Subir para git remoto
4. Fix paginação Distribuição (para tribunais com >10 resultados/página)
5. Considerar TLS + exposição via Cloudflare Tunnel
6. Adicionar mais empresas — competitors para teste real do ranking

---

## Fase 4 — Competitive Intelligence (2026-04-17 → 2026-04-19)

### Stage 1 — Contratos públicos (BASE/IMPIC via ptdata) ✅
- Migration 0008 `public_contracts` + 0009 cache de totais em `companies`
- `app/services/contracts.py`: dois passos por empresa (summary reliable; list best-effort, ptdata 504s em supplier index são frequentes)
- Cron diário 06:00 · on-create backfill por empresa nova
- UI: `ContractsSection` em CompanyDetail com KPIs + tabela paginada
- Manual trigger via `/admin/scraping` (card "Contratos públicos")

### Stage 2a — Classificador DRE ✅
- Migration 0010 adiciona `dre_publications.relevance` + `change_kind`
- `app/services/dre_classifier.py`: regex PT para HIGH (liquidação, capital, gerência, sede) / MEDIUM (denominação, objecto, quotas) / LOW
- UI mostra badges destacados; toggle para esconder irrelevantes
- Backfill sobre entries existentes

### Stage 2b — Recon dre.pt ✅
- Resultado: não tem filtro NIF — não serve para o caso

### Stage 2c — MJ histórico via Playwright 💤
- Migration 0011 adiciona `dre_publications.source` ('dre' | 'mj')
- Código completo em `app/services/mj_publications.py` + admin endpoint
- **DORMENTE** — reCAPTCHA v3 invisível no meu.registo.justica bloqueia Chromium headless (score 0.1)
- Revive: adicionar `playwright>=1.48` ao pyproject + `playwright install --with-deps chromium` ao Dockerfile + integrar 2Captcha (€5 vida-inteira) OU LLM local. Instruções no docstring do módulo.

### Stage 3 — Entity Relations ✅
- Migration 0012 `entity_relations`
- `app/services/relations.py`: 3 sinais (same_process 1.0 · shared NIF 0.95 · shared name 0.70)
- Filtros anti-ruído: name blocklist institucional (bancos, Seg Social, MP, AT…) + NIF individual apenas (1/2/3) + MAX_COMPANIES_PER_PARTY=4
- Cron diário 04:00, recomputa ~18s
- UI: `RelationsSection` com paginação (5/pág), expansão "+N pessoas", drill-down por pessoa → processos lado-a-lado nas 2 empresas (via endpoint `/companies/{id}/party-processes`)
- `DISTINCT ON` na drill-down para colapsar eventos CIRE repetidos (3855/22.4T8STS aparecia 6× → 1× com "6 eventos")

### Stage 4 — Intelligence Summary ✅
- `app/api/intelligence.py`: endpoint único agrega judicial + contratos + publicações + relações
- `_insights()`: heurísticas Portuguese — "Risk score elevado", "N processos novos/30d", "Alteração detectada nos últimos 90d", "Activa em contratos", "Ligada a N empresas"
- UI: `IntelligenceSummary` card hero no topo de CompanyDetail (KPIs + bullets de insights)

---

## Refactor — tribunal-sweep distribuição (2026-04-19)

**Antes**: `N_empresas × N_tribunais` HTTP requests por run (17 160 para 66 empresas × 260 tribunais). Cada query tinha `parte=distintivo` mas o servidor CITIUS devolvia homónimos humanos → filtragem local via `is_company_party`. CARLOS SILVA rebentava timeouts.

**Depois**: 1 query por tribunal sem filtro de parte. Match local contra todos os distintivos. **~52× mais rápido** (34s vs ~30min no daily).

- `app/scrapers/distribuicao.py::scrape_distribuicao_tribunal_sweep()`
- `build_distintivos_map()` pré-computa `{distintivo_normalized: company}`
- `match_row_to_companies()` per-row match
- `scheduler.run_distribuicao` → `_run_distribuicao_sweep` (default) + `_run_distribuicao_per_company` (só usado quando `company_id` é passado, i.e. on-create backfill)
- Commit per-tribunal (antes era per-empresa)
- Progress tribunal-scoped (progress_total = N_tribunais)
- **Retired**: cron jobs isolated 20:00/20:30 + flag `scrape_isolated=TRUE` no CARLOS SILVA (coluna mantida no schema)

Código v1 arquivado em `backend/app/scrapers/_archive/` com header explicativo.

---

## Outros deltas desta janela

- **Rename** global: "Judicial Monitor" → "Segunor Intel Grid" (6 ficheiros)
- **Sinalizador falso positivo**: migration 0006 `false_positive_reports` + botão em ProcessDetail + blacklist de dedup_hash no ingest
- **Filtros avançados** em /processes: role (autor/réu/credor/…), tribunal (contém), date range; URL-persistente via useSearchParams
- **Paginação** em CompanyDetail > Processos (10/15/25/50/100)
- **Competitors arquivadas**: migration 0007 coluna `active`, tabs Ativas/Arquivadas em /competitors, default hide em /processes + /competitors/ranking
- **Racius bulk import** feito (30 empresas importadas), depois removido da UI após o uso one-shot
- **Auto-backfill on-create**: `schedule_backfill_for_company()` dispara distribuicao (180d) + cire (todos) + contracts para empresa recém-criada
- **180-day cap** no manual run (CITIUS só serve ~6 meses)
- **Progress % live** em `/admin/scraping` (progress_current/progress_total + refresh 5s)
- **Filter active=all** nos links das Relações (para processos de arquivadas)

## Schema à data

Migrations 0001..0012 aplicadas. Tabelas principais:
- `users, user_sessions` (auth)
- `companies` (active, scrape_isolated, contracts_total*, risk_score, monitoring_type, data_coverage_*)
- `processes, process_events, process_parties`
- `scraping_logs` (progress_current/total + params JSONB)
- `alerts, alert_dispatches`
- `dre_publications` (+ relevance/change_kind/source)
- `public_contracts`
- `false_positive_reports`
- `entity_relations`

## Cron actual (Europe/Lisbon)

```
03:00  distribuicao_daily   sweep full, todas as empresas activas monitored
03:30  cire_daily           per-NIF, days=30
04:00  relations_daily      recompute entity_relations
05:15  refresh_ptdata       CAE/morada enrichment
06:00  contracts_daily      BASE/IMPIC via ptdata
07:00  fetch_dre_morning    RSS série I + II
22:30  fetch_dre_night      RSS série I + II (2º varrimento)
Sun 02:00  vacuum_logs      housekeeping
```
