########## reference_ooeirense_paleta_infografico.md
---
name: reference_ooeirense_paleta_infografico
description: Peças de dados do O Oeirense: paleta de cor dos infográficos, o template de carrossel com barra empilhada (ranking de percentual por cidade) e o padrão de carrossel de RANKING na versão objetiva/imparcial
metadata: 
  node_type: memory
  type: reference
  originSessionId: b54e607d-3dd1-4ae0-892a-d2c725127e80
---

Infográficos e peças de dados do O Oeirense usam fundo navy `#0d1f2d` com acento **VERDE**. NÃO amarelo (`#FFDE00` é PMO) e NÃO ciano — Último prefere o verde do portal.

- Acento/destaque (números grandes, badges, borda, barra ativa): verde `#62c069`.
- Gradiente de marca do portal (séries/categorias): `#127b9d → #3ea081 → #62c069` (azul-petróleo → verde-água → verde) — o verde lidera.
- Azul-petróleo/ciano `#1696be`/`#127b9d`: usar só como cor secundária de categoria, não como acento principal. Título de post: `#066da8`.
- Cards internos sobre o navy: `#13293a`. Texto secundário esverdeado: `#9fc2ab` / `#8fb89f`.
- Fonte: Raleway. Assinatura no rodapé: "O OEIRENSE".

Caso de origem (jun/2026, infográfico da frota de Oeiras): saiu amarelo → corrigido pra ciano → Último pediu verde ("temos mais a cara do verde"). Ordem de preferência: verde > ciano > amarelo (evitar). Ver [[project_ooeirense_ig_template_v2]] e [[project_portal_ooeirense]].

## Template canônico de CARROSSEL O Oeirense
Para carrossel (não post único), clonar o padrão de `carroseis/ooeirense-mulheres-mercado-trabalho.html` (ou `carroseis/ooeirense-frota-oeiras.html`):
- Viewer 540×720, exporta html2canvas scale:2 → 1080×1440. Fonte **Montserrat**. Fundo `#0a1a26` (body `#1a2433`).
- Acento verde **`#43d3a5`** (este é o verde do carrossel, mais menta que o `#62c069`); azul secundário `#3f7a99`; cards sólidos `#13293a`.
- Componentes: `.chapeu` pill gradiente `#14536b→#187a8c` (com ícone Tabler), `.titulo`, `.sub`, cards sólidos, `.bigbar` empilhada, timeline `.tl`, `.footer` gradiente `#127b9d→#3ea081→#62c069` com `@OOEIRENSE` + `SLIDE x/n`, atribuição `.fonte` "Apuração O Oeirense".
- Ícones: Tabler webfont via `cdn.jsdelivr.net/npm/@tabler/icons-webfont@2.47.0` (carrega no Puppeteer). Foto ilustrativa em `.photobox` (img + overlay + tag canto).
- Render p/ conferência: Puppeteer de `ARQUIVOS CLAUDE/screenshot-server/node_modules`, ativar `.slide` por índice, screenshot do `.viewer`. PNG final p/ Downloads (entregável, fora do workspace).

---

## Template de barra empilhada: ranking de percentual por cidade

Template de carrossel aprovado para expor **percentual por cidade do Vale do Canindé em barra empilhada** (rank das 17 cidades). Reusar sempre que a pauta for "percentual de cada cidade do Vale" em carrossel.

**Arquivo de referência:** `/Users/mac/carroseis/ooeirense-voto-rural-vale.html`
**Exportar:** `node /Users/mac/carroseis/exportar-slides.js <arquivo.html> <pasta-destino> <prefixo>` → PNGs 1080×1440 (Puppeteer local, sem cache). PNGs do exemplo em `SINO AGÊNCIA DIGITAL/O OEIRENSE/CARROSSEIS/VOTO-RURAL-VALE-2026/`.

Características aprovadas:
- Fundo dark navy `#0a1a26`, fonte Montserrat, textura leve (pontos+grade rgba branco ~4%/2,5%), rodapé degradê azul→verde com @OOEIRENSE + SLIDE n/total, crédito Sino em todo slide ([[project_ooeirense_campanha_bolha]] usa o mesmo ângulo).
- **Barra empilhada por cidade:** linha com nome + posição (verde `#43d3a5`) + volume de eleitores à direita; barra de 30px com 2 segmentos e o % dentro de cada. Cores: urbano/maioria `#3f7a99` (azul), rural/destaque `#43d3a5` (verde-água). ~6 cidades por slide.
- **Capa:** 2 blocos grandes lado a lado com o % gigante (3rem) + rótulo + volume, mais barra ilustrativa. Expor SEMPRE os dois rótulos/percentuais.
- Estrutura: capa → panorama (barra grande + 2 stats) → rankings (6+6+5) → extremos → "como ler"/CTA.

Regras editoriais que Último fixou:
- **Ordem de leitura = maioria em primeiro plano (à esquerda).** No caso rural×urbano, urbano primeiro (é 60,8%). Não super-expor o dado minoritário.
- **Coerência de afirmação:** não afirmar que "o interior decide" quando a maioria regional é urbana; o rural só "é maioria" nas cidades onde de fato passa de 50%.
- **Sem proporção reduzida** ("4 em cada 10"). Usar percentual real e "6 das 17 cidades" (17 já é número pequeno).
- Dado de seção mede onde vota, não onde mora — incluir card de ressalva.
- Dados das 17 cidades: ver [[project_tre_perfil_eleitorado_2026]] e CSVs em `ANALISE - POLITICA E ELEITORAL`.

---

## Padrão de carrossel de RANKING na versão objetiva/imparcial

Padrão aprovado para **carrosséis de ranking do @ooeirense** (ex.: 17 municípios do Vale do Canindé — eleitorado, ICMS). Versão "objetiva" consolidada em jun/2026.

**Template/diretriz:** layout vem de `carroseis/ooeirense-eleitorado-vale-2024-2026.html`; versão ICMS 2027 em `carroseis/ooeirense-icms-vale-2027.html`. Fundo navy `#0a1a26`, fonte Montserrat, acento verde `#43d3a5`, chapéu gradiente `#14536b→#187a8c`, linhas `.row/.rk/.rmain/.rname/.rsub/.rval/.rpct`, rodapé gradiente `#127b9d→#3ea081→#62c069` com `@OOEIRENSE` + `SLIDE x/y`. 540×720 no HTML, export 1080×1440 (Puppeteer do screenshot-server ou botão html2canvas).

**Características da versão objetiva (o que o Último pediu):**
1. **Capa já é o ranking.** Nada de slides de abertura/explicação antes. Intro curta no topo do slide 1 (título + 1 frase explicando o índice) e já começa a lista. Mais atrativo que introduzir o ranking em slides separados.
2. **Imparcialidade — nenhuma cidade em destaque.** Não realçar Oeiras (nem com borda/cor verde na linha). Todas as cidades com o mesmo peso visual; a ordem sai naturalmente do dado. "Já sabemos que Oeiras tem mais relevância; ser apenas objetivo e imparcial."
3. **Rodapé só com a FONTE.** Quando o material apenas reproduz dado oficial (TCE-PI, IBGE), NÃO creditar "Levantamento e cruzamento de dados: Sino Comunicação". Só `Fonte: ...`. O crédito Sino fica para quando há cruzamento/inteligência própria (ex.: eleitorado 2024×2026).
4. **~6 cidades por slide.** 17 cidades → 3 slides (6/6/5+total). Linha-total do território (`.row.totalr`) no fim, com o índice somado.
5. **rsub = descritor neutro.** O subtítulo de cada linha deve ser identificador neutro (ex.: `População: X hab.`), não uma métrica que "compete" com o índice do ranking e gere leitura contraditória.

**Pegadinha técnica (importante):** no `.rname` NÃO usar `overflow:hidden`/`text-overflow:ellipsis` — o **html2canvas (botão baixar) corta o texto na vertical**. Usar só `white-space:nowrap`. Sempre validar pelo caminho do html2canvas, não só pelo screenshot do Puppeteer (o Puppeteer renderiza certo e mascara o bug).

**Publicação:** carrossel via Graph API v25.0 — copiar PNGs pra `/tmp` (servido por ngrok :8099), criar N containers `is_carousel_item=true`, depois container `media_type=CAROUSEL` + `children` + caption, e `media_publish`. IG ID `17841477402712235`, token permanente System User. Ver [[project_ooeirense_telegram_trigger]].

Relacionados: [[project_pmo_carrosseis_workflow]], [[feedback_pmo_carrossel_conteudo]], [[project_ooeirense_blindagem_editorial]] (imparcialidade editorial), [[reference_vale_caninde_pop_censo2022]], [[project_tre_perfil_eleitorado_2026]].

########## reference_ooeirense_imagem_no_corpo.md
---
name: reference_ooeirense_imagem_no_corpo
description: "Marcador [imagem: URL | legenda | alinhamento | largura] insere figura no corpo da matéria do O Oeirense pelo MCP; implementado em 26/08/2026"
metadata: 
  node_type: memory
  type: reference
  originSessionId: 5e328bec-c5e7-4e23-990a-940112ac06fd
  modified: 2026-08-27T02:48:56.980Z
---

Desde **26/08/2026** dá para inserir imagens **no corpo** da matéria do O Oeirense pelo MCP, sem abrir o /admin. Basta uma linha isolada no `corpo`:

```
[imagem: https://... | Legenda e crédito | centro | 100]
```

Alinhamento aceita `esquerda|centro|direita` (ou left/center/right); largura, `50|75|100`. Só legenda também vale: `[imagem: URL]`. A URL precisa ser absoluta (http/https) — se não for, a linha fica como texto, de propósito.

Gera exatamente o mesmo HTML do editor TipTap do /admin (`<figure class="article-figure align-X size-Y">` + `<figcaption>`) e o nó `image` com os atributos `credit/align/width`, então **a matéria continua editável no /admin** e o CSS já existente (`globals.css`) estiliza sem mudança.

**Como pôr a imagem numa URL:** não mandar base64 pela conversa (400 KB de base64 = dezenas de milhares de tokens). Subir direto no Storage com o service-account e usar a URL — ver [[reference_ooeirense_firestore_admin_query]]:

```js
const bucket = admin.storage().bucket();       // storageBucket: 'o-oeirense.firebasestorage.app'
const filename = 'articles/body/' + Date.now() + '-' + crypto.randomUUID() + '.png';
const token = crypto.randomUUID();
await bucket.file(filename).save(buf, { contentType, metadata: { metadata: { firebaseStorageDownloadTokens: token } } });
// URL = https://firebasestorage.googleapis.com/v0/b/<bucket>/o/<encodeURIComponent(filename)>?alt=media&token=<token>
```

O MCP também ganhou o parâmetro `imagens_corpo` (array com `url` ou `base64` + `legenda/alinhamento/largura`), referenciado no texto por `[imagem: 1]`, `[imagem: 2]`. Serve quando a imagem já está numa URL externa; para arquivo local, o caminho do Storage acima é melhor.

**Onde está o código:** `src/lib/server/bot-articles.ts` (`parseImageLine`, `imageToHtml`, `resolveBodyImages`, `escapeHtml`), as três rotas `src/app/api/articles/*-from-bot/route.ts` (campo `bodyImages`) e `functions/src/mcp/server.ts`. No mesmo commit, `paragraphsToHtml` passou a **escapar HTML** do texto — antes interpolava cru em `dangerouslySetInnerHTML`.

Deploy exige **functions + hosting**, sempre com `--project o-oeirense` — ver [[feedback_ooeirense_deploy_workflow]] e [[feedback_firebase_cli_projeto_errado]].

########## reference_caged_oeiras_extracao.md
---
name: reference_caged_oeiras_extracao
description: Extrair Novo CAGED por município (admissões por sexo/setor/salário) — BigQuery Base dos Dados (histórico) + FTP MTE (atual); Oeiras = 2207009 / 220700
metadata: 
  node_type: memory
  type: reference
  originSessionId: fd3480f6-c9e8-42a0-aff8-c135a653cb57
---

Rotina validada (jun/2026) pra matérias de dados de mercado de trabalho do O Oeirense.

**Códigos de Oeiras (ARMADILHA):** IBGE 7 díg = `2207009` (usado na Base dos Dados, campo `id_municipio`). CAGED 6 díg = IBGE sem o dígito verificador = `220700` (usado nos arquivos do MTE). NÃO é `2207207` (esse é Padre Marcos). Sempre validar o código contra um nome/volume conhecido antes de gerar número.

**Fonte A — BigQuery Base dos Dados (histórico, fácil, só até ~out/2025):**
- Tabela: `basedosdados.br_me_caged.microdados_movimentacao` (270M linhas).
- Colunas: `ano`,`mes`,`id_municipio`(7díg),`sexo`('1'=Homem,'3'=Mulher),`cnae_2_secao`(letra A–U),`saldo_movimentacao`(1=admissão,-1=deslig.),`salario_mensal`,`indicador_fora_prazo`. (fora-do-prazo fica em tabela separada; esta = no prazo.)
- Nomes de seção: `basedosdados.br_bd_diretorios_brasil.cnae_2` (campos `secao`/`descricao_secao`).
- Auth SEM login: reusar credencial Firebase `~/.config/firebase/ultimofilgueira_gmail_com_application_default_credentials.json` (authorized_user, escopo cloud-platform), billing project `o-oeirense`. `google.auth.load_credentials_from_file(...)` + `bigquery.Client(project='o-oeirense', credentials=...)`.
- Lib: `google-cloud-bigquery` em venv. Python 3.14 NÃO tem wheel de pandas/pyarrow — instalar só `google-cloud-bigquery` (sem db-dtypes/pandas), iterar `query().result()` sem `to_dataframe()`.

**Fonte B — FTP MTE/PDET (atual, ~2 meses de lag):**
- `ftp://ftp.mtps.gov.br/pdet/microdados/NOVO%20CAGED/<ano>/<aaaamm>/CAGEDMOV<aaaamm>.7z` (CAGEDMOV = no prazo; FOR = fora do prazo; EXC = excluídas). FTP anônimo, alcançável.
- Arquivo `.txt` Latin-1, `;`-separado. Posições: `município`=$4 (6díg), `seção`=$5, `saldomovimentação`=$7, `sexo`=$14, `salário`=$21 (vírgula decimal). Extrair Oeiras: `7zz e -so CAGEDMOV<aaaamm>.7z | awk -F';' '$4=="220700" && $7=="1"{...}'`.
- 7z: `brew install sevenzip` → binário `7zz` (não há `7z`/`7za`).

**Concatenar A+B:** MTE e Base dos Dados são a mesma fonte e batem EXATO no mês de sobreposição (conferido out/2025: M=108/F=26). Então BigQuery (até out/2025) + meses novos do MTE = janela corrente consistente.

**Templates de infográfico** (pictograma "1 em N" + barra 100% empilhada por setor, Raleway, identidade clara do portal) em `~/claude-workspace/ooeirense/infograficos/`. Render via screenshot server porta 3099 em 2x (vetor → nítido); PNGs vão pra `~/Downloads`. Ver [[project_vale_caninde_dashboard]] e [[project_portal_ooeirense]].

########## project_portal_ooeirense.md
---
name: project_portal_ooeirense
description: Estado atual do portal de notícias O Oeirense em desenvolvimento — Next.js 16 + Firebase, painel admin com TipTap, banners funcionais, fontes corrigidas
type: project
originSessionId: e9722052-54d2-4825-a72c-cf1dc27dac78
---
Portal de notícias proprietário para O Oeirense — **EM PRODUÇÃO** em https://ooeirense.com.br (apex + www via Firebase Hosting com SSL válido desde 18/abr/2026).

**Stack:** Next.js 16 (App Router, Turbopack) + Firebase (Firestore, Auth, Storage) + TipTap v3 + Tailwind v4
**Diretório:** `/Users/mac/CLAUDE/ooeirense/`
**Dev server:** `npm run dev` → porta 3001 (launch.json configurado)
**Plano completo:** `/Users/mac/.claude/plans/elegant-weaving-fiddle.md`
**URL produção:** https://ooeirense.com.br (canônico) + https://www.ooeirense.com.br (redireciona)
**URL fallback Firebase:** https://o-oeirense.web.app

## Firebase Project
- **Project ID:** `o-oeirense` (número: 121688387634)
- **Web App ID:** `1:121688387634:web:5102e497ce5edf4b02cc2b`
- **API Key:** `AIzaSyDXx8_ObL_ii4erfHJFhmJzK2gSMg_4dP8`
- **Firestore:** ativo, localidade `nam5`, rules e indexes deployados
- **Auth:** Email/Password ativo
- **Storage:** ativo, US-EAST1, rules deployadas (qualquer subpasta aceita upload de imagem autenticado)
- **Service Account JSON:** `/Users/mac/Downloads/o-oeirense-firebase-adminsdk-fbsvc-7ebb62e8c8.json`

## Admin Users
- Email: `ultimofilgueira@gmail.com` | Senha: `OOeirense2026!` | UID: `cIYQgKOUYXfBuhEy0tJ3hZgo0Fz2` | role: admin
- Email: `ultimocampos@gmail.com` | UID: `HM71AHazT1NERp3vB5P6SHCNLMH3` | role: admin
- Script para criar/promover usuários: `scripts/set-admin-role.mjs`

## API Portal — Endpoints n8n

### POST /api/articles/create
- Auth: header `x-api-key: ooeirense-n8n-2026-secret`
- Cria artigo completo + guard 409 por `sourceUrl` (retorna `{duplicate:true, url, slug}`)
- Revalida ISR automaticamente via `revalidatePath`

### GET /api/articles/create?sourceUrl=<URL>
- Auth: mesma x-api-key
- Verifica duplicata SEM criar nada
- Retorna `{exists: false}` ou `{exists: true, articleId, slug, url}`
- **Útil para checar antes de processar no n8n** (evitar pipeline desnecessário)

## Workflows n8n — Estado atual (18/abr/2026)

### uvnMZfrW0Giut5lM — "Portal: Telegram → Publicar Matéria"
- **Status:** ATIVO
- Trigger: Schedule 2min + Telegram getUpdates (offset via staticData.publishLastUpdateId)
- Filtra `publicar <URL>` (prefixo obrigatório)
- Jina Reader → OpenAI GPT-4o → POST /api/articles/create → Telegram confirmação
- **Consumir Offset:** nó ao final que chama getUpdates com offset+1 para ack no Telegram (token: `AAHPUgR6b1y7oN7y5q1FMQ7vcoi3Io9nysE`)
- **IMPORTANTE:** Token do bot em TODOS os nós Telegram = `8684225036:AAHPUgR6b1y7oN7y5q1FMQ7vcoi3Io9nysE` (NOT o da CLAUDE.md que é inválido para este bot)

### R1zn1l8zDaVpFSkm — "Telegram → Instagram (post + story)"
- **Status:** ATIVO, funciona perfeitamente para publicações manuais
- Trigger: Schedule 1min + Telegram getUpdates (offset via staticData.lastUpdateId)
- Filtra URL simples (ignora `publicar <URL>` — tratado pelo workflow acima)
- Jina → OpenAI → Puppeteer (3099) → Instagram post + story → Telegram confirmação
- **Não há integração automática portal→Instagram ainda** (ver Pendências abaixo)

### k9drUE9oz9xLYDym — "OOeirense - Firestore Automation Polling"
- **Status:** DESATIVADO (problemas de autenticação OpenAI e parse dos resultados Firestore)
- Tentativa de acionamento automático de Instagram ao publicar no portal via Firestore
- **Abandonado em favor de abordagem futura**

## Quirks Críticos — n8n v2.15.1

### Draft vs Runtime (BUG IMPORTANTE)
- n8n mantém TWO versões: `workflow_entity` (draft) e `workflow_history` (snapshots)
- Runtime executa a partir do **snapshot mais recente do workflow_history**, NÃO do draft
- Editar via API PUT atualiza draft mas NÃO o snapshot → mudanças não têm efeito
- **Solução:** editar sqlite direto em AMBAS as tabelas + reiniciar n8n:
```python
import sqlite3, json
conn = sqlite3.connect('/Users/mac/.n8n/database.sqlite')
c = conn.cursor()
# 1. Atualizar draft
c.execute("UPDATE workflow_entity SET nodes=?, updatedAt=datetime('now') WHERE id=?", (json.dumps(nodes), wid))
# 2. Sincronizar snapshot mais recente
c.execute("SELECT versionId FROM workflow_history WHERE workflowId=? ORDER BY createdAt DESC LIMIT 1", (wid,))
vid = c.fetchone()[0]
c.execute("UPDATE workflow_history SET nodes=?, connections=? WHERE versionId=?", (json.dumps(nodes), json.dumps(conns), vid))
conn.commit()
# 3. WAL checkpoint
subprocess.run(['sqlite3', '/Users/mac/.n8n/database.sqlite', 'PRAGMA wal_checkpoint(TRUNCATE);'])
# 4. Reiniciar
subprocess.run(['launchctl', 'kickstart', '-k', f'gui/{os.getuid()}/com.ooeirense.n8n'])
```

### staticData
- `staticData` definido em Code nodes (via `$getWorkflowStaticData('global')`) persiste por execução
- NÃO persiste se a execução falhar no meio
- Limpar offset: chamar `getUpdates?offset=<N+1>&limit=0` diretamente pela API do Telegram

### fetch no Code node
- `fetch` NÃO está disponível em Code nodes n8n
- Para HTTP dentro de Code node: usar `await this.helpers.httpRequest({method, url, headers})`
- Para validação de URL: usar um nó HTTP Request separado antes do Code node

## Progresso (abril/2026)

### SESSÃO 17-18/abr — Fluxo portal→Instagram + correção duplicatas Telegram

**Problema resolvido — duplicatas Telegram (2 em 2 min):**
- Causa: nó `Consumir Offset` estava com token errado (`AAGqxSZv5VjKR9emBzSNbdY4PFBSk22HWA0` da CLAUDE.md → retornava 401)
- Token correto do bot: `AAHPUgR6b1y7oN7y5q1FMQ7vcoi3Io9nysE` (mesmo dos outros nós Telegram do workflow)
- Fix: corrigido token via sqlite em draft + history + reinício n8n
- Fila Telegram limpa, loop encerrado (exec 3627 foi a última a processar)

**Endpoint GET /api/articles/create adicionado ao portal:**
- Verifica duplicata por sourceUrl sem criar artigo
- Retorna `{exists: bool, articleId?, slug?, url?}`
- Útil para checar ANTES de processar no n8n

**Tentativa de integração automática portal→Instagram — REVERTIDA:**
- Ideia: após publicar artigo no portal, chamar webhook no R1zn1l8zDaVpFSkm para publicar no IG automaticamente
- Implementado: Webhook Portal + Preparar URL Webhook + validação de URL
- Problema: teste com URL fake/404 publicou post errado no Instagram ("Artigo não encontrado")
  - Post publicado no @ooeirense às ~22:26 — **deletar manualmente pelo app Instagram**
  - API Graph não permite DELETE de posts de mídia
- **Decisão: revertido completamente** — ambos os workflows restaurados ao estado anterior
- Pendência: pensar em solução mais segura (validação robusta de URL antes de publicar)

### SESSÃO 17/abr — Deploy + Dedupe + Watermark strip (CONCLUÍDO)

**Deploy em produção:**
- Firebase Hosting ativo: `https://o-oeirense.web.app` (SSR via Cloud Run `firebase-frameworks-o-oeirense:ssrooeirense`)
- `NEXT_PUBLIC_SITE_URL=https://o-oeirense.web.app` (domínio `ooeirense.com.br` ainda no Vupler — migrar depois)
- Deploy: `firebase deploy --only hosting` (do root `/Users/mac/CLAUDE/ooeirense/`)

**API `/api/articles/create` — endurecida:**
- Guard de duplicata por `sourceUrl`: retorna 409 `{success:false, duplicate:true, articleId, slug, url, message}` se já existir
- Revalidação via `revalidatePath` direto (sem round-trip HTTP)
- Revalida: `/`, `/[categorySlug]`, `/[categorySlug]/[slug]`

**Watermark strip gp1/lupa1 — no Code node "Extrair Dados":**
- Detecta watermarks e busca URL limpa via helpers.httpRequest
- Script retroativo: `scripts/fix-watermarked-images.mjs`

**Scripts de manutenção em `/scripts/`:**
- `check-article.mjs`, `find-duplicates.mjs`, `delete-duplicates.mjs`, `fix-watermarked-images.mjs`

### CONCLUÍDO (sessões anteriores)
**Fase 1 — MVP completo:** Setup, páginas públicas, admin CRUD artigos, editor TipTap, upload imagens
**Fase 2 — SEO + Integrações:** Sitemap, robots, OG image, JSON-LD
**Home page:** Últimas Notícias + Ganhou Destaque + Em Alta (layout Vupler-inspired)
**Header sofisticado:** 3 camadas (top bar + brand + nav) com identidade visual completa
**Footer 4 colunas dark**
**Módulo de Banners:** CRUD admin + slots no layout + tracking
**Middleware redirects 301:** `/noticia/{ID}/{slug}.html` → `/{categoria}/{slug}`
**Admin completo:** usuários, configurações, banners
**Breaking News bar:** campo no tipo, toggle no admin (componente pendente)

## Tema do portal (abril/2026)

**Admin-controlled via SiteSettings.theme ("dark" | "light")** — SSR lê do Firestore em `layout.tsx`, aplica `data-theme` no `<html>`. Sem toggle de usuário.

- Seletor em `/admin/configuracoes` (primeira section, com preview visual dos dois gradientes)
- CSS vars temáveis em `globals.css` bloco `[data-theme="light"]` — surfaces + on-surface-rgb + brand-label
- Light mode identidade: gradiente azul→verde de 5 stops (`#0b4f68 → #127b9d → #2c9a8e → #4fb271 → #62c069`) no topo, logo "O OEIRENSE" recebe mesmo gradiente via `background-clip: text` (classe `.brand-gradient-text` só ativa no light), footer com wash `#d9efeb → #e6f3e8 → #dff0ec`
- Utility classes em globals.css: `.surface-on`, `.surface-on-40`, `.surface-border-10`, `.surface-bg-5`, `.hover-surface-on` — RGB channel-only vars (`--on-surface-rgb`) permitem opacidade Tailwind
- Top bar (data + socials): texto `text-white/60` fixo — funciona nos dois temas pois tanto `#0d1f2d` (dark) quanto o gradiente azul→verde (light) têm fundo escuro o suficiente

**Why:** Último quer poder alternar visual do portal conforme a ocasião editorial sem depender do usuário.
**How to apply:** ao criar novos componentes que precisam ser temáveis, usar as utility classes `surface-*` em vez de cores hardcoded. Cores que devem ficar fixas (ex: cards de artigo) podem manter hex direto.

## SESSÃO 18-19/abr/2026 — Marco: site em produção + 435 artigos migrados

### DNS / domínio
- Domínio registrado HostGator. Vupler morreu (era `162.240.81.81`). Migramos pro Firebase Hosting:
  - `ooeirense.com.br` A → `199.36.158.100`
  - `www.ooeirense.com.br` CNAME → `o-oeirense.web.app`
  - `mail.ooeirense.com.br` A → `162.240.81.81` (preservado pro email HostGator)
  - MX → `mail.ooeirense.com.br` (email não quebrou)
  - TXT `hosting-site=o-oeirense` + `_acme-challenge` dos dois domínios (SSL provisionado)
- Firebase Hosting API (via service account) usada pra listar/inspecionar custom domains.

### Migração de artigos Vupler → Firestore (COMPLETO)
- **435 artigos** migrados (IDs 28-468 do Vupler + 2 colunas /coluna/achibata + 9 criados via portal/n8n).
- **108 IDs 404** (gaps esperados; IDs 1-27 nunca existiram).
- Scripts criados em `/scripts/`:
  - `discover-vupler-urls.mjs` — paginá categorias + colunas → `vupler-urls.json`
  - `import-vupler-articles.mjs` — parser melhorado (H1 real, chapéu plain-text, imagem `/uploads/posts/`, data via `Em DD/MM/YYYY às HH:MM:SS`, decode de entities HTML)
  - `check-migration-state.mjs`, `fix-watermarked-images.mjs` (retro)
- `createdBy='migration-v2'` identifica o batch.
- Imagens em `articles/migration/{legacyId}.{ext}` no Storage.
- **Data e ordem cronológica preservadas:** `publishedAt = createdAt = data real do Vupler`. 0 anomalias de ordem.

### Novas páginas / melhorias de UX
- `/noticias` — listagem completa paginada (30/página) com cabeçalho do card no mesmo padrão das sidebars.
- `/categoria/[slug]` — refeita com breadcrumb, header gradiente, lista compacta idêntica a /noticias, paginação (20/página).
- Home: Título "ÚLTIMAS NOTÍCIAS" do card é **link pra /noticias**. Botão "Mais Notícias" também. Card com título dentro (mesmo padrão das sidebars da direita). `flex-col + mt-auto` pro botão alinhar rodapé com coluna direita.
- Home: **5 artigos** em cada sidebar (Ganhou Destaque + Em Alta).

### Views + Analytics
- **Bug corrigido:** `incrementViews` era chamado server-side mas ISR cache bloqueava. Solução: endpoint `POST /api/articles/[id]/view` + `<ViewTracker articleId>` client component (dedup por sessão via `sessionStorage`, filtra User-Agent de bots).
- Server-side `incrementViews` removido; agora só client tracker.
- **GA4 ativo** com ID `G-XMF15J5948` — injetado no root layout quando `settings.analyticsId` está preenchido (regex `^(G-|UA-)`). Strategy `afterInteractive`, `anonymize_ip:true`.
- Google Analytics Admin API + service account `firebase-adminsdk-fbsvc@o-oeirense.iam.gserviceaccount.com` — precisa da API ativada + service account adicionada como Viewer nas properties GA (não feito ainda).

### Ranking das sidebars (home)
- **Score composto** em `lib/services/articles.ts`: `views × 10 + max(0, 30 - ageDays)`.
- Com views = 0 (início), recência define; conforme views acumulam, dominam.
- `getMostReadArticles` fetch 30, re-rank por score, slice 5.
- `getMostReadWeek` fetch 20 (últimos 7d), re-rank por score.
- **Dedup em page.tsx:** Em Alta tem prioridade; Ganhou Destaque filtra IDs que estão em Em Alta (`mostReadRaw.filter(a => !emAltaIds.has(a.id))`).

### Template Instagram (n8n)
- Workflow `R1zn1l8zDaVpFSkm` nos dois nós (`Montar HTML` e `Montar HTML Story`) atualizado com o template v2 dark (ver memória `project_ooeirense_ig_template_v2.md`).
- Feed: rodapé agora é faixa full-width gradient 78px (não border-image), `@OOEIRENSE` alinhado com título, dot removido, fonte melhorada (11px/1.4px letter-spacing), seta removida da story.

### Header / Footer
- **Tagline:** "Jornalismo Digital | Oeiras e região" (era "Portal de Notícias · Oeiras, PI"). Justificada via JS no `Header.tsx` — letter-spacing dinâmico p/ bater largura do nome.
- **Fonte:** logo agora usa Aller (carregada via `@font-face`), weight 700 (Bold) — era sans-serif system.
- **Mobile:** marca centralizada com subtitle justificado (inline-flex column align-stretch + text-align-last:justify). Footer — Institucional + Contato lado a lado via `md:contents` wrapper.

### CTA Instagram (article page)
- Bloco gradiente Instagram (azul→roxo→rosa→laranja) depois do share, com `@ooeirense` + "Seguir" + link pra `instagram.com/ooeirense` (target _blank).

### Cookie banner
- Redesenhado — pill compacta canto inferior direito (`max-w-sm`, sombra, "Saiba mais · Rejeitar · Aceitar"). Dismiss persiste via localStorage `ooeirense_cookie_consent`.

### Bug corrigido — dates.ts
- `isRecent` / `timeAgo` / `formatDate` quebravam quando recebiam Timestamp do admin SDK (instanceof do client falhava). Refatorado com `toDate()` duck-typed, aceita: Date, Timestamp (client+admin), `{seconds,nanoseconds}` serializado, string ISO, number ms.

### Avatar Instagram (gerado hoje)
- Script: `scripts/render-ig-avatar.mjs` — renderiza via screenshot-server (3099), fontes Aller embutidas base64.
- Output: `/Users/mac/CLAUDE/ooeirense/public/ig-avatar.png` (1080×1080) + cópia em `~/Downloads/ig-avatar-ooeirense.png`.
- Design: anel conic-gradient azul→teal→verde (story ring), centro navy, "O OEIRENSE" Aller Bold letras distribuídas flex space-between, linha gradiente, "JORNALISMO DIGITAL" Aller Regular com gradiente `background-clip:text`. Content deslocado `translateX(-14px)` pra compensar bearing óptico (O redondo vs E reto).

## SESSÃO 21/abr/2026 — Admin: editor de imagens, crédito de foto, timezone, bug bodyHtml

### Bug crítico — resolvido
**`createArticle`/`updateArticle` em [articles.ts:204](src/lib/services/articles.ts:204)** hardcodavam `bodyHtml: ""` e ignoravam o HTML do TipTap. Todo artigo publicado pelo admin saía com corpo vazio em produção. Fix: extrair `body._html` (HTML que o TipTap guarda dentro do próprio doc JSON) e persistir como `bodyHtml` top-level + regenerar `bodyPlainText`.

### Campo `coverCredit`
- Novo campo em `Article` + `ArticleFormData` — input abaixo do alt text no admin.
- Cover renderiza agora como `<figure>` com `<figcaption>` quando há crédito. Bot-articles.ts inicializa com `""`.

### Editor TipTap — imagens inline
- Extensão `ImageWithCredit` estende `Image` com 3 atributos: `credit`, `align` (left/center/right), `width` (50/75/100).
- `renderHTML` sempre emite `<figure class="article-figure align-{x} size-{y}"><img/><figcaption/></figure>` (figcaption só quando tem credit).
- `parseHTML` lê de volta `data-credit` e as classes `align-*`/`size-*`, então reload do editor não perde estado.
- Upload direto (sem prompt de URL nem de crédito) — ao selecionar imagem no editor aparece barra verde com: Esq/Centro/Dir · 50%/75%/100% · Crédito · Remover.
- CSS em `globals.css` — `float` com respiro em desktop, volta a bloco centralizado no mobile (`max-width: 640px`).

### Timezone BRT
- `Intl.DateTimeFormat` em [dates.ts](src/lib/utils/dates.ts) e `toLocaleDateString` do [Header.tsx](src/components/layout/Header.tsx) agora fixam `timeZone: "America/Fortaleza"`.
- Bug: Firebase SSR roda em UTC — antes o horário do artigo mostrava +3h (ex: "11:52" em vez de "08:52").

### Notas
- Todos os 9 arquivos modificados estão untracked/modified no repo — só o commit inicial do Next.js existe. Último roda `firebase deploy` direto sem commitar.
- Para reproduzir bug do bodyHtml em artigos antigos: `body._html` tem o HTML mas `bodyHtml` está vazio — basta rodar `updateArticle` via admin UMA vez que o patch novo já corrige.

## Pendências por Prioridade

### Média prioridade
- Share buttons flutuantes (desktop)
- "Leia também" automático após 3º parágrafo
- Timestamp preciso (data + hora) na página de artigo

### Descartado (abril/2026)
- Integração automática portal→Instagram — adiada (usar bot Telegram manual por enquanto)
- Breaking News bar — não priorizado

### Baixa prioridade
- Migração Vupler (~466 artigos) — aguardando export do banco
- `/autor/[id]` página de autor
- Analytics dashboard `/admin/analytics`
- Cloud Functions (`functions/`) — pasta inexistente
- PWA, dark mode

**Why:** Último quer migrar de plataforma SaaS (Vupler) para solução própria integrada às automações n8n existentes.

