Como uma planilha de controle manual se tornou um sistema web completo rodando inteiramente no browser.
O Problema Original
O controle operacional da recepção da unidade WPM era feito de forma manual: planilhas, anotações em papel, WhatsApp para pendências. Dados de alunos novos, vendas de addons, NPS e escala de trabalho não tinham uma fonte única da verdade.
Cada recepcionista registrava informações de forma diferente. Relatórios mensais levavam horas para compilar. Pendências se perdiam entre conversas.
A Decisão
A solução foi criar um sistema digital próprio. A escolha de começar como um único arquivo HTML foi deliberada: sem servidor, sem deploy, sem dependências externas. Qualquer computador com um browser poderia abrir e usar imediatamente.
Isso eliminou a barreira de entrada e permitiu iterações rápidas. O arquivo poderia ser passado por pen-drive, WhatsApp ou e-mail.
Escopo Inicial — O que precisava existir
Operacional
Registro de alunos novos com atendente responsável, vendas de addons vinculadas aos atendimentos, controle de pendências com status e responsáveis.
Gestão
Dashboard executivo com métricas do mês, NPS com ranking de recepcionistas, escala de trabalho mensal com regras trabalhistas.
Infraestrutura
Segregação por período mensal, exportação e importação de backups JSON, sistema de fechamento mensal que avança o período automaticamente.
WPM Gestão Interna — um sistema operacional completo de gestão de recepção rodando 100% no browser, sem backend.
Definição Técnica
O sistema é uma Single Page Application (SPA) single-file — um único arquivo .html com todo o HTML, CSS e JavaScript embutidos (~11.500 linhas na v34). O estado é armazenado em IndexedDB como backend primário e localStorage como espelho de fallback, segregado por período no formato YYYY-MM. Não há servidor, não há banco externo, não há login.
A arquitetura é dividida em camadas explícitas: config → persistência → schema → domínio/seletores → renderização → UI/eventos → diagnósticos. Todo o contrato de atualização de estado é explícito: Ação → Mutação → saveData() → requestRender().
Equipe Gerenciada
Recepcionistas
Wallace, PESSOA 2, PESSOA 3, PESSOA 4 — atendentes de alunos novos e hostess de pendências.
Professores
PESSOA A, PESSOA B, PESSOA C, PESSOA D, PESSOA E, PESSOA F, PESSOA G — aparecem na escala. PESSOA G não pode fazer abertura aos sábados e feriados (restrição interjornada).
Períodos Mensais
O sistema trata cada mês como uma unidade isolada. Dados de 2026-01 nunca vazam para 2026-02. O sistema suporta navegação completa entre períodos históricos com dados preservados.
Campos deriváveis — como dia da semana a partir de uma data — nunca são salvos no storage. São calculados em tempo de execução para economizar quota de armazenamento.
A separação de responsabilidades não é apenas conceitual — está formalizada no objeto APP_INTERNALS, que mapeia e expõe todas as funções públicas de cada camada para diagnóstico e inspeção em console.
Fluxo de Dados
O estado da aplicação é centralizado em duas variáveis: storage (store global com todos os períodos) e state (referência direta ao período ativo). Toda mutação passa por funções de ação que atualizam state, chamam saveData() e disparam requestRender() para as áreas afetadas.
Não há setState reativo ou framework. O contrato de atualização é explícito, auditável e determinístico.
Inicialização em Etapas
A inicialização é assíncrona e segue sequência rígida: (1) hydrateStorageCache — lê IDB + localStorage em paralelo; (2) syncAppState — normaliza o store; (3) bindings de UI; (4) initializeForms; (5) renderInitialViews + diagnósticos. Falhas mostram toast de erro sem travar a interface.
APP_INTERNALS — Mapa Formal dos Módulos
O objeto Object.freeze(APP_INTERNALS) expõe explicitamente, por categoria, todas as funções internas do sistema: config, persistence, schema, domain, actions, rendering, ui e diagnostics. Disponível em window.__APP_INTERNALS__ para inspeção em DevTools sem necessidade de quebrar o arquivo.
Zero build tools. Zero Node.js. Zero backend. Tudo nativo no browser — sem CDNs externas na v34, sem dependências de rede em runtime.
Core — 100% Nativo
Padrões de Engenharia
Persistência Dual
IndexedDB como backend primário (assíncrono, sem limite restritivo). localStorage como espelho síncrono e fallback. Cache Map em memória elimina leituras desnecessárias. Fila serializada via queueStorageOperation previne race conditions.
Renderização Eficiente
aplicarHtmlSeMudou() compara string HTML nova com innerHTML atual antes de tocar o DOM. aplicarPatchCards() faz diff por ID em listas. requestRender() com fila de coalescing evita renders redundantes em cascata.
Seletores Memoizados
lerSelectorMemorizado() armazena resultados indexados por período + nome + assinatura JSON. Cache com limite de 120 entradas antes de limpeza automática — evita memory leak em sessões longas.
Cada módulo tem lógica de domínio, seletor memoizado, formulário modal, render independente e re-render seletivo.
A camada de maior maturidade técnica do projeto. IndexedDB primário + localStorage espelho, mediados por cache em memória e fila serializada de escritas.
IndexedDB — Backend Primário
Preferido por suportar valores maiores, não bloquear a thread principal e ser mais adequado para dados estruturados. A função withIndexedDbStore() abstrai transações com promise wrapper. Singleton de conexão com idbOpenPromise evita aberturas paralelas.
localStorage — Espelho e Fallback
Mantido em sincronia após cada escrita no IDB. Garante compatibilidade com ambientes que bloqueiam IndexedDB, alimenta o storage event para broadcast cross-tab e serve como fallback imediato em leituras síncronas via readStoredValue().
Fila Serializada — Race Conditions Eliminadas
Toda escrita no backend passa por queueStorageOperation(), que enfileira operações assíncronas e as executa em sequência (nunca em paralelo). Previne corrupção silenciosa de dados em cenários de múltiplas escritas simultâneas — fonte recorrente de bugs em versões anteriores.
Broadcast Cross-Tab
Escritas emitem payload JSON em STORAGE_BROADCAST_KEY via storage event. Outras abas consomem via consumeStorageBroadcast(), resincronizam com syncAppState() e re-renderizam — estado consistente entre múltiplas instâncias abertas do sistema.
Gerenciamento de Quota + Snapshot Local
Toda operação de escrita trata QuotaExceededError explicitamente com toast orientativo. A interface de Configurações exibe uso estimado via Blob.size com barra de progresso e alerta acima de 80% dos 5MB estimados. Snapshot local salvo automaticamente a cada exportação, com timestamp exibido na health bar.
Diff de strings HTML, patch por ID, memoização por assinatura e fila com coalescing — sem React, sem Virtual DOM, sem build tool.
aplicarHtmlSeMudou — Diff de Strings
Antes de tocar o DOM, a função compara a string HTML nova com o innerHTML atual do elemento. Se forem idênticos, o DOM não é modificado — zero reflow, zero repaint. Especialmente eficaz em views ativas sem dados novos desde o último render.
aplicarPatchCards / Linhas — Diff por ID
Para listas de elementos (cards Kanban, linhas de tabela, cards de eventos), diff por ID: elementos existentes são atualizados só se o HTML mudou; novos são inseridos; removidos são eliminados. Preserva estado de foco e scroll sem recriar o DOM inteiro.
requestRender — Fila com Coalescing
A função requestRender(areas) enfileira áreas específicas (dashboard, students, addons, pending, nps, scale, events, settings, hero) sem renderizar imediatamente. O processo real acontece em microtask via Promise.resolve(), coalescendo múltiplas chamadas em um único ciclo — evita renders redundantes em cascata quando múltiplas ações ocorrem em sequência.
Sistema de design CSS completo baseado em custom properties, com acessibilidade de teclado, ARIA completo e drag-and-drop nativo.
Design System CSS
--bg, --primary (#FFC20F), --danger, --ok, --warning com variantes soft/strong. Aplicadas consistentemente em botões, pills, cards e gradientes.
backdrop-filter: blur(12px), z-index elevado, controles de período sempre visíveis durante scroll.
Fonte principal com fallback para system-ui. JetBrains Mono para valores numéricos e código.
Acessibilidade e Teclado
Tabs com role=tablist/tab, aria-selected, tabindex dinâmico. Modais com role=dialog, aria-modal, foco retornado ao elemento de origem ao fechar.
Setas ArrowLeft/Right + Home/End navegam entre abas. / foca busca da aba ativa. Esc fecha modal. Todos os botões com type="button" explícito.
HTML5 D&D API no Kanban. Highlight visual da coluna de destino. Classe dragging com transform e opacity durante o arraste.
Uma falha de layout crítica foi descoberta ao usar height:100vh; overflow:hidden em contêineres. Isso criava scroll traps e gaps de conteúdo. O padrão correto adotado: min-height:100vh no contêiner da app, scroll natural do body e position:sticky para elementos fixos.
34 versões produzidas ao longo da fase browser-only, agrupadas em 4 fases técnicas distintas.
Cada crise foi documentada e gerou uma regra de arquitetura permanente.
v7–v8 — Camada fora do layout
O que aconteceu
Adição de uma faixa extra de botões abaixo das abas foi implementada fora da malha principal de layout. Interface quebrada, sobreposição de elementos e scroll incorreto.
Regra gerada
Expansões de layout devem ser testadas em todas as viewports. Componentes novos fora da malha têm custo alto de estabilidade.
v21 — Tela vazia ao abrir
O que aconteceu
Mudança no namespace de storage quebrou silenciosamente o carregamento do estado. Aplicação abria sem mês ativo e sem dados — completamente inutilizável. Usuário em produção afetado.
Regra gerada
Mudanças de storage precisam de fallback automático e migração com chaves legadas. readStoredJsonWithFallback() implementado como solução permanente.
v23–v24 — Dados de teste invisíveis
O que aconteceu
O localStorage reutilizava dados de versões anteriores. O seed determinístico gerava dados, mas com namespace da versão antiga — invisíveis na nova.
Regra gerada
Namespace isolado por versão. RNG baseado na chave do período (makeRng('wpm-YYYY-MM')) garante reprodutibilidade e isolamento total.
v13–v17 — Degradação por patches iterativos
O que aconteceu
Aplicação sequencial de str_replace em arquivo de 5000+ linhas gerou funções duplicadas, CSS desconectado e bugs acumulados. Qualidade degradou progressivamente.
Regra gerada
Patches iterativos em arquivos massivos são anti-padrão. Prefira reescrita atômica para mudanças estruturais.
Aprendizados técnicos e de processo que guiarão a próxima fase do sistema.
A degradação de v13 a v17 foi causada por múltiplos str_replace em um arquivo de 5000+ linhas. Cada patch introduzia pequenas inconsistências que se acumulavam. A solução é a reescrita atômica completa do arquivo quando há mudanças estruturais.
A tabela de alunos usava a classe .pending-table (projetada para 8 colunas) numa tabela de 11 colunas. O calendário tinha classes definidas em JS mas ausentes no stylesheet. Regra: cada componente precisa de CSS dedicado com definição explícita de colunas.
Claude tendeu a arquiteturas de segurança mais robustas (esc(), sanitizeDeep, safeLocalSet). GPT tendeu a fluxos UX mais coesos e lógica de reset unificada. Os melhores resultados vieram de merges deliberados com trilha de auditoria explícita.
A decisão de retornar à v6 quando v7–v8 falharam foi a decisão certa. Versões estáveis devem ser marcadas explicitamente antes de evoluções arriscadas. Rollback não é fracasso — é parte do processo de engenharia.
O protocolo de validação da v34 — verificação de sintaxe JS, estrutura HTML, IDs/funções duplicadas, cobertura de esc(), autotestes de fluxo integrados — deve ser padrão em todas as entregas. Não existe "entrega rápida" sem validação.
Múltiplas escritas assíncronas paralelas eram fonte de corrupção silenciosa de dados. A introdução de queueStorageOperation() na v34 eliminou definitivamente esse vetor de erro sem impacto visível de performance para o usuário.
Sanitização em dois níveis independentes e complementares — na importação e na renderização.
Proteção XSS — Dois Níveis
Nível 1 — Importação: sanitizeDeep() remove < e > recursivamente de todo o JSON importado antes de qualquer processamento. Dados maliciosos não entram no store mesmo que escapem da validação de schema.
Nível 2 — Renderização: toda string de dados exibida no DOM passa pela função esc(), que escapa entidades HTML críticas. Proteção independente do caminho de entrada dos dados.
Normalização Defensiva
normalizeData() é chamada em todo ponto de entrada de dados: inicialização, importação, troca de período e salvamento de configurações. Garante que arrays existam, propriedades obrigatórias tenham valores válidos e referências inconsistentes sejam corrigidas automaticamente.
Nenhum dado bruto de usuário ou de import chega ao estado sem passar por normalização.
Sanitização de UIState
sanitizeUIState() valida o estado de UI persistido contra conjuntos de valores permitidos (validEventTypes, validEventStatuses, validTabs). Tabs inválidas são resetadas para dashboard. Migração automática de nomes legados de tabs.
Proteção do Storage
Todos os setItem() estão envolvidos em tratamento de QuotaExceededError via persistStoredValue(). Falhas de storage nunca ocorrem silenciosamente — o usuário recebe feedback claro com orientação de ação (exportar backup, limpar meses vazios).
Uma suíte de diagnósticos embutida que seria incomum mesmo em aplicações com stack completa de produção.
runSystemDiagnostics
Verifica integridade do store: existência e validade de todos os campos obrigatórios, coerência entre período ativo e store, ausência de referências quebradas (alunos com atendentes não cadastrados, pendências sem status válido). Resultados exibidos com pill de status (ok/warn/danger) e salvos em SYSTEM_REPORT_KEY.
runFlowSmokeTests
Suíte de smoke tests que simula fluxos completos — criação/edição/exclusão de alunos, troca de status de pendência, adição de eventos, troca de período e verificação de isolamento de dados. Opera sobre cópia do store para não contaminar dados reais. Resultados salvos em FLOW_TEST_REPORT_KEY.
renderPeriodAudit
Lista todos os períodos armazenados com métricas (alunos, pendências, eventos, escala, NPS, volume de addons) e status: Vazio / Completo / Revisar. Critério de "Completo": ≥30 alunos, ≥20 pendências, ≥10 eventos e escala > 0.
renderPersistenceTechPanel
Painel técnico em tempo real: modo de persistência ativo (IndexedDB ou localStorage), status da última operação, timestamp da última gravação, resultado do self-test de escrita/leitura, disponibilidade de broadcast cross-tab e versão do payload. Diagnóstico imediato sem abrir DevTools.
Regras que não podem ser quebradas em nenhuma versão, presente ou futura do sistema.
Alunos novos só podem ter um recepcionista como atendente. Pendências só aceitam recepcionistas como hostess. Professores só aparecem na escala. Esta regra reflete a hierarquia operacional real da unidade.
Ao cadastrar aluno com addon, a venda é criada no módulo de Addons. Ao editar, o addon é atualizado. Ao excluir, o addon é removido. Não pode existir addon órfão.
Um funcionário não pode ser escalado no sábado E no domingo da mesma semana. Como trabalham de segunda a sexta (5 dias), trabalhar nos dois dias do fim de semana resultaria em 7 dias consecutivos — acima do limite legal de 6 dias.
PESSOA G não pode pegar turnos de abertura (prof1) aos sábados ou feriados por conta do descanso interjornada. A lógica retorna false para PESSOA G nestas situações. Esta restrição deve ser preservada em todas as versões futuras.
Todos os campos de data no storage usam YYYY-MM-DD. Nunca armazenar strings localizadas ou timestamps com timezone. Previne bugs de fuso horário onde uma data se torna o dia anterior ou posterior dependendo do browser.
Dados do período 2026-03 nunca devem vazar para 2026-02 ou 2026-04. Toda operação que lê ou escreve dados verifica o período ativo correto. Reset de mês limpa apenas o período selecionado, preservando apenas a configuração da equipe.
Toda operação de persistência passa por persistStoredValue() que trata QuotaExceededError explicitamente. Escrita direta via localStorage.setItem() sem tratamento é proibida arquiteturalmente na v34.
Critérios de validação obrigatórios, todos verificados antes da aprovação da versão final.
Uma abordagem diferenciada: desenvolvimento paralelo com Claude (Anthropic) e GPT (OpenAI), com merges deliberados e avaliação crítica bilateral.
Claude (Anthropic)
Pontos fortes identificados
Arquitetura de segurança consistentemente mais robusta. Cobertura sistemática de esc() em todos os pontos de injeção. Implementação padronizada de sanitizeDeep(), camadas de persistência e diagnósticos integrados.
Área de melhoria
Algumas escolhas de UX menos intuitivas. Padrão de reset de período com lógica mais fragmentada em versões anteriores.
GPT (OpenAI)
Pontos fortes identificados
Fluxos UX mais coesos e intuitivos. Lógica unificada de reset de período com buildCleanPeriod() mais elegante. Menor fragmentação do estado de UI.
Área de melhoria
Cobertura de segurança menos sistemática. Tendência a simplificar sanitização em favor de brevidade de código.
Como o merge foi feito na V33 → base para v34
Base: V31 (Claude)
Toda a arquitetura de segurança — esc(), sanitizeDeep(), confirmações destrutivas, camadas de persistência. O esqueleto do sistema.
Patches: V32 (GPT)
Lógica unificada de resetSelectedMonth() e closeCurrentMonth() usando buildCleanPeriod(). Melhorias no fluxo de navegação entre períodos.
V34 (Refinamento final)
Evolução sobre o merge: IndexedDB dual, fila serializada, broadcast cross-tab, APP_INTERNALS, pipeline de render otimizado. Fase browser-only concluída.
Maclayne avalia ambas as IAs criticamente e espera que cada IA faça o mesmo com a outra. Não existe "a IA certa". Existe a análise técnica do output de cada uma, identificação dos pontos fortes de cada versão, e a síntese deliberada na versão final. Esta metodologia produziu resultados consistentemente superiores às versões produzidas por uma única IA em isolamento.
A fase browser-only foi o laboratório de regras de negócio. A próxima fase é a migração para arquitetura profissional com banco de dados, API e frontend componentizado.
Visão da Nova Arquitetura
O sistema browser-only provou as regras de negócio em condições reais. Agora o objetivo é evoluir para uma arquitetura que suporte múltiplos usuários simultâneos, banco de dados relacional, autenticação, CI/CD e testes automatizados E2E.
O v34.html passa a ser a referência funcional oficial — jamais reescrito, mas lido como fonte da verdade de comportamento e regras de negócio.
Frontend
Backend
Roadmap por Sprints
Sprint 0 — Fundação
Monorepo, Docker PostgreSQL, TypeScript configurado, ESLint/Prettier, README de setup local.
Sprint 1 — Modelagem
Schema Prisma com todas as entidades, migrations, seed de funcionários fixos e 12 períodos mensais.
Sprint 2 — API Backend
Fastify com CRUD de todos os módulos, validação Zod, testes com 80% de cobertura.
Sprint 3 — Frontend Base
React + Vite, identidade WPM, 8 abas, seletor de período, Dashboard e Alunos end-to-end.
Sprint 4 — Módulos Restantes
NPS, Escala, Eventos, Addons, Configurações com backup/export/import. Gestão mensal completa.
Sprint 5 — Deploy
E2E com Playwright, CI GitHub Actions, deploy Vercel (web) + Railway (API) + Neon (banco).
34 versões de operação real com a equipe. Todas as regras de negócio foram testadas e refinadas em condições reais. Os bugs encontrados geraram regras permanentes. A próxima fase não começa do zero — começa com um legado funcional sólido, documentado, auditado e avaliado com 9,5/10 por QA sênior.