teqo-backup/docs/plans/municipio-v2-shell-status-impl.md
Cursor Agent ce44ec1949
chore: drop unused search-only omnibox exports; finish B147 simplify notes
Knip: remove dead suggestions/remove/clear helpers after organizações moved
to a typed omnibox. Land deferred-helper note + empty-sinal int coverage.
2026-08-02 23:35:22 +00:00

7.5 KiB
Raw Permalink Blame History

Impl: Município v2 — shell + faixa de status

Status: aprovado Atualizado em: 2026-08-02 Issue: #330 Intenção: docs/plans/municipio-v2-shell-status.md Appetite restante: ~1–1,5 dia eng (herdado)

Leitura da intenção

  • Outcome: rota paralela /campanha/municipio/<slug>/v2 com faixa de status operável (nível · tendência · sinal · classe · silêncio/frescor · agregado de notas); select abre modal de motivo opcional; confirmação grava; rota antiga intacta; leader lockdown.
  • O que NÃO negociar: leader fora; auto-save no select; motivo obrigatório; hero nome/TI no corpo; painel de ajuda inline; padrão distinto para sinal; expor estimado; matar /municipios/[slug].
  • O que reavaliar: “nova rota + composição municipality/” (ok); hipótese de só reusar loaders vs extrair view model de status; soft-dep B134 (schema de nível ainda exige motivo + reversalSignals).

Abordagem recomendada

flowchart LR
  Page["municipio/[slug]/v2/page.tsx"]
  Loader["loadMunicipalityV2StatusData"]
  Strip["MunicipalityV2StatusStrip"]
  Modal["MunicipalityV2StatusReasonDialog"]
  LevelAPI["POST …/engagement-level"]
  TrendAPI["POST …/political-trend"]
  SignalFA["createMunicipalityListSignalFormAction"]
  Page --> Loader --> Strip
  Strip --> Modal
  Modal --> LevelAPI
  Modal --> TrendAPI
  Modal --> SignalFA

Opções consideradas:

A — Thin page + extract shared list controls into strip B — V2-owned status strip + shared optional-reason dialog; reuse write endpoints C — Full dual-path ops-sync surface for v2
Reusa UI da lista UI nova densa; writes iguais Prematuro (OH cutover)

Recomendação: B — because the list cells are Popover/Sheet autosave (trend) or multi-field E14 popover (level), which violate the product rite (select → optional reason modal → confirm). A new strip that calls the same endpoints keeps one write policy without twinning actions.

Rejeitadas: A because list UX ≠ select+modal (would force a second mode into list cells or ship the wrong rite). C because B147 is shell-only and appetite forbids dual-path.

Decisões de engenharia

  1. Rota singular nova src/app/(campaign)/campanha/(app)/municipio/[slug]/v2/page.tsx
    Opções: sob municipios/…/v2 | singular municipio/…/v2 | query ?v=2.
    Rec: singular — matches locked product URL. Rejected: plural (breaks contract); query (pollutes old detail).

  2. Dados da faixa — thin loader loadMunicipalityV2StatusData composing existing detail VM + territorial class + last typed signal preview.
    Opções: only getMunicipalityDetailViewModel | new loader | dossier loader.
    Rec: new thin composer over resolveAccessibleMunicipalityContext + strategy fields + computeMunicipalityTerritorialClass + latest sinal update (for type/note aggregate). Rejected: dossier (heavy); detail-only (no last signal type).

  3. Motivo opcional no nível (soft-dep B134) — relax engagement write schema so empty note OK; stop requiring reversalSignals on new writes (omit/empty in snapshot). Align list level control to the same rite (optional note only; drop reversal field from UI).
    Opções: leave schema + break “vazio OK” | v2-only bypass with placeholder text | relax schema + update list owner in this PR.
    Rec: third — product acceptance requires empty OK; edit the owner (schema + list control + action). Historical snapshot scrub / wizard trend step remain on #288 (B134). No Payload migration: reversalSignals lives only in allocationDecision.snapshot JSON.

  4. Sinal frio (questão aberta da intenção) — assume A: select value = sentinel “Sem sinal / frio” when cold; age in aggregate/tooltip.
    Threshold: MUNICIPALITY_COLD_SIGNAL_DAYS (21) from municipalitySignal.ts (lista).

  5. Chrome / nome — no body hero. Path rule → section title Municípios; generateMetadata uses municipality name. B145 entity-title-in-header stays soft-dep (no twin).

  6. Nível só unrestricted — strip shows level as readout for advisors; selects for trend/signal for all staff. Gate mirrors list (canMoveEngagementLevel).

Componentes / mudanças

  • MunicipalityV2Page (src/app/(campaign)/campanha/(app)/municipio/[slug]/v2/page.tsx): RSC; requireCampaignPageActor({ gate: 'noLeader' }); status strip; empty slots for conta/rede/agora (children).
  • loadMunicipalityV2StatusData (src/utilities/municipality/municipalityV2StatusData.ts): server-only composer.
  • buildMunicipalityV2StatusAggregate (src/utilities/municipality/municipalityV2StatusView.ts): pure aggregate text + cold sentinel helpers (unit-tested). Client-safe utilities (depends on municipalitySignal).
  • MunicipalityV2StatusStrip + MunicipalityV2StatusReasonDialog (src/components/campaign/municipality/): client strip + shared Dialog (motivo opcional).
  • Schema/action/list level: municipalityEngagementLevelSchema note → optional; drop reversalSignals from write; list control drops reversal field; int tests updated.
  • Chrome: path rule /campanha/municipio/[^/]+/v2.
  • Form action: thin alias over createMunicipalityListSignalFormAction.
  • Migration: nenhuma.
  • Access / Consent: existing municipality access; no Consent; leader blocked by page gate.
  • UI: Impeccable C — shape → craft → critique → polish; tokens data-theme='campaign'; CampaignHoverTooltip + campaignConceptHref / campaignConceptOneLiner.

Dados → forma

  • Forma: dense control strip (native selects + readouts), not cards/KPIs — qualitative status for CG decisions. Rejected: card tower (old overview); pill cluster for signal (product locked select parity).

Fases verificáveis

  1. Tracer / server — schema note optional + reversal optional; loader + pure aggregate; page RSC with strip read-only first; unit tests for aggregate/cold.
  2. UI — three selects + modal + writes + tooltips + class/silence; placeholders for children sections.
  3. Gates — pnpm gate:fast; entrega via pnpm push.

Rabbit holes / Não escopo (engenharia)

  • Conta / rede / agora / FAB / cutover (B148–B152).
  • Wizard de tendência (resto B134).
  • Scrub histórico de reversalSignals em snapshots.
  • Dual-path / ops-sync.
  • Redesign hysteresis UI beyond override/shock checkboxes already in list (keep minimal in modal when violations apply).
  • Extrair helper compartilhado confirmLevel/ListLevelControl.submit — 2 call sites; gatilho: 3º writer do mesmo endpoint+blocked path.

Riscos e mitigação

  • B134 race: #288 blocked; we land write-policy subset needed for aceite; comment on #288 that schema/list already optional.
  • Hysteresis in modal: keep override + triangulatedShock checkboxes when violations present (same as list), else confirm is one-tap.
  • Signal body required by schema for non-semanal: modal “motivo” maps to body; empty fails today — relax sinal body to optional in create schema when kind=sinal (motivo opcional), keeping signalType required.

Aceite de engenharia

  • Aceite de produto da intenção ainda coberto
  • Invariantes AGENTS/engineering-standards
  • Testes de domínio: unit aggregate/cold; int engagement level empty note; list control still submits

Self-score decision-quality: 4/5