Commit graph

789 commits

Author SHA1 Message Date
Francisco Solla
681f3bb566 feat(S13): cards personalizados da campanha no site público
Home ganha a seção Faça parte com os três modelos e a rota /cards
concentra catálogo + compositor client-side (dialog desktop, bottom
drawer mobile): nome no slot medido do mestre com Brexter 700 e foto
full-bleed sob o overlay oficial (pan/zoom clampados), download PNG
sem conta e sem enviar nome/foto. Núcleo puro em src/lib/card* com
unit tests; e2e cobre home→deep-link, nome→PNG real, erro fail-closed,
foto e drawer mobile. Sem migration/PII.
2026-09-12 03:25:43 -03:00
Francisco Solla
f3c1ad69ea chore(ai): Sollinha no nome canônico deepseek-flash (B201) 2026-09-12 02:47:25 -03:00
Francisco Solla
087635d08b fix(agenda): modais usáveis no desktop — criação, sync e feed (C148) 2026-09-12 02:23:23 -03:00
Francisco Solla
8983abc3f5 fix(social-feed): capas do Instagram voltam a carregar e legendas longas não esticam o card
As mídias da Graph API passaram a vir de *.fbcdn.net
(instagram.<pop>.fna.fbcdn.net), host fora do images.remotePatterns — o
otimizador respondia 400 e toda capa do Instagram quebrava. O pattern em
new URL() também pina search:'' e rejeitaria as URLs assinadas mesmo com o
host certo; agora são objetos planos (cdninstagram + fbcdn).

O título do card ganha line-clamp-3 (subtítulo, line-clamp-2) para a
legenda do Instagram não esticar o bento.

Testes: guard unitário do allowlist com URLs assinadas reais casadas pelo
matcher do próprio Next + seam parser→allowlist; e2e com legenda longa no
stub e asserção de -webkit-line-clamp; manifest de e2e afetado passa a
mapear o board de conteúdos para o spec frontend.
2026-09-11 18:09:03 -03:00
Francisco Solla
e481819d15 C147: Format files with prettier 2026-08-27 19:46:05 -03:00
Francisco Solla
9b728b3338 C147: Add logging to diagnose Google Calendar sync issues in production
- Add logging to readGoogleServiceAccountCredentials for missing/malformed env
- Add logging to runCampaignCalendarSync when sync is skipped
- Add logging to ensureGoogleCalendarPushChannel for channel status/errors
- Add logging to runSyncPass for sync outcomes (only when changes occur)
- Add logging to error catch blocks for better error tracking
- Use payload.logger where available, console.warn for env-only context
- All logs use consistent [GoogleCalendarSync] prefix
- Logging is conditional to reduce noise in production
2026-08-27 19:46:05 -03:00
Francisco Solla
fea6c64514 fix(admin): restaura S3ClientUploadHandler no importMap — admin em branco em prod (4ª recorrência da classe OPS69)
O importMap commitado perdeu a entrada do handler S3 no 1c865bd8
(regenerado sem as envs S3_*): com S3_* em prod, getFromImportMap
retorna undefined e a raiz do Payload não monta — tela branca, zero
erros de console, invisível no loop local (storage local em dev/test).

Fix da causa: regeneração com S3_* dummy (+2 linhas). Prevenção no
mesmo PR — o guard planejado no OPS69 (depends #77/#87) que nunca
existiu: pin unit, guard CI+deploy que regenera com envs dummy e falha
em divergência, wrapper do generate:importmap que injeta as envs, e
contrato compartilhado da chave. Post-mortem + changelog incluídos.
2026-08-26 20:27:22 -03:00
Francisco Solla
a9f0c61edf fix(campanha): restaura submitter no FormData do card de demanda (#921)
O dispatch manual do C139 montava new FormData(form) sem o submitter;
os botões de transição (name=status value=aprovada|rejeitada|…) nunca
viajavam e transitionDemandFormAction rejeitava toda decisão por status
vazio. submitterFrom (cast honesto nativeEvent as SubmitEvent) + segundo
arg do construtor nos handlers de transição e custo; C139 intacto.

Teste unit novo pina o glue FormData→status (SubmitEvent real, não
fireEvent.submit). E2e 'advisor opens a demand and decides it' verde
local (isolado e em runs de arquivo inteiro).
2026-08-25 21:48:30 -03:00
Francisco Solla
618e37ee66 fix(campanha): guarda CSRF mesma-origem proxy-aware atrás do túnel (#913) 2026-08-25 17:32:23 -03:00
Francisco Solla
e9441e3ff8 react-audit(família 3): mirror props→state via effect no badge — CampaignNotificationBell.tsx:56 reaplicava prop stale sobre o setUnreadCount(0) do open; sync no render com guarda de open 2026-08-25 13:40:32 -03:00
Francisco Solla
7a450bc814 react-audit(famílias 1/6): sortable heads eram client sem interatividade — ContactSortableHead.tsx:1, PeopleSortableHead.tsx:1, LeadershipSortableHead.tsx:1, StateDeputySortableHead.tsx:1 importavam *ListUrl por valor; como server components os serializadores de URL ficam fora do bundle (B14) 2026-08-25 13:40:32 -03:00
Francisco Solla
52ffbcd254 react-audit(família 7): erro assíncrono do sync sem role=alert — GoogleCalendarSyncDialog.tsx:212,251 não anunciava falha de 'Sincronizar agora'/'Reativar' para leitores de tela 2026-08-25 13:40:32 -03:00
Francisco Solla
d2aa70f0c5 react-audit(família 5): Media.ts sem hooks de revalidação — getCachedDocumentById('media') (documentReads.ts:43) nunca invalidava em edição/deleção no admin; afterChange+afterDelete espelham Post/Petition 2026-08-25 13:40:32 -03:00
Francisco Solla
34c04d6966 react-audit(família 2): view model mínimo cruza o fio RSC — PetitionForm.tsx:29 deixa de receber o doc Petition inteiro (richText/consent/tracking ficam no server) 2026-08-25 13:40:32 -03:00
Francisco Solla
e273e9ecfa react-audit(família 4): C140 leftover — dispatch manual preserva texto digitado em erro de validação (ActivityUpdateForm.tsx:34, ActivityResultForm.tsx:60, DemandWorkflowCard.tsx:104, MunicipalityUpdateDeliberation.tsx:227) 2026-08-25 13:40:32 -03:00
Francisco Solla
cf9b30bfd6 fix(audit-p6-2): keep actor-options inline — exported options type tripped knip types gate 2026-08-25 11:29:03 -03:00
Francisco Solla
b7d0dba650 fix(audit-p6-2): perfil page uses requireCampaignPageActor; P3-I guard widened to getCampaignUser* variants 2026-08-25 11:29:03 -03:00
Francisco Solla
761da2f4e9 react-audit(família 1): 'use client' desnecessário em MunicipalityPriorityIndicator.tsx:1 2026-08-25 08:21:49 -03:00
Francisco Solla
019cd456cc feat: S9+ (#771) — integridade de consentimento e alinhamento das ações públicas
- Consent.beforeDelete bloqueia delete com referências (APIError 409 composto
  por coleção), fim do 23502 cru no admin (contagens com overrideAccess).
- submitWhatsapp/submitPetitionSignature delegam o ciclo de transação ao dono
  withPayloadTransaction (rollback no commit-falho, AggregateError, after-commit),
  preservando os contratos { ok: true } / { ok: true, signatureNumber }.
- contactStateSchema exportado + contactCitySchema/contactCityFieldSchema
  consolidados; as 4 fichas usam os exports únicos (fim das cópias inline do
  predicate value in CitiesByState). withMutableConsentFixture purga consent
  via raw SQL para bypassar o hook no harness de teste.

Co-Authored-By: opencode <noreply@opencode.ai>
2026-08-25 00:13:47 -03:00
Francisco Solla
7259193060 S2-FOLLOWUP (#775) — validate do itemId YouTube deixa de ser código morto
O validate de itemId em social-feed-settings lia data.platform (doc inteiro,
sempre undefined) e nunca validava ID de vídeo em produção. Agora lê
siblingData.platform via helper validateExcludedItemId. Stub YouTube ganha
ids de 11 chars e o spec acompanha, destravando o e2e do describe
'Campaign home content section'. Sem migration.
2026-08-24 23:46:57 -03:00
Francisco Solla
a658a1025f fix(campaign): C115-F1 — snapshot de eventos do motor Google Calendar com compare-and-swap (Closes #694)
- recordLastSeenSnapshot: update condicional (id + updatedAt) em vez de
  last-writer-wins; miss descarta a visão contaminada (never-throw)
- token CAS capturado no topo de runSyncPass: mesmo doc alimenta as
  decisões D2-D4 e o guard da escrita (fecha a janela basis-vs-token)
- teste int de corrida determinístico (promise gates): passada obsoleta
  não ressuscita remoção permanente (RED confirmado antes do guard)
- convenção de hermeticidade da janela documentada no header da spec
2026-08-24 23:34:56 -03:00
Francisco Solla
d48914858a fix(campaignInvite): C121 — unifica chave de lock da redenção com contact-ficha (Closes #885) 2026-08-24 22:51:15 -03:00
Francisco Solla
ba77347616 fix(campaign): D13 — align supporter_import_batch.actor_id schema drift (Closes #643)
Make actor_id nullable to match its ON DELETE set null FK; set SupporterImportBatch.actor required:false so config agrees with schema. Keep the deleteCampaignUserImportBatches hook + hand-deletes as explicit cleanup (no schema cascade, per webauthn precedent). Adds int coverage for account-delete with an orphan batch.
2026-08-24 22:39:03 -03:00
Francisco Solla
6d962ffd9c fix(campaign): C114-LOCK — hooks do sync do Google Calendar seguram row lock por I/O de rede (Closes #870)
Os afterChange/afterDelete de activity e googleCalendarSync rodavam a reconciliacao completa dentro da transacao do save, segurando row lock + pool por todo o I/O do Google (15s por hop, N RTTs -> 45-75s). Corrige com deadline proprio do hook (GOOGLE_CALENDAR_SYNC_HOOK_TIMEOUT_MS=5s) via AbortSignal.timeout combinado com per-hop 15s no cliente (AbortSignal.any) — manual/webhook mantem 15s sem transacao como fallback confiavel. Fail-closed preservado (abort vira paused). Pin unit hook < per-hop + int hook <=7s com fetch lento.
2026-08-24 22:25:23 -03:00
Francisco Solla
6c582bbb3f fix(contact): C120 — serializa o append de telefone no reuso com lock de ficha (Closes #663) 2026-08-24 19:03:29 -03:00
Francisco Solla
a180721e11 feat(campaign): C144 — forms de escrita com options de município write-scoped (Closes #769) 2026-08-24 18:37:59 -03:00
Francisco Solla
078ca65949 review(B155+): simplify — useCallback nos shims, constantes no padrão dos irmãos, copy B36 preservada no caminho de erro, impl plan alinhado
- useMemo(() => fn, []) → useCallback (idiom do codebase; shim sem props no de municípios, saveErrorMessage no de state-deputies).
- ENDPOINT/GENERIC_ERROR → STATE_DEPUTIES_ENDPOINT / MUNICIPALITIES_ENDPOINT + SAVE_ERROR_MESSAGE (espelho de MunicipalityListAdvisorsControl).
- Prop opcional saveErrorMessage no wrapper de state-deputies: o call site B36 (fromStateDeputy) volta a falar em 'lideranças' no caminho de erro, como a form action removida.
- impl plan: LeadershipListStateDeputiesResponse (não ToggleResponse), useCallback, saveErrorMessage.
2026-08-24 18:14:56 -03:00
Francisco Solla
0bb8a96485 feat(campanha): B155+ — migrar quick-edits de célula de lideranças para rota JSON (célula → rota)
F2: POST /campanha/liderancas/state-deputies substitui
setLeadershipStateDeputyMembershipFormAction (chips de /campanha/liderancas B31
e /campanha/dobradinhas B36 — mesma action record).
F3: POST /campanha/liderancas/municipalities substitui
setLeadershipMunicipalitiesFormAction (B34).
Wrappers 'use client' LeadershipStateDeputiesColumnCell /
LeadershipMunicipalitiesColumnCell mantêm o contrato (state, formData) das
shared cells (intocadas); rotas reusam os schemas exportados + allowlists
floor/cap/scope; revalidate herdado das actions record (contrato B34/B31).
F1 (sinal de municípios) não existe mais: C87 o substituiu por form
multi-campo, que por regra B155 permanece em server action — registrado
como já resolvido no plano de intenção.
Guards: whitelist sharedSheetHostConventions (+2 wrappers), prewarm e2e
(+2 rotas), B34 aponta para a rota nova. Sem migration/UI (Impeccable A).

Closes #368
2026-08-24 18:14:56 -03:00
Francisco Solla
5844f026db fix(social-feed): deadline próprio de 5s no hook do save de credenciais encurta a janela de row lock (Closes #762) 2026-08-24 18:03:37 -03:00
Francisco Solla
23a061098a feat(campaign): serializa mutações do editor de relações por célula (Closes #378)
Fila encadeada por instância no MunicipalityRelationEditor: delta otimista
imediato + transporte serializado (próximo elo só dispara quando o anterior
assenta), falha reverte só o delta do próprio elo e a fila segue, busy
derivado da fila (nunca preso), drain aplica o último selectedIDs confirmado
só quando a resposta final o traz (JSON) — sem ele (dobradinhas) confia no
RSC. Substitui requestSeqRef/latestConfirmedRef-seq por fila; tempID de
create em contador próprio. Contratos dos wrappers e call sites intactos.
2026-08-24 18:00:20 -03:00
Francisco Solla
19683a0efb fix(petition): Lead tracking não falha mais assinatura commitada — guarda interna try/catch + spec (Closes #766) 2026-08-24 17:52:45 -03:00
Francisco Solla
b4484ec604 fix(social-feed): S11-POLISH rota sync 400 em não-configurado e dedup InstagramPersistDatabase (Closes #761)
Rota POST /api/social-feed/sync devolve 400 { ok:false, error } quando IG não configurado (antes 200 status:{}), alinhando ao impl doc S11; sem revalidateTag no branch 400. InstagramPersistDatabase deduplicado: alias de PostgresTransactionDatabase (owner único), instagramSync importa owner direto.

Co-authored-by: opencode/muse-spark-1.2-contributor-free
2026-08-24 17:46:50 -03:00
Francisco Solla
4034fa371e fix(E16): reconcile allocation_decision.rationale NOT NULL drift
Drop the NOT NULL constraint on allocation_decision.rationale to align
the DB schema with the Payload config (required: false since B134).
Hand-written idempotent migration following the consent_text_to_jsonb
precedent. Adds idempotency test in campaignMigrationReconciliation.

Closes #371
2026-08-24 17:24:13 -03:00
Francisco Solla
3b32dbc814 fix(OPS83-recurr): cast rotativa do verify #16 — serializar admin atrás do frontend (stub IG compartilhado) + notificação pula recipient sumido em vez de abortar transação 2026-08-24 15:11:39 -03:00
Francisco Solla
1d762755d9 feat(campaign): editor de permissão do assessor em modal central (desktop) + bottom drawer (mobile) no lugar do popover (Closes #813) 2026-08-23 20:41:29 -03:00
Francisco Solla
368f138282 feat(campaign): C88 deliberação na atualização — responsável, fio e resolvido (Closes #397)
Migration 20260823_161905_add_municipality_update_deliberation + access por mutationKind + ilha client nas 3 superfícies + specs unit/int/e2e.
2026-08-23 18:58:26 -03:00
Francisco Solla
9467e0f292 fix(campaign): C140 — manual dispatch in 18 forms prevents React 19 form reset on error
Fix manual dispatch em 18 formulários de /campanha que usavam <form action={submitAction}>: campos não controlados voltavam ao defaultValue após qualquer action assentada (sucesso ou erro) do React 19.

- WizardStepFormChrome alterado uma vez (cobre 3 wizard steps)
- 15 forms recebem dispatch manual arquivo a arquivo
- AdvisorPasswordResetButton (2 micro-forms sem campo digitável) ficou como está
- Unit spec novo campaignWizardStepFormChrome.unit.spec.tsx
- CHANGELOG-AGENTS.md cleaned (98 duplicates removed)

Also fixes:
- campaignPageActor writable gate only blocks advisors (was blocking coordinators too)
- campaignLeaderships e2e stability (waitForLoadState before attribute assertion)
2026-08-23 17:54:19 -03:00
Francisco Solla
a97cd56fce fix(campaign): desbloqueia deploy — gate 'writable' libera coordinator/candidate e FAB ganha data-slot do e2e (C142)
Dois bugs do C142 que o e2e full do verify do deploy pegou (o PR rodou só o blast radius):
- requireCampaignPageActor gate 'writable' checava advisorEditingAccess(user)==='none', que retorna 'none' para QUALQUER papel não-advisor — barrava coordinator/candidate de páginas de escrita (municipios/[slug]/editar redirecionava o coordinator pro detalhe).
- CampaignQuickActionsFab não renderizava data-slot='campaign-quick-actions-fab', o locator do e2e campaignPermissionProfile.
Regressões pino: campaignPageActorGate.unit.spec.ts (5 casos) + assert de data-slot no spec do FAB.
Verificado: tsc/lint/format/cycles, unit 2435, int 819, e2e local dos specs afetados (os 3 casos que falhavam no deploy passam).
2026-08-23 16:41:40 -03:00
Francisco Solla
f029c6f86e fix(S12): deep link #novidades aterrissa no formulário no mobile
A home é dona do container de scroll interno (h-dvh overflow-y-auto, body
overflow-hidden), então a navegação nativa por fragmento não aterrissa no
alvo; no mobile o reflow do h-dvh + imagens eager do hero deslocam o layout
após o scroll inicial.

Novo componente cliente CampaignHashScroll (renderiza null) montado no
layout da home: retenta o hash inicial até a seção entrar no viewport
(parando na primeira interação do usuário) e ouve hashchange para âncoras
in-page. scrollIntoView sem behavior herda scroll-smooth/motion-reduce.

E2E novos: describe mobile (load direto, CTA, reduced-motion) + load direto
desktop em describe próprio (evita corrida no beforeAll de consent).
2026-08-23 14:27:15 -03:00
Francisco Solla
1c865bd822 refactor(campaign): E1 simplify — pin loading/retry/stale-quirk no spec e tipo local
Review estrutural + qualidade (sem P1): spec ganha 3 pins do contrato mais
sutil (loading em voo, retry pós-falha, quirk gate→not-ready com resposta
stale — comportamento herdado dos 3 originais, preservado); UseAsyncSearchOptionsResult
vira tipo local (export especulativo, knip types:error); docstring documenta
o quirk e o trim; plano registra o ajuste de execução.
2026-08-23 13:16:47 -03:00
Francisco Solla
9a66a23cf6 refactor(campaign): E1 extrai useAsyncSearchOptions (3ª cópia da busca com debounce)
DemandResponsibleMultiSelect (C143), ResponsibleMultiSelect (C90) e
AsyncSearchCombobox trocam o efeito duplicado (requestId/setTimeout 250ms/
loading/failed) pelo hook compartilhado src/hooks/useAsyncSearchOptions.ts,
com isQueryReady opcional preservando a semântica de query vazia de cada
consumidor. Comportamento preservado; unit novo do hook (renderHook +
fake timers); sem schema/server/migration.
2026-08-23 13:16:47 -03:00
Francisco Solla
0d483a08db feat(campaign): C142 UI respects advisor permission profile somente_leitura
- Pure helpers: advisorEditingScope, rowEditingAllowed in campaignAdvisorProfile.ts
- gate: 'writable' in requireCampaignPageActor for route-level enforcement
- Shared cells accept readOnly prop: CampaignInlineEditableCell, MunicipalityRelationEditor, PhonesFieldEditor, DemandDescriptionEditor, etc.
- Municipality pages compute editingScope via resolveMunicipalityWriteScope and thread it to list/detail/edit surfaces
- Home actions, FAB mount rule, quick actions registry accept editingScope parameter
- Demands, activities, supporters, organizations create buttons gated
- E2E spec: campaignPermissionProfile.e2e.spec.ts covering 3 profiles
- Unit tests: campaignAdvisorProfile, accessShared, campaignComponents updated
- gate:fast passed (tsc, lint, format, knip, cycles, unit)
2026-08-21 23:01:35 -03:00
Francisco Solla
c723d6accf
merge(OPS75): convergência Forgejo→GitHub — GitHub como fonte única
* deploy: limpeza pós-deploy do build cache e tags locais antigas (INF3/F2)

O deploy compila no homeserver e acumulava build cache + tags locais
antigas no disco raiz (incidente 19/08: 45G de cache). Após o smoke:
docker builder prune -f + remoção das tags locais teqo-1313 exceto o
SHA em uso (best-effort, registry preserva o rollback).

* feat(S9): captura de novidades da campanha na home pública (form + CTA do hero)

Seção 'Receba as novidades da campanha' acima do rodapé: nome + WhatsApp
obrigatórios, e-mail/estado/cidade/comentário opcionais, toggle pré-selecionado
'Quero fazer parte do time' gravado como subscription.campaignLevel
(time|esporadico). CTA secundário do hero vira o atalho (#novidades, scroll
suave). Contact + Subscription em withPayloadTransaction com consentimento
novo fail-closed por chave estável 'campanha-novidades' — nada é gravado sem o
texto aprovado no admin. Migration 20260819_192605 (state de contact nullable
+ coluna campaign_level). Impl plan, changelog, int + e2e.

* plans(C141,C143,C142): permissões granulares por assessor + responsáveis de demanda + UI por perfil

* plans(C141,C143,C142): permissões granulares por assessor + responsáveis de demanda + UI por perfil

* feat(S8): seção 'Nossa história' na home — vídeo da trajetória embutido do YouTube (Closes #91)

* docs(S8): resolução do conflito com S9 (história → newsletter) + nota do guard e2e dev-only

* feat(S10): pixel do Meta na página pública de campanha (campo admin em SiteSettings + PageView na home + Lead no sucesso da captura)

Campo tracking.facebookPixelId no global SiteSettings (mesma validação/normalização do precedente Petition, regra centralizada em facebookPixel.ts); home (grupo (home)) injeta o base code + PageView quando o ID está configurado (fail-closed) e o CampaignNewsletterForm dispara exatamente um Lead no sucesso do S9. Migration 20260819_213947_add_site_settings_facebook_pixel_id. Closes #94

* refactor(S10): simplify pass — Lead wiring unit-tested e raças de estado compartilhado do e2e resolvidas

e2e: stub hermético de connect.facebook.net no guard compartilhado (specs paralelos renderizam a home com o pixel); advisory lock no Postgres serializando o login do usuário de teste (Payload perde sessão em login concorrente read-modify-write — classe documentada no webauthn); seedTestUser vira create-if-missing (sem janela de usuário ausente nem reset de senha); Lead do form protegido do catch de erro (tracking nunca falha uma captura commitada); descrição do campo compartilhada; unidade nova campaignNewsletterLead cobre exatamente um Lead no sucesso.

* feat(C141): perfis de permissão por assessor — Visão × Edição configuráveis, enforcement fail-closed (Closes #105)

* chore(C141): regenera changelog agregado após rebase com S10

* plans(C144): options write-scoped para formulários de escrita pós-C141

---------

Co-authored-by: Francisco Solla <admin@solla.dev>
2026-08-21 01:41:18 -03:00
Francisco Solla
3f74b696e1 feat(C141): perfis de permissão por assessor — Visão × Edição configuráveis, enforcement fail-closed (Closes #105)
(cherry picked from commit 60857ad34d)
2026-08-20 14:25:01 -03:00
Francisco Solla
beb5ac4b9a docs(OPS73): changelog + evidência do deploy do fix do admin em prod (Closes #114) 2026-08-20 02:12:08 -03:00
Francisco Solla
aeb33eb409 refactor(S11): quebra o ciclo config→feed — getInstagramFeed muda para instagramFeedView
O  em instagramFeed.ts fechava um ciclo rejeitado pelo
check:cycles quando o grafo do config alcançava o feed via o hook da global
(config → global → instagramSync → instagramFeed → config; o madge segue
imports dinâmicos também). Move o loader cached (único importador de
@payload-config entre eles) para instagramFeedView.ts; instagramFeed vira puro
(fetch/parse/persist/describe/builders/gate), sem next/cache nem config. Com a
aresta config recurvada eliminada, o hook volta a ser um import estático — e o
config deixa de puxar unstable_cache, então as int suites que mockam next/cache
passam sem mudança. Gates: check:cycles 0, unit 2463, int 791, lint/tsc/build,
e2e frontend serial 19/19 + admin 5/5.
2026-08-20 00:54:58 -03:00
Francisco Solla
3ce5a69555 feat(S11): feed do Instagram configurado não aparece na home — painel de status da sincronização no admin
O fail-closed silencioso da S3 deixava a assessoria cega: token errado não deixava
rastro além do picker vazio. Fim da caixa-preta:

- `instagramSyncStatus` (jsonb hidden) na global `social-feed-settings`, escrito pelo
  render, pelo afterChange (sync awaited quando a credencial IG muda) e pela rota
  `POST /api/social-feed/sync` (botão, auth admin + same-origin).
- `InstagramApiError` + `describeInstagramError`: o fetch parseia o body de erro da
  Graph API e o mapeia para copy pt-BR com a correção (OAuthException/expired →
  "token inválido/expirado ou emitido via Facebook Login — gere pelo Instagram Login";
  "Invalid user id" → confira o ID; 5xx → indisponível; TypeError → rede; AbortError →
  demorou demais). O ID só casa com "user" E "id" juntos (token rejections real falam
  "the user must be logged in" — não podem virar a branch de ID).
- `InstagramSyncStatusPanel` (campo ui irmão do picker): 5 estados fiéis ao rascunho;
  gate respeita o master `enabled`; `outcome` transiente limpo ao editar credencial;
  aria-live nos boxes interativos. Home inalterada (fail-closed intacto).
- Bug real: `afterChange` roda dentro da transação do save (row lock até os hooks
  retornarem); persistir o status via pool numa 2ª conexão deadlockava no lock da
  própria linha (~80-230s/save). Fix: persists do sync-hook na MESMA transação via
  `getPostgresTransactionDatabase` (C6); render/rota sem transação seguem no pool.
  Diagrama: UPDATE transactionid Lock vs idle in transaction; fix verificado 45ms vs 232s.
- Stub IG ganha estado `invalid-token`; runbook docs/ops/instagram-feed-token-runbook.md;
  migration add_instagram_sync_status (jsonb nullable).
- Débitos registrados: #124 (janela de lock) e #125 (polish P3).
- Gates: unit 2463 + int 791; lint/format/tsc/cycles; build; e2e admin 5/5 + frontend serial 19/19.
2026-08-20 00:54:58 -03:00
Francisco Solla
2cc8f2c14d feat(C143): demandas visíveis apenas para responsáveis explícitos + candidato/coordenador
- campaignDemand.responsibles (hasMany → campaignUser, staff elegível) com
  hook beforeValidate que descarta ids não-staff (fail-closed)
- canRead/UpdateCampaignDemand por membership: assessor só vê demandas onde
  é responsável; unrestricted/admin sempre; leader nunca (fim do escopo
  municipal de demandas)
- criador entra automaticamente como responsável no hook de criação (form,
  wizard, rascunhos C90); migration com backfill created_by_id → responsável
- UI: picker multi-select na criação (nova + wizard) e card de gestão no
  detalhe com save full-replace + advisory lock; sugestões = assessores do
  município; rótulo 'Visível para: Responsáveis · Candidato · Coordenador'
- int tests reescritos para a regra nova + 5 casos novos; e2e novo de
  visibilidade (lista/URL/gestão); fixture e2e com createCampaignDemand
- impl plan + changelog; importMap regenerado com S3_* dummy (OPS69)
2026-08-19 23:55:07 -03:00
Francisco Solla
624126daca ops(OPS73): importMap regenerado com envs S3 (handler S3ClientUploadHandler) + impl plan do deploy do fix 2026-08-20 02:19:58 +00:00
Francisco Solla
eebe81cd1e feat(S8): seção 'Nossa história' na home — vídeo da trajetória embutido do YouTube (Closes #91) 2026-08-20 02:19:58 +00:00