# Changelog — SGG

Formato: cada bloco de implementação gera uma entrada. O versionamento de
**schema/arquitetura** (v1→v4) é separado do versionamento de **implementação**
(Bloco 1→9). O schema está congelado em v4; ajustes pós-v4 entram como
migrations incrementais (ver `MIGRATIONS.md`).

---

## Hotfix 16.1.7.1 — Aplicação automática da quantidade por página

**Escopo:** `Views/citizens/index.php`. **Um único arquivo.**
**Sem migration, sem banco, sem controller, sem model, sem rota, sem
permissão, sem biblioteca externa.**

### Auditoria prévia
O listener de `change` do seletor **já existia** desde o 16.1.7
(`index.php:274`) e o auto-submit já funcionava. O botão "Aplicar" existia
apenas como fallback sem JavaScript e era **ocultado por JS** (`go.hidden =
true`). No celular ele extrapolava a largura da tela antes de ser escondido.

### O que mudou (4 remoções de código)
1. O elemento `<button id="czPerPageGo">Aplicar</button>` foi **removido do
   HTML** — não ocultado por CSS.
2. A regra CSS órfã `.cz-perpage__go{...}` foi removida.
3. `var go = document.getElementById('czPerPageGo')` foi removida.
4. `if (go) go.hidden = true;` foi removida.

O listener de `change` **não foi tocado nem duplicado**: continua sendo o
único, garantindo *uma alteração = uma submissão*.

### Largura no mobile
Estimativa do bloco do seletor na media query de ≤600px:

| | largura |
|---|---|
| Antes (com "Aplicar") | ~318 px |
| Depois | ~234 px |

Num iPhone SE (320px, ~288px úteis) o bloco **estourava** antes e agora cabe
com folga. Também cabe em 360px (Samsung A07), 390px e 412px. `min-height:44px`,
`max-width:110px` e `order:3` foram preservados: o seletor continua fácil de
tocar e "Importar Planilha"/"Novo cidadão" seguem acessíveis. Nenhuma largura
fixa em px, nenhum `position:fixed`, `z-index` ou `overflow-x` novo — navegação
inferior e safe-area intactas.

### Sem JavaScript
Conforme decidido para este hotfix, o fallback com botão **não** foi recriado.
Sem JS o seletor permanece visível, porém sem auto-submit.

### Testes
`php -l` OK e `node --check` OK. **95 casos, 95 PASS**: ausência do botão no
DOM, no CSS e no HTML servido; uma alteração gerando exatamente uma submissão
(e `input` não submetendo); as seis transições 250→25, 25→50, 50→100, 100→250,
250→500 e 500→25 submetendo automaticamente com o `per_page` correto;
`page` ausente do formulário (retorno à página 1 a partir de um cenário real de
187 páginas); preservação de busca, bairro, liderança, consentimento e WhatsApp
isolados e combinados; as cinco quantidades ainda renderizando 25/50/100/250/500
linhas; contador e intervalo preservados; larguras de tela de 320 a 412px.

Duas asserções minhas falharam por erro do teste: uma cenário de `page=3` que na
verdade retornava zero resultados (refeito com 187 páginas reais) e outra que
encontrava a palavra "Aplicar" num **comentário do próprio JavaScript servido** —
o comentário foi reescrito, de modo que a palavra não aparece mais em lugar
nenhum do HTML entregue ao navegador.

### Não-regressão
`diff -rq` recursivo: **1 arquivo alterado** (+ VERSION/CHANGELOG), **0 novos,
0 removidos**, 264 antes e 264 depois. `Citizen.php`, `CitizenController.php`,
`routes.php`, `app.css`, `demands/quick.php`, `citizens/form.php`,
`citizens/import.php`, `config.php`, `.htaccess`, `database.sql`, `sw.js` e
`manifest.json` **byte a byte**. Whitelist, padrão 250, filtros, busca,
paginação, contador, chips e o toggle de filtros preservados.

---

## Hotfix 16.1.7 — Quantidade de cidadãos por página

**Escopo:** `Models/Citizen.php`, `Controllers/CitizenController.php`,
`Views/citizens/index.php`.
**Sem migration, sem alteração de banco, sem rota nova, sem permissão nova,
sem índice novo, sem biblioteca externa.**

### Auditoria prévia
Rota `GET /citizens` → `CitizenController@index` → `Citizen::paginate($filters,
$page)` → `Views/citizens/index.php`. O controller **não passava o 3º
argumento**, então valia o default do model: **20 registros por página**. Com
mais de 1.600 cidadãos reais isso significava ~81 páginas. `Citizen::paginate`
tem **um único chamador**, e não havia nenhuma convenção de `per_page` no
sistema — daí a adoção do parâmetro sugerido.

### Parâmetro e whitelist
Parâmetro GET **`per_page`**. Whitelist explícita
`Citizen::PER_PAGE_OPTIONS = [25, 50, 100, 250, 500]`, padrão
`Citizen::PER_PAGE_DEFAULT = 250`.

`Citizen::normalizePerPage()` só aceita inteiro ou string de dígitos puros e
depois confronta com `in_array(..., true)`. Qualquer outra coisa — `0`, `-1`,
`1`, `20`, `999`, `999999`, `abc`, `1e3`, `25.5`, `0x19`, `250abc`,
`25 OR 1=1`, `25' OR '1'='1`, `25;DROP TABLE citizens`, array, bool — cai em
**250**. Espaço em volta é tolerado (`" 25 "` → 25), mas só se o resultado
pertencer à whitelist.

O `<select>` do HTML **não é fonte confiável**: a validação é feita no servidor.

### Teto no model (defesa em profundidade)
`LIMIT`/`OFFSET` são interpolados (não aceitam placeholder nessa forma), então
`paginate()` passou a aplicar `max(1, min(500, $perPage))` e `max(1, $page)`.
Nenhuma chamada — nem futura, nem por engano — consegue pedir mais de 500
linhas. A assinatura mantém `$perPage = 20` por retrocompatibilidade.

### Preservação de parâmetros
A view passou a montar um `$baseParams` único (busca, bairro, liderança,
consentimento, WhatsApp **e** `per_page`), reaproveitado em três lugares:
links de paginação, links de remoção de chip e campos ocultos do seletor. O
formulário de busca/filtros ganhou um `hidden per_page`.

### Volta à página 1 ao trocar a quantidade
O formulário do seletor **deliberadamente não envia `page`**. Trocar 25 → 500
recomeça na página 1, evitando cair numa página que deixou de existir (com 25
havia 209 páginas; com 500, apenas 11).

### Interface
Na toolbar já existente, ao lado do contador:
`5208 cidadão(s) encontrado(s) · Exibindo 1–250   Exibir [250 ▼] por página`

O total continua sendo o total do filtro, não o que cabe na tela. O intervalo
só aparece quando há mais de uma página.

### Sem JavaScript
O seletor é um formulário GET nativo com botão **Aplicar**, que funciona
sozinho. O JavaScript apenas aplica a troca no evento `change` e, por isso,
esconde o botão. Nenhuma biblioteca externa.

### Mobile
Select e botão com `min-height:44px`. Abaixo de 600px o seletor ocupa a linha
inteira com `order:3`, indo para baixo em vez de empurrar "Importar Planilha"
e "Novo cidadão" para fora da tela; o select recebe `max-width:110px`.
Nenhuma largura fixa em px — zero risco de scroll horizontal.

### Performance (5.208 cidadãos, conexão aquecida)
| per_page | tempo | queries |
|---|---|---|
| 25 | 1,5 ms | 2 |
| 50 | 1,5 ms | 2 |
| 100 | 8,9 ms | 2 |
| 250 | 9,1 ms | 2 |
| 500 | 10,8 ms | 2 |

**Sempre 2 queries** (COUNT + SELECT), independentemente da quantidade e dos
filtros: nenhuma query N+1 introduzida. Última página (OFFSET máximo): 11,8 ms.
Nenhum cache e nenhum índice novo.

### Testes (PHP 8.3.6 + MariaDB 10.11, banco real)
`php -l` 3/3 e `node --check` OK. **201 casos, 201 PASS**: padrão 250 sem
parâmetro; 25/50/100/250/500 conferidos contra o banco; última página com o
resto; busca, bairro, liderança, consentimento e WhatsApp isolados e
combinados, com totais conferidos por `COUNT` independente; navegação para a
página 2 sem sobreposição; preservação de `per_page` em paginação, chips e
formulário de filtros; retorno à página 1 ao trocar a quantidade; todos os
payloads maliciosos; regra de WhatsApp (`consent_contact=1 AND
whatsapp_opt_out=0`) não invertida; isolamento por tenant e `deleted_at` com
`per_page=500`; mobile.

Duas asserções minhas falharam por erro do teste, não do código: uma media a
abertura da conexão PDO junto com a consulta (5.027 ms → 1,5 ms com conexão
aquecida) e outra tratava `max-width` como largura fixa. Uma terceira passava
**vacuamente** (`.every()` sobre array vazio, num cenário sem paginação) e foi
refeita com um filtro que produz 5 páginas reais.

### Não-regressão
`diff -rq` recursivo: **3 arquivos alterados** (+ VERSION/CHANGELOG), **0 novos,
0 removidos**, 264 antes e 264 depois. 13 métodos do model e 10 do
`CitizenController` byte a byte; `routes.php`, `demands/quick.php`,
`citizens/form.php`, `citizens/import.php` e os controllers de importação,
Demanda Rápida, demandas e comunicação **inalterados**.

---

## Hotfix 16.1.6 — Busca inteligente e localização avançada de cidadão na Demanda Rápida

**Escopo:** `Models/Citizen.php`, `Controllers/CitizenController.php`,
`Controllers/QuickDemandController.php`, `routes.php`, `Views/demands/quick.php`.
**Sem migration, sem alteração de banco, sem tabela nova, sem permissão nova.**

### Auditoria prévia
`QuickDemandController::citizenOptions()` carregava até **500 cidadãos** a cada
abertura da Demanda Rápida (`SELECT id, name ... ORDER BY name LIMIT 500`),
materializados como `<option>` num `<select>`. Consequência medida com 5.208
cidadãos: **4.708 (90,4%) eram inalcançáveis** — quem estivesse além do 500º
nome em ordem alfabética simplesmente não existia para a equipe de rua, que
acabava recadastrando duplicata pelo `＋ Novo`, poluindo a base.

Confirmado que **não havia endpoint equivalente**: `/map/search` e
`/transparencia/pesquisa` consultam outros domínios e não retornam cidadãos.

Colunas **reais** confirmadas em `database.sql`: `citizens.nickname`
VARCHAR(120), `citizens.phone`, `citizens.email`, `citizens.city`,
`citizens.neighborhood_id` → `neighborhoods.city` e `neighborhoods.region_id`
→ `regions.name`. **Não existe coluna `whatsapp` separada** — `phone` É o campo
de WhatsApp (a importação mapeia `whatsapp→phone`), então pesquisar `phone` já
atende o requisito de busca por WhatsApp. Índices existentes
(`idx_citizens_name`, `idx_citizens_phone`, `idx_citizens_neigh`) são
suficientes: **nenhum índice novo, nenhuma migration**.

### Endpoint criado
`GET /citizens/lookup` → `CitizenController@lookup`, registrado **antes** das
rotas com parâmetro (comprovado com o Router real: `/citizens/lookup` não é
sombreada por `/citizens/{id}/edit`; não existe `GET /citizens/{id}`).

Proteção: autenticado (`AuthMiddleware::require`) + `citizens.view` + tenant
atual + `deleted_at IS NULL`. A permissão é verificada no corpo do método (em
vez de `requirePermission`) só para devolver **JSON** a um consumidor `fetch`,
em vez de uma página HTML 403.

Dois modos sobre a mesma consulta parametrizada:
- `mode=suggest` (autocomplete): 12 resultados, **sem `COUNT`** — pede
  `perPage+1` linha e detecta excesso pela linha sobrando;
- `mode=search` (modal): 20 por página **com** total, para paginar.

Exige 2+ caracteres (ou, no modal, ao menos um filtro territorial). Sem isso a
resposta seria "os primeiros N cidadãos" — o carregamento em massa que este
hotfix elimina.

### `Citizen::lookup()` — SQL
Pesquisa `name`, `nickname`, `phone` e `email`. Quando o termo contém 2+
dígitos, o telefone também é comparado **só por dígitos**
(`REPLACE(REPLACE(REPLACE(REPLACE(phone,'(',''),')',''),'-',''),' ','')`), de
modo que `3432324545` encontre `(34) 3232-4545` independentemente da máscara —
mesmo tratamento já usado na checagem de duplicidade do cadastro rápido.

Território por `LEFT JOIN neighborhoods` + `LEFT JOIN regions`; a cidade efetiva
é `COALESCE(NULLIF(c.city,''), n.city)`. Cidadão sem bairro continua aparecendo
(LEFT JOIN), com campos territoriais nulos.

**Curingas do LIKE escapados** (`addcslashes($q, '%_\\')`): o termo digitado é
tratado como texto literal — sem isso, `%` sozinho casaria com a base inteira.
Isso é semântica de busca; a proteção contra injeção já vem do placeholder.

100% parametrizado: nenhum valor de entrada entra cru no SQL. `LIMIT`/`OFFSET`
são os únicos interpolados e são inteiros validados (`perPage` teto 50).

### Interface
O `<select>` pré-carregado deu lugar a **input de busca + `hidden citizen_id`**.
O backend da demanda continua recebendo `citizen_id` exatamente como antes.

`[ Digite nome, apelido ou telefone... ] [ 🔍 Procurar ] [ ＋ Novo ]`

- **Autocomplete**: dispara com 2+ caracteres, debounce de 300 ms, número de
  sequência descartando respostas fora de ordem (race condition), navegação por
  teclado (↑/↓/Enter/ESC). Cada opção mostra **nome (apelido), telefone e
  bairro • cidade • região**. Com excesso de resultados exibe "Muitos resultados
  encontrados. Continue digitando ou use Procurar."
- **Modal "Procurar"**: filtros por nome/apelido/telefone, **bairro, cidade e
  região** (listas oficiais, sem texto livre), 20 por página com "Carregar
  mais" acumulativo e contador "Exibindo X de Y". Fecha por X, ESC, clique fora
  e após selecionar.
- **Cidadão escolhido**: exibido em destaque com telefone e território, e botão
  **Trocar** que desfaz o vínculo e devolve o campo de busca.

"Localização avançada" significa **buscar** o cidadão por informações
territoriais. A localização da OCORRÊNCIA nunca é tocada: coordenadas do
cidadão não são copiadas para a demanda e o bairro do cidadão não é vinculado
ao bairro da ocorrência — comprovado em DOM real.

### Integração com `＋ Novo` (preservado)
O modal de cadastro rápido segue intacto (16.1/16.1.1/16.1.2). Apenas o `pick()`
foi reapontado: como o `<select>` não existe mais, a seleção passa pela função
compartilhada `window.sggCitizenPick(id, name, meta)`. `quickStore` passou a
devolver também `meta` (telefone • bairro • cidade • região) via
`Citizen::lookupOne`, para o seletor exibir o recém-cadastrado já identificado.
Como ele já está no banco, é imediatamente encontrável pelo autocomplete, sem
recarregar a página. O `[Selecionar]` da lista de duplicados também foi
enriquecido com telefone e bairro.

### Erro de validação da demanda
`viewData()` deixou de carregar qualquer lista. Quando a Demanda Rápida volta
com erro, `QuickDemandController::selectedCitizen()` busca **uma única linha**
por ID (`Citizen::lookupOne`, escopada por tenant e `deleted_at IS NULL`) e a
tela reexibe o cidadão escolhido — vínculo preservado, sem recarregar lista.

### Degradação por permissão
Sem `citizens.create`: some o `＋ Novo`; busca e modal seguem funcionando (o CSS
compartilhado do overlay, incluindo o fix 16.1.1 `.qc-overlay[hidden]`, é
declarado no bloco alternativo). Sem `citizens.view`: campo desabilitado com
aviso, `＋ Novo` preservado. Sem nenhuma das duas: formulário da demanda e
câmera intactos.

### Performance (medida com 5.208 cidadãos)
| | Antes (16.1.5) | Depois (16.1.6) |
|---|---|---|
| Consulta na abertura | 5,5 ms / 500 linhas | **0 consultas / 0 linhas** |
| HTML de `<option>` | 23,3 KB | **0 KB** |
| Cidadãos inalcançáveis | 4.708 (90,4%) | **0 (100% pesquisável)** |
| Autocomplete | — | 4,7 ms (média) |
| Modal 1ª página + total | — | 8,3 ms |
| Reexibir após erro | — | 0,4 ms (1 linha) |

`EXPLAIN` confirma uso de `idx_citizens_name`. Escala sem alterar `LIMIT`:
o custo por interação independe do tamanho da base.

### Testes (PHP 8.3.6 + MariaDB 10.11, banco real)
`php -l` 5/5 e `node --check` OK. **231 casos, 231 PASS**: busca por nome,
apelido e telefone (com e sem máscara); exibição de nome/telefone/bairro/
cidade/região; homônimos distinguíveis; nenhum resultado; debounce (4 teclas →
1 requisição); race condition (resposta antiga descartada); falha de rede;
modal Procurar com filtros de bairro/cidade/região e combinações; paginação
acumulativa sem sobreposição; seleção, remoção e troca; `＋ Novo` com seleção
automática e duplicidade; erro de validação preservando o cidadão; preservação
de título, descrição, endereço, bairro da ocorrência, origem, categoria,
secretaria, responsável, coordenadas, fotos e vídeos; 5.208 cidadãos;
`citizens.view` e `citizens.create`; isolamento por tenant (bairro, região e
cidade de outro tenant descartados); SQL parametrizado e payloads de injeção;
mobile (alvos ≥44px, quebra em duas linhas, sem scroll horizontal, safe-area).

### Câmera
**Byte a byte idêntica**: bloco HTML md5 `dbaf9634c9a664acee62b517b80d4f0c`
(3.180 bytes) e JS md5 `43b74ccfb6d9997c87193f9b8c725abb` (7.300 bytes). Os 4
inputs (`#cameraPhotoInput`, `#cameraVideoInput`, `#galleryInput`, `#mediaFiles`
único portador de `name="media[]"`) e o DataTransfer inalterados.

### Não-regressão
`diff -rq` recursivo: **apenas 5 arquivos diferem** (+ VERSION/CHANGELOG).
`config.php`, `.htaccess`, `sgg_storage`, `uploads`, assets, migrations,
`database.sql`, `manifest.json` e `sw.js` **byte a byte idênticos**.
`Citizen::paginate()` (16.1.5) byte a byte; regra de WhatsApp
(`consent_contact=1 AND whatsapp_opt_out=0`) não invertida; nenhuma rota
removida; 9 métodos do `CitizenController` e 3 do `QuickDemandController`
inalterados.

---

## Hotfix 16.1.5 — Filtros avançados na listagem de cidadãos

**Escopo:** `Models/Citizen.php`, `Controllers/CitizenController.php`,
`Views/citizens/index.php`, `public/assets/css/app.css`.
**Sem migration, sem alteração de banco, sem nova tela.**

### Auditoria prévia
Listagem: rota `GET /citizens` → `CitizenController@index` → `Citizen::paginate`
→ `Views/citizens/index.php`. A busca atual cobre `name`, `phone`, `email` via
`LIKE` parametrizado, escopada por `tenant_id` e `deleted_at IS NULL`. Colunas
**reais** confirmadas no schema (`database.sql`): `neighborhood_id` (FK →
`neighborhoods`), `is_community_leader TINYINT(1)`, `consent_contact TINYINT(1)`,
`whatsapp_opt_out TINYINT(1)`. Índices **já existentes**: `idx_citizens_tenant`,
`idx_citizens_neigh`, `idx_citizens_leader`, `idx_citizens_optout`,
`idx_citizens_name` (ordenação). **Nenhum índice novo criado** — os booleanos de
baixa seletividade (`consent_contact`) não justificam índice isolado.

### Filtros adicionados (backend, parametrizados)
Combinam com a busca textual atual via `AND`, todos com placeholders (nenhum valor
de `GET` entra cru no SQL):
- **Bairro** (`neighborhood_id`): inteiro validado no controller —
  `Citizen::neighborhoodExists()` confirma existência **e** tenant; ID
  inexistente/forjado é descartado. Lista oficial de `neighborhoods` (sem texto
  livre, sem criar bairro).
- **Liderança comunitária** (`community_leader`): `''` todas | `1` só lideranças |
  `0` só não-lideranças → `is_community_leader = ?`.
- **Consentimento de contato** (`contact_consent`): `''` | `1` | `0` →
  `consent_contact = ?`. **Independente** do opt-out de WhatsApp.
- **WhatsApp** (`whatsapp_status`): `allowed` → `consent_contact = 1 AND
  whatsapp_opt_out = 0` (regra segura, **não** invertida); `optout` →
  `whatsapp_opt_out = 1`.

### Consentimento × opt-out (regra preservada)
`consent_contact` é o consentimento **geral** de contato (LGPD). `whatsapp_opt_out
= 1` significa "não deseja WhatsApp"; `= 0` **não** autoriza WhatsApp
automaticamente. Por isso "Pode receber WhatsApp" exige consentimento **e**
ausência de opt-out. Os dois filtros são independentes na consulta.

### Interface
Busca atual mantida no topo. Botão **Filtros** (`aria-expanded`/`aria-controls`)
abre área recolhível com os quatro selects (desktop: 4 colunas; mobile: 1 coluna,
campos e botões ≥44px, sem rolagem horizontal). Botões **Filtrar** e **Limpar
tudo**. Chips de **filtros ativos** com remoção individual (preservando os demais
parâmetros) e link **Limpar filtros**. Contador passa a exibir o **total filtrado**
("N cidadão(s) encontrado(s)"), e a paginação usa esse total.

### Coluna "Contato" (badges separados)
Deixa de exibir "Autoriza" como se representasse WhatsApp. Agora:
"Contato autorizado" / "Sem consentimento" e, separadamente, "WhatsApp permitido"
(consent + sem opt-out) ou "Não enviar WhatsApp" (opt-out).

### URL e paginação
Parâmetros `GET`: `q`, `neighborhood_id`, `community_leader`, `contact_consent`,
`whatsapp_status`, `page`. Paginação preserva todos os filtros
(`http_build_query`); trocar de filtro reenvia o formulário (volta à página 1);
o botão Voltar do navegador restaura o estado (tudo via GET).

### Permissões
Inalteradas: listagem exige `citizens.view`; Editar/Excluir seguem
`citizens.update`/`citizens.delete`. Nenhuma permissão nova.

### Retrocompatibilidade
`Citizen::paginate()` aceita `array $filters` **ou** `string` (tratada como `q`),
preservando qualquer chamada antiga. Único chamador real (`CitizenController`)
atualizado.

### Testes executados (MariaDB 10.11 + PHP 8.3)
`php -l` OK nos 3 arquivos PHP. 21/21 casos de consulta PASS (busca, cada filtro,
combinações, busca+filtros, combinação vazia, isolamento por tenant, parâmetros
inválidos ignorados). Paginação com filtro preservada entre páginas, sem
sobreposição. `EXPLAIN` confirma uso de índice existente. Retrocompat da
assinatura validada.

---

## Hotfix 16.1.3 — Atualização do Modelo de Importação de Cidadãos

**Escopo:** `Controllers/CitizenImportController.php` e `Views/citizens/import.php`.
**Sem migration, sem alteração de banco, sem nova tela ou rotina de importação.**

### Auditoria prévia
`citizens` já possuía `latitude`, `longitude` e `whatsapp_opt_out` — este último é
o **nome real** do campo (confirmado no schema e no cadastro individual). O modelo
gerava 17 colunas; o importador **ignorava coordenadas** e gravava
`whatsapp_opt_out` **fixo em 0**. Formato do modelo: **CSV** (sem Composer/XLSX),
mantido conforme a instrução.

### Modelo (20 colunas)
`nome; apelido; telefone; whatsapp; email; instagram; data_nascimento; cep;
endereco; numero; complemento; bairro; cidade; latitude; longitude; origem;
lideranca_comunitaria; consentimento_contato; whatsapp_opt_out; observacoes`

Exemplo: `Maria da Silva;Maria;34999990000;34999990000;maria@email.com;@maria;15/03;
38400000;Rua das Flores;123;Apto 2;Centro;Uberlândia;-18.9186;-48.2772;Importação;
nao;sim;nao;Apoiador` — **data sem ano**, conforme `birth_day`/`birth_month`.
Instruções objetivas vão no próprio CSV em linhas `#`, que o parser ignora.

### Compatibilidade com planilhas antigas
A constante foi dividida: `HEADERS` (modelo, 20) × **`REQUIRED_HEADERS` (validação,
as 17 originais)**. As três colunas novas são **opcionais** — arquivos antigos
importam normalmente, com coordenadas `NULL` e opt-out no padrão seguro `0`.

### Regras implementadas
- **latitude/longitude:** opcionais; negativo e ponto/vírgula decimal; espaços
  removidos; limites −90..90 e −180..180; **par completo obrigatório** (isolada é
  rejeitada); texto/data inválidos recusados; vazio ⇒ `NULL` sem bloquear a importação.
- **whatsapp_opt_out:** `sim/1/true/yes` ⇒ 1 (não deseja WhatsApp); `nao/0/false/no`
  ⇒ 0; vazio ⇒ 0; valor desconhecido ⇒ erro por linha. **Independente** do
  consentimento LGPD.
- **Aniversário:** combinações impossíveis passam a ser rejeitadas (31/02, 31/04,
  00/10, 15/13) via `checkdate()`; 29/02 aceito; `15/03/1980` segue aceito com o
  ano descartado.
- **Bairro:** apenas associado à lista oficial; nunca criado automaticamente.
- **consent_logs:** LGPD como `granted` e opt-out como `updated`, ambos canal
  `importacao`.
- **Segurança:** neutralização de CSV/Formula injection (`=`, `+`, `-`, `@`) nos
  campos de texto livre.

### Mensagens de erro por linha
`latitude inválida` · `latitude fora do limite permitido (-90 a 90)` ·
`longitude fora do limite permitido (-180 a 180)` · `latitude informada sem
longitude` · `valor inválido em whatsapp_opt_out. Use sim ou nao` ·
`data de nascimento inválida (dia incompatível com o mês)` · `bairro "X" não encontrado`.

### Testes executados
`php -l` (156 arquivos) e `node --check`: 0 erros. Suíte com verificação explícita
(descoberto que `zend.assertions=-1` desabilita `assert()` — testes refeitos com
checagens reais): cabeçalho 20/17 ✓ · planilha nova com coords e opt-out ✓ ·
**planilha antiga aceita** ✓ · 11 variações booleanas ✓ · opt-out inválido ✓ ·
5 casos de coordenada + vírgula decimal ✓ · 4 datas impossíveis + 29/02 ✓ ·
bairro inexistente sem criar ✓ · linhas `#` ignoradas ✓ · CSV injection ✓.
**Persistência real no banco** (lat/lon/opt-out/consent/liderança/dia-mês/bairro)
e **2 registros em `consent_logs`** com canal `importacao` ✓.

---

## Hotfix 16.1.2 — Complementação do Cadastro Rápido de Cidadão

**Escopo:** `Views/demands/quick.php` (modal ampliado + CSS + wiring JS) e
`Controllers/CitizenController.php` (`validate()` compartilhado reforçado).
**Sem migration, sem alteração de banco, sem novo endpoint.**

### Auditoria prévia (comprovação técnica)
Comparação campo a campo entre `citizens/form.php`, o modal e a tabela
`citizens`: **todas as colunas já existiam** (`cep`, `city`, `birth_day`,
`birth_month`, `consent_contact`, `whatsapp_opt_out`, `latitude`, `longitude`,
`email`) e **`collect()` já coletava os 21 campos**. O endpoint já aceitaria
tudo — faltavam apenas os inputs no HTML. Por isso: nenhuma migration.

### Implementação
- **Paridade total (19 campos):** verificado programaticamente que nenhum campo
  do cadastro oficial está ausente no modal. Mesmos `name=`, mesmas colunas,
  mesmo model, mesmo controller, mesmas validações — sem estrutura paralela.
- **Layout em seções:** Dados do cidadão · Contato · Endereço e território ·
  Aniversário · Consentimentos · Localização do cidadão · Observações. Grade de
  2 colunas no desktop, 1 no mobile; inputs ≥44px; scroll interno; safe-area.
- **CEP:** reutiliza `sggCep()` (mesmo módulo do cadastro completo), com IDs do
  modal. Cidade "Uberlândia" readonly (igual à ficha) — `cep.js` não sobrescreve
  campos readonly. Bairro pelo `select` oficial, selecionado só quando há
  correspondência; nunca cria bairro novo. Falha de CEP não impede o cadastro.
- **Aniversário sem ano:** dois selects (dia/mês); preenchimento parcial segue
  permitido. Adicionada validação de combinação impossível ao `validate()`
  **compartilhado** (via `checkdate()` do core — aceita 29/02), beneficiando
  também o cadastro completo.
- **Liderança comunitária / LGPD / opt-out:** checkboxes nos campos já
  existentes, desmarcados por padrão; LGPD e opt-out permanecem independentes;
  consentimento registrado em `consent_logs` pelo fluxo normal.
- **Localização do cidadão:** botão "📍 Usar minha localização" reutilizando
  `sggGeolocate()` com IDs próprios (`qc_latitude`/`qc_longitude`). Como a
  função só escreve nos IDs recebidos por config, a localização da **demanda**
  (`occur_latitude`/`occur_longitude`) é intocada — **zero colisão de IDs**
  entre modal e formulário da demanda (verificado programaticamente).
- **Segurança (servidor, não só JS):** limites de latitude (−90..90) e longitude
  (−180..180); `neighborhood_id` precisa existir no tenant (bloqueia ID
  forjado); `maxlength` nos campos; CSRF e `citizens.create` mantidos.

### Testes executados (PHP CLI + MariaDB 10.11)
`php -l` (156 arquivos) e `node --check`: 0 erros. 29/02 aceito ✓ · 31/02
rejeitado ✓ · preenchimento parcial preservado ✓ · lat 99 e lon −200 bloqueados
✓ · bairro forjado rejeitado ✓ · persistência de cep/city/endereço/nº/compl/
bairro/dia/mês/LGPD/opt-out/liderança/lat/lon ✓ · `consent_logs` registrado ✓ ·
LGPD e opt-out independentes ✓ · paridade de campos: nenhum ausente ✓ · fix
16.1.1 preservado ✓ · câmera (JS e inputs) byte a byte idêntica ✓.

---

## Hotfix 16.1.1 — Correção definitiva do modal "Novo Cidadão"

**Escopo:** apenas `Views/demands/quick.php` (**1 regra de CSS**). Sem migration,
sem banco, sem controllers/models/permissões, **sem nenhuma alteração de
JavaScript** — o JS da câmera e o JS do modal permanecem byte a byte idênticos
à base (comprovado por comparação programática).

### Sintoma
Ao abrir a Demanda Rápida, o modal "Novo Cidadão" já aparecia sozinho; depois de
aberto, X/Cancelar/ESC/clique fora não o fechavam.

### Investigação (sem presumir a causa)
Auditados HTML, CSS, JS, ordem de carregamento, eventos, atributo `hidden`,
`display`, z-index e conflitos globais. Constatado: `open()` só é chamado no
clique (sem execução automática); nenhum CSS global define `.qc-*` nem
`[hidden]`; z-index sem conflito; `qcMsg`/`qcDups` (que também usam `hidden`)
funcionavam — pista decisiva: as classes deles **não declaram `display`**.

### Causa raiz
O atributo `hidden` oculta via folha de estilo do **navegador**
(`[hidden]{display:none}`). Na cascata CSS, **declarações de autor vencem as do
navegador independentemente de especificidade**. O `.qc-overlay{display:flex}`
(autor) sobrepunha o `display:none` do UA — com `hidden` presente ou não. Por
isso o modal (1) nascia visível e (2) nunca fechava: o JS setava `hidden=true`
corretamente, mas o CSS mantinha `display:flex`.

### Correção (causa raiz, mínima)
```css
.qc-overlay[hidden]{display:none}
```
Origem autor + especificidade (0,2,0) > (0,1,0): vence sem `!important`, sem
workaround e sem `setTimeout`. O atributo `hidden` volta a comandar o estado e
todo o JS existente passa a funcionar como projetado (abrir no botão; fechar por
X, Cancelar, ESC, clique no overlay, pós-cadastro e pós-seleção; scroll
restaurado em `close()`; sem elemento invisível bloqueando a tela).

### Prova empírica (DOM real via jsdom, CSS extraído do próprio arquivo)
- Carga da página (`hidden`): `display=none` → **oculto** ✓
- Abrir (`hidden=false`): `display=flex` ✓
- Fechar (`hidden=true`): `display=none` ✓
- Reabrir+fechar: estado estável `none` ✓
(Antes do fix, o mesmo teste reproduzia o bug: `display=flex` mesmo com `hidden`.)

### Garantia de não-regressão
Mudança de 1 seletor novo, escopado a `.qc-overlay[hidden]` — não afeta nenhum
outro elemento. `php -l` (156 arquivos) e `node --check`: 0 erros. Câmera e
formulário da demanda intocados; dados da demanda preservados por construção
(modal fora do `#quickForm`, sem reload/navegação).

---

## Hotfix 16.1 — Cadastro Rápido de Cidadão na Demanda Rápida (modal AJAX)

**Escopo:** `Controllers/CitizenController.php` (novo `quickStore` + `findSimilar`),
`Controllers/QuickDemandController.php` (2 variáveis de view), `app/routes.php`
(1 rota), `Views/demands/quick.php` (botão + modal + CSS + JS isolados).
**Sem migration; sem mudança de banco.** Cadastro completo de cidadãos, tela de
cidadãos, fluxo da demanda e arquitetura da câmera: intocados (JS e inputs da
câmera byte-idênticos à base).

### O que faz
- Botão **"＋ Novo"** ao lado do select Cidadão (só com `citizens.create`;
  caso contrário o botão e o modal nem são renderizados).
- Modal abre **sem sair da tela** (sem redirect/reload). Fica **fora** do
  `#quickForm`, com form próprio — nenhum campo da demanda é tocado ou perdido.
- Envio via **fetch AJAX** para `POST /citizens/quick-store` (CSRF verificado,
  RBAC `citizens.create`). Resposta JSON.
- **Reuso total**: `collect()` + `validate()` + `Citizen::create` + `Audit::log`
  + `ConsentLog::fromChange` — as mesmas validações e persistência do cadastro
  normal; nenhuma segunda estrutura. Campos = os existentes na ficha (a ficha
  não possui Instagram/Facebook; não foram criados, cumprindo "não criar segunda
  estrutura" e "não alterar o cadastro completo").
- **Validações do fluxo rápido:** nome obrigatório (padrão) + telefone
  obrigatório; e-mail/nascimento validados pelo `validate()` existente.
- **Duplicidade** antes de criar: telefone comparado **só por dígitos**
  (máscaras diferentes batem) OU nome+bairro. Encontrando semelhantes, mostra
  lista com **[Selecionar]** (usa o existente e fecha) ou **[Cadastrar mesmo
  assim]** (`force=1`).
- **Após salvar:** "✓ Cidadão cadastrado com sucesso.", `<option>` inserida no
  select, novo cidadão **selecionado automaticamente**, modal fecha, usuário
  segue exatamente onde estava.
- **Mobile (Samsung A07):** modal em tela cheia (`100dvh`), inputs ≥44px,
  botões grandes, safe-area inset, scroll interno com `-webkit-overflow-scrolling`.

### Testes executados (PHP CLI + MariaDB 10.11)
- `php -l` em 156 arquivos e `node --check` (2 blocos JS da quick.php +
  assets): 0 erros.
- Dedup por telefone: `34999990000` encontra `(34) 99999-0000` ✓; por
  nome+bairro com telefone diferente ✓; sem falso positivo p/ cadastro
  distinto ✓; candidato aparece 1× mesmo batendo pelos dois critérios ✓.
- Câmera: JS e inputs byte-idênticos à base; formulário da demanda inalterado
  (modal fora do form). Rota estática registrada antes de `/citizens/{id}`.

---

## Bloco 16 — Consolidação da Plataforma / Auditoria Geral (rumo à v1.0)

Auditoria técnica completa do SGG como Arquiteto/Tech Lead/DBA/Segurança/QA/UX.
**Não cria módulos, não adiciona funcionalidades e não altera regras de negócio.**
Somente melhorias de ganho técnico real, sem regressão. **Alterados:**
`Controllers/MediaController.php`, `Controllers/AuthController.php`,
`Controllers/CommunicationController.php`, `Models/Communication.php`,
`VERSION.txt`, `CHANGELOG.md`, `MIGRATIONS.md`, `README_INSTALACAO.txt`.

### Melhorias implementadas
- **Segurança — path traversal (defesa em profundidade):** `MediaController::download`
  e `::raw` passam a validar o caminho servido com `realpath` + `Storage::isSafe()`,
  confinando à raiz de storage (mesma proteção já presente em `downloadAttachment`).
  Arquivos legítimos continuam servidos normalmente; caminhos fora da raiz são
  bloqueados. Testado: arquivo válido → servido; `../../etc/passwd` → bloqueado.
- **Auditoria — login malsucedido:** `AuthController::login` passa a registrar
  `login_failed` (apenas o e-mail tentado, nunca a senha) para observabilidade de
  segurança / detecção de brute-force. Login bem-sucedido e logout já eram logados.
- **Performance — N+1 na Central de Comunicação:** a listagem buscava 2 queries
  por item (miniatura Antes/Depois) → 2×N. Novo `Communication::beforeAfterThumbs()`
  busca todas as miniaturas da página em **2 queries** (uma por estágio), com
  preferência por imagem. Resultado equivalente/superior, muito menos consultas.

### Auditoria — pontos verificados e já conformes (sem alteração)
- **CSRF:** 81 rotas POST e 81 `Csrf::verify()` — cobertura 100%.
- **SQL injection:** consultas usam placeholders `?`; `$where`/`$base` são SQL
  estático; `SystemList` valida tabela por whitelist. Sem interpolação de input.
- **XSS:** saída escapada por `View::e()` (helpers `$val`/`$err` escapam internamente).
- **Uploads:** MIME real por `finfo`, whitelist de extensão/MIME, limite de tamanho,
  checagem extensão×MIME. `.php` renomeado é rejeitado.
- **Headers/sessão:** X-Frame-Options, nosniff, Referrer-Policy, CSP; cookies
  httponly + samesite=Lax + secure.
- **RBAC:** todas as rotas protegidas por `requirePermission`/`require`; perfil/conta
  são self-service via `Auth::id()` (nunca aceitam id da URL); branding/manifest
  são assets públicos por design.
- **Migrations:** 21 migrations idempotentes; banco vazio → `database.sql` →
  migrations → 59 tabelas (rodadas 2× sem divergência).
- **Auditoria de ações:** login/logout/CRUD/upload/download/status/atribuição/
  publicação cobertos.

### Sugestões (NÃO implementadas — fora do escopo/risco)
- `.htaccess` de produção tem `display_errors On` (gerado pelo cPanel); recomenda-se
  `Off` em produção — mas o arquivo é gerido pela hospedagem e foi preservado.
- Extração futura de componentes de view (cards/badges/filtros) em partials
  reutilizáveis; não feito agora para não arriscar regressão visual.

### Testes executados
- `php -l` em 156 arquivos e `node --check` nos JS: 0 erros.
- DB: instalação limpa + todas migrations (idempotência dupla). Fluxo de
  comunicação (paginate + thumbs em lote) funcional. `isSafe` valida arquivo bom
  e bloqueia traversal. `quick.php` idêntico; JS da câmera A07 byte-idêntico.

---

## Bloco 15 — Central de Comunicação e Marketing

**Escopo (novos):** `migrations/021_communication.sql`, `Models/Communication.php`,
`Controllers/CommunicationController.php`, `Views/communication/{index,show,dashboard}.php`.
**Escopo (alterados):** `routes.php`, `Views/layouts/app.php` (menu lateral + mobile),
`Controllers/DemandController.php` (hook de encaminhamento), `Views/demands/show.php`
(checkbox "Encaminhar para Comunicação"), `database.sql` (schema para install novo),
`VERSION.txt`, `CHANGELOG.md`, `MIGRATIONS.md`.

### Arquitetura
Módulo de comunicação institucional **desacoplado** da demanda, com estrutura,
status, permissões e auditoria próprios. A demanda continua responsável pelo
atendimento; a Comunicação, pela comunicação institucional. Dados de bairro/
região/solicitante/secretaria são lidos por JOIN (não duplicados); apenas um
snapshot mínimo (protocol/title) é guardado. Nomeado `communication_*` (não
`marketing_*`) para escalar a eventos, visitas, obras, campanhas etc. Não
implementa publicação automática, integrações (Instagram/Facebook) nem IA —
apenas a arquitetura.

### Tabelas (migration 021, idempotente)
- `communication_items` — 1 por encaminhamento; FK `demand_id` com **UNIQUE**
  (nunca duplica), status próprio, responsável, quem encaminhou, notas internas,
  `published_at` (tempo médio).
- `communication_statuses` — 10 status configuráveis (Recebido…Cancelado).
- `communication_status_history` — auditoria de status/eventos.
- `communication_checklists` — checklist de produção (11 itens padrão) + progresso.
- `communication_publications` — publicações por canal (status/data/usuário/link).
- `communication_assignments` — histórico de atribuições.

### Permissões (próprias, 6)
`communication.view/create/update/assign/publish/archive`, seedadas a admin e
chefe_gabinete (todas), marketing (todas menos create), equipe_rua (view+create),
vereador (view). Não reutiliza permissões de demanda.

### Fluxo
Equipe de Rua conclui a Situação final e marca "Encaminhar para Comunicação" →
cria item (registra usuário/data/protocolo/vínculo). Tela principal com cards
Antes/Depois. Status próprios que **não alteram** o status da demanda. Checklist
com progresso. Responsável atribuível. Publicações por canal com link.
Observações internas (só Comunicação). Dashboard com indicadores, alertas e
filtros (bairro/região/secretaria/responsável/status/período/publicado/antes/
depois/vídeo/pendente de mídia). Auditoria de todas as ações.

### Testes executados (PHP CLI + MariaDB 10.11)
- `php -l` em 156 arquivos e `node --check` nos JS: 0 erros.
- Migration 021 em banco limpo e existente; idempotente (6 tabelas, 10 status,
  6 permissões estáveis; no-op contra database.sql atualizado).
- Encaminhamento cria 1 item e **não duplica** a demanda (2ª chamada é no-op).
- Checklist 11 itens com progresso; status próprio isolado do status da demanda;
  responsável + auditoria; publicação por canal + link + `published_at`;
  dashboard (antes/depois/vídeo/publicado) e filtros (after/video/status/published).
- Permissões por papel conferidas. `quick.php` idêntico; JS/inputs da câmera A07
  byte-idênticos. Módulos existentes sem regressão.

---

## Hotfix — Complementação do Registro de Evolução da Demanda

**Escopo:** `app/Views/demands/show.php`, `DemandController@show` +
`@storeFinalSituation`, `Demand` (dois helpers novos). **Sem mudança de banco**
(nenhuma migration nova; `demand_stage` intacto). Câmera A07, galeria, preview,
DataTransfer, storage e Demanda Rápida inalterados (JS e inputs da Situação
final byte-idênticos à base; `quick.php` idêntico).

### 1. Resultado da demanda (status configuráveis)
Seletor "Resultado da demanda" na Situação final, alimentado por
`Demand::finalStatusOptions()`, que lê os status configuráveis
(`demand_statuses`) e exibe só desfechos relevantes: os marcados `is_final=1`
(ex.: Resolvida, Arquivada), `Aguardando retorno`, e `Parcialmente/Não
resolvida` **quando cadastrados**. Opções triviais (Nova, Em análise) são
omitidas. Padrão "Manter status atual". Ao escolher outro status, o registro
chama `Demand::changeStatus` (histórico) e `Audit::log('change_status', ...)`.
Nenhum campo duplicado — reusa a tabela de status existente.

### 2. Observação final visível
`Demand::latestFinalSituation()` recupera a observação mais recente e a seção
Situação final passa a exibir **texto, data e usuário** diretamente, sem
obrigar a procurar no histórico. Registros só-mídia (sem texto) não exibem bloco
de observação vazio.

### 3. Metadados das mídias finais
Cada mídia final exibe título, data/hora, usuário que enviou e descrição
(quando houver), em lista com layout **compacto no mobile** (`@media
max-width:600px`).

### 4. Observação individual editável
Link **"Editar observação"** por mídia, apontando para a tela de edição de mídia
já existente (`/media/{id}/edit`), que edita `media_files.description` (com
guarda de propriedade no `MediaController`). **Sem coluna nova.**

### 5. Permissões (correção)
`storeFinalSituation` passa a exigir **`media.upload` + `media.link`** (enviar +
vincular mídia) e **não exige mais `demands.update`**. A Equipe de Rua
(`equipe_rua`), que já possui essas permissões, volta a conseguir registrar
evolução sem receber acesso de edição total da demanda. O seletor/atualização
de status só é oferecido e aplicado a quem tem `demands.update`. Nenhuma
permissão nova criada.

### 6. Flash correto
Permissão negada agora responde `Response::forbidden` (403); mensagens de falha
usam `Session::flash('error', ...)` (tipo já existente) em vez de `'success'`.
A tela da demanda passa a renderizar `alert--error`.

### 7. Limite de mídias
A Situação final carrega até 200 mídias por estágio e, se houver mais, exibe
"Exibindo X de Y — Ver todas na galeria" — sem esconder mídia silenciosamente.

### Testes executados (PHP CLI + MariaDB 10.11)
- `php -l` em 151 arquivos: 0 erros. `node --check` nos JS: 0 erros.
- `finalStatusOptions`: retorna Resolvida/Aguardando retorno/Arquivada; exclui
  Nova/Em análise; inclui "Parcialmente resolvida" quando cadastrada.
- Fluxo situação final: observação extraída com autor e data; status muda para
  `resolvida` e registra histórico `em_andamento → resolvida`.
- Situação final só com observação: status mantido. Registro só-mídia: sem
  observação fantasma.
- `equipe_rua` tem `media.upload`+`media.link` e **não** `demands.update` →
  pode registrar evolução; seletor de status não aparece. `visualizador`
  (sem media perms) é bloqueado (403).
- `quick.php` idêntico à base; JS/inputs da câmera da Situação final idênticos.
- Documentos/anexos inalterados e funcionando.

---

## Bloco — Registro de Evolução da Demanda (Situação inicial / Situação final)

**Escopo:** tela da demanda (`app/Views/demands/show.php`), `DemandController`,
`MediaFile`, rota e migration 020. Demanda Rápida (`quick.php`), câmera A07,
RBAC, storage externo, mapa, Waze, agenda, tarefas e comentários: auditados e
inalterados (`quick.php` comprovadamente idêntico à base por `diff`).

### O que mudou
- "Mídias vinculadas" → **Situação inicial** (mídias de estágio `initial`).
  Mídias antigas sem classificação passam a `initial` via backfill (migration
  020), continuando visíveis nessa seção.
- Nova seção **Situação final** (mídias de estágio `final`): Tirar foto, Gravar
  vídeo, Escolher da galeria, preview, remoção individual e observação da
  situação final. Reutiliza **exatamente** a arquitetura estática de câmera do
  Samsung A07: inputs especializados (`#fs_cameraPhotoInput` image/* capture
  sem multiple; `#fs_cameraVideoInput` video/* capture sem multiple;
  `#fs_galleryInput` image/*,video/* multiple sem capture) + input final único
  `#fs_mediaFiles` (`name="media[]"`) sincronizado por DataTransfer; acionamento
  por `<label for>`; ocultação por `.file-input-visually-hidden`; preview via
  FileReader→dataURL (imagem) e ObjectURL+revoke (vídeo). **Nunca** input único
  com `capture` dinâmico.
- Título das mídias finais: `PROTOCOLO — Situação final — TÍTULO` (reusa
  `MediaFile::buildDemandMediaTitle`, idempotente — não duplica protocolo).
- Observação geral da situação final gravada no **histórico auditável** da
  demanda (`Demand::addComment` + `Audit::log`), sem coluna nova. Descrição
  individual reutiliza `media_files.description`.
- Upload de **Anexos** movido para dentro de **Documentos** (tabela/arquivos de
  attachments preservados). Documentos passa a oferecer "Gerar documento desta
  demanda" e "Adicionar documento/arquivo". A seção isolada "Anexos" deixa de
  existir.
- Registro de usuário, data, estágio e auditoria em cada envio de situação
  final (`Audit::log('upload'/'final_situation', ...)`).

### Banco
- **Migration 020** (`020_media_demand_stage.sql`): coluna
  `media_files.demand_stage VARCHAR(20) NOT NULL DEFAULT 'initial'` + índice
  `idx_media_demand_stage (demand_id, demand_stage)`. **VARCHAR, não ENUM**
  (compatibilidade MySQL 5.7/8.0+ e MariaDB 10.3+; extensível sem ALTER de
  tipo). Idempotente (INFORMATION_SCHEMA para coluna/índice; backfill só toca
  mídias vinculadas a demanda sem estágio válido). `database.sql` atualizado
  para instalações novas.

### Testes executados (PHP CLI + MariaDB 10.11)
- `php -l` em 151 arquivos PHP: sem erros. `node --check` nos JS: sem erros.
- Migration 020 em banco limpo e em banco existente; idempotência provada
  (rodada 2×, coluna/índice permanecem 1). Backfill: mídia antiga → `initial`.
- Estágios não se misturam: `initial` e `final` isolados por consulta.
- Foto e vídeo finais salvos como `final`; observação persiste em `description`
  e no histórico. Título idempotente (protocolo aparece 1×).
- `quick.php` idêntico à base (câmera da Demanda Rápida intacta).

---

## Hotfix crítico 14.2.2 — Câmera Android (Samsung A07) na Demanda Rápida

**Escopo:** somente `app/Views/demands/quick.php` (frontend). Backend, headers,
service worker, CSS/JS globais: auditados e inalterados.

### Causa técnica
A versão anterior usava UM input reutilizado, alternando `capture`/`multiple`
via JavaScript imediatamente antes de `input.click()`. Aparelhos Samsung
(Chrome Android e Samsung Internet, inclusive PWA) não respeitam de forma
confiável o `capture` adicionado dinamicamente, e `accept="image/*,video/*"`
não expressa intenção inequívoca de fotografia — o Android abria o seletor
genérico em vez da câmera. No iPhone funcionava porque o iOS Safari sempre
apresenta a folha nativa (Câmera/Galeria/Arquivos) para inputs de arquivo,
independente do timing do atributo.

### Arquitetura adotada (inputs estáticos e especializados)
- `#cameraPhotoInput` — `accept="image/*" capture="environment"`, sem multiple.
- `#cameraVideoInput` — `accept="video/*" capture="environment"`, sem multiple.
- `#galleryInput` — `accept="image/*,video/*" multiple`, sem capture.
- `#mediaFiles` — input FINAL, único portador de `name="media[]"`, sincronizado
  via DataTransfer com a coleção acumulada (sem duplicação no POST).
- Botões separados: “📷 Tirar foto”, “🎥 Gravar vídeo”, “🖼️ Escolher da galeria”,
  acionados por `<label for=…>` (ativação nativa direta do gesto do usuário —
  sem setTimeout, sem alteração de atributos pós-clique, sem modal).
- Ocultação por classe `.file-input-visually-hidden` (input presente no layout,
  1×1px transparente) em vez de `hidden`/`display:none`.
- `value` limpo no evento `click` do próprio input, permitindo re-tirar a mesma
  foto em sequência.

### Fallback honesto (sem DataTransfer editável)
Se o navegador não permitir montar FileList: o `name="media[]"` é transferido
para o input da última seleção, o usuário é avisado de que apenas a última
seleção será enviada, e a remoção limpa a seleção — sem fingir acúmulo.

### Diagnóstico opcional (modo development)
`localStorage.setItem('sgg_debug_media','1')` habilita `console.debug` com UA,
input acionado, nome/MIME/tamanho dos arquivos. Desligado por padrão; nenhum
dado sensível; nada é logado em produção.

### Auditorias realizadas (sem alteração)
- Headers/.htaccess: sem `Permissions-Policy`/`Feature-Policy` bloqueando câmera.
- Service worker: só GET de assets estáticos; não interfere no upload.
- Backend: `normalizeFiles` + `Uploader::media()` validam MIME real (finfo) para
  JPEG/PNG/WEBP/GIF/MP4/MOV/WEBM/3GP. HEIC/HEIF não é aceito e falha com
  mensagem clara ("Extensão não permitida…") exibida no flash — não silenciosa.
- Preview (FileReader p/ imagem, ObjectURL+revoke p/ vídeo) preservado.
- Título de mídia com protocolo (hotfix anterior) preservado.

**Validações:** `php -l` e `node --check` sem erros; `name="media[]"` presente em
exatamente um input; nenhuma referência órfã aos IDs antigos; `diff -rq` contra a
base prova que só quick.php + documentação mudaram. Teste em aparelho físico
Samsung A07 não é possível neste ambiente — validação final em dispositivo é do
usuário (checklist no relatório).

---

## Hotfix — Título das mídias de demanda: prefixo do protocolo

**Regra:** toda mídia vinculada a uma demanda passa a ter o título editorial no
formato `PROTOCOLO — TÍTULO` (ex.: `SGG-2026-000009 — Sinalização de trânsito`).
O protocolo vem primeiro. Não altera o título/protocolo da demanda, o
`original_name`, o arquivo físico, o caminho, coordenadas nem a descrição —
apenas o campo `title` da mídia.

### Regra centralizada e idempotente
Novo método único `MediaFile::buildDemandMediaTitle($protocol, $title, $fallback)`:
- Compõe `PROTOCOLO — TÍTULO`.
- **Idempotente:** se o texto-base já começar com o protocolo, não prefixa de
  novo (nunca gera `SGG-… — SGG-… — …`).
- **Fallback:** título vazio → título da demanda → nome original do arquivo.
  Nunca grava só o protocolo sem contexto.

### Aplicação
- **Demanda Rápida** (`QuickDemandController::store`): usa o protocolo de
  `Demand::find()` (já carregado) e grava o título padronizado. O log de
  auditoria de upload agora inclui `protocol` e `title` (campos antigos mantidos).
- **Mídia adicionada depois** (`MediaController::store`): quando há `demand_id`,
  resolve o protocolo/título da demanda e aplica a mesma regra por arquivo
  (fallback por nome original quando o título informado está vazio). Auditoria
  ganha `demand_id`, `protocol` e `title` nesse caso.

### Exibição
- `app/Views/demands/show.php` — "Mídias vinculadas" continua usando `$m['title']`;
  adicionado `title="…"` (tooltip com o texto completo) para que as reticências do
  CSS não escondam o protocolo. Nenhuma mudança de CSS.
- `app/Views/media/show.php` — já exibia `title` e o `original_name` separado;
  sem alteração.

### Mídias existentes (Opção B — histórico padronizado)
- `migrations/019_media_demand_title_protocol.sql` — migration **de dados**,
  idempotente: prefixa o protocolo nos títulos de mídias já cadastradas com
  `demand_id`. Não altera estrutura, `original_name` nem arquivos. Aplicar apenas
  se desejar padronizar o histórico.

### Antes / depois
```
antes:  Sinalização de trânsito
depois: SGG-2026-000009 — Sinalização de trânsito
```

**Testes:** `php -l` sem erros; 8 casos unitários da regra (incluindo idempotência
e fallbacks) — todos PASS; migration executada em MariaDB 10.11 duas vezes
seguidas com resultado idêntico (idempotência) e contagem de protocolo por
título = 1 (sem duplicação); `original_name` intacto; mídia sem demanda intocada.

---

## Hotfix 14.2.1 — Demanda Rápida: câmera Android + preview de mídia

**Escopo:** somente `app/Views/demands/quick.php` (frontend). Nenhuma alteração
em banco, permissões, RBAC, storage, uploads, controllers ou demais módulos.

### Causa raiz 1 — Android não abria a câmera
Os disparadores eram `<label for="…">` apontando para dois `<input type="file">`,
ambos com `name="media[]"` e **`multiple`**. No Android Chrome/Samsung Internet o
atributo `capture` é **ignorado quando o input também possui `multiple`** (por
especificação, `capture` só direciona à câmera na seleção de arquivo único).
Resultado: no Android abria o seletor genérico (ou nada), enquanto o iOS Safari,
mais permissivo, ainda abria a câmera — daí a divergência.

**Correção:** um **único** `<input>` real; os botões viraram `<button type="button">`
que, via JS, ajustam o input antes de abri-lo — câmera: `capture="environment"` e
**sem** `multiple`; galeria: **sem** `capture` e **com** `multiple`. Elimina também
a duplicação de `name="media[]"` que existia com dois inputs.

### Causa raiz 2 — Preview quebrado (só aparecia o X)
O preview usava `URL.createObjectURL()` para `img.src` **sem revogar** e sem
dimensões/estado de erro. Em WebViews Android o blob de Object URL pode ser
invalidado antes do `<img>` carregar, colapsando a miniatura e deixando apenas o
botão de remover visível.

**Correção:** imagens agora usam **`FileReader` → dataURL** (compatível com todos
os WebViews); vídeos usam Object URL **com `revokeObjectURL`** ao remover/re-render
(evita vazamento). Adicionado estado visual `is-broken` e dimensões explícitas.

### Outras melhorias
- Acúmulo de arquivos (várias fotos + galeria) mantido em `DataTransfer` e
  reescrito no input real, garantindo que o POST envie exatamente o que está no
  preview. Fallback seguro quando o navegador não permite editar `input.files`.
- `input.value=''` antes de abrir, permitindo reselecionar a mesma foto/arquivo.
- Botão **X** remove apenas a imagem correspondente sem quebrar as demais.
- Miniatura 100% com `object-fit:cover`, bordas arredondadas e botão X consistente.

**Backend inalterado:** `QuickDemandController::store()`, `normalizeFiles()`,
`Uploader::media()` e `MediaFile::create()` já tratavam corretamente `media[]`
(único ou múltiplo), imagens e vídeos — validados e mantidos.

**Testes:** `php -l` sem erros nos arquivos tocados; `node --check` no JS embutido
sem erros; verificação de ausência de referências órfãs aos IDs antigos.

---

## [Importação de Cidadãos] — 2026-06-25

**Importação de cidadãos por planilha CSV.** Cadastro em massa a partir de um
modelo, com prévia e validação antes de gravar. Sem novas dependências (CSV
nativo), sem tabela nova, sem alterar a estrutura de `citizens`.

### Fluxo
- Botão **"Importar Planilha"** na listagem de Cidadãos → `/citizens/import`.
- **Baixar modelo CSV** (UTF-8, separador `;`, com linha de exemplo) → preencher
  → **enviar** → **prévia** (resumo + status por linha: OK / ERRO / DUPLICADO,
  com motivo) → **confirmar** grava só os válidos; **cancelar** descarta.

### Validações por linha
- Nome obrigatório; e-mail válido (se informado); data `DD/MM` ou `DD/MM/AAAA`
  (só dia e mês são guardados, por LGPD); bairro precisa existir em
  `neighborhoods`; telefone normalizado.
- Duplicidade detectada dentro da planilha e contra o banco (telefone, ou
  nome+endereço+número, ou nome+telefone). Linhas com erro não bloqueiam o lote.

### Origem "Importação" (corrigido)
- A origem é garantida em `request_sources` pelo **slug estável `importacao`**
  (a tabela tem `slug NOT NULL` e `UNIQUE(tenant_id, slug)`): busca por slug e,
  se não existir, insere `slug='importacao'`, `name='Importação'`, `is_active=1`
  e `sort_order` calculado. Idempotente.
- **Sem fallback para "qualquer origem"**: se a origem não puder ser garantida,
  a importação é abortada com mensagem clara e **nenhum** cidadão é gravado.

### Importação
- Importa só válidos; ignora duplicados; não importa inválidos.
- `consent_channel = importacao` + `consent_at` quando há consentimento; registro
  em `consent_logs` (granted). Auditoria (`import`). Mantém `tenant_id`/
  `mandate_id`, `created_by`/`created_at`.

### Segurança e compatibilidade
- Permissão **`citizens.create`** (sem permissão nova). CSRF em todas as ações.
  Upload validado (extensão `.csv`, MIME básico, ≤5MB, ≤5000 linhas); arquivo
  lido apenas como texto, nunca executado. Prévia em **sessão** (sem lixo em
  disco). CSV puro com `fgetcsv` — sem Composer, XLSX ou Node.
- `instagram` (sem coluna própria) vai para observações; `whatsapp` vira telefone
  quando o telefone está vazio.

### Arquivos
- Novos: `app/Controllers/CitizenImportController.php`,
  `app/Views/citizens/import.php`.
- Alterados: `app/routes.php` (5 rotas estáticas antes de `/citizens/{id}`),
  `app/Views/citizens/index.php` (botão), `VERSION.txt`, `CHANGELOG.md`.
- **Não alterados**: estrutura de `citizens`, banco (nenhuma tabela nova),
  permissões, `Citizen::create`, `ConsentLog` (reaproveitados).

### Verificação
Origem criada com slug correto e idempotente, sem fallback; fluxo completo
(modelo, upload, prévia, importar) com `source_id` apontando para "Importação" e
`consent_logs` gerado; bloqueio por permissão.

---

## [Hotfix UX 14.2] — 2026-06-24

**Refino final mobile/PWA (iPhone/Safari).** Ajustes finos sobre o 14.1,
mantendo tudo que já funcionava. Apenas layout/CSS/JS — sem alteração de banco,
rotas, permissões, controllers, models ou regras de negócio.

### Safe area do iPhone
- Barra inferior: `padding-bottom: max(10px, env(safe-area-inset-bottom))` —
  não colide mais com a barra do Safari.
- Conteúdo: `padding-bottom: calc(96px + env(safe-area-inset-bottom))` para que
  botões no fim da página não fiquem escondidos atrás da bottom nav.

### Botão central "Nova Demanda"
- Refinado: centralização, área de toque >=64px, sombra suave, sem corte no
  iPhone. Continua abrindo `/demands/quick` (rota existente, sem nova rota).

### Bottom sheet "Mais"
- Altura máxima 80vh com **scroll interno** e `-webkit-overflow-scrolling`,
  respeitando a safe-area no rodapé.
- **Header com título e botão X**; fecha por toque fora, ESC ou X.
- Itens adicionados quando permitidos: **Cargos e Permissões** e **Auditoria**.
- "Sair" continua via **POST + CSRF**.

### PWA / iPhone
- Meta tags adicionadas: `apple-mobile-web-app-capable`,
  `apple-mobile-web-app-status-bar-style`, `mobile-web-app-capable`.
- `viewport-fit=cover` já existia e foi preservado (não duplicado).

### Arquivos alterados (3, apenas front-end)
- `app/Views/layouts/app.php` — meta tags PWA + header/X e novos itens do sheet.
- `public/assets/css/app.css` — safe-area, padding do conteúdo, scroll do sheet,
  header/close, refino do FAB.
- `public/assets/js/app.js` — (do 14.1) abertura/fechamento do sheet, já cobre X
  e ESC.
- `VERSION.txt`, `CHANGELOG.md`.

### Verificação
15 checagens: meta tags PWA sem duplicar viewport, safe-area na barra, sheet com
scroll, botão X, itens Cargos/Auditoria, FAB e logout preservados, e telas sem
regressão (Demandas, Demanda Rápida, Cidadãos, Perfil).

---

## [Hotfix UX 14.1] — 2026-06-24

**Bottom Navigation profissional (mobile).** Redesenho da barra inferior no
padrão de apps modernos, mantendo 100% das funcionalidades. Apenas front-end:
sem alteração de banco, rotas, permissões, integrações ou regras de negócio.

### Mudanças
- Nova estrutura da barra: **Painel · Demandas · [Nova Demanda] · Cidadãos ·
  Mais**.
- **Botão central flutuante (FAB)**: circular (64px), elevado acima da barra,
  sombra suave, cor principal do sistema, ícone de raio e rótulo "Nova Demanda".
  Abre a Demanda Rápida pela **rota existente `/demands/quick`** (nenhuma rota
  nova criada). O item separado "Demanda Rápida"/"Rápida" foi removido da barra.
- **Menu "Mais"**: bottom sheet que sobe de baixo para cima, com Meu Perfil,
  Agenda, Documentos, Aniversários, Mapa Territorial, Relatórios, Configurações
  e Sair. Cada item aparece somente se o perfil tiver a permissão correspondente
  (reaproveita o controle de permissões já existente). "Sair" usa POST + CSRF,
  como no menu lateral.
- **Responsividade/Acessibilidade**: `safe-area-inset-bottom` (sem sobreposição
  com notch/barra do sistema), área de toque mínima 44×44px, compatível com
  tema claro/escuro.

### Arquivos alterados (3, apenas front-end)
- `app/Views/layouts/app.php` — nova barra inferior + bottom sheet "Mais".
- `public/assets/css/app.css` — estilos do FAB, da barra e do bottom sheet.
- `public/assets/js/app.js` — abertura/fechamento do bottom sheet (Esc/backdrop).
- `VERSION.txt`, `CHANGELOG.md`.

### Verificação
16 checagens: FAB presente apontando para `/demands/quick`, "Rápida" removida da
barra, bottom sheet com itens por permissão, safe-area, toque 44px, logout
seguro, e nenhuma regressão (Demandas, Demanda Rápida, Cidadãos, Perfil, Agenda).

---

## [Bloco 14] — 2026-06-24

**Inteligência do Gabinete.** Camada de leitura que transforma os dados já
existentes em indicadores de decisão, sem criar novos cadastros. Nova área em
`/intelligence`, protegida por `reports.view`.

### Indicadores executivos
- Cards: demandas abertas, resolvidas, atrasadas, vencendo; visitas realizadas;
  tarefas abertas e vencidas; cidadãos. Demandas por mês (12 meses) e por
  categoria.

### Rankings
- **Bairros** (reaproveita `Territory::byNeighborhood`): demandas, atrasadas,
  visitas e % de resolução por bairro, com região.
- **Secretarias/órgãos** (novo `Intelligence::departmentRanking`): total,
  abertas, resolvidas, atrasadas e tempo médio de resolução (horas).

### Produtividade da equipe
- Por assessor: demandas atribuídas/resolvidas/atrasadas, visitas realizadas,
  tarefas concluídas. Filtro individual mostra cards do assessor e os bairros
  que mais atende.

### Heatmap
- Mapa de calor das demandas reaproveitando `leaflet.heat` (já presente) e
  `Territory::heatPoints`. Prioridade da coordenada: demanda → bairro → centro
  de Uberlândia. Endpoint JSON `/intelligence/heatmap`.

### Metas
- Estrutura simples (nova tabela `goals`, migration 018): metas mensais de
  demandas resolvidas, visitas realizadas e tarefas concluídas, com meta,
  realizado, percentual e barra de progresso. Upsert por mês/ano/métrica.

### Botão Waze na demanda
- Tela de detalhe da demanda ganha **🚗 Abrir no Waze**
  (`https://waze.com/ul?ll=LAT,LNG&navigate=yes`) usando a coordenada da demanda
  ou, na ausência, a do bairro. Sem coordenada, o botão não aparece (ou exibe
  "Sem coordenadas para navegação" quando a seção Mapa está visível). O botão
  "Ver no mapa" foi preservado.

### Arquivos
- Novos: `IntelligenceController.php`, `Models/Intelligence.php`,
  `Views/intelligence/index.php`, `migrations/018_goals.sql`.
- Alterados (aditivos): `routes.php` (3 rotas), `layouts/app.php` (item de menu),
  `Models/Demand.php` (find passa a trazer lat/lng do bairro para o Waze),
  `Views/demands/show.php` (botão Waze), `database.sql`, `MIGRATIONS.md`,
  `VERSION.txt`.
- **Não alterados**: SLA, protocolo, demanda tradicional, demanda rápida, mapa
  territorial, configurações, cargos, portal público.

### Banco
- 1 tabela nova (`goals`). Total: **53** (52 + goals). Migration idempotente,
  testada em banco limpo.

### Verificação
22 verificações de lógica via CLI (indicadores, rankings, produtividade, heatmap,
filtro por assessor, metas) + validação HTTP das telas e do botão Waze (aparece
com coordenada, some sem). Demanda tradicional, demanda rápida e mapa intactos.

---

## [Bloco 13] — 2026-06-23

**Governança Administrativa e Estrutura do Sistema.** Cria uma camada
administrativa para configurar listas, status, cargos e menu sem alterar código.
Ambiente de teste → ajustes estruturais aplicados com compatibilidade total.

### Configurações administrativas (Administração → Configurações)
- CRUD de Categorias, Secretarias/Órgãos, Origens, Regiões, Status de demanda,
  Prioridades, Status de visita, Status/Prioridades de tarefa — com
  ativar/desativar (sem DELETE destrutivo). Protegido por `settings.manage`.

### Status e prioridades configuráveis (abordagem B — tabelas dedicadas)
- Novas tabelas `demand_statuses`, `demand_priorities`, `visit_statuses`,
  `task_statuses`, `task_priorities`, cada uma com **slug estável idêntico ao
  valor ENUM anterior** (nova, em_analise, resolvida, baixa, media, alta...).
- Campos de negócio migram de ENUM para VARCHAR(30) guardando o **mesmo slug** —
  todo o SQL existente (filtros, dashboard, relatórios, recálculo de SLA)
  continua funcionando sem alteração.
- `sla_status` **permanece ENUM**: é calculado pelo sistema, não configurável.
- Helpers `statusLabels()`/`priorityLabels()` passam a ler das tabelas, com
  **fallback** ao conjunto fixo caso a tabela não exista (nunca quebra).

### Cargos e Permissões (Administração → Cargos e Permissões)
- CRUD de cargos sobre o RBAC existente (`roles`, `permissions`,
  `role_permissions`), com matriz de permissões agrupada por módulo e atalho
  "todas". Cargos de sistema protegidos contra remoção; cargos com usuários não
  podem ser excluídos. Nada do modelo de permissões anterior foi removido.

### Menu reorganizado (por permissão)
- Nova seção **ADMINISTRAÇÃO** (Configurações, Cargos e Permissões, Equipe,
  Identidade). Seção Gestão mantém Sala de Situação, Relatórios, Auditoria.
- Transparência renomeada: **Gestão da Transparência** (`/transparency`) e
  **Portal Público** (`/transparencia`, abre em nova aba). Menu mobile mantém a
  barra de atalhos; o menu completo (com Administração) fica no drawer lateral.

### LGPD — consent_logs conectado
- A tabela `consent_logs` (antes ociosa) passa a registrar
  **granted/revoked/updated** sempre que o consentimento do cidadão muda, no
  cadastro e na edição.

### Débito técnico documentado
- `demand_public_tokens` permanece sem uso, reservada para a futura consulta
  pública de protocolo. Documentada, não removida.

### Migrations (idempotentes, testadas em banco limpo e com dados)
- `015_admin_status_tables.sql`, `016_admin_status_fks.sql`,
  `017_settings_manage_seed.sql`. O `database.sql` já as inclui para instalação
  nova. Total de tabelas: **52** (47 + 5 novas).

### Arquivos
- Novos: `AdminSettingsController`, `AdminRoleController`, `SystemList`,
  `ConsentLog`, views `admin/settings/*` e `admin/roles/*`, migrations 015-017.
- Alterados: `routes.php`, `layouts/app.php`, `Models/Demand.php`,
  `TechnicalVisit.php`, `Task.php`, `CitizenController.php`, `database.sql`,
  `MIGRATIONS.md`, `VERSION.txt`.
- **Não alterados**: regra de SLA, protocolo, demanda tradicional, Demanda
  Rápida, mapa, mídia, agenda.

### Verificação
Validação essencial (20 itens): import limpo, migrations em banco com dados,
demandas/demanda rápida/status/prioridades/visitas/tarefas/relatórios/mapa
funcionando, menu desktop e mobile, admin acessa e edita Configurações e Cargos,
usuário sem permissão bloqueado (403), e consentimento gerando log.

---

## [Hotfix 12.1] — 2026-06-23

Melhorias na **Demanda Rápida** mantendo a experiência simples e mobile-first.
Sem regressão: a tela tradicional de Demandas permanece intacta (acessível em
`/demands/new`); a única mudança nela é o botão "Nova demanda", que passa a
abrir a Demanda Rápida.

### Mídia (foto/vídeo) — correção iPhone
- Dois botões claros: **📷 Tirar foto / gravar vídeo** (input com
  `capture="environment"`) e **🖼️ Escolher da galeria** (input **sem** capture —
  essencial para abrir a galeria no iPhone). Ambos aceitam `image/*,video/*`,
  permitem **múltiplos arquivos**, com preview em miniatura e remoção
  individual antes de salvar. Compatível com iPhone, Android e desktop.

### Correção da longitude
- O painel de coordenadas usava `data-geo-lng`, mas o `geolocation.js` escreve
  em `data-geo-lon`; por isso "Lng" aparecia como "—". Corrigido o atributo. A
  latitude e a longitude agora são preenchidas, exibidas, enviadas no POST,
  salvas em `demands` e usadas pelo Mapa Territorial.

### Novos campos (mantendo a tela simples)
- **Título** (obrigatório), antes da descrição. Ao salvar: `title` = Título,
  `description` = "O que está acontecendo".
- **Número**, após o endereço detectado; compõe o endereço final
  (`occur_address` = "Rua X, 123") e é salvo em `occur_number`, preservando o
  endereço editável.
- **Origem** (de `request_sources`, padrão "Presencial"), antes de Cidadão.
- **Secretaria/órgão** (de `departments`, opcional) e **Responsável interno**
  (usuários ativos, pré-preenchido com o usuário logado e alterável), abaixo de
  Categoria/Prioridade. Salvos em `source_id`, `department_id` e `assigned_to`
  — os mesmos campos da demanda tradicional.

### Navegação
- Barra inferior mobile simplificada: **Painel, Demandas, Cidadãos,
  Aniversários, Identidade** (removido o atalho separado "Rápida").
- Botão **"Nova demanda"** em `/demands` agora abre `/demands/quick`.

### Arquivos alterados
- `app/Views/demands/quick.php` (campos novos, mídia dupla, correção da
  longitude), `app/Controllers/QuickDemandController.php` (novos campos e
  múltiplas mídias), `app/Views/demands/index.php` (botão Nova demanda),
  `app/Views/layouts/app.php` (barra mobile).
- **Não tocados**: `DemandController`, `demands/form.php`, models, rotas, banco.

### Banco de dados
- **Nenhuma tabela nova e nenhuma migration.** Reusa `demands` (incl.
  `occur_number`, `source_id`, `department_id`, `assigned_to`), `media_files`,
  `request_sources`, `departments`. Schema permanece em 47 tabelas.

### Verificação
Validação essencial (20 itens): título obrigatório, descrição, mídia
câmera+galeria com múltiplos arquivos/preview/remoção, latitude **e longitude**
preenchidas/salvas, número compondo o endereço, origem/secretaria/responsável
salvos (responsável = usuário logado por padrão), demanda no mapa, botão Nova
demanda → Rápida, barra mobile sem "Rápida", e tela tradicional intacta.

---

## [Bloco 12] — 2026-06-23

**Demanda Rápida Mobile** — experiência paralela e simplificada para registro de
demandas em campo, em menos de 20 segundos pelo celular. A tela tradicional de
Demandas permanece **intacta**.

### Novo
- Rota `/demands/quick` (GET form + POST salvar) e item de menu
  "⚡ Demanda Rápida" na sidebar e no menu mobile, junto de Demandas/Cidadãos.
- `app/Controllers/QuickDemandController.php` e `app/Views/demands/quick.php`
  (mobile-first): foto/vídeo via câmera (`capture="environment"`) ou galeria,
  com preview e remoção antes de salvar; descrição obrigatória; botão "Usar
  minha localização"; bairro, cidadão, categoria e prioridade com padrões.

### Reaproveitamento (sem duplicar regra de negócio)
- Salvamento usa **a mesma lógica da demanda tradicional**: `Demand::create`
  (protocolo SGG-AAAA-NNNNNN, SLA, coordenadas), `Uploader::media` +
  `MediaFile::create` (mídia em `sgg_storage/media/demands`, vinculada à
  demanda via `demand_id`), e `Audit::log`. Geolocalização reaproveita
  `geolocation.js`. Categoria padrão "Outros", prioridade padrão "Média".

### Regras de negócio
- Permissões: `demands.create` (criar) e `media.upload` (anexar). Sem
  `media.upload`, a demanda é criada **sem mídia**, com aviso amigável.
- As demandas da Demanda Rápida aparecem no Mapa Territorial normalmente
  (coordenada da demanda → do bairro → centro de Uberlândia).
- Suporte arquitetural a `/demands/quick?from_whatsapp=1` para pré-preenchimento
  futuro (sem webhook neste bloco).

### Arquivos alterados
- Novos: `QuickDemandController.php`, `demands/quick.php`.
- Aditivos: `app/routes.php` (2 rotas, antes de `/demands/{id}`),
  `app/Views/layouts/app.php` (item de menu nos dois menus).
- **Não tocados**: `DemandController`, `demands/form.php`, `demands/index.php`.

### Banco de dados
- **Nenhuma tabela nova e nenhuma migration.** Reusa `demands`, `media_files`,
  `audit_logs` e toda a estrutura existente. Schema permanece em 47 tabelas.

### Verificação
Validação essencial: form abre no mobile, câmera/galeria com preview,
geolocalização, criação da demanda com protocolo, mídia vinculada e salva em
`media/demands`, auditoria, presença no mapa, e confirmação de que a tela
tradicional de Demandas continua intacta. `php -l` ok.

---

## [Hotfix CEP] — 2026-06-22

### Objetivo
Implementar CEP como fluxo principal de cadastro de endereços.
O padrão do sistema passa a ser: CEP → preenchimento automático → número → salvar.

### Novo arquivo: public/assets/js/cep.js
- `sggCep(cfg)`: escuta o campo CEP. Após 8 dígitos (ou no blur), consulta
  `https://viacep.com.br/ws/{cep}/json/` e preenche logradouro, bairro, cidade e estado.
  Move o foco automaticamente para o campo número. Formata o CEP para `00000-000`.
- `sggGeoOnSubmit(formId, cfg)`: intercepta o submit do formulário. Se lat/lng
  estiverem vazios e houver endereço preenchido, geocodifica via Nominatim
  (`/search`) e salva lat/lng antes de submeter. Se GPS foi usado
  (`geoSource=gps`), mantém as coordenadas do GPS (mais precisas).

### Formulários atualizados
| Módulo | Campos novos | CEP | Geocode on submit |
|--------|-------------|-----|-------------------|
| Cidadãos | — | ✓ (já existia) | ✓ |
| Demandas | `occur_cep`, `occur_number`, `occur_city`, `occur_state` | ✓ | ✓ |
| Visitas  | `cep`, `address_number`, `city`, `state` | ✓ | ✓ |
| Agenda   | `location_cep`, `location_number`, `location_city`, `location_state` | ✓ | ✓ |

### Migration 014 — campos de endereço estruturado
- `demands`: `occur_cep`, `occur_number`, `occur_city`, `occur_state`.
- `technical_visits`: `cep`, `address_number`, `city`, `state`.
- `agenda_events`: `location_cep`, `location_number`, `location_city`, `location_state`.
- Arquivo: `migrations/014_cep_address_fields.sql` (INFORMATION_SCHEMA-safe, MySQL 5.7+).
- `database.sql` atualizado.

### Controllers atualizados
- `DemandController::collect()`: coleta `occur_cep`, `occur_number`, `occur_city`, `occur_state`.
- `VisitController::collect()`: coleta `cep`, `address_number`, `city`, `state`.
- `AgendaController::collect()`: coleta `location_cep`, `location_number`, `location_city`, `location_state`.

### Models atualizados
- `Demand::create()` e `update()`: novos campos no INSERT/UPDATE.
- `TechnicalVisit::create()` e `update()`: idem.
- `AgendaEvent::create()` e `update()`: idem.

### GPS como complemento
- Botão 📍 permanece em todos os formulários.
- `geolocation.js` atualizado: ao usar GPS, marca `geoSource=gps`.
- `sggGeoOnSubmit` respeita essa marcação e não sobrescreve coordenadas do GPS.

---

## [Hotfix Geo Completo] — 2026-06-22

### Objetivo
Auditoria e correção completa do fluxo geo em todos os módulos com endereço físico:
formulário → controller → model → banco → mapa.

### Matriz de status pós-correção
| Módulo   | Banco lat/lng | Form visível | Controller lê | Model salva | Mapa COALESCE |
|----------|:---:|:---:|:---:|:---:|:---:|
| Cidadãos        | ✓ | ✓ text readonly | ✓ | ✓ | ✓ |
| Demandas        | ✓ | ✓ text readonly | ✓ | ✓ | ✓ |
| Visitas Técnicas| ✓ | ✓ text (original)| ✓ | ✓ | ✓ |
| Agenda          | ✓ | ✓ text readonly | ✓ | ✓ | ✓ |
| Mídias          | ✓ migr.013 | ✓ NOVO | ✓ NOVO | ✓ NOVO | ✓ NOVO |

### Formulários — lat/lng visíveis
- **Cidadãos, Demandas, Agenda**: `type="hidden"` substituído por `type="text" readonly`.
  O usuário vê e confirma as coordenadas antes de salvar.
- **Mídias upload**: adicionados botão `📍 Marcar localização atual da mídia`,
  campos de lat/lng e painel de confirmação.
- **Mídias edição**: idem — botão `📍 Atualizar localização da mídia` + campos.

### Migration 013 — media_files
- Adicionado `latitude DECIMAL(10,8) NULL` e `longitude DECIMAL(11,8) NULL`.
- Índice `idx_media_geo (latitude, longitude)`.
- Arquivo: `migrations/013_geo_media_files.sql`.
- Usa `INFORMATION_SCHEMA.COLUMNS` para verificar existência antes de adicionar
  (compatível com MySQL 5.7, 8.0 e MariaDB — seguro para cPanel).
- `database.sql` atualizado com as novas colunas e índice.

### Backend — Mídias (fluxo completo)
- `MediaController::store()`: coleta `latitude` e `longitude` do POST.
- `MediaController::update()`: idem para edição de metadados.
- `MediaFile::create()`: INSERT inclui `latitude`, `longitude` (null-safe).
- `MediaFile::updateMeta()`: UPDATE inclui `latitude`, `longitude` (null-safe).

### Mapa Territorial — Territory::mediaPoints()
- Novo método com COALESCE de 4 níveis:
  1. `media_files.latitude` / `longitude` (coordenada direta)
  2. `demands.occur_latitude` / `occur_longitude` (demanda vinculada)
  3. `technical_visits.latitude` / `longitude` (visita vinculada)
  4. `neighborhoods.latitude` / `longitude` (bairro)
- Registros sem nenhuma fonte de coordenada são excluídos da camada.

### Regras do mapa (todos os módulos)
| Módulo   | Coord real | Fallback 1 | Fallback 2 |
|----------|-----------|-----------|-----------|
| Cidadãos | `c.latitude/longitude` | bairro | Uberlândia |
| Demandas | `d.occur_latitude/longitude` | bairro | Uberlândia |
| Visitas  | `v.latitude/longitude` | bairro | Uberlândia |
| Agenda   | `e.latitude/longitude` | bairro | Uberlândia |
| Mídias   | `m.latitude/longitude` → demanda → visita | bairro | Uberlândia |

---

## [Hotfix Geo Territorial] — 2026-06-22

### Objetivo
Garantir que tudo que tiver endereço salva latitude/longitude e que o mapa
use coordenadas reais (não apenas o centro do bairro).

### Migration 012 — agenda_events
- Adicionado `latitude DECIMAL(10,8) NULL` e `longitude DECIMAL(11,8) NULL`.
- Índice `idx_agenda_geo (latitude, longitude)`.
- Arquivo: `migrations/012_geo_agenda_events.sql` (idempotente via `IF NOT EXISTS`).
- `database.sql` atualizado com as novas colunas.

### Backend — AgendaController + AgendaEvent
- `AgendaController::collect()`: coleta `latitude` e `longitude` do POST.
- `AgendaEvent::create()`: INSERT inclui `latitude`, `longitude`.
- `AgendaEvent::update()`: UPDATE inclui `latitude`, `longitude`.

### Frontend — Formulário de Agenda
- Campo "Local" expandido para `form-col-2` com label "Local / Endereço do compromisso".
- Botão `📍 Usar minha localização` abaixo do campo local.
- Inputs hidden `latitude` e `longitude` (preenchidos e persistidos).
- Painel "📍 Coordenadas detectadas: Lat: X Lon: Y" (visível ao capturar ou em edição).
- Script `geolocation.js` incluído.

### Mapa Territorial — Territory Model
- `agendaPoints()`: trocado `n.latitude` por `COALESCE(e.latitude, n.latitude)`.
  Agora usa coordenada real do evento quando disponível.
- Filtro `AND COALESCE(e.latitude, n.latitude) IS NOT NULL` (exclui sem local).

### MapController
- `focus()` para tipo `agenda`: trocado `n.latitude` por
  `COALESCE(e.latitude, n.latitude)` — o "Ver no mapa" aponta para o local real.

### Regra de fallback no mapa (todos os módulos)
| Módulo     | Coord 1 (real)        | Coord 2 (fallback bairro) | Coord 3 (fallback cidade) |
|------------|-----------------------|---------------------------|---------------------------|
| Demandas   | `occur_latitude/lon`  | `n.latitude/longitude`    | -18.9186, -48.2772        |
| Cidadãos   | `c.latitude/longitude`| `n.latitude/longitude`    | -18.9186, -48.2772        |
| Visitas    | `v.latitude/longitude`| `n.latitude/longitude`    | -18.9186, -48.2772        |
| Agenda     | `e.latitude/longitude`| `n.latitude/longitude`    | -18.9186, -48.2772        |

### Mídias (media_files)
Sem lat/lng próprio na tabela (sem migration). O mapa usa o vínculo com
`demand_id`, `visit_id` ou `neighborhood_id` para posicionamento.

### MIGRATIONS.md
Adicionadas entradas para migration 011 e 012.

---

## [Storage Refactor Completo + Geo 1.1 Revisado] — 2026-06-22

### Storage Refactor Completo

#### Estrutura sgg_storage/
- Pasta `sgg_storage/` incluída no ZIP, ao lado de `/sgg`.
- Subpastas: `avatars/`, `branding/logos/`, `branding/favicons/`, `media/demands/`,
  `media/visits/`, `media/general/`, `attachments/demands/`, `documents/`, `temp/`.
- `.htaccess` em cada pasta bloqueando acesso HTTP direto.
- `index.html` vazio em cada pasta (previne listagem em servidores sem `Options -Indexes`).

#### Storage.php reescrito
- `root()`: raiz para novos uploads (`storage_root` → `uploads_dir` → fallback auto).
- `legacyRoot()`: raiz legada (`uploads_dir`), null se não configurado.
- `dir($subdir)`: cria e retorna subpasta dentro de `root()`.
- `path($relative)`: caminho absoluto para novo arquivo.
- `existingPath($relative)`: busca arquivo em `root()`, depois em `legacyRoot()` —
  garante que uploads antigos continuam acessíveis após migração.
- `isSafe($abs)`: anti path-traversal, aceita ambas as raízes.

#### config.example.php
- `storage_root` usa `dirname(__DIR__, 2) . '/sgg_storage'` — funciona sem editar
  quando `sgg_storage/` está ao lado de `sgg/`.

#### Subdirs padronizados
| Tipo             | Subdir novo               | Subdir antigo  |
|------------------|---------------------------|----------------|
| Avatar           | `avatars/`                | `users/avatars/` |
| Logo             | `branding/logos/`         | `branding/`    |
| Favicon          | `branding/favicons/`      | `branding/`    |
| Mídia demanda    | `media/demands/YYYY/MM/`  | `media/YYYY/MM/` |
| Mídia visita     | `media/visits/YYYY/MM/`   | `media/YYYY/MM/` |
| Mídia geral      | `media/general/YYYY/MM/`  | `media/YYYY/MM/` |
| Anexo demanda    | `attachments/demands/ID/` | `demands/ID/`  |

#### Controllers atualizados
`SettingController`, `ProfileController`, `MediaController`, `DemandController`,
`FileController`, `MediaController`, `PublicController`, `ProfileController`
— todos usam `Storage::existingPath()` para leitura (fallback automático) e
`Uploader::*()` com subdir correto para escrita.

### Hotfix Geo 1.1 (Revisão de Entrega)

#### Painel visual de coordenadas
- Todos os 3 formulários (Demandas, Cidadãos, Visitas) exibem painel
  "📍 Coordenadas detectadas: Lat: -18.xxxxx Lon: -48.xxxxx" após clique.
- Painel persiste em edição quando coordenadas já estão salvas.
- JS: nova função `updateCoordsPanel()` atualiza `[data-geo-lat]`/`[data-geo-lon]`.
- CSS: `.geo-coords-panel` e variantes (adicionados em `app.css`).

#### ZIPs entregues
- `SGG-Completo-Storage-Geo-Corrigido.zip`: `/sgg` + `/sgg_storage`.
- `SGG-Atualizacao-sgg-Storage-Geo.zip`: apenas `/sgg` (para atualizar código).

---

## [Hotfix Geo 1.1 + Storage Refactor] — 2026-06-22

### Hotfix Geolocalização 1.1 — Correções e lat/lng em Demandas/Cidadãos

#### Problemas corrigidos
- **Endereço não preenchido em Demandas**: `geolocation.js` agora extrai o
  logradouro de múltiplos campos Nominatim (`road`, `residential`, `path`, etc.)
  e usa `display_name` como fallback — resolve o caso comum em Uberlândia onde
  `road` retorna vazio.
- **lat/lng não salvo em Demandas**: adicionados inputs hidden `occur_latitude`
  e `occur_longitude` no formulário; `DemandController` e `Demand::create/update`
  agora persistem os valores.
- **lat/lng não salvo em Cidadãos**: adicionados inputs hidden `latitude` e
  `longitude`; `CitizenController` e `Citizen::create/update` persistem os valores.
- **Status de confirmação**: a mensagem de sucesso agora exibe o endereço detectado.

#### Sem migration de banco
`occur_latitude`, `occur_longitude` (demands) e `latitude`, `longitude` (citizens)
já existiam no schema original.

#### Arquivos alterados
- `public/assets/js/geolocation.js` — reescrito com parsing robusto
- `app/Views/demands/form.php` — hidden inputs occur_latitude/occur_longitude
- `app/Views/citizens/form.php` — hidden inputs latitude/longitude
- `app/Controllers/DemandController.php` — collect() inclui occur_lat/long
- `app/Controllers/CitizenController.php` — collect() inclui lat/long
- `app/Models/Demand.php` — INSERT/UPDATE com occur_latitude/occur_longitude
- `app/Models/Citizen.php` — INSERT/UPDATE com latitude/longitude

---

### Storage Refactor — Arquivos fora do /sgg

#### Motivação
Arquivos de upload dentro de `/sgg/uploads` eram sobrescritos ao atualizar o
sistema. Com `sgg_storage` externo, atualizações não afetam os arquivos enviados.

#### O que mudou
- **`app/Core/Storage.php`** (novo): helper centralizado que resolve a raiz de
  armazenamento na ordem: `storage_root` → `uploads_dir` → fallback automático
  `sgg_storage/` ao lado da pasta `/sgg`.
- **`config/config.example.php`**: nova chave `storage_root` com exemplo cPanel.
- **Uploader.php**, **FileController.php**, **MediaController.php**,
  **DemandController.php**, **ProfileController.php**, **PublicController.php**:
  todos trocados para `Storage::root()`.
- **`MIGRACAO_STORAGE.md`**: guia passo a passo para mover arquivos no cPanel.

#### Compatibilidade retroativa
Instalações existentes sem `storage_root` continuam funcionando via `uploads_dir`.
Nenhuma migration de banco foi necessária.

---

## [Hotfix Geo] — 2026-06-21

Geolocalização mobile: botão para preencher endereço pela localização atual.

### Funcionalidades
- **Botão "📍 Usar minha localização"** nos formulários de Demandas, Cidadãos
  e Visitas Técnicas.
- **Geolocalização nativa** via `navigator.geolocation.getCurrentPosition`
  (sem dependência externa, funciona em qualquer navegador moderno).
- **Geocodificação reversa** via OpenStreetMap/Nominatim (gratuito, sem API key).
  Campos preenchidos automaticamente:
  - Demandas: `occur_address`, `occur_reference`, `neighborhood_id`
  - Cidadãos: `address`, `address_number`, `neighborhood_id`
  - Visitas: `address`, `latitude`, `longitude`, `neighborhood_id`
- **Match de bairro**: comparação normalizada (sem acentos, case-insensitive)
  contra as opções do select de bairro.
- **Fallback**: se Nominatim falhar, preenche só lat/lng em Visitas;
  exibe aviso para preenchimento manual dos demais campos.
- **Confirmação** antes de sobrescrever dados já preenchidos.
- **Erros amigáveis**: mensagens distintas para permissão negada, GPS
  desabilitado e timeout.
- **Privacidade**: localização só é capturada após clique explícito do usuário.
- **Mobile**: botão ocupa largura total em telas pequenas (100% width).
- **Sem migration**: todos os campos preenchidos já existiam no banco.

### Arquivos
- `public/assets/js/geolocation.js` — módulo JS reutilizável (novo)
- `public/assets/css/app.css` — estilos `.btn--geo` e `.geo-status`
- `app/Views/demands/form.php` — botão geo + script
- `app/Views/citizens/form.php` — botão geo + script
- `app/Views/visits/form.php` — botão geo + script

---

## [Hotfix 11.2] — 2026-06-21

Correção de navegação: acesso ao perfil confuso/não funcional após Hotfix 11.1.

### Problemas corrigidos
- **Meu Perfil ausente do menu lateral**: adicionado como link fixo no primeiro
  grupo da sidebar, acessível para qualquer usuário logado sem dependência de JS.
- **Dropdown sem fallback**: adicionado `<noscript>` com link direto para `/profile`
  no topo direito, garantindo acesso mesmo se JavaScript falhar.
- **JS frágil**: código do dropdown envolvido em `DOMContentLoaded` — não depende
  mais da posição do `<script>` na página.
- **Trigger sem feedback visual**: adicionado `border` no hover e no estado aberto,
  deixando claro que o elemento é clicável.
- **z-index insuficiente**: dropdown elevado para `z-index: 200` (acima de sidebar
  e topbar que usam 20–30).

### Resultado
"Meu Perfil" aparece em três lugares: sidebar, dropdown do avatar e noscript
fallback. O dropdown abre/fecha corretamente, ESC funciona, clicar fora fecha.

Arquivos: `public/assets/js/app.js`, `app/Views/layouts/app.php`,
`public/assets/css/app.css`.

---

## [Hotfix 11.1] — 2026-06-21

Correção de bug grave: avatar exibido em tamanho original quebrando o layout.

### Causa
As classes `.avatar-sm` e `.avatar-lg` tinham `width/height` mas sem `max-width`,
`max-height` nem `overflow:hidden` no container, permitindo que imagens grandes
ignorassem as dimensões CSS em alguns contextos.

### Correções
- **`.topbar-avatar`** — substitui `.avatar-sm` no cabeçalho: 36×36 px,
  `max-width`, `max-height`, `object-fit:cover`, `flex-shrink:0`.
- **`.profile-avatar-wrap`** — container 120×120 com `border-radius:50%`
  e `overflow:hidden`; a `img` interna fica 100% do container, nunca maior.
- **`.profile-avatar`** — substitui `.avatar-lg`: 100%×100% dentro do wrap.
- **`.avatar-initial --sm/--lg`** — substitui `.avatar-placeholder` com
  dimensões fixas equivalentes.
- **HTML**: todos os `<img>` de avatar recebem `width` e `height` explícitos.
- **JS preview**: reescreve o container como `.profile-avatar-wrap` + `.profile-avatar`.
- Sem alteração de banco, sem nova migration.

Arquivos: `public/assets/css/app.css`, `app/Views/layouts/app.php`,
`app/Views/profile/index.php`.

---

## [Hotfix 10.1.1] — 2026-06-20

Autoatribuição de bairros por região no cadastro/edição de usuário, para
acelerar a configuração territorial da equipe. Mudança de front-end + um ajuste
de consulta; sem novas tabelas, migrations ou listas fixas.

- Ao selecionar a **Região responsável**, o sistema marca automaticamente todos
  os bairros daquela região (carregados do banco via `data-region` em cada
  checkbox — a relação oficial região↔bairro, sem duplicar dados em JS).
- A marcação é apenas uma sugestão inicial: o usuário mantém **liberdade total**
  para desmarcar bairros, adicionar bairros de outras regiões e fazer
  combinações personalizadas.
- **Troca de região**: se nenhum bairro foi alterado manualmente, aplica direto
  a nova região; se houve ajuste manual, exibe confirmação ("Deseja substituir a
  seleção atual pelos bairros da nova região?") antes de substituir.
- Ajuda visual abaixo da região: orientação inicial e contador ("N bairro(s)
  selecionado(s) automaticamente para a região X").
- `User::neighborhoods()` passou a retornar também `region_id` (necessário para
  o `data-region`). Nenhum outro comportamento alterado.
- O JavaScript de autoatribuição foi envolvido em `DOMContentLoaded` (com
  fallback para DOM já carregado), garantindo a execução no navegador
  independentemente da ordem de carregamento dos scripts da página.

Arquivos: `app/Views/users/form.php`, `app/Models/User.php`,
`app/Controllers/UserController.php`.

Verificação: data-region casa com a região do bairro no banco; ajuda e contador
presentes; confirmação ao trocar região com edição manual; persistência da
seleção mista (auto + manual + extra de outra região) e desmarcação respeitada.

---

## [Hotfix 10.1 + 10.2] — 2026-06-20

Correções operacionais de qualidade de dados, usabilidade e renderização,
priorizadas pelo uso diário do gabinete. Sem novos módulos.

### Hotfix 10.2 — Mapa Territorial e inteligência
- **Mapa não renderizava**: a inicialização passou a ser defensiva — aguarda o
  Leaflet do CDN (`typeof L`), valida o container, e chama `invalidateSize()`
  após o render e no `resize` (corrige o caso clássico de container em grid/flex
  com altura calculada após o paint). O container ganhou `min-height`. O mesmo
  ajuste foi aplicado ao **mapa do painel executivo** e ao **mapa público**.
- O mapa **aparece mesmo sem dados**, centralizado em Uberlândia, em desktop e
  mobile.
- **Dashboard Territorial, Inteligência Territorial e Sala de Situação**:
  validados com dados reais; indicadores consistentes com o banco.

### Hotfix 10.1 — Qualidade de dados e usabilidade
- **Bairros duplicados / UTF8**: migration `009` audita e saneia
  `neighborhoods` — normaliza o charset do nome para utf8mb4, **funde
  duplicados** (elege o menor id como sobrevivente e **remapeia
  `neighborhood_id` em 10 tabelas** que o referenciam, preservando demandas,
  visitas, cidadãos, etc.), copia coordenadas para o sobrevivente, remove o
  índice antigo `(tenant, name, city)` e reforça a **unicidade por
  `(tenant, name)`** para impedir novos duplicados. Idempotente. Garante
  integridade para mapa, dashboards, inteligência e relatórios.
- **Filtro por assessor nas demandas**: a tela de demandas ganhou o filtro
  **Responsável / Assessor**, que funciona em conjunto com status, prioridade,
  categoria e bairro — permitindo gestão individual da equipe.
- **Aniversariantes → WhatsApp direto**: o botão deixou de gerar mensagem
  interna e passou a ser **📱 Parabenizar**, que abre o WhatsApp do cidadão
  (`https://wa.me/<telefone>`) com a mensagem de parabéns **já preenchida** (com
  o nome do gabinete). O telefone é normalizado (só dígitos, DDI 55). O botão
  só aparece para quem tem telefone, **consentiu o contato e não marcou
  opt-out** (LGPD preservada).
- **Dashboard desatualizado**: o texto "Blocos 1 e 2 ativos…" foi substituído
  pela lista de **módulos ativos** (Demandas, Cidadãos, Agenda, Mídia, WhatsApp,
  Documentos, Mapa Territorial, Visitas Técnicas, Tarefas, Relatórios, Auditoria
  e Sala de Situação).

### Banco de dados
- **Nenhuma tabela nova.** Migration `009_hotfix101_neighborhoods.sql`
  (idempotente) sane­ia bairros e reforça a unicidade. `database.sql` passa a
  nascer com `uq_neigh_tenant_name` (unicidade por nome) e segue em **47
  tabelas**.

### Verificação (resumo)
Validação essencial: 20 verificações de aplicação (mapa com init defensivo e
centro em Uberlândia; dashboards territorial/inteligência/situação; WhatsApp
wa.me com telefone normalizado e trava LGPD; filtro de assessor combinando com
status; dashboard atualizado) + saneamento de bairros testado com duplicados
reais (4 grupos → 0, demanda remapeada ao sobrevivente, idempotente). `php -l`
ok em todos os arquivos.

---

## [Fase 10] — 2026-06-19

Inteligência, produtividade e hardening: transforma os dados já existentes em
apoio à decisão e endurece a segurança. Sem novos módulos CRUD; reutilização
máxima das 47 tabelas.

### Sala de Situação do Mandato (`/situation`)
- Visão executiva (vereador, chefe de gabinete e admin) com **10 números-chave**:
  demandas abertas, atrasadas, resolvidas e da semana; visitas realizadas;
  compromissos de hoje; tarefas pendentes; aniversariantes do dia; protocolos e
  documentos emitidos.
- **Alertas executivos automáticos**: bairro com mais demandas atrasadas, região
  com maior volume, categoria mais recorrente, demandas sem movimentação há mais
  de X dias, demandas vencidas por SLA, tarefas vencidas e visitas pendentes.
- Lista de compromissos do dia e aniversariantes.

### Relatórios estratégicos (expansão de `/reports`)
- **Bairros críticos** (abertas, atrasadas, recorrentes).
- **Regiões críticas** (volume, reincidência, atraso).
- **Categorias críticas** (quantidade, abertas, recorrência e tendência de 30
  dias).
- **Secretarias mais demandadas** (encaminhamentos, resolvidas, tempo médio de
  resolução em dias).
- **Produtividade da equipe** (por assessor: demandas, resolvidas, visitas,
  tarefas concluídas, compromissos).
- **Demandas recorrentes** por bairro e por categoria.

### Auditoria gerencial (expansão de `/audit`)
- Eventos técnicos traduzidos em **linguagem amigável** (ex.: "Demanda #145
  atualizada por João Silva", "Ofício OF-2026-0012 criado por Maria Souza").
- **Últimas atividades**, **ações mais comuns** e **usuários mais ativos**.
- O registro técnico detalhado continua disponível abaixo, com os filtros.

### Hardening de segurança
- **Troca de senha obrigatória no primeiro acesso**: fim do `admin123`. O admin
  (e todo novo usuário) precisa definir uma senha nova antes de usar o sistema;
  o middleware redireciona para a troca enquanto a senha for provisória.
- **Política de senha forte**: mínimo de 8 caracteres, com maiúscula, minúscula
  e número (aplicada na troca e na criação de usuários).
- **Headers de segurança**: `X-Frame-Options`, `X-Content-Type-Options`,
  `Referrer-Policy` e uma `Content-Security-Policy` básica compatível com os
  CDNs já usados (Leaflet, tiles do OpenStreetMap).
- **Rate limit de login** (5 tentativas → bloqueio temporário) e **regeneração
  de sessão** no login: já existiam no núcleo; confirmados e mantidos. Timeout
  de sessão por inatividade também já presente.

### Performance
- Adicionado **um único índice**: `idx_demands_created (created_at)`, usado pelos
  relatórios por período. As demais tabelas-alvo (`tasks`, `audit_logs`,
  `message_outbox`) **já possuíam** os índices necessários (status, responsável,
  usuário, data, agendamento), então nada foi alterado nelas.

### Banco de dados
- **Nenhuma tabela nova.** Migration `008_fase10_hardening.sql` (idempotente):
  `users.must_change_password` + `users.password_changed_at` e o índice
  `idx_demands_created`. `database.sql` segue em **47 tabelas**.

### Verificação (resumo)
Validação essencial única: 22 verificações — headers de segurança, troca de
senha forçada (fraca rejeitada, forte aceita), sala de situação e alertas,
relatórios estratégicos, auditoria gerencial humanizada, bloqueio da sala de
situação para papel não executivo, e migration/índice aplicados. `php -l` ok.

---

## [Hotfix 9.1] — 2026-06-19

Ativação dos módulos **Relatórios** e **Auditoria**, que ainda apareciam como
"Em breve" no menu. Telas funcionais reaproveitando dados e tabelas existentes.
Sem tabelas novas e sem permissões novas.

### Relatórios (`/reports`, permissão `reports.view`)
- **13 indicadores agregados**: demandas (total, resolvidas, atrasadas),
  cidadãos, aniversariantes do mês, visitas realizadas, tarefas abertas e
  concluídas, documentos, mensagens WhatsApp enviadas e falhadas, mídias e
  compromissos da agenda.
- **Distribuições de demandas**: por status, por bairro, por categoria e por
  responsável (com barras proporcionais).
- **Filtros**: período (de/até), região, bairro, responsável e status.
- Tudo agregado — nenhuma linha individual de cidadão é exposta.

### Auditoria (`/audit`, permissão `audit.view`)
- Leitura da tabela existente `audit_logs` com data/hora, usuário, ação,
  módulo (tabela), registro afetado, IP (quando houver) e um resumo dos
  detalhes (valores principais).
- **Filtros**: usuário, ação, módulo/tabela e período. Paginação.
- Rótulos amigáveis para ações e módulos.

### Menu
- "Relatórios" e "Auditoria" saíram de "Em breve" e agora apontam para
  `/reports` e `/audit`, exibidos apenas a quem tem a permissão correspondente.

### Segurança
- A auditoria **não é pública**: exige `audit.view`. Relatórios exigem
  `reports.view`. Usuários sem a permissão são bloqueados na rota e nem veem o
  link no menu (validado). Os relatórios trabalham só com agregados, sem expor
  dados sensíveis individuais.

### Banco de dados
- **Nenhuma tabela nova e nenhuma permissão nova.** `reports.view` e
  `audit.view` já existiam no schema; `audit_logs` já era alimentada por todos
  os módulos. `database.sql` segue em **47 tabelas**.

### Verificação (resumo)
Validação essencial: 13 verificações — `/reports` e `/audit` abrem, filtros
básicos funcionam, logs aparecem, menu aponta corretamente e as permissões
protegem as telas (usuário sem permissão é bloqueado). `php -l` ok.

---

## [Bloco 9] — 2026-06-19

Visitas Técnicas e Tarefas: interfaces para os módulos que já tinham tabela
modelada no schema v4, fechando o acompanhamento operacional do gabinete.

### Adicionado — Visitas Técnicas (`visits.view` / `visits.manage`)
- **CRUD de visitas** (`/visits`) com título, objetivo, endereço, coordenadas,
  agendamento e realização, status (agendada, realizada, cancelada, remarcada)
  e constatações/achados.
- **Vínculos** a demanda, cidadão, bairro e responsável (equipe).
- **Mudança de status** direto na tela; ao marcar "realizada", a data de
  realização é preenchida automaticamente.
- **Cards** (agendadas, realizadas, no mês), filtros e busca.
- **Integração com o mapa**: visitas com coordenada (própria ou do bairro)
  aparecem na camada "Visitas técnicas" e têm atalho "Ver no mapa"
  (`/map/focus?type=visit`).

### Adicionado — Tarefas (`tasks.view` / `tasks.manage`)
- **CRUD de tarefas** (`/tasks`) com descrição, responsável, prioridade
  (baixa→urgente), prazo e status (aberta, em andamento, concluída, cancelada).
- **Comentários** por tarefa (`task_comments`), com autor e data.
- **Mudança de status**; ao concluir, registra a data de conclusão.
- **Cards** (abertas, em andamento, concluídas, atrasadas), ordenação por
  status/prioridade/prazo, filtros e busca. Tarefas atrasadas destacadas.

### Integração com Demandas
- A tela da demanda ganhou atalhos para **registrar visita técnica** e **criar
  tarefa** já vinculadas àquela demanda.

### Banco de dados
- **Nenhuma tabela nova e nenhuma migration.** Reutiliza `technical_visits`,
  `tasks` e `task_comments`, já presentes no schema v4, e as permissões
  `visits.*` / `tasks.*` já seedadas. `database.sql` segue em **47 tabelas**.

### Verificação (resumo)
Validação essencial única: 16 verificações aprovadas — criação, status
(realizada/concluída com data automática), comentários, listas, integração com
o mapa e com a demanda, e auditoria. Todos os PHP passam `php -l`.

---

## [Bloco 8] — 2026-06-19

Portal de Transparência: site público (sem login) para divulgação das ações do
mandato, com gestão administrativa de publicações. Princípio central:
**PUBLIC = apenas o que for explicitamente publicado**, sempre filtrado no
backend (a interface nunca é a única barreira).

### Adicionado — Portal público (sem login, responsivo)
- **Página inicial** com foto/identidade do vereador (nome, partido, número,
  legislatura, slogan, redes e contatos públicos), indicadores e destaques.
- **Linha do tempo pública**: `mandate_timeline` com `is_public = 1`, mais
  recente primeiro.
- **Ações do mandato**: `public_reports` publicados (lista e detalhe).
- **Indicadores públicos** (agregados): demandas recebidas/resolvidas, visitas
  realizadas, bairros atendidos, compromissos realizados.
- **Mapa público** (Leaflet/OSM): apenas bairros atendidos e ações realizadas,
  de forma agregada — nunca cidadãos, telefones, endereços ou coordenadas
  privadas.
- **Galeria pública**: `media_files` com `is_public = 1` (fotos e vídeos),
  servidos por endpoint que revalida a flag pública.
- **Documentos públicos**: ofícios, indicações e requerimentos com
  `is_public = 1` (lista e leitura).
- **Pesquisa pública**: ações, bairros e documentos — somente conteúdo público.
- **Transparência territorial** e **Prestação de contas**: números, evolução
  mensal de demandas resolvidas e atuação por bairro, sempre agregados.
- **SEO**: title, description, Open Graph e favicon a partir de
  `tenant_branding` e `mandate_profile`. Design institucional, leve e
  responsável (celular, tablet, desktop), sem dependências pesadas.

### Adicionado — Gestão da transparência (admin, `public.manage`)
- Painel de transparência com contadores do que está publicado.
- CRUD de **ações do mandato** (`public_reports`) com publicar/despublicar.
- **Publicar/Despublicar** documentos (e estrutura para mídia/timeline) via
  toggle, com `published_at` nos documentos.
- **Auditoria** de publicação, despublicação e alterações (ação `publish`/
  `unpublish` em `audit_logs`).

### Privacidade e LGPD
- O portal **nunca** exibe nome de cidadão, telefone, e-mail, endereço,
  coordenadas privadas, observações internas, protocolos internos ou histórico
  interno. Indicadores e território são sempre **agregados**, jamais
  individualizados. Todas as consultas públicas passam pelo Model `PublicPortal`,
  que aplica o filtro de publicação no backend.

### Banco de dados
- **Nenhuma tabela nova.** Reutiliza `mandate_timeline` (`is_public`),
  `public_reports` (`is_published`), `media_files` (`is_public`) e a permissão
  `public.manage`. A tabela `documents` recebeu um marcador de publicação
  intencional (`is_public` + `published_at`) — justificado porque não havia como
  publicar um documento sem ele (a regra proíbe expor por status interno).
  ALTER em tabela existente, não tabela nova. `database.sql` segue em **47
  tabelas**. Migration `007_bloco8_documents_public.sql` (idempotente).

### Verificação (resumo)
Validação essencial única: 21 verificações aprovadas — portal abre sem login;
nome/telefone de cidadão nunca aparecem; timeline/documentos só exibem itens
públicos; acesso direto a item privado dá 404; indicadores e mapa público sem
dados pessoais; busca pública não encontra cidadão nem item privado;
publicar/despublicar com auditoria. Import limpo (47 tabelas), `documents.is_public`
presente. Todos os PHP passam `php -l`.

---

## [Hotfix 7.1] — 2026-06-19

Correções de segurança, privacidade, usabilidade e documentação no módulo de
Mapa Territorial, identificadas na revisão do Bloco 7. Sem tabelas ou migrations
novas; sem mudança de arquitetura.

### Privacidade — dados territoriais ≠ dados pessoais
- As **camadas e a busca do mapa** não expõem mais nome de cidadão a quem tem
  apenas `reports.view`. O nome só é exibido a usuários com `citizens.view`.
- Com apenas `reports.view`, os cidadãos aparecem **anônimos** ("Cidadão") e
  **não são pesquisáveis**; a camada "Cidadãos" é ocultada na interface.
- O heatmap de cidadãos usa apenas coordenadas (nunca nomes), preservando a
  análise territorial sem expor dados pessoais.

### Segurança — proteção contra XSS
- O JavaScript do mapa passou a **escapar** todo dado vindo do banco antes de
  inserir em HTML (popups dos marcadores, resultados da busca, tooltips). Nomes,
  títulos, bairros, protocolos e descrições são neutralizados, evitando XSS
  armazenado (ex.: um nome com `<img onerror=...>` não executa).

### Usabilidade — "Ver no mapa" com contexto
- Novo endpoint `/map/focus?type=demand|agenda|visit&id=N` que resolve a melhor
  coordenada no servidor (própria → bairro → centro) e abre o mapa **já focado**
  no ponto, com zoom aproximado e marcador. Evita abrir o mapa sem contexto.
- Demanda: usa coordenada própria quando existe; senão, a do bairro (a interface
  avisa quando é aproximada). Agenda e visitas abrem focadas na feição.

### Documentação
- **Coordenadas aproximadas** documentadas explicitamente no `README`,
  `DATABASE_INVENTORY.md` e `IMPLEMENTATION_STATUS.md`: as coordenadas dos
  bairros são aproximadas (centro do bairro), servem para inteligência
  territorial e não representam a localização exata de imóveis ou cidadãos.
- **README atualizado** para refletir os Blocos 1 a 7 (Núcleo, Demandas, Mídia,
  Documentos, WhatsApp, Agenda e Mapa/Inteligência Territorial), substituindo a
  descrição antiga que citava apenas os Blocos 1 e 2.

### Verificação (resumo)
Validação rápida: 14 verificações aprovadas — as quatro telas (mapa, dashboard,
inteligência, executivo) abrem, camadas carregam, busca funciona, "Ver no mapa"
foca com contexto, e a separação de privacidade está aplicada (admin vê nomes;
usuário só com `reports.view` vê cidadãos anônimos e não os encontra na busca).
Escape XSS presente. `php -l` ok. Sem baterias de regressão.

---

## [Ajustes rápidos — pré-Bloco 7] — 2026-06-19

- **`DATABASE_INVENTORY.md`**: inventário definitivo das 47 tabelas (nome,
  finalidade, bloco e observações), agrupado por área, com débitos técnicos e
  o mapa de migrations. Referência para manutenção.
- **Agenda — resultado e encaminhamentos**: `agenda_events` ganhou os campos
  `resultado` (TEXT) e `encaminhamentos` (TEXT) para registrar conclusões e
  compromissos assumidos em reuniões, audiências, visitas e eventos. Migration
  incremental `005_bloco7_agenda_resultado.sql` (idempotente). Sem tabela nova.

### Débito técnico registrado
- **Participantes estruturados**: hoje `agenda_events.participants` é texto
  livre. Fica registrado o débito de, no futuro, modelar participantes de forma
  estruturada (vínculo a `users`/`citizens`), permitindo histórico e relatórios
  de presença. Não implementado agora, por decisão de escopo.

## [Bloco 7] — 2026-06-19

Inteligência territorial: o sistema passa a apoiar a decisão do mandato com
mapa, agregações por bairro/região, rankings e alertas.

### Adicionado — Mapa Territorial
- **Mapa interativo** com **Leaflet + OpenStreetMap** (sem APIs pagas, sem
  bibliotecas pesadas no servidor; Leaflet e leaflet.heat via CDN).
- **Camadas com filtros independentes**: demandas abertas, resolvidas,
  atrasadas e recorrentes; visitas técnicas; compromissos; cidadãos. Cada
  camada é carregada sob demanda por endpoint JSON (`/map/layer`).
- **Mapa de calor (heatmap)** por demandas, cidadãos ou visitas.
- **Busca territorial** (`/map/search`): bairro, cidadão, demanda e protocolo,
  com foco direto no ponto ao selecionar.
- **Fallback de coordenadas**: feições sem coordenada própria herdam a
  coordenada do bairro (COALESCE), de modo que o mapa é útil desde já.

### Adicionado — Inteligência e painéis
- **Dashboard Territorial**: bairro com mais demandas, bairro com mais
  cidadãos, região mais ativa, demandas atrasadas por região e compromissos por
  região; tabela por bairro com demandas, cidadãos, visitas e compromissos.
- **Inteligência Territorial**: rankings de bairros mais atendidos, menos
  atendidos (com cidadãos e poucas demandas = possível demanda reprimida),
  maior reincidência e regiões por volume.
- **Dashboard Executivo** (vereador): mapa-resumo (heatmap), indicadores e
  **alertas de concentração** (ex.: bairro com 3+ demandas atrasadas ou 3+
  recorrentes).

### Integrações
- **Demanda → mapa**: a tela da demanda ganhou "Ver no mapa" (foca nas
  coordenadas da ocorrência ou do bairro).
- **Compromisso → mapa**: a tela do compromisso permite ver no mapa a camada de
  compromissos.

### Banco de dados
- **Nenhuma tabela nova.** Todas as coordenadas já existiam em `neighborhoods`,
  `demands` (`occur_*`), `technical_visits` e `citizens`. `database.sql` segue
  em **47 tabelas**. Adicionado seed de coordenadas dos bairros de Uberlândia
  (dado de referência) no `database.sql` e como `006_bloco7_coords_seed.sql`
  (idempotente) para instalações existentes. Permissão usada: `reports.view`.

### Performance
- Carregamento de camadas sob demanda, limites por consulta e marcadores leves
  (circleMarker). Sem dependências de servidor — compatível com cPanel.

### Verificação (resumo)
Validação essencial única: 16 verificações aprovadas — resultado/encaminhamentos
na agenda, camadas (demandas/cidadãos/agenda/heatmap) em JSON, busca territorial
(bairro e demanda), as quatro telas (mapa, dashboard, inteligência, executivo) e
integração "Ver no mapa". Import limpo (47 tabelas), migrations 005/006 sem erro,
23 bairros com coordenadas. Todos os PHP passam `php -l`.

---

## [Hotfix Bloco 5] — 2026-06-19

- **config.php fora do pacote**: o ZIP distribuído passa a conter apenas
  `config/config.example.php`. A instalação copia o exemplo para `config.php`
  e preenche as credenciais. Evita sobrescrever a configuração do servidor e
  reduz risco de exposição de credenciais. (O sistema já avisa, com mensagem
  clara, quando o config.php ainda não existe.)
- **README — seção "Instalação em cPanel"**: explica host recomendado
  `127.0.0.1`, a diferença entre `localhost` (socket Unix) e `127.0.0.1` (TCP),
  e quando usar cada `base_path` (`''`, `/sgg`, `/sgg/public`) com exemplos.
- **Teste de conexão Z-API**: em Configurações > WhatsApp, novo bloco "Testar
  conexão" que envia uma mensagem real para um número informado, validando
  instância, token, client-token e o envio de ponta a ponta. Recusa com
  mensagem clara quando a Z-API não está configurada.

## [Bloco 6] — 2026-06-19

Central de agenda política, institucional e territorial: compromissos com
calendário próprio (sem bibliotecas), status, vínculos e painéis.

### Adicionado — Agenda do Vereador
- **CRUD de compromissos** (`/agenda`) dos tipos reunião, visita, audiência,
  evento, compromisso interno/externo, atendimento, sessão e agenda externa.
- **Campos**: título, descrição/observações, início e término, local, bairro,
  região, responsável, participantes e status.
- **Status** com fluxo agendado → confirmado → realizado, além de cancelado e
  reagendado, alterável direto na tela do compromisso.

### Adicionado — Calendário (sem bibliotecas pesadas)
- **Quatro visualizações** em HTML/CSS puro, compatíveis com cPanel:
  **mês** (grade 6x7 com navegação), **semana** (7 colunas), **dia** (linha do
  tempo) e **lista** (paginada, com filtros). Navegação anterior/próximo e
  filtros por tipo, status e responsável.

### Adicionado — Territorial, dashboard e integrações
- **Agenda territorial**: compromisso associado a bairro e região (região
  derivada do bairro), preparando o futuro mapa.
- **Dashboard da agenda**: cards (hoje, semana, mês, visitas agendadas,
  reuniões agendadas, concluídos), próximos compromissos e agregação por
  bairro e por região.
- **Integração com Demandas**: na tela da demanda, atalho para agendar visita/
  reunião/compromisso já vinculado àquela demanda.
- **Integração com Cidadãos**: compromissos podem ser associados a um cidadão,
  formando histórico de relacionamento (vínculo + filtro por cidadão).
- **Equipe**: cada compromisso tem um responsável (vereador, chefe de gabinete,
  assessor ou equipe de rua).
- **Notificações internas (estrutura)**: lista de "próximos compromissos"
  pronta para alimentar lembretes internos. Sem notificações externas.
- **Auditoria**: criação, edição, cancelamento, reagendamento e conclusão.
- **Menu**: "Agenda" e "Painel da Agenda" ativados.

### Banco de dados
- **Nenhuma tabela nova.** A tabela existente `agenda_events` foi ampliada
  (necessidade real, justificada) com: coluna `status`, coluna `region_id` (FK)
  e ENUM `type` ampliado (audiência, compromisso interno/externo). `database.sql`
  segue em **47 tabelas**. Migration incremental **`004_bloco6_agenda.sql`**
  (idempotente) para quem já tem o Bloco 5.

### Verificação (resumo)
Validação essencial única do fluxo principal: 16 verificações aprovadas —
criação, status (realizado/reagendado), região derivada, auditoria, as quatro
visualizações do calendário, dashboard, integração com demanda, e os itens
testáveis do hotfix (tela e recusa do teste Z-API sem configuração). Import
limpo (47 tabelas) e migration 004 sem erro. Todos os PHP passam `php -l`.

---

## [Bloco 5] — 2026-06-19

Integração WhatsApp via Z-API: caixa de saída, modelos de mensagem, fila com
trava de consentimento (LGPD) e envio real, sem dependências pesadas.

### Adicionado — Mensagens (WhatsApp)
- **Caixa de saída** (`/messages`) com cards (rascunhos, na fila, enviadas,
  falhas), busca e filtros por status e tipo.
- **Nova mensagem** manual, com destinatário avulso ou cidadão, preenchimento
  de telefone automático e aplicação de modelo.
- **Fluxo de envio em etapas**: rascunho → fila (aprovação) → enviada, com
  cancelamento a qualquer momento antes do envio.
- **Trava de consentimento (LGPD)**: mensagens para cidadãos sem consentimento
  ou com opt-out **não entram na fila nem são enviadas** — verificação feita ao
  aprovar e novamente no envio.
- **Envio real via Z-API** usando cliente HTTP em cURL nativo do PHP (sem
  bibliotecas externas, compatível com cPanel). Atualiza status, conta
  tentativas, guarda a referência do provedor e a mensagem de erro.
- **Logs de envio** (`message_logs`) com a resposta do provedor em JSON e
  trilha de ações (criada, na fila, enviada, falhou, cancelada).

### Adicionado — Modelos de mensagem e configuração
- **CRUD de modelos de mensagem** (`/message-templates`, permissão
  `messages.manage_templates`) com categoria, corpo, variáveis e slug único.
- **Configuração Z-API** (`/settings/whatsapp`): ativar/desativar, ID da
  instância, token e client-token. Os **tokens nunca são reexibidos em claro**
  (máscara); para manter o valor atual, basta deixar o campo em branco.
- **Menu**: "Mensagens / WhatsApp" e "Modelos de mensagem" ativados.

### Integrações
- As mensagens de aniversário (Bloco 1) e qualquer rascunho gerado pelo sistema
  passam a aparecer e a ser gerenciados nesta caixa de saída.

### Banco de dados
- **Nenhuma tabela nova** (modelagem congelada). Reutiliza `message_outbox`,
  `message_templates` e `message_logs`, já presentes no schema v4, e as
  permissões `messages.*` já seedadas. `database.sql` segue em **47 tabelas**.
  As credenciais Z-API ficam na tabela `settings` (chave/valor). Sem migration.

### Segurança
- Tokens Z-API mascarados na interface e gravados apenas quando um novo valor é
  informado. Envio sempre passa pela checagem de consentimento. Erros de
  conexão são tratados (status `failed` + log), sem expor exceções ao usuário.

### Verificação (resumo)
Teste essencial único: 13 verificações aprovadas — rascunho com consentimento
resolvido, trava LGPD impedindo fila/envio sem consentimento, recusa de envio
com Z-API desligada, tratamento de falha de conexão (status failed + tentativa
+ log com payload), CRUD de modelo com slug, e token mascarado na tela. Todos
os PHP passam `php -l`.

---

## [Bloco 4] — 2026-06-19

Documentos oficiais do gabinete: ofícios, indicações, requerimentos,
memorandos e respostas, com modelos reutilizáveis e geração a partir de
demandas.

### Adicionado — Documentos
- **CRUD de documentos** (`/documents`) com listagem, filtros (busca, tipo,
  status), edição, exclusão (soft delete) e detalhe.
- **Numeração automática por tipo e ano**, com prefixo próprio: ofício
  `OF-2026-0001`, indicação `IND-...`, requerimento `REQ-...`, memorando
  `MEM-...`, resposta ao cidadão `RC-...`, outro `DOC-...`. A sequência é
  gerada de forma transacional (lock) e **não interfere** no protocolo de
  demandas (`SGG-...`).
- **Status** do documento (rascunho, emitido, enviado, respondido, arquivado);
  ao emitir, registra a data de emissão automaticamente.
- **Vínculo** opcional a uma demanda e a uma secretaria de destino.

### Adicionado — Modelos
- **CRUD de modelos** (`/templates`, permissão `templates.manage`) com tipo,
  assunto, corpo e ativação.
- **Variáveis `{{...}}`** substituídas ao gerar o documento — por exemplo
  `{{protocolo}}`, `{{cidadao}}`, `{{categoria}}`, `{{bairro}}`, `{{endereco}}`,
  `{{descricao_demanda}}`, `{{vereador}}`, `{{data}}`. A tela mostra a lista de
  variáveis como ajuda.
- **Geração a partir de modelo + demanda**: ao escolher um modelo e uma
  demanda, o corpo já vem preenchido com os dados reais (protocolo, cidadão,
  bairro, etc.), pronto para edição.

### Adicionado — Impressão / PDF e integrações
- **Impressão/PDF via navegador**: tela com papel timbrado (nome do gabinete,
  vereador, cidade/UF e assinatura) e botão de impressão. Sem dependência de
  biblioteca de PDF no servidor — funciona em qualquer cPanel.
- **Integração com Demandas**: a tela da demanda ganhou atalho "Gerar documento
  desta demanda", que abre o editor já vinculado e com variáveis preenchidas.
- **Auditoria** de criação, alteração, exclusão e impressão de documentos.
- **Menu**: "Documentos" e "Modelos" ativados (saíram de "Em breve").

### Banco de dados
- **Nenhuma tabela nova** (modelagem congelada, conforme diretriz). Reutiliza
  `documents`, `document_templates` e `protocol_counters`, já presentes no
  schema v4, e as permissões `documents.*` / `templates.manage` já seedadas.
- `database.sql` permanece em **47 tabelas**. **Não há migration neste bloco.**

### Verificação (resumo)
Teste essencial único: 15 verificações aprovadas — numeração por tipo/ano e
sequencial, prefixos por tipo, emissão preenchendo data, criação de modelo,
substituição de variáveis a partir da demanda (protocolo e cidadão reais no
corpo), impressão com papel timbrado, integração com a demanda e auditoria.
Um bug foi encontrado e corrigido (nomes de coluna `occur_address`/
`occur_reference` na montagem das variáveis). Todos os PHP passam `php -l`.

### Hotfix (pós-aprovação do Bloco 4)
- **Conexão em cPanel**: `config.example.php` e `README_INSTALACAO.txt` agora
  recomendam `'host' => '127.0.0.1'` em vez de `'localhost'`, com explicação
  (localhost pode tentar socket Unix e falhar; 127.0.0.1 força TCP).
- **`mandate_id`**: documentos passam a gravar o mandato ativo (ou 1 no MVP),
  mantendo compatibilidade multi-mandato.
- **Numeração sem colisão**: a sequência por tipo/ano passou a usar a tabela
  `document_counters` (contador transacional com `FOR UPDATE`) em vez de
  `COUNT(*)`, eliminando risco de número duplicado em acessos simultâneos.
- **Variável `{{secretaria}}`**: resolvida a partir da secretaria da demanda
  (campo direto ou secretaria padrão da categoria) ou de uma secretaria
  escolhida no documento.
- Verificado com teste essencial focado (5/5), sem tabelas novas
  (`document_counters` já existia no schema).

---

## [Bloco 3] — 2026-06-19

Central de mídia do mandato: fotos, vídeos, áudios e documentos de campo,
com galeria, coleções, download seguro, auditoria e integração com demandas.

### Adicionado — Biblioteca de mídia
- **Upload** de fotos, vídeos, áudios e documentos com validação de MIME real,
  limites por tipo (imagem 15 MB, vídeo 200 MB, áudio 50 MB, documento 30 MB),
  nome em disco com hash e organização automática por ano/mês.
- **Upload individual e múltiplo** com **drag-and-drop** e **barra de
  progresso** (envio assíncrono via XHR, resposta JSON).
- **Galeria** com visualização em **grade** e **lista**, busca e filtros por
  tipo, categoria, bairro, região e tag.
- **Metadados**: título, descrição, autor (quem enviou), data de captura,
  origem e **tags** (relação N:N, reaproveitadas entre arquivos).
- **Organização**: categoria (evento, campanha, institucional, visita,
  demanda, bairro), bairro, região (derivada do bairro), e vínculo opcional a
  demanda e visita técnica.
- **Coleções/galerias** por bairro, evento, campanha ou institucional, com
  itens ordenáveis e capa.

### Adicionado — Segurança e governança
- **Arquivos fora do acesso público**: nada é servido por URL direta. Download
  e preview (inline) passam por controller com verificação de permissão
  (`media.view` / `media.download`).
- **Download seguro** registrado em `media_download_logs` (usuário, IP, agente).
- **Auditoria** de upload, alteração, exclusão, download, vínculo a coleção,
  marcação de uso e seleção para compartilhamento.
- **Perfis**:
  - *Equipe de Rua*: envia mídia, consulta o **próprio** conteúdo e vincula a
    demandas/visitas; **não** exclui conteúdo de terceiros (regra aplicada no
    controller, além das permissões).
  - *Marketing*: visualiza a biblioteca, baixa arquivos, **marca como
    utilizado** (registra em `media_usage_logs`) e cria coleções.

### Adicionado — Dashboards e integrações
- **Dashboard de mídia** com cards: total de fotos, vídeos, áudios, documentos,
  downloads, coleções e uploads do mês.
- **Dashboard territorial**: mídias por bairro e por região.
- **Integração com Demandas**: ao abrir uma demanda, veem-se as mídias
  vinculadas e há atalho para adicionar novas já associadas àquele protocolo.
- **Preparação para WhatsApp**: tabela `media_share_queue` e ação "Selecionar
  para WhatsApp" que enfileira a mídia. **O envio por Z-API não é implementado
  neste bloco** — apenas a estrutura de seleção/fila.

### Banco de dados
- `database.sql` consolidado em **47 tabelas** (era 44): adiciona `media_tags`,
  `media_file_tags` e `media_share_queue`, e a coluna `region_id` em
  `media_files`. As demais tabelas de mídia e as permissões `media.*` já
  existiam no schema v4.
- Migration incremental **`migrations/003_bloco3_midia.sql`** (idempotente) para
  quem já tem o Bloco 2.

### Limitações conhecidas
- **Thumbnails de vídeo**: sem geração automática (FFmpeg indisponível em cPanel
  comum). Vídeos exibem ícone; a capa pode ser enviada como mídia separada.
- **Visitas técnicas**: o vínculo `visit_id` já existe no banco e no model, mas
  a interface de visitas é do **Bloco 5**; a navegação mídia↔visita ficará
  completa quando aquele módulo existir.

### Verificação (resumo)
Teste essencial único (sem rodadas repetidas, conforme diretriz de créditos):
20 verificações, todas aprovadas — upload com MIME real, região derivada do
bairro, tags, galeria, download seguro com log, coleção, fila WhatsApp,
dashboard de mídia, integração com demanda e a regra de permissão da equipe de
rua (bloqueio 403 ao excluir conteúdo de terceiros). Import limpo (47 tabelas) e
migration 003 sem erro. Todos os PHP passam `php -l`.

---

## [Bloco 2] — 2026-06-19

Gestão de demandas com protocolo, SLA, recorrência, histórico e anexos.
Inclui ajustes estruturais em Cidadãos e Equipe, incorporados neste bloco
(sem bloco intermediário), via migrations.

### Adicionado — Demandas
- **CRUD de demandas** com listagem, busca, filtros (status, prioridade,
  categoria, bairro, atrasadas) e paginação.
- **Protocolo automático** sequencial e transacional no formato
  `SGG-AAAA-NNNNNN` (contador por ano, com trava de concorrência).
- **SLA**: cada demanda herda o prazo (em dias) da categoria, ou aceita um
  prazo manual. Cálculo de `due_date` e status de SLA (dentro / vencendo /
  vencido / cumprido), recalculado automaticamente.
- **Recorrência**: demandas com mesma categoria + bairro + ponto de
  referência são agrupadas; a partir da 2ª ocorrência a demanda é marcada
  como recorrente, com contador de ocorrências.
- **Fluxo de status** (nova, em análise, encaminhada, em andamento,
  aguardando retorno, resolvida, arquivada) com registro de primeira
  resposta e data de resolução.
- **Histórico/timeline** completo: criação, mudanças de status, comentários
  internos, atribuições e anexos.
- **Anexos** administrativos (JPG, PNG, WEBP, PDF, até 10 MB) com validação
  de MIME real, armazenamento fora do webroot e download por controller.
- **Mensagem ao cidadão** pré-montada (status do protocolo) pronta para
  copiar e enviar por WhatsApp.

### Adicionado — Ajustes estruturais (via migrations)
- **Cidadãos**: apelido (`nickname`), liderança comunitária
  (`is_community_leader`), origem do contato (`source_id` →
  `request_sources`), seleção de bairro a partir da tabela `neighborhoods`
  (em vez de texto livre) e cidade fixa "Uberlândia".
- **Equipe**: telefone e cargo (já existentes) agora no formulário, região
  responsável (`region_id`) e bairros atendidos (`user_neighborhoods`, N:N).
- **Dashboard**: cards de demandas (abertas, resolvidas, atrasadas e total
  de protocolos), além dos cards de cidadãos.
- **Menu lateral**: reorganizado por áreas (Atendimento, Produção,
  Território, Comunicação, Gestão); módulos ainda não implementados aparecem
  como "Em breve".

### Dados iniciais (seeds Bloco 2)
- 5 regiões e 23 bairros de Uberlândia (editáveis no sistema).
- 6 secretarias/órgãos de exemplo, com vínculo padrão categoria→secretaria.

### Banco de dados
- Total de 44 tabelas (era 43). Nova tabela: `user_neighborhoods`.
- Migrations: `001_bloco2_ajustes.sql`, `002_bloco2_seeds.sql`.
- Documentação nova: `MIGRATIONS.md` e `BACKUP.md`.

### Correções
- Tratamento de campos numéricos opcionais vazios (aniversário e SLA), que
  poderiam ser interpretados como zero.
- Compatibilidade de `mb_substr` (uso protegido por `function_exists`).

### Verificação
Testado end-to-end (PHP 8.3 + MariaDB, instalação limpa via `database.sql` e
também via migrations sobre o Bloco 1): criação de demanda com protocolo
sequencial, SLA herdado e manual, detecção de recorrência, mudança de status
com SLA cumprido e histórico, comentários, upload/download de anexo, cidadão
com os novos campos, usuário com região e bairros atendidos, cards do
dashboard, menu com "Em breve" e RBAC. Todos os PHP passam em `php -l`.

---

## [Bloco 1] — 2026-06-19

Núcleo do sistema: estrutura MVC, autenticação, identidade do gabinete e
cadastro de cidadãos com aniversariantes.

### Adicionado
- **Core MVC leve** (PHP 8 puro, sem framework): autoloader manual, Router com
  parâmetros `{id}`, front controller único (`public/index.php`), camada
  `Database` (PDO + prepared statements), `Request`, `Response`, `View` com
  layout, `Config`, `Session` segura, `Csrf`.
- **Autenticação**: login, logout, hash de senha bcrypt (cost 12), proteção
  anti brute-force (bloqueio temporário após 5 tentativas), regeneração de
  sessão, expiração por inatividade.
- **RBAC**: middleware `AuthMiddleware::requirePermission()`, permissões
  carregadas na sessão, helper `Auth::can()`.
- **Identidade do gabinete** (`/settings`): logos (principal, reduzida),
  favicon, cores (primária/secundária/destaque), slogan, nome público; dados
  institucionais do mandato (vereador, partido, número, legislatura, cidade/UF,
  contatos, redes). Upload seguro com validação de MIME e extensão, nome com
  hash, armazenamento fora do webroot, e arquivos servidos por controller.
- **Cidadãos** (`/citizens`): CRUD completo com listagem, busca, paginação,
  edição e exclusão (soft delete). Campos de aniversário (dia/mês, sem ano) e
  `whatsapp_opt_out`. Consentimento LGPD registrado.
- **Aniversariantes** (`/birthdays`): do dia e do mês, filtro por consentimento,
  geração de mensagem de parabéns em **rascunho** na `message_outbox` (controle
  manual; trava LGPD impede gerar para quem não consente ou tem opt-out).
  Mensagem gerada registra interação no histórico do cidadão.
- **Equipe** (`/users`): listagem e CRUD de usuários com perfil de acesso.
- **Dashboard** inicial com indicadores (total de cidadãos, com consentimento,
  aniversariantes de hoje).
- **Auditoria básica** das ações principais (login/logout, criar/editar/excluir
  cidadão, salvar identidade, criar/editar usuário, gerar mensagem).
- **Layout responsivo**: sidebar no desktop, navegação inferior no mobile,
  identidade visual sóbria (azul institucional) parametrizada pelas cores do
  gabinete.

### Dados iniciais
- Usuário admin: **admin@admin.com** / **admin123**.
- 7 papéis, permissões, categorias de demanda, origens de solicitação,
  modelos de mensagem (aniversário e protocolo), settings padrão.

### Observações técnicas
- Schema importa 43 tabelas (v4). O Bloco 1 usa apenas parte delas; as demais
  ficam prontas para os próximos blocos.
- Verificado end-to-end com PHP 8.3 + MariaDB: login, CRUD de cidadãos,
  validação, busca, aniversariantes, trava LGPD, RBAC, settings e logout.

### Pendente para próximos blocos
Demandas, anexos, mídia, documentos, visitas, tarefas, agenda, mensagens em
lote, dashboard completo, mapa, transparência. Ver `IMPLEMENTATION_STATUS.md`.
