portal_publico/plano.md

468 lines
53 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Plano — Portal De Paula (Protótipo Interno)
## Visão geral
Reformulação do portal interno da De Paula Contadores. O pedido original foi
"estruturar um protótipo visual, mas funcional, para avaliarmos e
estruturarmos melhor o frontend do portal" — reaproveitando a essência do
portal atual (referência: capturas de tela do sistema em produção, tema
escuro roxo/dourado, sidebar com dezenas de seções, busca de aplicações,
cards de atalho, login com logo circular), não recriando do zero.
Decisões de arquitetura tomadas no início e válidas até a rodada 13:
- **Stack: HTML + CSS + JS puro**, sem framework, sem build step, sem
dependência externa além das fontes do sistema.
- **Sem backend**: todo o estado (sessão, usuários, perfis de acesso,
favoritos, widgets, compromissos, tema) vivia no `localStorage` do
navegador.
- **Visual**: modernizar a estrutura existente (sidebar + topbar + busca +
cards + login), não clonar pixel a pixel as capturas de referência.
Isso valeu como protótipo de validação de fluxo/desenho. Na rodada 13 o
usuário definiu a stack real de backend e pediu a migração completa — ver
essa seção para a arquitetura atual. Este documento foca no histórico de
decisões **estruturais/transversais** — rodadas que mudaram mais de uma
aplicação ao mesmo tempo, o mecanismo de autenticação, o modelo de
permissões em si, a identidade visual, ou a organização de pastas do
projeto — e no que ainda está em aberto no nível do portal como um todo.
Para arquitetura técnica atual (páginas, API, ordem de scripts, modelo de
permissões, como o CSS está dividido entre arquivos), ver `CLAUDE.md`.
**A partir de 2026-08-26, o histórico específico de cada aplicação foi
extraído deste arquivo** para um `CHANGELOG.md` dentro da pasta de cada
uma (`portal_api/<app>/CHANGELOG.md` para as 3 com pacote Python;
`docs/<app>/CHANGELOG.md` para as demais — ver `README.md` na raiz para o
mapa completo), pelo mesmo motivo que levou a documentação técnica a ser
dividida por aplicação (`CLAUDE.md`/`docs/*.md`): reduzir conflito de
edição quando mais de uma aplicação está sendo trabalhada ao mesmo tempo, e
manter cada changelog focado no que interessa a quem só mexe naquela
aplicação. A numeração de rodada usada aqui é a mesma referenciada nos
changelogs de cada aplicação (e vice-versa) — uma rodada que só aparece no
`CHANGELOG.md` de uma aplicação específica não tem entrada aqui, e
vice-versa.
## Cronologia de construção
### 1. Protótipo inicial — login + shell
Tela de login (logo, usuário/senha, "esqueceu senha" apontando para
Integração e Inovação) e o shell principal: sidebar com as 14 seções do
documento original de arquitetura, topbar com hamburger/"Acessar
Ramais"/notificações, busca de aplicações e uma grade de cards de atalho
fixos (Links, OS, Ramais, Relatórios, Calendário, Solicitações). Tema
claro/escuro com toggle salvo em `localStorage`. Um modal de "simular perfil
de acesso" (persona) deixava escolher entre as 7 personas do documento
original para ver o menu mudar — mecanismo **removido depois** (ver rodada
4).
### 4. Login real (fim do simulador de persona)
O usuário pediu para poder testar de verdade o cadastro de acessos —
"podemos testar na realidade como irá funcionar". Isso trocou o mecanismo
inteiro:
- Login parou de aceitar qualquer senha; passou a validar contra contas
reais em `pid_users_v1`/`v2` (`assets/js/users.js`).
- Duas contas de demonstração criadas: `gabriel`/`gabriel` (perfil
Integração e Inovação) e `bruno`/`bruno` (sem perfil vinculado, para
testar o estado "sem acesso").
- O modal de simulação de persona foi **removido**; a visibilidade do menu
passou a vir do perfil de acesso de verdade vinculado ao usuário logado
(`access.js` → `pidResolveAccess`).
- Cada um dos 7 perfis do documento original ganhou um registro real em
Perfis de Acesso (Diretoria, Departamento Pessoal, Gerencial, Legalização,
Fisco/Contábil, Financeiro, Protocolo), mais Integração e Inovação (8 ao
todo) — ver `docs/perfis-usuarios/CHANGELOG.md` pro histórico específico
da tela de Perfis de Acesso/Usuários.
### 6. Bugs encontrados e corrigidos
Três bugs reais surgiram e foram corrigidos durante o uso, em áreas
diferentes do portal:
- **`users-admin.js` ficou preso na assinatura antiga** de
`pidResolveAccess()` (`{ profile }` em vez de `{ profiles }`) depois da
migração da rodada 5 (ver `docs/perfis-usuarios/CHANGELOG.md`) — a tela de
Usuários redirecionava sempre de volta pro Principal antes de renderizar,
dando a impressão de "não abre". Corrigido ajustando a desestruturação.
- **CSS sobrescrevendo o atributo `hidden`**: componentes que definem seu
próprio `display` (`.no-access`, `.notif-badge`, `.app-card`) ignoravam o
`hidden` do JS, porque uma regra de autor com `display` explícito vence a
regra padrão do navegador `[hidden]{display:none}` mesmo com
especificidade igual. A mensagem de "sem perfil vinculado" continuava
aparecendo por cima do dashboard normal mesmo com o perfil já vinculado.
Corrigido com uma regra global em `base.css`:
`[hidden] { display: none !important; }` — resolveu esse caso e mais dois
latentes (badge de notificação, filtro de busca nos cards).
- **Submenus recolhidos continuavam clicáveis/focáveis**: o accordion do
sidebar só usava `max-height:0` + `overflow:hidden` para esconder, o que
não impede clique nem navegação por Tab. Corrigido adicionando
`visibility:hidden` + `pointer-events:none` ao estado fechado.
### 13. Migração para backend Django + PostgreSQL
O usuário definiu a stack real do backend: **Python 3.13 + Django 6.0 +
PostgreSQL 14**, e pediu três coisas na mesma rodada:
1. Construir esse backend completo (models, API REST, banco Postgres) —
não só adaptar o frontend a um contrato assumido.
2. Migrar para o banco **tudo** que hoje existia no `localStorage`, exceto
a preferência de tema: usuários, perfis de acesso, favoritos, widgets e
compromissos do Calendário Individual.
3. Autenticação via **sessão/cookie do Django** (decisão explícita, não
token/JWT) — o que levou à escolha de servir o frontend estático pelo
próprio Django (mesma origem), evitando complicação de cookie de sessão
cross-origin.
O que mudou estruturalmente:
- Projeto Django novo, inicialmente em `Portal/backend/` (`config/` + app
único `portal_api/`) — reorganizado na rodada 14 para o padrão de pastas
de um projeto Django de verdade. Ver `CLAUDE.md` → "Arquitetura" para o
detalhamento de cada arquivo e a tabela de endpoints.
- Model de usuário customizado (`Usuario`, estendendo `AbstractUser`) desde
o início — trocar depois de criado o projeto é doloroso.
- Senha passou a usar hashing nativo do Django (`set_password`/
`check_password`) — antes era texto puro no protótipo; melhoria de
segurança real, não só troca de armazenamento.
- Catálogo de módulos/aplicações do menu (antes `PID_MODULES`/
`PID_MODULE_APPS` hardcoded em `profiles.js`) virou `portal_api/catalogo.py`,
fonte única da verdade, exposto só leitura via `GET /api/catalogo/`.
- A união de permissões entre múltiplos perfis de um usuário (rodada 5),
antes calculada no cliente (`access.js` → `pidResolveAccess`/
`pidApplyAccessVisibility`), passou a ser calculada uma única vez no
servidor (`permissoes_efetivas()` em `views.py`) e consumida pronta via
`GET /api/me/`.
- A lógica de "quem vê qual compromisso" do Calendário Individual (rodada
10 — `pidEventsVisibleTo`) também migrou pro backend
(`CompromissoAgendaViewSet.get_queryset`).
- Todo o frontend que lia/escrevia `localStorage` foi reescrito para
`async`/`fetch` contra a API (`assets/js/api.js`, hoje `static/js/api.js`
— ver rodada 14 — é o módulo novo), mantendo a mesma estrutura de
arquivos e nomes de função sempre que possível para minimizar o tamanho
do diff.
- `assets/js/users.js` (armazenamento local de usuários) foi **removido**
— usuários agora só existem no banco, via `/api/usuarios/`.
**Limitação do ambiente onde essa rodada foi feita**: só havia Python 3.8 e
nenhum servidor PostgreSQL disponíveis, então não foi possível rodar
`makemigrations`/`migrate`/`runserver` nem testar o fluxo ponta a ponta
durante a implementação — o código foi revisado estaticamente com cuidado
(sintaxe, convenções do DRF, coerência entre serializers/views/frontend).
Rodar de verdade — incluindo gerar a migração inicial — fica para o
ambiente com Python 3.13 + Postgres 14 (ver rodada 17 no `CHANGELOG.md` de
Links & Ferramentas/Acessos Gerais, onde esse ambiente ficou disponível).
### 14. Reorganização de pastas no padrão Django
O backend tinha sido criado numa subpasta `Portal/backend/`, com o
frontend (HTML/CSS/JS) solto na raiz de `Portal/` ao lado dela — funcional,
mas não era a estrutura convencional de um projeto Django. Pedido: "reorganize
a estrutura das pastas, conforme o padrão de um projeto Django". Mudanças:
- `manage.py`, `requirements.txt`, `.env`, `config/` e `portal_api/` subiram
de `Portal/backend/` para a raiz de `Portal/` — `Portal/` passou a **ser**
o projeto Django, não conter um subprojeto.
- As 5 páginas HTML foram movidas para `Portal/templates/` (antes soltas na
raiz) — é para lá que `TEMPLATES[0]["DIRS"]` aponta agora.
- `assets/css`, `assets/js` e `img/` viraram `static/css`, `static/js` e
`static/img` (pasta `assets/` removida) — `STATICFILES_DIRS` (novo em
`settings.py`) aponta pra lá, e `STATIC_ROOT` foi adicionado para
`collectstatic` em produção.
- Os 5 templates passaram a usar `{% load static %}` + `{% static 'css/x.css' %}`
em vez de caminhos hardcoded (`assets/css/x.css`) — forma idiomática do
Django de referenciar estáticos.
- `config/urls.py` perdeu os `re_path`/`static_serve` manuais para
`assets/`/`img/` — `django.contrib.staticfiles` já serve `static/`
automaticamente em `DEBUG` a partir de `STATICFILES_DIRS`, sem
configuração extra.
- Bug corrigido de passagem: o `.env` já existia (com as credenciais do
Postgres) mas `settings.py` nunca chamava `load_dotenv()` — ele nunca
tinha sido lido de verdade. Corrigido ao mexer em `settings.py` por causa
da mudança de `BASE_DIR`.
Nenhuma mudança de comportamento — só de organização de arquivos; os
endpoints da API, os models e a lógica do frontend continuam os mesmos da
rodada 13.
### 15. Dispensar notificações persiste por usuário
Bug relatado: ao clicar no X de uma notificação individual, o card do
dropdown fechava inteiro (comportamento incorreto) e, além disso, qualquer
notificação dispensada (X individual ou "Limpar tudo") voltava a aparecer
depois de um reload da página.
- **Fechamento indevido do dropdown**: causado pela ordem de eventos —
`list.innerHTML` era reconstruído (removendo o botão clicado do DOM) antes
do clique terminar de subir até o listener global em `document` que fecha
o dropdown em clique fora; como o botão já não estava mais na árvore,
`dropdown.contains(event.target)` avaliava falso, fechando o card como se
fosse clique externo. Corrigido com `event.stopPropagation()` no handler
de dismiss em `notifications.js`.
- **Notificação reaparecendo no reload**: era o comportamento documentado
até então (`notifications.js` guardava a lista só em memória, "zera a cada
reload") — o usuário pediu explicitamente para persistir no backend em vez
de usar `localStorage`. Criado o model `NotificacaoDispensada` (chave
natural `usuario` + `notif_id`, mesmo padrão de `Favorito`/`WidgetUsuario`)
e o endpoint `/api/notificacoes-dispensadas/`. `notifications.js` agora
busca os IDs já dispensados no carregamento e filtra a lista antes de
renderizar, e persiste via POST a cada X clicado ou "Limpar tudo".
Importante: só o **estado de dispensada** passou a ser real — o conteúdo
das notificações de "nova ferramenta" (`PID_NEW_TOOLS_NOTIFICATIONS`)
continua mockado/hardcoded no frontend, só os itens de compromisso vêm de
dados reais. Ver rodada 87 para o restante da evolução desse sistema.
### 20. Logo com texto branco + sidebar maior
Dois bugs visuais de marca reportados com print: (1) a logo da tela de
login (`logo.png`) tem o texto "DePaula Contadores" em preto, ilegível
contra o fundo escuro do card de login (`--bg-surface`); (2) a logo da
sidebar (`logo-mono.png`, versão toda em branco/cinza claro) estava
pequena demais (`.sidebar__logo` era 56×56 fixo, espremendo uma arte que é
bem mais larga que alta) — pedido explícito pra usar ali uma versão com o
**D colorido e a letra branca**, num tamanho maior.
Essa variante (D colorido + texto branco) não existia como arquivo — só
existiam `logo.png` (D colorido, texto preto) e `logo-mono.png` (tudo
branco, D incluído). Gerada uma nova, `logo-branco.png`, com um script
Python usando Pillow: varre `logo.png` pixel a pixel e recolore pra branco
só os opacos quase-neutros/escuros (`max(r,g,b) < 70` e `spread(r,g,b) < 12`)
— o degradê dourado/marrom do D nunca bate nesse filtro porque mesmo na
sombra mais escura mantém um matiz quente (testado: pixel mais escuro do D
amostrado foi `(80,65,42)`, spread 38; texto era `(0,0,0)` puro, spread 0).
572.209 pixels alterados. Resultado confirmado visualmente (Read da imagem
gerada) antes de usar.
Trocado `logo.png` → `logo-branco.png` no login (`index.html`) e
`logo-mono.png` → `logo-branco.png` nos 5 shells (mesmo arquivo nos dois
lugares — ambos os fundos são escuros, então a mesma variante serve para
os dois). `.sidebar__logo` foi de `56×56` fixo pra `width:200px;
height:auto` (a arte é ~1.41:1, largura bem maior que altura — forçar
quadrado a esmagava); adicionado encolhimento pra `44px` nos dois estados
de sidebar colapsada (`.is-collapsed` no desktop, breakpoint mobile) pra
não vazar da faixa de 76px.
`logo.png` e `logo-mono.png` continuam no repo (não usados em nenhum
template agora) — o primeiro como fonte pra regerar `logo-branco.png` se a
arte oficial mudar, o segundo como variante alternativa disponível. Essa
logo foi substituída pela marca "P.I.D." na rodada 73.
### 25. Favicon
Pedido: "no ícone da aba do navegador, deve constar esta logo minimalista do escritório" — o usuário passou um print de referência: só o D (o mesmo pen/quill em degradê dourado/marrom de `logo.png`), sem o texto "De Paula Contadores", sobre fundo transparente.
Esse recorte isolado do D não existia como arquivo — as três variantes de `static/img/` sempre têm o texto junto. Gerado `favicon.png` a partir de `logo.png` reaproveitando o mesmo filtro criado na rodada 20 pra `logo-branco.png` (pixel opaco quase-neutro/escuro = texto, nunca o D, que mantém matiz quente até na sombra mais escura do degradê) — mas em vez de recolorir esses pixels pra branco, apagados (`alpha=0`) pra isolar só o D; resultado recortado pelo bounding box do que sobrou e centralizado num canvas quadrado transparente (o D é mais alto que largo), redimensionado pra 192×192. Resultado comparado visualmente (`Read` da imagem gerada) contra o print do usuário antes de usar — bateu.
Ligado via `<link rel="icon" type="image/png" href="{% static 'img/favicon.png' %}">` no `<head>` das 7 páginas (`index.html` + os 5 shells + `ramais.html`). Nenhuma mudança de Python; `manage.py check` limpo por precaução. Substituído pelo ícone da marca "P.I.D." na rodada 73.
### 26. Bug: busca de aplicações deixava grupos do menu abertos depois de limpar
Reportado com print: depois de pesquisar em "Pesquise por Aplicação..." (`search.js`) e apagar a busca, os grupos do menu (Portais, Geradoc etc.) que a busca tinha aberto pra mostrar o resultado continuavam abertos, em vez de voltar ao estado de antes de buscar.
Causa: `search.js` só tinha o caminho de **abrir** um `nav-group` (`classList.add("is-open")`) quando ele dava match durante a digitação — nunca fechava de volta quando o termo era apagado. Corrigido guardando, no início de cada sessão de busca (primeira tecla digitada), quais grupos já estavam abertos manualmente (`gruposAbertosAntesDaBusca`, um `Set`); ao limpar o campo, cada grupo volta exatamente pro estado daquele snapshot, em vez de simplesmente remover `is-open` de todo mundo (o que fecharia até um grupo que o usuário tinha aberto manualmente antes de buscar). Só front-end (`static/js/search.js`), nenhuma mudança de backend.
### 27. Botão "Criar Ausência" fora do padrão + texto do menu no tema claro
Dois ajustes pequenos:
- **Botão "Criar Ausência"** (`ramais.html`) usava `btn-outline`, destoando dos outros dois botões da mesma barra de ações ("Adicionar Ramal", "Novo Chamado"), ambos `btn-solid`. Trocado pra `btn-solid`, sem nada mais mudar no comportamento — ver `docs/ramais/CHANGELOG.md` pro restante do histórico de Ramais.
- **Texto do menu no tema claro**: a sidebar é intencionalmente congelada (fundo sempre escuro nos dois temas, ver `CLAUDE.md` → "CSS — organização entre arquivos"), mas as variáveis de texto (`--sidebar-text-primary`/`--sidebar-text-secondary`/`--sidebar-text-muted`) nunca tinham sido sobrescritas pro tema claro — pedido explícito pra virarem branco puro nesse tema, pra melhorar a legibilidade contra o fundo que continua escuro. Adicionada uma exceção deliberada em `tokens.css` (`:root[data-theme="light"]`) só pra essas três variáveis; fundo/borda da sidebar continuam frozen como antes. `CLAUDE.md` atualizado pra documentar a exceção, já que a regra geral (não usar overrides de tema na sidebar) continua valendo pro resto.
### 28. Fonte "Bree Serif" no título da tela inicial
Pedido: trocar a fonte do texto "Portal De Paula" no topbar da tela inicial (`portal.html`) pra Bree Serif, serif, negrito — só ali, não em `--font-sans` (usada em todo o resto do portal) nem no `.topbar__title` dos outros shells (que mostram outros títulos, como "Ramais"/"Administração").
Primeira fonte externa do projeto — até aqui só fontes do sistema (`--font-sans`). Carregada via Google Fonts (`<link rel="preconnect">` + `<link ... family=Bree+Serif>`) no `<head>` de `portal.html`. Estilo aplicado num id novo, `#portal-title` (no mesmo `<h1 class="topbar__title">` de sempre, só ganhou o id), colocado em `widgets.css` por ser o único CSS próprio da página — mesmo não sendo um widget. Ajustado por iterações diretas do usuário logo em seguida: negrito removido, tamanho aumentado (1.3rem), negrito devolvido, negrito removido de novo — estado final é Bree Serif 400/1.3rem. Na mesma leva de ajustes, o usuário pediu a mesma fonte também no título "Portal De Paula" da tela de login (`.login-card__title`, `login.css`) — `index.html` ganhou o mesmo `<link>` do Google Fonts.
**Este título foi removido de `portal.html` na rodada 73** (identidade visual "P.I.D."), quando o slogan da marca foi movido pro login — ver `login.css` em "CSS — organização entre arquivos" no `CLAUDE.md`.
### 29. Animações — pedido explícito pra ignorar `prefers-reduced-motion`
Pedido: "inclua animações nas páginas, para deixá-las fluidas", com duas condições explícitas — rápidas ("otimizando o tempo") e que **ignorem** a preferência de acessibilidade do sistema operacional/navegador do usuário (`prefers-reduced-motion`). Isso é uma inversão deliberada da prática padrão de acessibilidade (que normalmente desativaria animação quando essa preferência está ativa) — decisão do dono do produto pro próprio portal interno, registrada aqui pra não ser "corrigida" de volta sem confirmar de novo.
Implementado com três `@keyframes` genéricos em `base.css` (`pidFadeIn`, `pidFadeSlideUp`, `pidScaleIn` — único CSS carregado por toda página, inclusive `index.html`), aplicados via `animation` (não `transition`, que não anima a troca `display:none` ↔ visível causada por `hidden`) em: `.modal-overlay`/`.modal-card` (todo modal do app), `.account-dropdown`/`.notif-dropdown`, `.page-content` (toda navegação entre páginas) e `.login-card`. Botões (`.btn-solid`/`.btn-outline`/`.btn-ghost`/`.btn-danger-outline`/`.icon-btn`) ganharam `transform: scale()` no `:active` como feedback de clique, e `.pa-table td` (reaproveitada por Perfis de Acesso/Usuários/Ramais) ganhou `transition: background` no hover da linha, que antes trocava sem transição nenhuma. Durações todas curtas (120–200ms, a maioria usando os tokens `--transition-fast`/`--transition-base` que já existiam). Nenhum `@media (prefers-reduced-motion: reduce)` foi adicionado — ausência deliberada, não descuido (ver `CLAUDE.md` → "Animações").
### 54. Sexta cor de tema: Vermelho
Adicionada mais uma opção de tema de cor (`--accent`) ao seletor de swatches do modal "Gerenciar Usuário": "Vermelho" (`key: "vermelho"`, `#ef4444` no tema escuro / `#b91c1c` no tema claro), ao lado das 5 já existentes (roxo/azul/verde/âmbar/rosa). Só duas mudanças, já que a UI do seletor é gerada dinamicamente a partir de `PID_COLOR_THEMES` (`theme.js`, consumida por `account.js`): nova entrada no array `PID_COLOR_THEMES` e o par de blocos `:root[data-color-theme="vermelho"]`/`:root[data-theme="light"][data-color-theme="vermelho"]` em `tokens.css`, mesmo padrão das outras cores.
Como `--danger` (usado em feriados/selos de ausente etc.) já era um tom de vermelho (`#e5484d`) escolhido antes de "vermelho" existir como cor de tema selecionável, agora há uma sobreposição visual esperada quando o usuário escolhe o tema "Vermelho": elementos que usam `--accent` (ex.: pill "somente_eu" do Calendário Individual) ficam bem parecidos com elementos de erro/alerta que usam `--danger`. Diferente de `--gold`/`--teal`/`--slate`/`--coral` (escolhidos deliberadamente fora das cores de tema pra nunca colidir com `--accent`), `--danger` não foi criado pensando nisso — é uma coincidência aceita ao adicionar "Vermelho" à lista, não um bug. Se isso incomodar na prática, ajustar `--danger` para um tom fora da família "vermelho" é a correção futura, não mudar a cor do tema.
### 73. Nova identidade visual "P.I.D." — favicon, login e sidebar
Usuário forneceu uma nova marca (`C:\Users\Depaula\Documents\Projetos\LOGOS\pid-marca`, ver `pid-marca-leiame.md`) e pediu a adoção progressiva no Portal, mantendo o logo cursivo "D De Paula Contadores" só nos documentos/PDFs gerados pela aplicação (contrato, procuração, recibo do Indicador de Desempenho, simulação de custo de contratação) — decisão explícita: esses documentos são emitidos como se o próprio escritório os tivesse gerado, carregam a identidade dele perante o cliente, não a do Portal como ferramenta interna.
- **Favicon**: trocado de `favicon.png` (variante antiga do "D", rodada 25) pra `pid-icone-escuro.svg` (ícone quadrado da marca nova, variante fundo escuro) nas 11 páginas.
- **Login (`index.html`)**: o card ficou **congelado escuro nos dois temas** (tokens dedicados `--login-card-bg`/`--login-field-bg`/`--login-border`/`--login-text-*` em `tokens.css`, nunca redefinidos em `:root[data-theme="light"]` — mesmo padrão da sidebar) — pedido explícito depois de uma primeira tentativa (só a caixa de login/senha escura) não ter sido o que o usuário queria; só o fundo da página ao redor do card continua claro/escuro por tema. A logo alterna entre `pid-icone.svg`/`pid-icone-escuro.svg` conforme `data-theme` (`pidSyncLoginLogo()` em `theme.js` — **removido na rodada 76**), e o texto "Portal De Paula" virou "Portal Interno da De Paula". Um slogan da marca ("Grandes aplicações de todos os tamanhos.", fonte "Pinyon Script"/dourado `#c6a24a`) foi adicionado — testado primeiro no topbar de `portal.html` (rodada 28), removido de lá a pedido do usuário, e reintroduzido no login (rodapé, depois movido pra logo abaixo do título), tamanho ajustado até ficar legível sem competir com o título.
- **Sidebar** (10 shells): `sidebar__brand` passou a ter duas logos sempre no DOM, sobrepostas com `position:absolute` dentro de um container de altura fixa, alternando por `opacity`+`scale()` (crossfade, não troca de `src`) conforme a sidebar está expandida (`pid-logo-horizontal-escuro.svg`, assinatura com texto) ou colapsada (`pid-icone-escuro.svg`, só o símbolo) — sempre as variantes "-escuro", já que a sidebar é sempre escura nos dois temas. Tamanho da logo expandida aumentado em duas rodadas (176px/70% → 220px/92% → 250px/96% da largura útil) até o subtítulo "PORTAL INTERNO DA DE PAULA" ficar legível.
Ver `CLAUDE.md` ("Logos em `static/img/`") pro estado final detalhado de cada arquivo.
### 74. Animação de intro pós-login (porta de `pid-intro-escuro.html`)
Usuário forneceu um segundo arquivo da mesma pasta de marca, `pid-intro-escuro.html` — um bundle de "Design Canvas" (Anthropic) com uma animação de apresentação da marca P.I.D. (composição em 4 cenas: o ícone se desenha, ganha rosto, desliza pra revelar o wordmark "P.I.D." + tagline, e por fim voa/esmaece). Pediu pra reproduzir essa animação como transição entre o login e a tela Principal: ao clicar "Entrar" com sucesso, a animação toca por inteiro e só depois a tela Principal aparece, "pela barra lateral primeiro".
O arquivo fornecido não é HTML/CSS simples — é um bundle auto-contido do editor de canvas com um runtime próprio (React + motor de composição por tempo autorado "OM", fontes e o JSX fonte embutidos como blobs base64/gzip dentro de um `<script type="__bundler/manifest">`). Decodificado (script Python ad-hoc, gzip+base64 por uuid) pra extrair o JSX de verdade (`pid-intro.jsx`) e a biblioteca de easing/interpolação (`animations-v3.jsx`) — essas fórmulas na íntegra permitiram portar a coreografia **fielmente** (mesmas durações de cena, mesmas curvas de easing hand-rolled tipo Popmotion, mesmo timing de blink dos olhos) pra vanilla JS/CSS, sem carregar o runtime React/canvas original (pesado demais e não pensado pra produção).
- **Cenas** (mesma duração do original, 9.7s no total): Build (2.6s, contorno do ícone se desenha, aba dourada cai, tela do "rosto" abre) → Face (2s, olhos aparecem com pop e piscam, sorriso dourado se desenha) → Wordmark (2.8s, ícone desliza pra esquerda enquanto "P.I.D." + régua dourada + "Portal Interno da De Paula" aparecem) → Portal (2.3s, wordmark some, ícone encolhe e "voa" até o canto superior esquerdo real da janela — não as coordenadas de um frame de vídeo 1920×1080 do arquivo original, recalibradas pra mirar onde a logo colapsada da sidebar (`.sidebar__logo--icon`, 40px) realmente fica — e a cena inteira esmaece).
- **Arquivos novos**: `static/css/login-intro.css` (só `index.html`) e `static/js/login-intro.js` (motor de animação — easing/`animate`/`clamp`/render por `requestAnimationFrame`, tudo portado das fórmulas extraídas). Cores/formas idênticas às já usadas em `pid-icone-escuro.svg`/`pid-logo-horizontal-escuro.svg` (hardcoded, não são tokens de tema — é a paleta fixa da marca). Fontes "Space Grotesk" (wordmark) e "DM Mono" (tagline) adicionadas ao `<link>` de fontes de `index.html`, junto de "Bree Serif"/"Pinyon Script" já usadas ali.
- **Fluxo**: `auth.js`, no sucesso do POST de login, chama `pidPlayLoginIntro(onDone)` em vez de navegar direto — só ao final da animação (`onDone`) marca `sessionStorage.pid_reveal_portal="1"` e navega pra `portal.html`.
- **"Aparecendo primeiro pela barra lateral"**: novo `static/js/portal-reveal.js` (só `portal.html`, IIFE de topo tipo `theme.js`) — se a sessionStorage tiver a marca, aplica `html.pid-entering` antes da primeira pintura (evita flash) e remove a classe ~350ms depois do `DOMContentLoaded`. `layout.css` esconde `.main-content` (topbar+conteúdo) enquanto essa classe está presente, sem afetar `.sidebar` — como a sidebar não é tocada, ela fica visível o tempo todo e "aparece primeiro", com o resto do app surgindo num fade logo depois. Acesso direto a `portal.html` (sem passar pelo login) não aciona nada disso, já que a sessionStorage nunca é setada nesse caminho.
**Ainda não testado num navegador de verdade** — a próxima rodada deve conferir o resultado visual completo (timing, posição do "fly" final em diferentes tamanhos de tela, e o crossfade da sidebar) antes de considerar fechado.
**Três ajustes no mesmo dia, depois do primeiro teste real** (funcional, mas com pontos a melhorar): (1) a cor de fundo do overlay (`#241c33`, roxo-tinto do arquivo original) destoava do fundo real do Portal — trocada pra `#121017`, o mesmo valor de `--bg-canvas` no tema escuro, pra a sequência login→animação→portal ler como uma coisa só; (2) o overlay aparecia de repente por cima do card de login ainda visível ("de um modo bruto") — agora o card se dissolve (`.login-card.is-leaving`) enquanto o overlay sobe (`.login-intro.is-visible`), um cross-fade de 350ms, e só depois disso o ícone começa a se desenhar; (3) a cena final (Portal) tinha uma pausa parada depois do ícone já ter chegado ao canto até o fade acontecer — comprimida de 2,3s pra 0,95s (wordmark some quase na hora, ícone voa em 0,6s, tudo esmaece logo em seguida, sem intervalo morto) — duração total da animação caiu de 9,7s pra 8,35s.
**Quarto ajuste, mesmo dia**: a revelação da tela Principal (rodada 74) só escondia `.main-content`, deixando a sidebar sempre visível desde o início (sem nenhuma entrada própria) — usuário pediu mais fluidez, com a sidebar de fato aparecendo antes do resto. `portal-reveal.js` passou a aplicar duas classes em sequência: `pid-entering-sidebar` (esconde só a sidebar, com um leve `opacity`+`translateX(-16px)`) sai primeiro, 120ms depois do `DOMContentLoaded`; só 200ms depois disso é que `pid-entering` (esconde `.main-content`) sai, revelando o resto do app.
**Quinto e sexto ajustes, rodada seguinte**: (5) usuário pediu pra acelerar a construção do ícone — `Build` e `Face` (2,6s/2s no original) foram pra **1s cada**, `Wordmark` ("a parte onde aparece o nome do portal") ficou **igual** (2,8s, pedido explícito de não mexer), e `Portal` ("a parte na qual ele some") foi arredondada pra **~1s** também — total da animação caiu de 8,35s pra 5,7s; os deslocamentos internos de cada cena precisaram ser reproporcionados pra caber nas durações novas (não é só trocar `CUES`, os offsets tipo `Build+1.15` etc. também mudaram), e o terceiro blink do olho (na cena Portal) foi removido por não caber mais visivelmente numa cena tão curta. (6) A entrada da sidebar em `portal.html` (ajuste anterior, `opacity`+`translateX(-16px)`) foi considerada "ainda não satisfatória" — pequena demais pra ler como "a barra lateral surgindo da esquerda pra direita". Trocada por um slide de verdade: `translateX(-100%)` (a largura inteira da sidebar, não 16px) com uma transição própria de 420ms e curva de chegada suave (`cubic-bezier(0.16, 1, 0.3, 1)`, não `var(--transition-base)`); o tempo de espera antes de revelar o resto do app também subiu de 200ms pra 420ms, pra não sobrepor as duas entradas.
**Sétimo ajuste, rodada seguinte**: o fade de entrada (card se dissolvendo + overlay subindo, antes do ícone começar a se desenhar) estava "muito rápido" — a duração (350ms, tanto em `login.css`/`login-intro.css` quanto em `PID_INTRO_FADE_MS` de `login-intro.js`) subiu pra **500ms**, um ajuste pequeno de propósito ("bem pouco, só pra ficar mais fluído").
### 75. Logo da sidebar vira link pra "Principal", com os olhos do ícone piscando ao clicar
Usuário pediu duas coisas sobre a marca da sidebar (`sidebar__brand`, presente nos 10 shells): (1) transformá-la num botão/link que navegue pra `portal.html` (tela Principal) e (2) uma animação de piscar nos olhos do ícone (o "disquete") ao clicar.
`.sidebar__brand` deixou de ser `<div>` e virou `<a href="portal.html" id="sidebar-brand-link">` — mudança de tag que não quebra nada, já que todo CSS do crossfade (rodada 20/73) é por classe. O ponto difícil: as duas logos ali (`pid-logo-horizontal-escuro.svg` expandida, `pid-icone-escuro.svg` colapsada) eram `<img src="...svg">`, e uma `<img>` é uma imagem opaca — não dá pra CSS/JS da página alcançar elementos internos dela (os olhos) pra animar. Solução: **inlinear** o markup das duas logos como `<svg>` de verdade, direto no HTML dos 10 shells (os arquivos `.svg` continuam existindo em `static/img/`, só não são mais referenciados por essa parte da sidebar — favicon continua usando o arquivo normalmente).
Dentro do SVG inline, os dois olhos (círculo creme + glint escuro) ficam cada um num `<g class="sidebar-icon-eye__lid">` (renomeada pra `.pid-icon-eye` na rodada seguinte, ver abaixo) sem nenhum `transform` de atributo XML — detalhe técnico importante: um `transform` CSS aplicado a um elemento que já tem `transform` de atributo **substitui** o atributo inteiro (perderia a posição do olho). `static/js/sidebar-brand.js` (novo, incluído nos 10 shells) escuta o clique: ignora cliques modificados (ctrl/cmd/shift/meio, deixa abrir em nova aba normal), senão previne a navegação, adiciona `.is-blinking` nos olhos (dispara `@keyframes pidSidebarBlink` em `layout.css`, `scaleY(1)→0.05→1`, 200ms) e só navega de fato 260ms depois — tempo da piscada terminar de tocar antes da página trocar, mesmo espírito já usado na animação de intro do login (rodada 74).
### 76. Mesma piscada no ícone do login + logo do login deixa de alternar por tema
Usuário pediu duas coisas: (1) o mesmo efeito de piscar do olho (rodada 75) também no ícone do card de login, ao clicar; (2) usar a **mesma logo nos dois temas** no login (parar de alternar `pid-icone.svg`/`pid-icone-escuro.svg` conforme `data-theme`, ver rodada 73).
(2) foi resolvida primeiro, e simplificou (1): já que o card do login é congelado escuro nos dois temas desde a rodada 73, a alternância de logo por tema nunca fez muito sentido — removida (`pidSyncLoginLogo()` tirada de `theme.js`, os atributos `data-logo-dark`/`data-logo-light` tirados de `index.html`); a logo do login agora sempre usa as cores de `pid-icone-escuro.svg`, sem checagem de tema nenhuma. Isso deixou `pid-icone.svg` (a variante clara) sem nenhum consumidor no Portal.
Pra (1), mesmo problema técnico da rodada 75 (não dá pra animar dentro de um `<img>`) — o ícone do login virou `<button type="button" id="login-logo-btn">` com o SVG inline dentro (igual à sidebar), e `static/js/login-logo-blink.js` (novo, só `index.html`) dispara a piscada ao clicar — sem `preventDefault`/delay/navegação, já que estamos na própria tela de login, é só um toque decorativo.
**Refatoração de nomes, pra reaproveitar entre os dois lugares**: como agora dois contextos diferentes (sidebar e login) piscam o mesmo ícone, `.sidebar-icon-eye__lid`/`pidSidebarBlink` (que moravam em `layout.css`, não carregado por `index.html`) foram renomeados pra `.pid-icon-eye`/`pidIconBlink` e movidos pra `base.css` (único CSS comum às duas telas) — `layout.css` e os 10 shells foram atualizados pra usar o nome novo.
### 77. Fundo da tela de login no tema claro batendo com o fundo real do Portal
Usuário reportou que, no tema claro, o fundo da tela de login (`.login-page`, a área ao redor do card) ficava com um tom acinzentado/amarronzado bem diferente do fundo de verdade da tela Principal nesse tema (um lavanda bem claro, quase branco). Causa: `.login-page` sempre aplicou um scrim escuro + um brilho radial por cima do `--bg-canvas`, pensados pra dar profundidade num fundo escuro — no tema claro, esse mesmo scrim escuro por cima de um `--bg-canvas` já claro produzia esse cinza sujo, nada parecido com o `--bg-canvas` puro que `portal.html` usa (via `body` em `base.css`).
Corrigido com um override `:root[data-theme="light"] .login-page { background: var(--bg-canvas); }` — no tema claro, zera o scrim/glow e usa só a cor sólida, igual ao resto do Portal; no tema escuro (padrão) nada mudou, o scrim+glow continuam dando a profundidade de antes.
### 78. Mesmo ajuste na animação de intro: fundo claro no tema claro, texto do wordmark escuro
Sequência natural da rodada 77 (fundo de `.login-page`): o usuário pediu o mesmo pro fundo da **animação de intro** (`#login-intro`, rodada 74) — no tema claro, também devia usar o `--bg-canvas` claro, não o `#121017` escuro fixo. Só que aqui tinha uma pegadinha que `.login-page` não tinha: o wordmark da animação ("P.I.D." + tagline) usa texto **claro**, pensado pra contrastar contra um fundo escuro — mudar só o fundo pro claro deixaria esse texto ilegível.
`login-intro.css` ganhou os overrides de tema claro: `.login-intro` vira `background: var(--bg-canvas)`, e `.login-intro__letter`/`.login-intro__tagline` viram cores escuras (`#241c33`/`rgba(36, 28, 51, 0.62)`) só nesse tema. O ícone (corpo roxo + tela escura do disquete) não precisou de nenhum ajuste — já tem contraste de sobra nos dois fundos. Tema escuro (padrão) ficou intocado.
### 79. Fundo de `.login-page` no tema claro refinado: brilho roxo mais claro + base levemente mais escura
Depois de ver o fundo liso (`var(--bg-canvas)`, rodada 77), usuário pediu um refinamento: "um tom de roxo um pouco mais claro e o fundo branco levemente escurecido", só no tema claro. Trocado por `radial-gradient(circle at 20% 20%, rgba(var(--accent-rgb), 0.12), transparent 45%)` sobre `#ece7f2` (levemente mais escuro que o `--bg-canvas` puro, `#f3f1f7`) — o brilho radial já existia no tema escuro com opacidade `0.25`, aqui ficou bem mais sutil (`0.12`) pra não ficar turvo sobre um fundo claro. A cor exata é fixa só nesta regra (não altera o token `--bg-canvas`), então o resto do Portal no tema claro continua com o tom original.
### 81. "Mais informações" por aplicação (botão "?")
Pedido explícito do usuário: um botão "?" ao lado do nome de qualquer aplicação, mostrando "Mais informações" no hover e abrindo, ao clicar, um modal com um texto de ajuda (objetivo/processo/cuidados/resultado esperado). Visualizar é livre a qualquer autenticado; editar é restrito a quem tem o perfil **"Inovação"** vinculado.
- Novo model `AjudaAplicacao` (chave natural `app_key`, `texto`, `atualizado_em`/`atualizado_por`) + endpoint `GET`/`PATCH /api/ajuda-aplicacoes/<app_key>/`. A checagem de quem pode editar é por **nome fixo** do perfil (`Usuario.eh_perfil_inovacao()`/`models.PERFIL_INOVACAO_NOME`), mesmo padrão já usado pro selo "Restrito" de Relatórios Gerenciais (nome === "Diretoria") — decisão explícita do usuário pra não precisar aparecer na árvore de Perfis de Acesso. `GET /api/me/` ganhou `eh_perfil_inovacao`.
- Ligado por ora só em Importação de Plano de Saúde (`static/js/ajuda-aplicacao.js`, `pidCriarBotaoAjuda()`), com um texto inicial já estruturado e salvo no banco — mecanismo genérico o bastante pra outra aplicação só precisar do botão+tooltip no HTML.
- **Ganhou imagens embutidas** (pedido explícito, mesmo mecanismo de "Observações" de Acessos Gerais): editor `<div contenteditable>` com colar/arrastar imagem, sanitizado no servidor via `nh3` antes de salvar. As constantes de allowlist do nh3 foram generalizadas (`ACESSO_GERAL_OBSERVACOES_ALLOWED_*` → `RICHTEXT_ALLOWED_*`, `serializers.py`) pra serem compartilhadas pelos dois campos.
A correção de um bug real do `seed_portal.py` encontrado nesta mesma rodada (reescrevia `nome` de um `PerfilAcesso` já existente) e o restante da limpeza de nomenclatura estão no `CHANGELOG.md` de Perfis de Acesso/Usuários.
### 82. Modal de confirmação/aviso genérico — fim do `window.confirm`/`window.alert` nativo
Usuário viu o popup nativo do Chrome ("192.168.x.x:8000 diz...") na confirmação de "Sair sem salvar" do modal de "Mais informações" (rodada 81) e pediu, de forma geral: nenhum popup deve usar o diálogo nativo do browser — sempre um modal dentro do próprio Portal, no padrão visual dele.
- `static/js/confirm-modal.js` (novo, incluído logo depois de `api.js` em **todo** shell, inclusive `index.html`): `pidConfirm(mensagem, opcoes)` (Promise<boolean>) e `pidAlert(mensagem, opcoes)` (Promise<void>) compartilham o mesmo modal (`#pid-confirm-modal`), que empilha por cima de qualquer modal já aberto (`.modal-overlay--top`, z-index maior) sem fechá-lo. `opcoes.perigoso` troca o botão de ação pra `.btn-danger-outline`.
- **Migração completa** (pedido explícito — "migre as demais"): todo `window.confirm()`/`window.alert()` do app (31 ocorrências em 8 arquivos — `acessos-gerais.js`, `calendar-individual.js`, `links-ferramentas.js`, `importacao-plano-saude.js`, `ramais.js`, `profiles.js`, `indicador-desempenho.js`, `users-admin.js`) foi trocado por `pidConfirm`/`pidAlert`. Um modal bespoke que já existia em `importacao-plano-saude.js` só pra esse mesmo motivo (`#ips-regracad-confirm-fechar-modal`, no "Cadastro de Regras") foi removido e consolidado no componente genérico. **Não migrado**: um `window.prompt()` em `users-admin.js` (renomear departamento) — tipo de popup diferente (pede texto), sem componente equivalente ainda.
### 87. Notificações de "nova ferramenta": gate de permissão + expiração automática em 10 dias
Usuário viu, pelo dropdown do sino, notificações de ferramenta antigas (algumas com mais de duas semanas) ainda listadas como novas, e pediu duas correções: (1) notificação de uma aplicação que o usuário não tem permissão de acessar nunca deve aparecer; (2) toda notificação de "nova ferramenta" deve valer por só 10 dias — depois disso, cair sozinha pra dispensadas, sem esperar o usuário clicar no X.
- Cada entrada de `PID_NEW_TOOLS_NOTIFICATIONS` (`notifications.js`) ganhou um campo `access` (`{type:"gerencia"}` ou `{type:"module", module:"..."}`) reproduzindo o mesmo gate que já esconde o item correspondente no menu (`pidApplyAccessVisibility` em `access.js`) — `pidNotifToolElegivel()` filtra a lista antes de montar `toolNotifications`, então uma notificação sem permissão nunca aparece nem no sino nem no histórico.
- `date: "DD/MM"` (string pré-formatada, sem ano) virou `dataIso: "AAAA-MM-DD"` — `pidNotifToolExpirada()` compara contra a data de hoje (`PID_NOTIF_TOOL_EXPIRA_DIAS = 10`); passado o prazo, `notifications.js` chama `POST /api/notificacoes-dispensadas/` sozinho no carregamento da página (mesma chamada do X manual), então a notificação passa a seguir 100% as regras já existentes de dispensa/histórico/restauração (ver rodada 15) — nenhum mecanismo novo no backend.
- `toolNotificationsAgora` (recorte "elegível agora", análogo a `pidEventosElegiveisAgora()` pros compromissos) exclui as expiradas por prazo tanto na carga inicial quanto depois de um "Restaurar" — restaurar uma notificação de ferramenta com mais de 10 dias mantém o rastro no histórico, mas não a devolve ao sino.
- Ver `CLAUDE.md`, seção "Armazenamento" → "Histórico de notificações" pro detalhamento completo.
### 88. Não Conformidades (Relatórios > Qualidade) — nova aplicação
Pedido (2026-08-27): evoluir a skill do Claude `analise-ncs` (ver `projects/Controladoria - Analise NCs/`), que gerava sob demanda um Excel de 7 abas a partir de dois arquivos exportados do Sigsistem (ocorrências .xlsx + ações .xls, esse último HTML disfarçado), pra uma aplicação dentro do Portal com gestão **contínua**: identificar novas ocorrências sem análise, análises sem ação, acompanhamentos recentes sem tratativa da Qualidade, priorizar vencidas/vencendo em 7 dias, permitir marcar manualmente o que foi tratado, e um dashboard (ações abertas, motivos de abertura, clientes/colaboradores com maior incidência).
Decisões de negócio confirmadas antes de implementar: status de tratativa é controle interno do Portal (sem escrita de volta no Sigsistem), mas reabre sozinho quando uma importação nova traz algo diferente do que existia no momento do tratamento; guardar o histórico completo de acompanhamentos (não só o último, como a skill fazia); colaborador com maior incidência = `Indicado para Descrever Análise da Ocorrência`; motivo de abertura = `Tipo(s) de Causa(s) da Ocorrência`; permissão de toggle único **restrita por padrão** (só Integração e Inovação nasce com acesso — diferente de "Relatório Setorial", que herda automático via `SECTORAL_KEYS`).
Nova aplicação em Relatórios > Qualidade (subgrupo novo em `catalogo.py`, mesmo formato de subgrupo-com-uma-tool já usado em Auditorias) — pacote Python puro `portal_api/nao_conformidades/` (sem pandas, mesmo padrão de `planos_saude`/`indicadores`), 4 models novos, 3 `ModelViewSet` + dashboard, tela nova com 3 abas. Núcleo técnico: reabertura automática por diff (`nao_conformidades/diff.py`) — cada ocorrência/ação tratada guarda um snapshot congelado, comparado a cada importação nova contra o estado atual (ação por data do último acompanhamento; ocorrência por hash de texto da análise ou ação nova aberta). Validado de ponta a ponta com dados reais da amostra fornecida: totais batem com o relatório de referência da skill em tudo, exceto "Total de Ações Abertas" (a skill original contava também linhas sem nenhuma ação de fato aberta — artefato do script, decisão deliberada de não replicar); reimportação idempotente; dois bugs de robustez corrigidos (`assunto` virou `TextField` — um export real trouxe 395 caracteres; e um `SerializerMethodField` que chamava uma propriedade inexistente no model). Interação em navegador não testada nesta rodada (sem ferramenta de automação de browser disponível) — pipeline/API validados via chamadas HTTP diretas, tela construída seguindo os padrões visuais já validados de `importacao-plano-saude.html`/`indicador-desempenho.html`. Detalhe completo em `portal_api/nao_conformidades/CLAUDE.md`.
### 89. Não Conformidades — redesign de Dashboard/Gestão a partir de teste real na tela
Depois de testar a rodada 88 na tela, o usuário deu uma lista concreta de feedback de UX. Resumo (detalhe completo em `portal_api/nao_conformidades/CHANGELOG.md`): Dashboard virou a aba padrão ao abrir; 4 cards e as 3 tabelas de ranking ficaram clicáveis (drill-down direto pra Gestão já filtrada); rankings de Clientes/Colaboradores passaram a mostrar a contagem quebrada por tipo com cor (NC/Reclamação/Outros) em vez de restringir o filtro, a pedido explícito do usuário; dois KPIs de tempo médio novos (preenchimento de análise, abertura de ação) — um terceiro (resolução de ações) foi descartado antes de implementar porque o campo correspondente está 100% vazio no export real, e o usuário confirmou pra não criar um card sem dado; seletor de período virou chips em vez de `<select>`. Na Gestão: toda linha ganhou um botão de expandir (mesmo padrão de `.cc-painel-fiscal__toggle`) que revela ocorrência completa ou o histórico de acompanhamentos (mais recente primeiro) — substituiu o modal de histórico, que ficava colado no botão "Marcar como tratado"; a coluna única "Status" de Ações virou duas (Prazo e Tratativa), com filtros em chips combináveis; um campo de busca único passou a filtrar as 3 listas da Gestão de uma vez, por colaborador/empresa/assunto.
Dois bugs reais corrigidos durante o teste: uma corrida (`ativarTab()` tinha efeito colateral de carregamento que corria em paralelo com o carregamento explícito de um drill-down, e o que terminasse por último vencia — às vezes mostrando o filtro errado) e um alinhamento de texto (célula de painel de detalhe herdava `text-align:right` de `.pa-table td:last-child` por ser sempre "a única/última" célula da linha). Instalado Playwright ad-hoc neste ambiente pra validar a tela de ponta a ponta (login, drill-downs, expandir, alternar tratativa, filtros combinados) — não é dependência do projeto, só ferramenta de teste desta sessão.
## Limitações conhecidas / decisões assumidas
- Calendário De Paula (empresa) não tem mais versão interna — depende
inteiramente do link externo `depaula-tvcorporativa.lovable.app` (ver
rodada 9 no `CHANGELOG.md` de Calendário Individual). Não recriar um
`calendario.html` interno sem confirmar com o usuário.
- Sem teste automatizado (nem no frontend, nem no backend Django) — todo o
fluxo é validado manualmente no navegador ou via `django.test.Client`/
`APIRequestFactory` ad-hoc dentro de cada rodada de implementação, nunca
numa suíte que rode sozinha.
- Tema (claro/escuro e cor) continua só no `localStorage` — é preferência
de navegador, decisão deliberada de manter fora da migração para o
banco.
- Limitações específicas de escopo de cada aplicação (v1 cobrindo só uma
modalidade/departamento, uma operadora ainda não validada com arquivo
real, etc.) estão documentadas no `README.md`/`CLAUDE.md` de cada
aplicação — ver `README.md` na raiz pro mapa completo.
## Roadmap / próximos passos
Nenhuma pendência estrutural explícita em aberto no momento — cada rodada
acima foi fechada a pedido do usuário. Ao retomar o projeto, perguntar o
que vem a seguir em vez de assumir.
Uma limitação específica de aplicação que segue em aberto: o Indicador de
Desempenho resolve o departamento de um colaborador pelo `gerente`, o que
quebra o caso de um gerente que supervisiona pessoas de departamentos
diferentes (ver `portal_api/indicadores/CHANGELOG.md`, rodada 45, e
`portal_api/indicadores/CLAUDE.md` → "Departamento organizacional") — só
mexer nisso se o usuário confirmar que quer uma exceção por colaborador.
O passo natural mais amplo que falta é validar de ponta a ponta num
navegador de verdade um conjunto de funcionalidades que só foram revisadas
estaticamente ou testadas parcialmente logo após a migração pra Django
(rodadas 13–14): Links & Ferramentas, a tela de Ramais completa, Acessos
Gerais (incluindo a restrição de seção por perfil e as observações ricas
com imagem), Eventos Corporativos no Calendário Individual com um perfil
sem a permissão de criar evento, Importação de Plano de Saúde ponta a
ponta com um arquivo real de cada operadora, Simulação de Custo de
Contratação conferindo o PDF gerado e a Lei 15.270/2025, e Indicador de
Desempenho com uma apuração completa nova — ver o `CHANGELOG.md` de cada
aplicação para o que já foi de fato testado rodada a rodada desde então.
A tela de Não Conformidades (rodadas 88–89) já foi validada de ponta a
ponta, inclusive em navegador de verdade via Playwright (login, drill-downs
do Dashboard, expandir ocorrência/ação, marcar tratado/reabrir, filtros
combinados) — ver `portal_api/nao_conformidades/CHANGELOG.md`. Falta só o
upload real do formulário "Nova Importação" pelo navegador (testado até
agora só via API), e a conferência do usuário sobre o resultado visual do
redesign da rodada 89 e dos novos cards de tempo médio da rodada 90.
### 90. Não Conformidades — tempo médio de execução da ação e ciclo completo
O usuário trouxe um export do Sigsistem diferente do original ("com
finalizadas" — traz o histórico de ações já concluídas, 3474 linhas contra
287 do export padrão, `Data de Finalização` preenchida em 3187), viabilizando
o terceiro KPI de tempo médio que tinha sido descartado na rodada 89 por
falta de dado real. `tempos_medios` do dashboard ganhou uma 3ª etapa
(execução da ação, abertura → finalização) e um KPI de ciclo completo
(emissão da ocorrência → finalização da ação), cada um com o tamanho da
amostra (`n`) junto da média — inclusive retroativo às 2 etapas que já
existiam. A execução também é quebrada por categoria de ação
(Correção/Ação Corretiva/Outros), porque a média geral (~62 dias) escondia
uma variância enorme (1 a ~793 dias) entre tipos bem diferentes. No
frontend, uma primeira versão trocou os cards por um funil visual com setas
entre etapas — revertida no mesmo dia a pedido do usuário, que preferiu
manter o card grid original (agora com 4 cards em vez de 2, cada um com o
`n` da amostra). Detalhe completo em
`portal_api/nao_conformidades/CHANGELOG.md` (rodada 90) e
`portal_api/nao_conformidades/CLAUDE.md` (seção "Tempos médios").
### 91. Não Conformidades — upload travando com arquivo real grande (upsert virou lote)
Usuário reportou "Erro ao processar a solicitação." ao reprocessar os mesmos dois arquivos "com finalizadas" da rodada 90, e perguntou se seria a extensão `.xls`/`.xlsx` ou incompatibilidade com o ambiente Linux de produção. Nenhum dos dois: reproduzido localmente com os arquivos reais, o upload funcionava, só levava ~60s — tempo o bastante pra estourar o timeout de um worker WSGI em produção (gunicorn, 30s por padrão) ou disparar o autoreload do `manage.py runserver` em dev, derrubando a conexão sem nenhum problema de dado por trás. Causa raiz: o upsert (rodada 88) fazia uma ida ao banco por item (`update_or_create`/`get_or_create`), aceitável nos ~300 registros da amostra inicial mas não nos ~1700 ocorrências/~3400 ações/~9000 acompanhamentos do export real (quase 28 mil idas ao banco). Reescrito pra lote (`bulk_create`/`bulk_update`) e pra pular quem não mudou nada desde a última importação (reimportação periódica traz de volta o histórico inteiro, não só o novo) — reimportar o mesmo arquivo sem mudança real caiu de ~60s pra ~4s. Nenhuma migração, nenhuma mudança de contrato de API. Detalhe completo em `portal_api/nao_conformidades/CHANGELOG.md` (rodada 91) e `portal_api/nao_conformidades/CLAUDE.md` (seção "Upsert").