portal_publico/plano.md

49 KiB
Raw Blame History

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").

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) e pidAlert(mensagem, opcoes) (Promise) 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

Evolui a skill do Claude analise-ncs (Excel sob demanda a partir de dois exports do Sigsistem) pra uma gestão contínua dentro do Portal: reabertura automática por diff quando algo muda desde o último tratamento, histórico completo de acompanhamentos e um dashboard (ações vencidas/vencendo, motivos de abertura, clientes/colaboradores com maior incidência). Nasce restrita ao perfil "Inovação" (mesmo padrão adotado depois pro Dashboard Contábil). Histórico técnico completo (models, pacote portal_api/nao_conformidades/, e as rodadas seguintes de refinamento) em portal_api/nao_conformidades/CHANGELOG.md/CLAUDE.md — a partir daqui, rodadas específicas desta aplicação não duplicam entrada aqui (ver nota no topo deste arquivo).

92. Dashboard Contábil (Relatórios > Contabilidade) — nova aplicação

Nova aplicação em Relatórios > Contabilidade (nasce restrita ao perfil "Inovação", mesmo padrão de "Não Conformidades"): o contador anexa o PDF de Balancete + DRE (modelo Questor), a ferramenta extrai as contas/linhas e roda um motor de regras de auditoria, com revisão de achados/observações. Subgrupo "Contabilidade" novo em catalogo.py dentro de relatorios. Histórico técnico completo (fórmulas, models, desafio de extração do PDF) e das rodadas seguintes (94+, incluindo o botão "Gerar Dashboard") em portal_api/dashboard_contabil/CHANGELOG.md/CLAUDE.md — a partir daqui, rodadas específicas desta aplicação não duplicam entrada aqui (ver nota no topo deste arquivo).

93. Menu "Relatórios" — remoção do placeholder "Relatório Setorial"

Pedido (2026-09-14): remover o item "Relatório Setorial" (href="#", sem tela própria) do menu Relatórios, já que o tópico agora tem itens de verdade ("Qualidade" → Não Conformidades, "Contabilidade" → Relatório Contábil). Removido de MODULE_APPS["relatorios"] em catalogo.py e do <li data-app="relatorio-setorial"> hardcoded nas 13 páginas-shell (o menu é replicado por template, não um componente único — ver "Ordem de <script>" no CLAUDE.md raiz pro mesmo padrão de duplicação entre shells). O item "Indicadores" (outro placeholder href="#" dentro de Relatórios) não foi tocado, não fazia parte do pedido. Perfis já existentes no banco podem manter uma chave órfã "relatorio-setorial" dentro de permissoes["relatorios"]["apps"] — inofensiva, mesmo padrão já visto quando "Gerar Contrato"/"Gerar Procuração" foram removidos de Geradoc (ver comentário em catalogo.py).

94. Temas sazonais (Halloween) — logo/mascote vampiro, intro de abertura, teias de aranha e opt-out

Pedido (2026-09-15): deixar uma logo e uma introdução de abertura com tema de Halloween, aplicadas só durante outubro, revertendo pro P.I.D. normal depois — sem descartar nada do que já existe. Implementado como mecanismo 100% runtime (nenhum arquivo original alterado ou removido — fora da janela ativa, tudo volta sozinho): troca de ícone/favicon/assinatura pelo mascote vampiro, uma intro de abertura pós-login alternativa, uma animação de "susto" ao clicar na logo, teias de aranha decorativas nos cantos do login/tela Principal (com uma aranha "matável" ao clicar, óbito persistido) e um toggle no menu da conta pro usuário desligar temas sazonais por preferência própria (ou acessibilidade — aracnofobia). Ainda em preview (PID_HALLOWEEN_PREVIEW_FORCE força ligado pra revisão visual, antes de confirmar a janela de outubro definitivamente). Histórico técnico completo (rodadas 94-100) e detalhamento em docs/temas-sazonais/CHANGELOG.md/temas-sazonais.md — a partir daqui, rodadas específicas desta aplicação não duplicam entrada aqui (ver nota no topo deste arquivo).

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.