Skip to content

Arquitetura de Alto Nível

O Prime Crown é uma plataforma SaaS full-stack para gestão de limpeza residencial de alto padrão. A interface React possui três runtimes suportados: navegador/PWA e aplicativos Android/iOS empacotados com Capacitor. Todos consomem o mesmo backend Cloudflare e compartilham componentes, stores, tipos e regras de negócio.

No fluxo nativo de notificações, somente os proxies críticos de Push Notifications e Preferences ficam no bundle inicial. Assim, Subscribe chama o bridge nativo sem depender de baixar chunks pela rede depois do toque; App.getInfo() é enriquecimento best-effort posterior e carrega @capacitor/app sob demanda, reutilizando a promise e liberando retry após falha. Os timeouts restantes cobrem chamadas nativas realmente travadas.

Se a autorização do Android já estiver concedida, mas não houver token local nem registro no backend, Subscribe não espera um novo diálogo do sistema: lê o token já emitido diretamente do cache/FCM e o persiste na conta autenticada. Timeout de rede ou backend não invalida automaticamente o token Firebase; somente unsubscribe/reset explícito faz essa operação destrutiva.

O Android também registra o plugin nativo NativePushToken, que consulta FirebaseMessaging.getToken() diretamente. Esse caminho é usado antes de aguardar o evento do Capacitor, porque o evento pode não disparar novamente quando o Firebase já possui um token. A inclusão do plugin exige uma nova versão do shell Android.

O mesmo plugin expõe diagnóstico sem revelar o token: autorização global via NotificationManagerCompat, importância dos canais de mensagens e alertas, e prontidão do Firebase. A permissão já concedida é lida diretamente no Android sem aguardar a ponte de push do Capacitor. PrimeCrownMessagingService.onNewToken() persiste o último token em armazenamento nativo e o plugin consulta memória/cache antes de Preferences e de Play Services; assim, um token já emitido não fica preso atrás de uma nova chamada getToken() lenta. O POST autenticado ao backend encerra o caminho crítico, enquanto Preferences e metadados são gravados depois. Login, resume, diagnóstico e Subscribe compartilham uma única reconciliação em voo; unsubscribe/reset limpam o cache nativo junto do token Firebase para nunca regravar um token invalidado. O Profile traduz cada camada em orientação específica. No login e em cada retorno ao app, uma inscrição desejada e autorizada executa reconciliação idempotente com o backend, inclusive quando o token local já existe. android:doctor bloqueia a release se plugin, registro no MainActivity, provider web ou chamadas Firebase/NotificationManager estiverem ausentes.

O POST primário registra ainda o limite inferior de build 10 quando a disponibilidade síncrona de NativePushToken comprova um shell moderno. O renderer avançado existe desde o build 5, portanto essa prova autoriza com segurança FCM data-only mesmo se App.getInfo() falhar. Sem esse campo, o servidor usa deliberadamente o payload legado; com ele, PrimeCrownMessagingService restaura Reply, Mark as read, MessagingStyle e agrupamento estável por conversa.

O botão Check native também executa a reconciliação e sempre mostra um toast conclusivo. Se um token já pertencer a outra conta autenticada anteriormente no mesmo aparelho, /api/native-push-tokens transfere sua propriedade para a sessão atual; manter o dono antigo causaria tanto o limbo de registro quanto entrega indevida de notificações após troca de conta.

O registro primário envia token, plataforma e dispositivo ao backend imediatamente. installId e appBuild são enriquecidos depois em uma segunda escrita best-effort, pois travamento de Preferences/App não pode impedir a criação do destino. O reparo acionado pelo Profile devolve o botão em no máximo 15 segundos; o watchdog mais amplo de Subscribe ainda reserva tempo para um diálogo real de permissão do sistema.

graph TD
    subgraph ClientLayer ["Client Layer"]
        UI["React 19 SPA (Vite)"]
        PWA["Browser / Installed PWA"]
        Android["Android (Capacitor 8)"]
        Platform["Platform Capability Layer"]
        Zustand["Zustand Stores (State)"]
        Query["TanStack Query (Cache)"]
        PWA --> UI
        Android --> UI
        UI --> Platform
        UI --> Zustand
        UI --> Query
    end

    subgraph EdgeLayer ["Edge Backend (Cloudflare Pages Functions)"]
        Hono["Hono Router & Zod Validation"]
        Auth["JWT & Role Authorization"]
        Hono --> Auth
    end

    subgraph StorageLayer ["Persistence & External Services"]
        D1[("Cloudflare D1 (SQLite)")]
        R2["Cloudflare R2 (Media/Audio)"]
        EvoAPI["WhatsApp Evolution API"]
        Gemini["Google Gemini API (AI Engine)"]
    end

    Query <-->|/api/boot & Atomic REST| Hono
    Hono <--> D1
    Hono <--> R2
    Hono <--> EvoAPI
    Hono <--> Gemini
  • npm run build e npm run build:web produzem o mesmo PWA em dist/; o deploy Cloudflare existente não depende do Android.
  • npm run pwa:doctor audita os manifestos principal/chat, ícones, atalhos tipados, service worker, isolamento do boot offline, cache/rotas Cloudflare, associações nativas, acessibilidade do viewport e VAPID. O atalho Live Map aponta para o ViewState compartilhado LIVE_TRACKER e continua sujeito ao mesmo RBAC das demais entradas externas.
  • npm run android:sync produz o build web e sincroniza dist/ com o projeto nativo em android/.
  • src/shared/platform/runtime.ts é a fonte central de detecção do runtime. Componentes não devem consultar diretamente window.Capacitor.
  • O diagnóstico de boot consulta essa mesma fonte e identifica explicitamente Android App, iOS App, PWA instalada e Browser/Desktop. Logs de suporte nunca classificam um shell Capacitor como navegador apenas porque sua WebView não publica display-mode: standalone.
  • O login Google usa providers por runtime: Google Identity Services/redirect no PWA e @capgo/capacitor-social-login no Android. O seletor nativo devolve um ID token com o OAuth Web Client ID como audience; o token entra no mesmo fluxo de sessão do backend, sem duplicar usuários ou regras de autorização. Somente o provider Google é compilado no aplicativo. A sessão autenticada é revalidada ao retomar o shell nativo, recuperar rede ou voltar a uma aba PWA/browser visível; sinais simultâneos são agrupados em uma única leitura, e retornos anônimos do provider continuam ignorados até a troca do token terminar. Login, redefinição de senha e o fallback do gate operacional seguem 100dvh e a safe area compartilhada, portanto teclado, orientação e barras móveis redimensionam também as superfícies anteriores à sessão.
  • O service worker é registrado na build publicada, inclusive quando ela é aberta pelos shells Capacitor. Android e iOS usam https://app.primecrowncleaning.co.uk como origem remota canônica para preservar as chamadas relativas /api, os cookies HttpOnly, OAuth e o RP ID do WebAuthn; o worker mantém o shell e o último boot disponível em perdas breves de sinal. O boot usa uma chave canônica única, apaga o snapshot em 401/403 e em toda troca de identidade (login, logout e impersonação), inclusive chaves antigas com query string. O dist/ sincronizado pelo Capacitor continua sendo artefato de build, não uma base de dados nativa independente.
  • As entradas públicas /quote, /pay, /statement, /proposal e /rate usam correspondência de segmento completo. O caminho-base e seus descendentes entram no fluxo público, mas prefixos coincidentes como /payroll permanecem no shell autenticado em web, PWA e WebViews.
  • Fatura, extrato, proposta de horário e avaliação públicas usam system-viewport-safe, cancelam fetches no teardown e bloqueiam POST duplicado antes do próximo render. A avaliação distingue 404 de indisponibilidade/rede, oferece retry real, arredonda stars da URL para 1–5, normaliza o feedback e usa CFInput, Button e IconBadge com alvos touch. Clipboard passa pelo provider nativo/web e só produz feedback depois da confirmação. Sem uma integração PISP credenciada, fatura e extrato mostram dados para Faster Payments manual em vez de simular um redirect Open Banking ou transmitir dados bancários a terceiros.
  • Conciliação bancária e QuickBooks adotam useModalLifecycle: a camada participa da pilha compartilhada de Back/History, recebe z-index coordenado, contém/restaura foco, trava scroll e limita Escape ao modal superior. Safe areas são preservadas; locks síncronos impedem ações concorrentes e o fechamento é recusado enquanto uma operação financeira está em andamento.
  • Manual da residência, pausa de férias e aviso de trânsito compartilham o mesmo lifecycle; férias e trânsito bloqueiam nova ação e fechamento enquanto processam. Pausa de férias usa UUID idempotente e um único UPDATE D1 para todas as visitas elegíveis, portanto retry nativo não duplica e falha SQL não deixa um intervalo parcialmente cancelado. Custódia de chaves usa inventário e trilha D1 reais, nunca exemplos em memória.
  • UUIDs operacionais do frontend usam createUuid: crypto.randomUUID quando disponível e RFC 4122 v4 sobre crypto.getRandomValues nas primeiras versões do iOS 15. O contrato cobre pedidos/recorrências, clientes/endereço, mensagens/toasts, incidentes, suprimentos, logs, telemetria, idempotência, push, mídia e CSV. O pwa:doctor percorre todo src de produção e impede que Math.random volte a gerar identidade operacional.
  • vite.config.ts substitui o token __BUILD_ID__ do worker por um hash de 12 caracteres calculado a partir do index.html emitido e do template do próprio worker. O arquivo publicado muda em todo release que altera o grafo da aplicação, portanto PWAs e WebViews descobrem builds web-only sem depender de incremento manual de versão; a ativação elimina o namespace anterior.
  • useAppUpdate detecta tanto o evento de instalação quanto um worker que já esteja em registration.waiting ao montar. Sessões longas solicitam nova verificação ao retornar ao primeiro plano ou ao shell nativo; eventos simultâneos são deduplicados por um minuto, enquanto a reconexão força uma tentativa porque a anterior pode ter ocorrido offline. A troca silenciosa só acontece no Dashboard ocioso, sem modal ou reserva em andamento e fora do Chat isolado; em qualquer outra superfície o banner pede confirmação antes do reload, preservando formulários e conversas nas quatro experiências. createControllerChangeHandler não recarrega no primeiro claim, mas recarrega uma única vez em toda substituição posterior, inclusive após uma primeira instalação ocorrida na mesma sessão longa; se outra aba já promoveu a versão, o botão converge por reload network-first.
  • createPreloadErrorHandler é a única autoridade de recuperação automática de lazy chunks e limita a ação a uma tentativa por sessão a cada 60 segundos. As rotas usam React.lazy sem fallback de reload próprio. A primeira falha atualiza/promove o worker e recarrega network-first; uma repetição propaga para o ErrorBoundary em vez de criar um loop de splash. Storage indisponível desativa a autorrecuperação por segurança.
  • Falhas anteriores ao mount de React usam renderEarlyBootFailure: DOM criado por APIs seguras, mensagem em textContent, sem innerHTML/handler inline, safe areas, role=alert e botão de 48 px com foco. O fallback continua operacional mesmo quando os estilos ou a árvore React não carregam.
  • Falhas posteriores ao mount chegam ao ErrorBoundary: a causa sanitizada vai para a telemetria administrativa e permanece visível/copiável no ErrorFallback, enquanto stack e árvore de componentes ficam restritas ao desenvolvimento. telemetrySanitizer remove query/hash de URLs e redige tokens, JWTs, senhas, cookies, authorization, API keys, credenciais e códigos também em mensagens, stacks e contexto recursivo; ciclos/profundidade e 8.000 caracteres limitam volume. A tela anuncia o erro, foca seu título, adapta ações a telefones e respeita safe areas/modo escuro sem vazar credenciais de reset, OAuth ou deep links.
  • A ingestão /api/atomic/system-logs repete o sanitizer e não confia em userId/timestamp do cliente: autor e horário vêm da sessão/Worker. Zod limita campos/payload, cada conta pode gravar 20 relatórios em 5 minutos e excesso retorna 429 + Retry-After; inserir antes de contar e remover somente o ID recém-inserido fecha a corrida entre requests. GET de admin/manager limita paginação a 1–500, rejeita filtros excessivos e substitui erros internos por mensagens públicas estáveis.
  • Exceções geradas no backend passam por logErrorToSystemLogs, que sanitiza nome, mensagem, stack, causa e objetos circulares antes do D1. Orçamento/chat público, verificação de WhatsApp e envio de fatura devolvem mensagens 500 estáveis, sem propagar mensagens do banco, objetos do Resend ou credenciais do Evolution; detalhes permanecem somente na telemetria sanitizada.
  • Escritas e leituras inesperadamente interrompidas nos CRUDs atômicos centrais passam por atomicServerError: o diagnóstico sanitizado é classificado em system_logs, e todas as plataformas recebem um único contrato de retry. Respostas funcionais 4xx permanecem no endpoint e não são convertidas em falha genérica.
  • Custódia física usa tabelas próprias (key_tags, key_custody_records) e /api/field/key-custody, fora do snapshot de boot para não distribuir dados de segurança a papéis sem necessidade. Apenas ADMIN/MANAGER acessam o endpoint. Cadastro e mudança de posse disparam auditoria por trigger no mesmo statement; estado e versão esperados tornam a transição concorrente condicional e retornam 409 em vez de sobrescrever outro aparelho.
  • Capacidades nativas futuras devem ser adicionadas atrás de providers com fallback web, preservando o PWA como implementação padrão. O stack compartilhado espelha no History cada camada transitória, inclusive drawer móvel, modais, lightbox e detalhes do chat: Back/gesto fecha somente o topo. O sidebar permanente do desktop fica fora do histórico. Uma operação com closeDisabled pode rejeitar o fechamento; nesse caso browser/PWA restauram a entrada consumida, igualando a retenção já aplicada por Android Back e impedindo navegação sob um modal bloqueado.
  • Navegações externas aguardáveis usam openExternalUrl; handlers que não podem aguardar usam launchExternalUrl, que contém e registra falhas do Browser/AppLauncher, aceita callback de feedback e reivindica a URL sincronamente para impedir intenções duplicadas por toque duplo. Sem callback próprio, uma falha produz feedback tátil e toast recuperável. Âncoras mantêm seu comportamento normal na web e são interceptadas somente no nativo. Fotos de evidência usam o lightbox compartilhado, evitando abrir URLs autenticadas em uma sessão externa sem cookies.
  • Exportações de relatório completo, gráficos, usuários, histórico, payslips, faturas e QuickBooks reivindicam um lock em ref antes do primeiro await; o estado React serve somente à apresentação. Assim dois toques no mesmo frame não geram dois PDFs/CSVs nem duas share sheets no PWA, desktop, Android ou iOS, e o lock sempre é liberado em finally.
  • Evidências do checklist mantêm o item ocupado durante todo o ciclo de captura/seletor e upload, inclusive resultados restaurados pelo Android após recriar a WebView. O epoch invalida respostas de outra tarefa/sessão; cancelamento ou revogação de edição permanece silencioso, enquanto falhas reais recebem spinner, toast recuperável e feedback tátil compartilhado.
  • Mensagens de voz preservam a prévia local até o upload e o envio/agendamento serem confirmados. Um lock síncrono impede dois envios no mesmo frame; falha ou timeout mantém a gravação pronta para retry, enquanto troca de conversa invalida o ticket e limpa somente o estado pertencente ao composer anterior.
  • Texto agendado também permanece no composer até confirmação autoritativa; falha mantém rascunho e menu prontos para retry. O conteúdo só é limpo se ainda for o mesmo snapshot enviado, evitando apagar uma edição mais nova. Agendamento e geração de rascunho por IA possuem locks síncronos e epochs por usuário/conversa.
  • Mídia moderna em R2 usa {clientId}/direct|team/{job?} e aplica o mesmo escopo de linhas de boot/requests no upload, leitura e exclusão. Clientes acessam somente a própria pasta; funcionários precisam enxergar o cliente e estar atribuídos às tarefas team; o par tarefa/cliente é validado inclusive para administradores. Chaves legadas continuam legíveis para conversas antigas, mas somente admin/manager pode criar ou apagá-las. Falhas de R2 recebem contrato estável, e o antigo /api/test-db não integra mais a superfície publicada.
  • NativeBridge integra pause/resume ao focusManager do TanStack Query, trata o botão voltar e converte App Links em intenções de navegação existentes (view, reqId e channelId). Ao montar, ele reconcilia o estado autoritativo de App.getState() sem emitir uma retomada artificial; um evento de lifecycle mais novo sempre prevalece sobre uma leitura inicial atrasada, e o teardown invalida ambos. No app instalado, toque longo sem seleção não expõe menus de imagem/link da WebView; clique direito físico, modificadores, campos editáveis e texto já selecionado preservam o menu do sistema. Plugins exclusivos do Capacitor são carregados por import() somente após detectar Android/iOS ou iniciar a ação nativa, com uma exceção deliberada: Push Notifications e Preferences permanecem eager porque a recuperação crítica de inscrição precisa funcionar sem buscar outro chunk após um deploy ou perda de rede. Lifecycle, privacidade, status bar, haptics, login social, badge, atualização, teclado, rede, splash, configurações, câmera, localização, clipboard, navegador externo, filesystem e share sheet continuam sob demanda. appInfoProvider adia App.getInfo() até a aba About; o enriquecimento de push também reutiliza o mesmo módulo dinâmico somente depois que o destino já foi registrado. Providers de listeners preservam cleanup síncrono por contrato e descartam/removem registros que resolvam depois da desmontagem. Arquivos temporários compartilhados usam um subdiretório exclusivo por entrega, preservam o nome visível e são apagados do cache nativo em finally; exportações simultâneas com o mesmo nome não se sobrescrevem. Ele já fica ativo durante carregamento e login; uma intenção recebida antes da sessão/boot é preservada e retomada depois da autenticação, pela mesma validação RBAC usada para mensagens do service worker. Retomada nativa e recuperação da rede também reconciliam boot e canais de chat; PWA usa os eventos equivalentes do navegador, com limiares/deduplicação para não repetir chamadas.
  • Cada entrada interna do History API carrega pcView e pcDepth. Assim reload restaura a tela, Back reduz para a profundidade gravada e Forward a recupera — inclusive antes do botão Voltar do Android decidir entre navegar e encerrar o app. Na restauração de uma nova identidade, a tela persistida passa novamente por RBAC para não herdar um módulo administrativo da sessão anterior.
  • O teardown também cobre inicializações tardias do push no NativeBridge; falhas assíncronas de App lifecycle, teclado e rede são contidas e observáveis no console, sem rejeições globais nem listeners órfãos após troca de sessão/remount. Callbacks de push invalidam-se sincronamente antes da remoção assíncrona dos handles, portanto eventos já enfileirados não persistem token, exibem mensagem nem navegam após desmontagem/troca de sessão. Callbacks de teclado já enfileirados verificam a atividade antes de alterar safe areas, portanto não conseguem reabrir o layout depois que o bridge restaurou o estado sem teclado.
  • initializeNativePush registra seus quatro listeners como uma transação. Falha intermediária, leitura de permissão ou registro APNs/FCM executa rollback dos handles já criados; o teardown normal tenta remover todos com allSettled, evitando que um erro de plugin impeça os demais cleanups.
  • Atualização flexível Google Play também possui rollback: falha ao iniciar remove o listener recém-criado, e download concluído/cancelado/falhou converge por cleanup idempotente. A conclusão aceita somente o primeiro evento DOWNLOADED, impedindo chamadas concorrentes a completeFlexibleUpdate() quando o Play repete o estado. Erros de conclusão ou remoção não escapam como rejeição global; iOS mantém apenas a abertura da ficha oficial na App Store.
  • O Android declara https://app.primecrowncleaning.co.uk/* e primecrown://*. src/shared/platform/androidAppLinks.ts mantém os certificados da upload key e da app signing key do Google Play; a rota functions/.well-known/assetlinks.json.ts publica esse contrato mesmo quando o Pages ignora diretórios estáticos ocultos, e public/.well-known/assetlinks.json é o fallback do build. Assim, App Links permanecem verificados em builds locais assinados e nas instalações distribuídas pela loja.
  • Ícones legacy/adaptativos e splash screen usam a mesma coroa azul do manifesto PWA. O foreground adaptativo permanece dentro da safe zone do Android para suportar máscaras diferentes dos launchers.
  • Push compartilha notificationPermissionProvider entre onboarding e preferências, normalizando consulta, solicitação e observação no PWA, Android e iOS sem duplicar fluxos de UI. A Permissions API e o listener com cleanup ficam no provider; Android/iOS revalidam no resume. Leituras sobrepostas de saúde disparadas pelo provider global, Perfil, evento de token e retomada compartilham um singleFlight por modo habilitado/desabilitado; uma sessão impersonada nunca herda o voo da sessão normal. No Perfil, autorização/token/assinatura recebem tickets latest wins, e o diagnóstico Android é coalescido por usuário e confere o epoch antes de cada commit/finalização; logout não recebe estado tardio do plugin. A ação manual Check native possui ainda lock síncrono próprio e revalida owner/lifecycle depois de registro, refresh e diagnóstico, bloqueando toque duplo e toast de uma conta anterior. A lista de dispositivos usa um singleFlight keyed com passe final: uma rajada de mudanças não aborta/reabre cada request, mas a intenção mais recente roda uma vez depois da leitura atual para observar eventual token recém-registrado. O AbortSignal do lifecycle ainda interrompe imediatamente o transporte no logout/troca de conta. Ativar ou desativar invalida imediatamente checks em voo e publica o resultado autoritativo, impedindo que um refresh atrasado restaure “Subscribed”. Web Push/VAPID atende browser/PWA, FCM atende Android e APNs atende iOS; tokens nativos são registrados em native_push_tokens por /api/native-push-tokens. O ID estável da instalação é criado por uma única operação compartilhada mesmo quando callbacks de token chegam em paralelo, e os imports dos plugins Preferences/Push também são deduplicados; assim um aparelho não ganha duas identidades durante o mesmo boot. O POST de registro usa AbortSignal e é cancelado no teardown; enquanto estiver em voo, seu token permanece disponível somente ao cleanup de logout, que consegue removê-lo mesmo antes da persistência local. Antes do logout, o endpoint Web Push ou token nativo é removido da conta enquanto o cookie ainda é válido e a inscrição local é encerrada; cada etapa tem limite de três segundos para nunca bloquear a saída offline. O alvo capturado também segue no corpo de /api/auth/logout, que repete a exclusão no mesmo request que revoga a sessão e sempre a restringe ao reference_id do JWT, cobrindo timeout entre as duas chamadas sem remover outros aparelhos. O estado da autorização e a lista de dispositivos são relidos ao retornar das Configurações nativas. Chat Android usa MessagingStyle com avatar de iniciais, Reply + Mark as read e shortcut de Conversations; iOS usa alerta APNs agrupado; a PWA aproxima o mesmo fluxo (stack, avatar SVG, ações). Recebimento em foreground alimenta os mesmos toasts/histórico e o toque converte data.url pela navegação nativa validada.
  • A conversa visível só é espelhada em Capacitor Preferences quando isNativePlatform() confirma Android/iOS. Browser, PWA e desktop mantêm o valor apenas em memória e não executam nem solicitam a ponte nativa ao abrir o chat.
  • A paleta global e SearchIntelligenceService formam uma fronteira lazy do layout autenticado. O atalho Ctrl/Cmd+K publica imediatamente um status modal enquanto o chunk é solicitado e só então monta busca, índice e folha adaptativa; usuários que não abrem a pesquisa não pagam esse código no shell inicial.
  • O gate completo de instalação/permissões é solicitado apenas para sessões EMPLOYEE/MANAGER, as únicas sujeitas ao fluxo operacional; um fallback bloqueante e anunciado impede exposição momentânea do app antes da verificação. As instruções detalhadas de GPS negado formam outra fronteira lazy e só são baixadas quando o modal realmente abre. Clientes e administradores não carregam esses módulos no shell comum.
  • Login, redefinição e troca obrigatória de senha são fronteiras lazy independentes do shell autenticado. Ao detectar uma sessão anônima, o chunk de Login e a configuração pública cacheável começam em paralelo; somente o chunk participa do bloqueio visual, enquanto Google reutiliza a resposta aquecida ao montar. Login/redefinição preservam um carregamento de página integral; a troca obrigatória só é solicitada quando mustChangePassword está ativo e usa fallback bloqueante, portanto nenhum conteúdo protegido fica momentaneamente utilizável. O aviso de atualização também só entra no bundle quando precisa ser exibido.
  • Recursos contínuos do shell são separados por função: rastreamento e recuperação de permissão GPS são carregados somente para EMPLOYEE; diagnóstico periódico do WhatsApp somente para ADMIN/MANAGER. O diagnóstico administrativo relê o estado ao voltar do background e descarta respostas tardias depois do logout/desmonte. Clientes não baixam nem inicializam nenhum dos dois fluxos.
  • NativeBridge é carregado somente quando o runtime confirma Capacitor Android/iOS; launch URL, App/Universal Links, Back, câmera restaurada, lifecycle, teclado, rede, privacy screen e push permanecem nessa fronteira. A política leve de menu contextual foi separada em InstalledContextMenuGuard, preservando o toque longo de PWA/WebView sem fazer browser, PWA ou desktop baixarem os serviços nativos.
  • O pull-to-refresh do shell é uma fronteira lazy ativada somente quando maxTouchPoints ou (pointer: coarse) confirma entrada touch. Desktop sem toque não baixa o controller, indicador, hápticos ou soft refresh e não registra listeners; PWA/Android/iOS e desktops híbridos mantêm o gesto. Chat continua dono de seus scroll roots e o hook compartilhado recusa listeners em runtimes sem touch.
  • networkProvider multiplexa conectividade em todas as plataformas: Android/iOS compartilham um listener do plugin Capacitor e browser/PWA/desktop compartilham um único par online/offline, mesmo com layout, update checker e fila de chat ativos simultaneamente. Inscrições repetidas mantêm referência própria e a ponte é removida somente quando o último consumidor sai. Falha transitória anterior ao registro nativo recebe apenas duas novas tentativas compartilhadas (1 s/3 s); o último disposer cancela o timer, evitando polling de fundo. Falha isolada do snapshot inicial conserva o listener vivo, sem abrir outro handle.
  • A sincronização do badge do ícone é uma fronteira lazy por capacidade: sempre ativa no Capacitor e, na web, somente quando setAppBadge/clearAppBadge existe. Desktop sem Badge API não baixa hook/provider nem agenda escritas inúteis; Android, iOS e PWAs compatíveis preservam fila serializada, coalescing do valor mais recente e limpeza autoritativa no logout/troca de conta.
  • O monitor de conectividade permanece no shell, mas o componente visual OfflineBanner é solicitado somente na transição para offline. Um fallback leve com role=status, live region e a mesma promessa honesta aparece imediatamente enquanto o chunk chega; sessões sempre online não baixam ícone/componente de indisponibilidade.
  • Preferências síncronas do shell usam uiPreferenceStorage: localStorage continua sendo a persistência de UI, mas leitura, escrita e remoção possuem fallback em memória. Safari privado, iframe sandboxed e WebViews com storage restrito não derrubam o primeiro render nem os toggles da sidebar, háptico e notificações do chat; a escolha permanece funcional durante a sessão mesmo sem persistência disponível, e o serviço tátil lê o mesmo valor exibido pela tela.
  • O primeiro POST do token devolve a identidade no header X-Prime-Crown-Registration-Owner; o enriquecimento assíncrono de instalação/build reenvia esse valor como expectedUserId. A API retorna 409 antes de tocar o banco se o cookie já pertencer a outra conta, impedindo que metadata atrasada depois de logout/login reassocie silenciosamente o aparelho. O header evita ler o corpo no caminho crítico, e um registro explícito da nova sessão continua autorizado a mover o token.
  • Gravações e remoções do token em Preferences usam uma fila única. Cada save captura uma geração; rotação, recovery e logout invalidam tarefas antigas antes de tocar o bridge. Se uma gravação já começou, a remoção aguarda seu término e executa por último, portanto uma persistência best-effort nunca recria o token depois da limpeza.
  • unregisterNativePush também suspende os dois callbacks de registration (bridge global e listener temporário). Eventos FCM/APNs enfileirados depois da saída não repopulam memória nem POSTam; uma chamada posterior a ensureNativePushRegistration ou recovery remove a suspensão para a nova sessão.
  • Resultados restaurados da câmera usam consumo único por contexto opaco. O primeiro observer correspondente reivindica o arquivo; observers duplicados não repetem upload, e a entrega em microtask verifica novamente se o destino segue montado antes de chamar chat/checklist/evidência.
  • O provider permite uma única Activity de câmera por vez em toda a WebView, evitando que toques/superfícies concorrentes sobrescrevam o contexto persistido. O checklist complementa isso com lock síncrono, aria-busy e ticket de owner; um retorno direto só faz upload se usuário e item ainda forem os mesmos.
  • Captura operacional de fotos passa por cameraProvider: PWA usa o input com câmera traseira, enquanto Android/iOS usam a API atual Camera.takePhoto com orientação corrigida, limite de 1920 px e arquivo temporário fora da galeria. Cancelar retorna null; falhas de captura continuam distintas. O fluxo não depende mais do Camera.getPhoto depreciado e permanece pronto para a próxima geração do plugin Capacitor.
  • Localização diferencia permissão recusada, precisão aproximada e serviços globais desligados. Os códigos nativos OS-PLUG-GLOC-0007/0017 recebem orientação própria; Android abre diretamente Location Settings, enquanto iOS mantém o caminho manual suportado por App Review. Retomar o app revalida permissão e precisão sem reinício.
  • O gate operacional usa esse mesmo provider para consultar e solicitar permissão; Android registra FCM, iOS registra APNs e o PWA mantém Notification.permission e a assinatura Web Push.
  • A detecção de aplicativo instalado confia em display-mode, navigator.standalone, TWA ou Capacitor, nunca em parâmetros de URL. O beforeinstallprompt capturado antes do React é mantido por um provider tipado e descartado após instalação/consumo, impedindo que uma aba comum contorne o onboarding ou ressuscite um prompt já utilizado.
  • Sons de interface e o teste obrigatório do onboarding compartilham um único AudioContext. O motor aguarda a retomada autorizada pelo gesto e informa se o tom foi realmente agendado; a confirmação não é persistida quando Web Audio está bloqueado ou indisponível, e o teste não cria/fecha contextos extras no Safari/iOS ou nas WebViews.
  • Após uma recusa, o gate e o modal de rastreamento reutilizam nativeSettingsProvider para mostrar o caminho e abrir diretamente as configurações do aplicativo no Android; o PWA preserva as instruções de permissões do site e não carrega uma ação nativa.
  • As páginas públicas /privacy, /terms e /account-deletion são compartilhadas pelo PWA, Android e ficha da Play Store. Profile → About reúne informações técnicas da versão, links de Privacy/Terms e uma área destrutiva separada para exclusão. O pedido torna a conta inativa imediatamente, registra prazo de 30 dias e avisa administradores; o fluxo público continua acessível sem instalar ou autenticar no app.
  • public/store-assets/ mantém os recursos editoriais reutilizáveis da ficha Google Play. O banner 1024×500 segue a identidade dos ícones PWA/Android; public/store-assets/screenshots/ contém capturas do runtime real para telefone (1080×1920), tablet de 7 polegadas (1280×720) e tablet de 10 polegadas (1920×1080). Antes de publicar, revise as imagens para impedir exposição de dados pessoais ou operacionais.
  • src/shared/platform/cameraProvider.ts usa @capacitor/camera apenas no runtime nativo. Evidências, checklist e chat recebem um File comum e mantêm os inputs capture="environment" como fallback no PWA.
  • Consulta e solicitação da permissão de câmera usam o mesmo provider: Capacitor no Android/iOS e Permissions API + getUserMedia no navegador/PWA/desktop. O stream web de prova é encerrado assim que a permissão é confirmada; browsers sem consulta padronizada preservam estado unknown e continuam com o input nativo como fallback.
  • src/shared/platform/locationProvider.ts centraliza posição, permissão, observação e watch. Android usa @capacitor/geolocation com localização precisa e intervalos compatíveis com bateria; navegador/PWA preserva navigator.geolocation e observa a Permissions API quando disponível. Watches aceitam AbortSignal: desmontar bloqueia callbacks imediatamente e, se o ID nativo ainda estiver pendente, limpa-o assim que o plugin responder; cleanup é idempotente e falhas de remoção são contidas. useGeoPermission inicia observadores de câmera/localização em paralelo e usa commits latest wins: uma consulta lenta de mount/resume nunca sobrescreve leitura ou evento mais novo, e teardown invalida toda resposta pendente. O gate móvel agrupa câmera, localização e notificações em uma reconciliação singleFlight, e aplica outra à leitura de precisão GPS; focus, app-resume, visibilidade e evento FCM simultâneos reutilizam o trabalho já iniciado. O perfil aplica a mesma coalescência às permissões e converte falha de bridge em unknown, sem unhandledrejection. Gate móvel, perfil e rastreamento não consultam APIs web diretamente: nativo revalida no resume, PWA reage também à mudança de permissão e todos limpam seus listeners ao desmontar.
  • O canal de atualização da loja usa nativeUpdateReconciler: sinais concorrentes de mount/resume compartilham a leitura em andamento e colapsam a rajada em no máximo uma consulta final. Se uma repetição já estiver pedida, o resultado intermediário não chega à interface; uma falha intermediária também pode ser substituída pela tentativa final. Isso reduz bridge, rede e bateria sem permitir que uma resposta antiga da Play Store/App Store apague a disponibilidade mais nova, e resultados pendentes são invalidados ao desmontar. O service worker continua sendo um canal independente em todas as plataformas, inclusive dentro dos shells nativos.
  • O evento beforeinstallprompt é tratado como recurso de uso único tanto no convite opcional quanto no gate operacional dos funcionários. Uma trava síncrona impede dois toques antes do render; prompt() e userChoice são aguardados, aceite confirma com háptico, recusa encerra a oferta e falha troca para instruções manuais sem rejeição não tratada. Troca de usuário/tela invalida respostas tardias, e indisponibilidade de storage não impede dispensar o convite.
  • No gate operacional, solicitações de GPS, câmera, notificações e som possuem singleFlight independente, portanto toques simultâneos compartilham uma única chamada ao sistema sem bloquear permissões diferentes. O epoch acompanha o ID autenticado e impede que diálogo, posição, registro push ou teste de áudio iniciado por um funcionário altere o estado de outro após logout/impersonação. A troca de identidade reinicia a leitura autoritativa de GPS, câmera e notificações em checking antes de liberar etapas.
  • Captura de evidência mantém lock desde a abertura da câmera nativa até o fim do upload, com nome/busy acessível e remoção bloqueada durante a operação. O epoch inclui usuário, cliente, escopo, job e superfície; troca de serviço ou usuário descarta o retorno. Se o serviço ficar somente leitura, o upload é abortado e uma conclusão antiga não adiciona mídia. A confirmação também revalida o limite atual, enquanto falhas de câmera/rede aparecem na própria zona e cancelamento nativo permanece silencioso.
  • Checklist e chat aplicam a mesma fronteira de autorização ao retorno da câmera e do seletor. Perder edição/clock-in invalida a captura e aborta uploads de checklist; foto restaurada só entra se o item ainda for editável. No chat, um lock cobre câmera → entrega ao uploader, desabilita anexos/composer e descarta retorno se canal bloqueou ou já iniciou outro upload. Mudança de canal/usuário limpa o estado, falha de câmera gera toast e cancelamento não gera erro.
  • Aplicação de atualização usa um singleFlight compartilhado entre ação manual e auto-apply. O banner possui lock síncrono antes do próximo render, desabilita defer/dismiss durante a ação e vincula falha/loading à fonte web, Play ou App Store que iniciou o fluxo. No Android Flexible Update, a Promise permanece pendente durante download e instalação: cancelamento encerra sem falso erro, falha rejeita para oferecer nova tentativa e DOWNLOADED somente conclui após completeFlexibleUpdate(). Eventos repetidos não instalam duas vezes e o listener é removido mesmo quando o evento chega antes do handle assíncrono. Na atualização web, falha ao ler o registro ou enviar SKIP_WAITING em uma WebView restrita converge por reload imediato; o overlay não fica aguardando um evento que não pode chegar.
  • Ações destrutivas do painel de manutenção usam um único proprietário por lifecycle. Logs, mídia em memória e reset travam antes de abrir a confirmação, desabilitam os controles pares, aguardam a operação efetiva sem setTimeout decorativo e descartam finalizações depois do teardown; o reset expõe o mesmo loading acessível das demais plataformas.
  • Histórico de push e lista de canais também são particionados pelo lifecycle da identidade. Logout, login de outra conta e impersonação limpam a projeção imediatamente e invalidam buscas anteriores; retomadas concorrentes usam latest wins. No chat, até a remoção das proteções locais de “já lido” só ocorre no commit vencedor, impedindo uma resposta da conta anterior de alterar badges da próxima sessão.
  • O /api/boot segue o mesmo boundary no ServiceContext: cada mount, retry ou refresh de foco/rede propaga AbortSignal por DataService e CloudClient (inclusive durante backoff) e só hidrata stores/query cache se a identidade ainda for a proprietária. Sinais simultâneos de resume, visibilidade e rede da mesma conta compartilham uma única leitura por singleFlight; uma ação posterior, troca de conta ou retry continua podendo substituir/cancelar o trabalho correspondente. A lista de canais usa a mesma coalescência por usuário, inclusive entre carga inicial e retomada. Logout também impede que um 401 tardio do boot antigo acione reload na tela pública.
  • CloudClient aplica limites por padrão a toda a superfície compartilhada: GET e refresh de sessão encerram em 8 segundos, POST/PUT/PATCH/DELETE em 15 segundos e multipart em 60 segundos para acomodar mídia em rede móvel. Todos aceitam cancelamento, propagam o sinal por refresh e espera de retry e não repetem timeout/abort de mutação. O contrato funcional permanece igual: corpos 4xx de escrita continuam disponíveis ao consumidor, enquanto 5xx preservam o retry existente.
  • Mensagens em tempo real usam um epoch de sessão sincronizado durante o render. Fetch inicial/paginação, polling, catch-up, eventos WebSocket e flush offline verificam o ticket antes de publicar ou enviar o próximo item. Troca de conta fecha sockets, intervals, reconnects e timers de typing, limpa projeções; onclose só manipula a própria instância, portanto um socket antigo não remove nem ressuscita o novo.
  • Todo push emitido por sendPushToUser recebe recipientUserId no payload criptografado Web Push e nos dados FCM/APNs. Service worker e bridge preservam o carimbo; a UI rejeita recebimento, badge/read e navegação que não correspondam à sessão ativa. Effects de preferências/banner e timers de toast usam epoch, cobrindo eventos em trânsito durante logout ou impersonação.
  • Fotos nativas são capturadas pela câmera traseira, corrigidas quanto à orientação, limitadas a 1920 px/qualidade 85 e mantidas fora da galeria antes do upload R2. O Android solicita somente CAMERA, sem permissões de armazenamento externo.
  • Notas de voz usam Ogg/Opus mono 16 kHz (PTT WhatsApp) via src/shared/voiceRecorder.ts, com encode em Worker (fora da thread de áudio) para evitar picote; o Android declara RECORD_AUDIO para a captura iniciada pelo usuário no chat, sem gravação em background. O microfone permanece um recurso opcional na ficha de compatibilidade da Play Store.
  • src/shared/platform/fileDeliveryProvider.ts centraliza arquivos gerados e anexos remotos autenticados. Android/iOS gravam temporariamente no cache privado com @capacitor/filesystem e abrem o share sheet com @capacitor/share; PWA/navegador usa Web Share quando aceita arquivos e recorre ao download por Blob quando indisponível ou impedido. Downloads remotos têm teto de 12 segundos e aceitam AbortSignal; o lightbox e os cartões de documento/áudio incompatível do chat usam lock síncrono, loading acessível e cancelam o fetch ao fechar, trocar URL ou desmontar. Falha real preserva o cartão e oferece feedback para retry; cancelamento fica silencioso e nenhum share sheet abre depois da saída. O resultado tipado informa shared, downloaded ou cancelled, e o serviço de PDF acrescenta failed; tanto o AbortError web quanto a rejeição Share canceled/cancelled do Capacitor convergem para cancelamento normal, sem rejeição não observada, download surpresa, háptico ou confirmação falsa. Erros reais continuam propagados após a limpeza do cache temporário. Acionadores bloqueiam repetição enquanto preparam o arquivo. O botão de gráfico permanece visível e mede 44 px no touch, mas continua discreto por hover onde existe mouse.
  • A exportação administrativa de equipe, clientes e leads consome a lista já filtrada na tela e usa esse mesmo provider. Busca, categoria, VIP/tier, endereço incompleto, função e status ativo determinam o conteúdo do CSV; vazio gera orientação, não arquivo. O importador limita a leitura a 5 MB e aplica epoch + FileReader.abort(): nova escolha, fechamento e teardown invalidam callbacks antigos, enquanto erro limpa a prévia e produz feedback. A confirmação usa lock síncrono, aguarda onImport, bloqueia fechamento e só finaliza se o epoch ainda pertencer ao modal; não existe delay cosmético. runSequentialBatch aguarda cada save antes do próximo para preservar snapshots de rollback, expõe completedCount quando interrompido e só permite toast de sucesso depois que todo o lote chegou ao backend. Os botões icon-only de importar/exportar têm nome explícito, e os primitivos compartilhados reutilizam esse nome no status de loading.
  • Rollbacks otimistas de clientes, equipe, adiantamentos, serviços, entidades fiscais, leads e adicionais são condicionais por identidade de objeto. Uma falha só restaura a linha se aquela tentativa ainda for sua proprietária; deletes recusados reinserem apenas o item ausente e nunca sobrescrevem uma recriação. Edições paralelas em outros registros permanecem intactas enquanto o cache autoritativo reconcilia o endpoint.
  • Ações contextuais permanecem visíveis por padrão em telas sem hover e só adotam revelação por group-hover dentro de @media (hover: hover). Isso cobre payroll, fotos, reviews, grupos faturáveis e cards de Settings sem perder a apresentação discreta do desktop. Todos recebem nome contextual e alvo touch adequado; indicadores ainda ocultos por hover são exclusivamente decorativos e aria-hidden. O gate AST também exige texto ou nome ARIA em cada uso de Button e CFButton.
  • title permanece somente como tooltip desktop. Botões e links icon-only publicam aria-label contextual e usam alvo de 44 px nas superfícies touch; o inventário cobre chat/diretório, calendário, faturas, mapas, equipe, estoque, agenda e navegação. O doctor diferencia conteúdo textual de componentes de ícone na AST e rejeita qualquer controle que tente usar tooltip como único nome. Variantes group-hover/msg também entram no gate; somente decoração aria-hidden e setas desktop com scroll touch equivalente podem permanecer hover-only.
  • HapticsService usa @capacitor/haptics no Android para impactos e resultados semânticos; o PWA preserva navigator.vibrate e ambos respeitam a mesma preferência do usuário.
  • src/shared/platform/clipboardProvider.ts centraliza cópias de texto: @capacitor/clipboard é obrigatório nos shells Android/iOS; PWA e desktop tentam a Clipboard API e recorrem a uma seleção temporária, removida em finally, quando Safari/WebView não a disponibiliza. Esse fallback restaura foco e seleção anteriores sem scroll, preservando teclado e posição da tela. A interface só confirma sucesso após o provider resolver e mantém estado de retry nas falhas.
  • src/shared/platform/externalBrowserProvider.ts envia HTTPS externo ao @capacitor/browser no Android/iOS, entrega schemes de sistema como webcal:// ao @capacitor/app-launcher e mantém window.open protegido no PWA. WhatsApp, Google Reviews, mapas e assinatura de calendário não substituem a aplicação dentro da WebView.
  • Links do Profile para assinatura direta de calendário e o retorno do orçamento público ao site institucional também passam por esse provider; isso evita navegação bloqueada pelo domínio restrito do iOS sem mudar o comportamento normal dos anchors no PWA.
  • O provider aceita somente HTTP(S) e os schemes de sistema webcal:, mailto:, tel: e sms:. Caminhos relativos de mídia são resolvidos contra a origem oficial antes de chegar ao browser nativo; javascript:, data: e schemes desconhecidos são rejeitados.
  • Ações de telefone/e-mail nos cartões de clientes e contatos do chat usam o mesmo contrato: Android/iOS entregam ao discador ou cliente de e-mail pelo App Launcher, enquanto a PWA conserva tel:/mailto: como anchors do navegador.
  • Token push e identificador estável da instalação nativa são persistidos por @capacitor/preferences, não no localStorage da WebView. A primeira leitura migra valores de builds anteriores e remove a cópia legada; preferências visuais da PWA continuam separadas.
  • Mensagens criadas sem rede ficam em IndexedDB (prime-crown-offline/chat-messages), nunca em localStorage. A fila migra o formato legado uma vez e remove cada mensagem somente após confirmação individual do backend; uma queda durante o flush preserva os itens restantes para a próxima reconexão.
  • Cada item offline carrega o ID do usuário que o criou, e a reconexão consulta apenas a partição da sessão atual. Logout, revogação remota e impersonação limpam toda a store antes da troca de identidade, portanto outra conta no mesmo aparelho não envia, lê nem encontra o conteúdo pendente da sessão anterior.
  • Logout offline grava somente uma barreira booleana opaca antes de remover a identidade. Na inicialização seguinte, AuthContext processa essa barreira antes de consultar /api/auth/me, repete /api/auth/logout e continua desautenticado se a rede ainda não voltou. Login por senha, Google, Apple, biometria ou impersonação confirmado remove a barreira, evitando tanto restauração indevida do cookie HttpOnly antigo quanto bloqueio de uma nova sessão legítima.
  • O cache local de alertas operacionais também usa IndexedDB, particionado pelo ID do usuário. A hidratação assíncrona combina alertas recebidos durante o boot com o cache existente; chaves legadas notif_history_v1_* são migradas e removidas. O mesmo boundary de sessão apaga todas as partições e a dica de e-mail Google em sessionStorage; preferências estritamente visuais continuam no localStorage.
  • Preferências visuais associadas a entidades usam opaqueStorageMarker, com namespaces separados e sem armazenar IDs operacionais legíveis. Fixação de canais, preferências de notificação e dismiss do banner WhatsApp migram automaticamente as chaves legadas e removem os valores antigos; dicas de passkey compartilham o algoritmo estável. notif_prefs_v2_<marker> preserva som, posição e categorias por conta sem escrever o ID real na chave. O marcador não protege segredos e seu uso fica restrito a estado de interface.
  • O fluxo de orçamento chama chat-initiate com issueGuestToken: false: cria/atualiza lead, canal operacional e lead_quote aberta, mas não cria cliente/atendimento nem gera um JWT de convidado que a tela de confirmação não consome. O padrão do endpoint permanece true para widgets de chat existentes. A confirmação administrativa da cotação é a única fronteira que materializa cliente e service_request.
  • chat-initiate e check-number compartilham uma allowlist CORS: domínios Prime Crown, previews HTTPS do projeto Pages, desenvolvimento local e origins Capacitor. Origens de navegador desconhecidas recebem 403 antes de tocar o banco; integrações servidor-a-servidor sem Origin permanecem compatíveis e as respostas variáveis usam Vary: Origin.
  • Os mesmos endpoints consomem um orçamento persistente em public_api_attempts antes do parsing e dos efeitos externos: 5 requisições/15 min para iniciar lead/chat e 30/10 min para verificar telefone. As chaves SHA-256 derivadas do IP não expõem o endereço em claro; janelas são isoladas por endpoint e um bloqueio retorna 429 com Retry-After para web, PWA e shells nativos tratarem igualmente.
  • src/shared/platform/networkProvider.ts usa @capacitor/network como fonte compartilhada no Android/iOS e eventos do navegador no PWA. No shell nativo, todos os consumidores dividem um único listener e uma única leitura inicial, mantidos enquanto houver assinantes; uma geração invalida inicializações tardias após teardown. Até o plugin responder, o sinal inicial do WebView evita anunciar rede artificialmente disponível. Fila offline do chat, sincronização, polling e refetches deixam de depender diretamente de navigator.onLine no WebView.
  • src/shared/platform/statusBarProvider.ts sincroniza ícones do sistema e --status-chrome. O PWA usa theme-color (status + navegação); no Android API 36 (edge-to-edge) as faixas azuis de cima e de baixo são CSS (html[data-native] + padding do body com background-clip: content-box, sem rolagem no chrome), sem APIs legadas de background/overlay. O shell iOS em ios/ reutiliza os providers Capacitor e respeita safe areas do sistema.
  • O estilo claro/escuro dos glifos nativos segue a luminância da cor efetivamente aplicada ao chrome. O modo do conteúdo é apenas fallback quando não há cor primária, evitando ícones claros sobre marcas claras em temas escuros.
  • Universal Links e credenciais compartilhadas do iOS usam App.entitlements e /.well-known/apple-app-site-association. A resposta é construída com APPLE_TEAM_ID no Cloudflare e associa o bundle uk.co.primecrowncleaning.app; o scheme privado primecrown:// permanece como fallback para deep links internos e aceita destinos no host (primecrown://chat) ou em ?view=.
  • Toda entrada de navegação externa passa pela matriz canRoleAccessView: URL inicial, clique de push/Web Push, Universal Link, custom scheme e o roteador defensivo convertem destinos sem permissão em Dashboard antes de aplicar seleções secundárias. O backend continua responsável pelo isolamento real dos dados.
  • Filas extensas do Dashboard usam renderização progressiva compartilhada: cards montam 24 grupos e a lista 50 linhas por etapa, sempre conservando o total e oferecendo expansão explícita em um controle touch de 44 px. Grupos de fatura, conta e recorrência nunca são divididos. O agrupamento prepara índices lineares para faturas, clientes, funcionários e recorrências, evitando buscas quadráticas sem alterar a prioridade da primeira fatura ativa. O mesmo React/CSS e os mesmos contratos de teclado, status acessível e háptico atendem desktop, PWA, Android e iOS.
  • O Pages usa seu fallback SPA nativo quando não existe 404.html na raiz; não há reescrita catch-all que possa entrar em ciclo com a canonicalização de /index.html. Rotas de instalação e metadados continuam explícitas. /slides usa a resolução natural do index.html do diretório, enquanto assets/404.html isola somente chunks inexistentes e devolve 404 real em vez do HTML da aplicação.
  • /slides é uma rota permanente de treinamento. Os scripts versionados capture-slide-*.mjs e capture-wizard-shots.mjs mantêm suas imagens a partir do app local em viewport móvel e perfil isolado do Chrome; são ferramentas de autoria e não entram no bundle de produção.
  • O seletor administrativo de simulação preserva o <select> nativo e a identidade corrente no boot, mas só monta as listas extensas de funcionários e clientes ao receber foco ou toque; blur as descarta. Assim, centenas de opções não ocupam permanentemente o DOM nem a árvore de acessibilidade do Dashboard, enquanto mouse, teclado, VoiceOver, TalkBack e picker do sistema continuam percorrendo a lista completa quando solicitada.
  • Cada seção operacional constrói um DashboardRelationshipIndex antes de renderizar cards ou linhas. Entidades fiscais e funcionários passam a ser resolvidos por mapa; equipes atribuídas são preparadas em ordem visual estável; séries recorrentes são agrupadas e ordenadas uma única vez para indexar posição/total. Cards deixam de assinar individualmente a store fiscal e de filtrar, buscar e ordenar coleções globais, mantendo exatamente os mesmos valores, avatares e indicadores em cards e lista nas quatro plataformas.
  • A auditoria financeira do Dashboard monta no máximo 12 claims por etapa. Cliente, entidade fiscal e visitas vinculadas são indexados em uma passagem, preservando a ordem global das visitas e o fallback fiscal anterior; cada card deixa de repetir três buscas completas e de assinar a store fiscal. Aprovar/rejeitar usa lock síncrono compartilhado entre os dois botões e libera loading em finally, inclusive quando o backend recusa a decisão; teardown impede estado tardio. A expansão continua explícita por botão touch/teclado e o total permanece anunciado.
  • DashboardIntelligenceService.classifyRequests distribui as visitas entre todas as filas operacionais e financeiras em uma única passagem, conservando ordem e sobreposições de negócio. A leitura de faturas também produz, num só percurso, o índice de visitas faturadas, aprovações e pendências do cliente. O setor liquidado reutiliza o resultado booleano de faturamento da própria visita, eliminando a busca quadrática anterior sem mudar a separação completa de PENDING entre alocação e confirmação.
  • useDashboardController é o único proprietário das queries, stores e ações operacionais consumidas pela página; Dashboard não cria um segundo conjunto de observers para os mesmos dados. useDashboardData prepara uma vez os mapas de clientes e serviços, usa-os na desnormalização e os reutiliza nos fallbacks de cards, sem voltar a varrer as coleções por consulta. Escopo de funcionário/cliente e ordenação cronológica permanecem cobertos por testes puros.
  • Os resumos por papel usam classificadores lineares compartilhados. No cliente, uma passagem encontra visita ativa, próxima visita, total futuro e último atendimento sem avaliação; outra distribui todos os setores operacionais, em vez de filtros e sorts independentes. No funcionário, uma passagem separa hoje, próximos sete dias, visita ativa e concluídos. Ambos assinam useNowMinute, relógio único que pausa em background, para atravessar limites de horário corretamente após foreground sem criar timers por card.
  • O mapa operacional prepara em uma passagem visitas ativas e o primeiro trabalho em andamento por funcionário, além do índice de clientes. Marcadores deixam de executar buscas cruzadas por pessoa, e presença online usa o mesmo relógio foreground. O primeiro conjunto válido recebe fitBounds uma vez — inclusive quando chega depois de um estado vazio — enquanto atualizações posteriores não deslocam o operador; Recenter continua explícito. Todo nome inserido nos fragments HTML do Leaflet é escapado antes de chegar ao DOM.
  • A disponibilidade semanal estabiliza as datas derivadas da semana e indexa pedidos, horas por funcionário e totais diários numa única passagem. A matriz deixa de varrer toda a agenda para cada célula, preservando ordem, atribuições múltiplas e exclusão de cancelamentos. Em telas estreitas, a coluna da equipe permanece fixa durante o scroll horizontal; a grade é uma região nomeada e cada dia anuncia funcionário, data, estado, tarefas e horas a VoiceOver/TalkBack.
  • A inteligência de carga e desempenho da equipe também agrega por atribuição em passagem única. Horas executadas/futuras, avaliações, receita, pontualidade, compliance e contagens funcionário-cliente deixam de refiltrar o histórico por profissional. Retenção conserva a regra de três atribuições com pelo menos uma visita concluída. A tabela de capacidade mantém o profissional fixo durante o scroll móvel, reduz padding em telas estreitas e diferencia ausência real de equipe ativa.
  • Crescimento e margem indexam serviços, clientes, entidades fiscais e funcionários antes de percorrer visitas concluídas. Candidatos acumulam avaliações e último serviço sem ordenar um histórico por cliente; margem acumula receita, custo, horas, VAT, equipe e presença de taxa negociada sem refiltrar pedidos. O resumo da tela separa setores filtrados em uma passagem e distingue busca sem correspondência de uma carteira realmente acima da meta.
  • Os radares de retenção agrupam pedidos por cliente em uma passagem. Reengajamento ordena cada histórico concluído uma vez para inferir frequência, última visita e risco; inteligência de churn reutiliza os grupos para pontualidade, sentimento, incidentes e inatividade. Ambos recebem o relógio foreground compartilhado, portanto dias inativos e severidade avançam ao retomar PWA ou shell nativo. A interface distingue ausência de histórico, carteira saudável e filtro sem resultados; pills publicam grupo/estado pressionado e mantêm scroll touch em viewports estreitos.
  • O Portfolio filtra clientes antes de indexar pedidos e calcula score, LTV, lifecycle e momentum uma vez por histórico agrupado. O controller usa o mesmo relógio foreground, então o limite de inatividade avança sem reload. Cada cartão recebe a métrica pronta e não assina a lista global de pedidos, evitando recomputação e renderização em cascata nas quatro superfícies.
  • Relatórios de prova social e ranking de equipe também usam agregação linear: avaliações, promotores, receita, pontualidade e frequência funcionário-cliente são acumulados uma vez. IDs repetidos na atribuição são normalizados por pedido, cancelamentos permanecem excluídos e a ordenação por contas retidas/avaliação conserva o contrato visual existente.
  • Capacidade e governança recebem o relógio foreground do hook de relatórios. O intervalo mensal é criado uma vez e horas são somadas em passagem única; leakage, avaliações e última visita são agregados simultaneamente por cliente, sem filtros/ordenações por cartão. Virada do mês e limites de 15/30 dias avançam ao retomar browser, PWA ou shell nativo.
  • O Business Intelligence usa um agregador puro para histórico de seis meses, distribuição de serviços e eficiência da equipe. Um índice resolve nomes de serviço e uma passagem por pedidos concluídos alimenta os três painéis; o controller não assina mais clientes sem uso e recebe o relógio foreground para mover a janela mensal sem reload.
  • A divisão fiscal indexa clientes e entidades e acumula pedidos/faturas em uma passagem por coleção. O contrato preserva VAT absorvido, reembolsos parciais, taxas de cancelamento, métodos oficiais no modo HMRC, sobretaxas SENT/PAID e consolidado; entidades desconhecidas não contaminam totais publicados.
  • A visão financeira do colaborador cria um snapshot indexado de holerites visíveis, IDs quitados, tarefas pendentes e tarefas por período. DRAFT permanece privado, somente PAID quita trabalho, custos manuais (inclusive zero) prevalecem e a ordem dos pedidos é preservada. O caixa em mãos usa mapas de cliente/entidade para aplicar VAT sem buscas por linha.
  • O editor de endereços mantém atualizações estruturais imutáveis. Remover o endereço padrão cria a lista remanescente e clona somente o primeiro endereço promovido; nenhum objeto recebido da store/props tem isDefault alterado antes do save. Índice inválido e tentativa de remover a última localização preservam a referência original e não publicam uma falsa mudança.
  • O resumo financeiro central aplica o escopo RBAC antes dos cálculos e sempre ordena uma cópia dos pedidos; abrir Finance nunca muta a ordem do cache TanStack compartilhado com agenda, dashboard ou operações. Salários, horas e receita por serviço são acumulados juntos, com lookup indexado do catálogo e sem alterar P&L/fiscal.
  • Avisos de reposição ordenam uma cópia da lista do cliente por urgência; a entidade compartilhada nunca é mutada para compor texto. Auditoria de IA e envio usam single-flight por cliente, fechando a janela de toque duplo sem impedir operações independentes ou retry após falha.
  • As seis linhas de tabela que funcionam como ação — agenda do dashboard, itens da fatura, pagamentos, serviços históricos, faturas históricas e auditoria — preservam a área inteira para toque, mas também publicam role=button, nome contextual, foco visível e ativação por Enter/Espaço. O seletor lateral da equipe completa a sétima superfície com role=option, aria-selected e nome que anuncia adicionar/remover. Pagamentos sem fatura deixam de anunciar clique; os expansíveis publicam aria-expanded. O doctor deriva esse inventário diretamente da AST TSX e rejeita qualquer elemento estático com onClick sem contrato equivalente.
  • Push nativo seleciona o transporte pela plataforma persistida em native_push_tokens: Android usa FCM e builds novos recebem data-only para o renderer PrimeCrownMessagingService; iOS envia o device token diretamente ao APNs e recebe alerta visível com som, badge e thread-id. O AppDelegate encaminha o token ao plugin Capacitor. Resultado e configuração são apurados por transporte, portanto um envio VAPID bem-sucedido não esconde FCM ausente em uma conta com PWA e Android.
  • O NativeBridge fornece um AbortSignal à inicialização FCM/APNs. Teardown interrompe canais, listeners, consulta de permissão, registro e saves de token; handles que resolvem depois do abort são removidos uma única vez. Back e resultado restaurado da câmera repetem o guard active dentro do callback, cobrindo o intervalo entre cleanup React e remoção assíncrona do listener Capacitor.
  • O provider de teclado trata Show/Hide como um par atômico: qualquer falha parcial remove imediatamente os handles que chegaram e restaura os safe areas, impedindo que a interface receba o deslocamento de abertura sem o evento de fechamento correspondente. Qualquer listener que resolva depois do teardown também é removido exatamente uma vez. A restauração dos safe areas continua síncrona, evitando que remontagens ou logout deixem o conteúdo deslocado. Toasts, ações de bottom sheets e o rodapé de disponibilidade leem as variáveis vivas do shell, portanto acompanham teclado, orientação e barras do sistema em Android/iOS sem mudar o espaçamento desktop. Sheets full-screen aplicam o inset superior ao cabeçalho completo — inclusive fechar e ações — enquanto sheets parciais evitam reserva duplicada. Se o header padrão estiver oculto, o contêiner reserva topo e barra de gestos para o conteúdo customizado; consumidores cujo rodapé já possui safe area, como o composer do chat, declaram essa propriedade para não duplicar o inset.
  • Watches de localização recebem o mesmo ownership explícito: um AbortSignal já cancelado impede a chamada ao plugin, cancelamento durante o registro limpa o ID tardio e falha de registro desanexa o listener de abort antes de propagar o erro.
  • Conectividade nativa mantém um único listener do plugin, mas contabiliza cada assinatura React separadamente — inclusive quando duas superfícies reutilizam a mesma função. Cada disposer é idempotente e o listener Capacitor só é removido quando o último owner sai.
  • Mudanças de tema pintam CSS/meta tags imediatamente, enquanto chamadas ao plugin da status bar são serializadas por geração. Intenções obsoletas aguardando import são descartadas, uma falha não envenena a fila e a última cor sempre determina o contraste final dos ícones nativos.
  • O badge do launcher usa uma fila latest-wins comum a Android, iOS e App Badge API: uma escrita já iniciada termina antes da próxima, contagens intermediárias ainda aguardando são descartadas e a limpeza de logout permanece a intenção final. Rajadas de push atualizam o número correto sem martelar o plugin com cada estado transitório.
  • Escritas no clipboard nativo são serializadas na ordem dos toques através do carregamento dinâmico e da bridge; uma cópia antiga não pode terminar depois e substituir o texto mais novo. Falha de uma operação não envenena a fila seguinte. Na web, a chamada permanece imediata para conservar a ativação transitória exigida pelo Clipboard API, com fallback de seleção que restaura foco e ranges.
  • Abertura das configurações do sistema usa um single-flight compartilhado por gates e modais: o lock é adquirido antes do carregamento do plugin, portanto toque duplo ou duas superfícies simultâneas produzem uma única intent Android/iOS. O retorno ao foreground reconcilia as permissões autoritativas, e sucesso, resposta negativa ou falha da bridge liberam uma nova tentativa.
  • O contraste da status bar nativa deriva da luminância relativa da cor realmente pintada, usando o ponto em que glyphs claros e escuros oferecem contraste equivalente. Assim todas as paletas configuráveis — inclusive Amber e Emerald, antes classificadas como escuras por um limite arbitrário — recebem o estilo de ícone mais legível; cor malformada permanece no fallback conservador.
  • O listener global de haptics observa controles na captura, mas agenda o tick genérico somente depois do dispatch do clique. Se Button, CFButton ou a ação emitir light, selection, success, warning ou error no mesmo gesto, esse feedback explícito cancela o tick pendente. Android/iOS recebem uma única chamada da bridge por gesto; PWA usa a mesma arbitragem sobre navigator.vibrate, e desktop sem suporte nem instala o listener.
  • A lista compartilhada de dispositivos representa tokens nativos como native:<plataforma>:<token>; a API ainda aceita o prefixo legado fcm: durante a transição. Assim, diagnóstico e remoção distinguem corretamente Android/FCM de iOS/APNs sem criar telas separadas.
  • A recuperação de permissões usa nativeSettingsProvider: Android abre detalhes/notificações do app pelas configurações Android; iOS usa somente as telas app-specific oficialmente suportadas pelo sistema.
  • O elemento raiz recebe data-platform="android" ou data-platform="ios" antes do primeiro render. CSS compartilhado permanece o padrão; workarounds de edge-to-edge, momentum scroll, seleção e interação que diferem por sistema usam esse atributo, evitando inferência por user agent.
  • privacyScreenProvider no Android fica sempre desligado (FLAG_SECURE bloqueava screenshots e o diálogo de Subscribe). No iOS aplica blur só em background para o app switcher; transições rápidas são serializadas e estados ainda enfileirados são coalescidos, garantindo que o último foreground/background vença mesmo quando a bridge demora. Captura pela câmera e upload de evidências continuam normais. PWA não carrega o plugin nativo.
  • O teste de push nativo dispara o POST sem timer em background e sem consultar livePushEndpoint/token pela ponte Capacitor; a comparação de endpoint é exclusiva do PWA. O cliente limita o teste a 15 segundos e os transports FCM, APNs e Web Push limitam cada chamada externa a 10 segundos, portanto o estado de loading sempre converge para sucesso ou erro acionável.
  • O canal de chat visível continua imediato em memória para web/PWA e é espelhado em @capacitor/preferences para o renderer nativo suprimir apenas a notificação da conversa realmente aberta. Essas escritas são serializadas e estados enfileirados obsoletos são coalescidos; sair ou trocar rapidamente de thread não pode ser sobrescrito por um set antigo e silenciar notificações futuras.
  • O Android desativa Auto Backup e exclui explicitamente todos os domínios de cloud/device transfer, evitando restaurar cookies WebView, tokens FCM e identificadores de instalação em outro aparelho. Dados de negócio continuam vindo do backend após uma nova autenticação.
  • O gate operacional revalida GPS, precisão, câmera e notificações ao retornar das Configurações usando o lifecycle nativo, sem depender da presença de navigator.geolocation dentro da WebView; PWA mantém focus e visibilitychange como sinais equivalentes. Cada domínio usa o mesmo coordenador latest wins do perfil: consultas antigas, checagem lenta de token e precisão fora de ordem não regressam uma etapa já resolvida; ação explícita publica seu resultado autoritativo e a saída do gate invalida operações pendentes.
  • A seção de dispositivos/diagnóstico push no Perfil particiona leituras por user.id. Nova leitura aborta a anterior; troca de conta ou teardown cancela lista, espera de cinco segundos e polling de ACK. Remoção, teste local e teste real possuem locks síncronos contra dois toques no mesmo frame e só alteram loading, dispositivos ou toasts enquanto o ticket da identidade continuar atual, evitando feedback tardio em outra tela ou sessão.
  • O viewport permite pinch-to-zoom em PWA e WebViews para acessibilidade. Login e formulários de contato recorrentes (orçamento, leads, perfil, clientes e equipe) publicam inputMode/autocomplete, abrindo teclado de e-mail/telefone e autofill do sistema. Todos os 48 campos numéricos são auditados: decimal para dinheiro/percentual/fração, numeric para contagem/tempo inteiro, com única exceção explícita do offset UTC assinado. As 16 buscas/filtros usam type="search", inputMode="search", enterKeyHint="search" e autofill desligado; o postcode preserva seu autocomplete de endereço. Os 17 campos temporais permanecem date, time ou datetime-local, abrindo os pickers do runtime, e todos expõem nome acessível. O doctor exige os contratos. Localização é declarada como capacidade de foreground: o iOS mantém as duas descrições exigidas internamente pelo plugin, mas o produto não solicita nem anuncia rastreamento em background.
  • O teclado Android usa @capacitor/keyboard, adjustResize e resizeOnFullScreen; o provider publica altura/visibilidade no elemento raiz para que telas em 100dvh e modais respeitem a área realmente disponível. keyboardWillHide e o próprio disposer usam a mesma restauração de --safe-bottom, --content-safe-bottom, --native-bottom-lift e altura, portanto desmontar o shell com o teclado aberto não deixa o próximo mount comprimido. O PWA preserva o comportamento nativo do navegador.
  • Breakpoints JS usam useMediaQuery: um mediaQueryStore multiplexa uma única assinatura de matchMedia por consulta e notifica somente quando o resultado cruza o breakpoint. Não use window.resize para derivar mobile/desktop; ele renderiza a cada pixel durante rotação, split-screen, teclado e redimensionamento.
  • Coleções com apresentações distintas também usam esse sinal como fronteira de montagem. Clientes, funcionários e custódia de chaves renderizam cards no mobile ou tabela no desktop, nunca ambos escondendo metade por CSS; a busca de usuários também possui uma única instância ativa. Isso reduz DOM, memória e controles interativos duplicados sem bifurcar cálculo, dados ou ações.
  • A agenda aplica a fronteira aos painéis auxiliares: equipe aparece a partir de 1024 px e tarefas sem alocação a partir de 1280 px; em viewports menores esses componentes drag-and-drop não são montados. O grid usa 80dvh e mínimo móvel de 420 px, retornando ao contrato amplo de 80 vh/700 px no desktop.
  • Polling que aciona hardware ou rede em ciclos curtos deve usar startForegroundInterval; o scheduler remove o timer quando a página oculta ou o shell Capacitor pausa e executa uma leitura fresca ao retornar. foregroundLifecycle multiplexa uma única assinatura global desses três eventos para schedulers, Auth, boot, canais, updates, permissões, saúde do push e histórico de notificações, descartando transições consecutivas para o mesmo estado. Proximidade operacional, polling de páginas, fila do chat e saúde/conexão do WhatsApp usam esse contrato. Textos relativos, SLA, agenda e centro operacional usam o único useNowMinute, que também para em background; não crie relógios locais por componente.
  • A gravação de voz também assina foregroundLifecycle: ao entrar em background, encerra e libera o microfone preservando o take. Não adicione listeners globais de visibilidade/pause/resume diretamente em componentes; a ponte nativa é somente a produtora do sinal compartilhado.
  • O reconcile de canais e o fallback de polling de mensagens usam startForegroundInterval com runImmediately: false: preservam o primeiro intervalo, removem o timer em background e reconciliam imediatamente na retomada. Não mantenha um setInterval ativo apenas para abortar seu callback quando document.visibilityState estiver oculto.
  • Listeners de pointer/touch que não chamam preventDefault devem ser { passive: true }. O touchmove do pull-to-refresh é a única exceção global: permanece { passive: false } porque cancela o scroll somente após reconhecer o gesto vertical no topo.
  • Feedbacks temporários e ações UI atrasadas usam useReplaceableTimeout: a próxima agenda substitui a anterior e o unmount cancela o timer. Não crie setTimeout(() => setState(...)) solto em componentes.
  • Menus ancorados usam usePopoverMenu com role="menu" e itens menuitem*: setas circulam, Escape restaura o gatilho e pointer fora fecha preservando o foco do alvo tocado. No mobile, prefira AdaptiveBottomSheet quando a mesma escolha precisa de uma superfície de alcance do polegar.
  • Utilities overflow-y-auto, overflow-x-auto e overflow-auto contêm overscroll no eixo que possuem; no iOS todas recebem momentum nativo. Não aplique touch-action: pan-x a rails/tabelas: isso impediria a mesma região de participar da rolagem vertical da página.
  • foregroundLifecycle combina document.visibilityState com pause/resume do Capacitor. Ambos precisam indicar foreground e o estado nativo é relido ao receber o primeiro assinante; consumidores não devem voltar a observar diretamente apenas um desses sinais.
  • Decisões funcionais por sistema operacional usam DeviceService (runtime nativo primeiro, detecção web depois). Não duplique regex de user agent em fluxos de calendário, passkey, compartilhamento ou abertura de apps.
  • Capacidades de entrada usam inputCapabilities; breakpoint responsivo deve ter uma fonte reativa compartilhada entre layout e controlador. No chat, ChatInbox fornece o mesmo md/768 px ao controlador e ao ClientChatView, invalida overlays móveis ao cruzar para desktop e monta somente a árvore correspondente ao breakpoint. Não deixe uma segunda conversa escondida por CSS: ela repetiria polling/WebSocket, leitura, composer e efeitos de roteamento WhatsApp.
  • Shells full-screen usam 100dvh; tokens visuais não devem reintroduzir min-h-screen/100vh, pois a viewport estática pode cobrir conteúdo com teclado ou chrome móvel.
  • Reconexão deve ser assinada por networkProvider. Ele compartilha um único par online/offline na web e um único @capacitor/network no nativo; Auth, boot, canais, atualização e fila offline recebem o mesmo sinal. Não recrie eventos primecrown:network-* no NativeBridge nem adicione listeners diretos de online em consumidores.
  • A splash nativa permanece visível até o primeiro commit React e é ocultada por splashScreenProvider; um auto-hide Android de três segundos impede bloqueio permanente se a origem HTTPS falhar antes de executar o app. O PWA não participa desse lifecycle.
  • src/shared/platform/appBadgeProvider.ts mantém o contador de mensagens não lidas no ícone. Android usa @capawesome/capacitor-badge quando o launcher oferece suporte; navegador/PWA preserva a App Badge API existente.
  • Atualizações do badge passam por uma fila que preserva ordem, normalizam entradas inválidas e limpam o ícone ao desmontar a sessão autenticada. Isso impede corridas entre leitura/logout e evita que uma conta deixe seu contador visível para a próxima; o service worker usa a mesma normalização.
  • src/shared/platform/appUpdateProvider.ts consulta atualizações distribuídas pelo Google Play e pela App Store após confirmação no banner compartilhado. Android usa o fluxo imediato/flexível quando permitido; iOS abre a ficha da App Store. Os assets remotos continuam atualizando pelo service worker, com texto e ação próprios para cada origem e os mesmos estados de progresso, falha e nova tentativa. O adiamento é registrado por origem: escolher “Later” para conteúdo web não oculta uma atualização da loja, e a ação sempre corresponde à origem atualmente anunciada.
  • A classe .touch-target preserva controles compactos no desktop e aplica área mínima de 44×44 px somente em dispositivos com ponteiro coarse. Todo botão com largura/altura explícita menor que esse limite — incluindo busca no chat, gravação/reprodução de voz, agendamento, resposta, encaminhamento, reação, filtros de chat, seleção de equipe/frequência da cotação, navegação semanal, folha, faturamento, estoque, configurações, CTAs dos banners globais, logout do drawer, recuperação e navegação lateral — usa o contrato; ações rápidas e dismisses ficam alinhados às convenções de toque de Android/iOS sem criar uma segunda árvore de componentes. O teste percorre a AST de todos os arquivos TSX, reconhece dimensões h/w/min-h/min-w inclusive importantes e mantém assertions explícitas para os controles cuja geometria final depende da composição. As únicas exceções nomeadas são setas hover-only com rolagem touch equivalente e checkboxes da tabela explicitamente desktop; uma exceção nova precisa ser registrada e revisada pelo teste.
  • Seletores visuais nunca comunicam estado apenas por cor, borda ou check decorativo. Serviço/frequência do orçamento, equipe, data/hora/endereço, período, método de pagamento, tema, recursos de IA/WhatsApp, filtros e reconciliação publicam aria-pressed; abas reais usam tablist/tab + aria-selected. Painéis recolhíveis publicam aria-expanded. O mesmo DOM atende mouse, teclado, PWA, TalkBack e VoiceOver, e tests/selection_state_semantics.test.ts protege o inventário compartilhado.
  • Os dois controles segmentados de chat — categorias da Inbox e tipos do Directory — usam oito botões aria-pressed. Eles preservam 36 px no desktop e recebem o piso touch-target de 44 px em ponteiro coarse, evitando uma variante separada para Android/iOS sem depender apenas da cor para comunicar a opção ativa.
  • Tablists usam foco roving: somente a aba ativa recebe tabIndex=0; setas horizontais/verticais circulam, Home/End saltam para as extremidades e o foco acompanha a seleção. CFTabs e o dashboard específico do chat obedecem ao mesmo contrato. Cada aba publica aria-controls e cada painel montado usa tabpanel + aria-labelledby, portanto teclado e leitores de tela navegam pela mesma estrutura em desktop, PWA e WebViews.
  • Listboxes seguem o modelo correspondente ao foco. A paleta combobox mantém o foco no campo por aria-activedescendant; opções ficam fora do Tab, a busca reinicia o índice e setas mantêm o resultado ativo visível. A multisseleção de equipe usa foco roving entre options, setas/Home/End para percorrer e Enter/Espaço para alternar aria-selected. Assim listas grandes não criam uma parada de Tab por item em nenhuma plataforma.
  • Menus ancorados usam usePopoverMenu para abertura por Arrow Down/Up no gatilho, foco inicial na opção marcada, circulação por setas, Home/End, typeahead sem distinção de acentos/caixa, fechamento por Escape com retorno ao gatilho e fechamento por Tab sem interromper a ordem natural. Iniciais repetidas circulam entre correspondências; Espaço e atalhos modificados permanecem reservados ao controle/sistema. Settings, conversas no desktop e ferramentas do composer compartilham o contrato; o seletor de conversas mobile preserva o bottom sheet adaptativo.
  • Sidebar alterna semanticamente entre navegação complementar permanente no desktop e drawer modal no mobile. Fechada, a versão mobile usa inert + aria-hidden, portanto itens traduzidos para fora da tela não recebem Tab nem aparecem antes do conteúdo em VoiceOver/TalkBack. BottomNav mantém aria-expanded e o ciclo de foco Menu → primeiro item → Menu; acima de lg, MainLayout não o monta, evitando uma segunda assinatura de canais e um ResizeObserver invisível no desktop.
  • Ações icon-only que exibem title no desktop também expõem aria-label para VoiceOver/TalkBack; toggles publicam o estado com aria-expanded. O doctor audita os sete controles compartilhados desse grupo e outras 21 ações compactas de navegação, mapa, seleção, adição e fechamento, impedindo que um ícone visual ou tooltip de mouse volte a ser o único nome da ação.
  • Os 22 inputs brutos com placeholder — buscas e entradas rápidas — mantêm um nome persistente por aria-label; ações concluídas pelo teclado indicam done/send e o distrito postal ativa capitalização. Placeholder permanece exemplo visual, não o único contrato acessível.
  • Telas fixas de sistema — autenticação, redefinição, boot e falha de conexão — usam o contrato system-viewport-safe: notch/status bar e área de gesto ficam reservados em Android, iOS e PWA, e 100dvh permanece contido quando o teclado redimensiona a WebView. Limites de agenda, menus de conversa/configuração e modais de manual/QuickBooks também usam dvh, nunca uma altura vh congelada pelas barras do navegador. A abertura automática do WhatsApp no orçamento público passa pelo adaptador externo, saindo da WebView privilegiada no app e mantendo o link de fallback na web.
  • O monitor de conectividade pertence ao MainLayout, não a uma página: Dashboard, Agenda, Chat, Perfil e módulos administrativos compartilham o mesmo aviso persistente e os toasts de perda/restauração de rede em browser, PWA, Android e iOS. O aviso distingue capacidade real: atualizações ao vivo pausam e mensagens de chat entram na fila; outras mutações não são anunciadas como cacheadas porque ainda exigem rede.
  • O mesmo contrato vale para textos transitórios e ações: o toast offline promete fila somente para chat, a instalação PWA descreve acesso a dados recentes durante interrupções breves e pull-to-refresh não inicia uma sincronização fictícia sem rede. Ao reconectar, os listeners globais retomam refresh e drenagem das filas.
  • Os sete editores multilinha têm nome contextual persistente. No compositor do chat, enterKeyHint="enter" acompanha a regra por runtime: Return cria nova linha em teclado touch e Enter envia no desktop, sem interceptar composição IME.
  • Os nove select brutos mantêm o picker nativo de cada runtime e publicam aria-label contextual; os demais seletores seguem a associação automática id/label de CFSelect. O doctor protege a fronteira manual.
  • Os 11 inputs brutos restantes fora de labels envolventes — financeiros, assunto, slider e toggles — têm nome direto; o range anuncia valor + unidade. Checkboxes já envolvidos por label e uploads realmente ocultos preservam a semântica nativa, sem atributos redundantes.
  • Button e CFButton compartilham o contrato de processamento: loading desabilita nova ativação, publica aria-busy, mantém o nome original em um status assistivo e torna o spinner decorativo. Todos os consumidores recebem a correção sem UI paralela por plataforma.
  • Todo botão HTML declara type explicitamente; os componentes compartilhados usam button por padrão e aceitam submit somente no ponto de envio. O doctor percorre a AST de todos os TSX, impedindo que composição de formulários ou Return em teclado móvel reintroduza submissões acidentais.
  • Toda tabela de dados possui caption assistivo (ou nome ARIA explícito), e todo cabeçalho HTML tem conteúdo/nome próprio e declara scope="col" ou scope="row". Colunas visualmente vazias de ações, opções ou expansão usam aria-label. O gate AST cobre inclusive tabelas aninhadas e cabeçalhos extraídos em componentes, mantendo o nome da coleção e a associação entre célula e título para VoiceOver, TalkBack e leitores desktop.
  • Colunas ordenáveis publicam aria-sort no <th> e usam um botão nativo interno para a ação; o cabeçalho estrutural nunca recebe onClick. Assim mouse, toque, Enter e Espaço compartilham o mesmo fluxo, e o doctor impede a regressão para células clicáveis sem semântica de controle.
  • Links que abrem outro contexto usam noopener noreferrer e passam pelo provider externo no runtime nativo; tel:, mailto: e webcal: são encaminhados ao app do sistema. O gate AST protege novas janelas contra opener e impede que protocolos externos permaneçam presos à WebView privilegiada.
  • PageLoader e ModalLoader formam o contrato de carregamento de superfícies: anunciam um status ocupado e educado e ocultam movimento decorativo da árvore assistiva. Fallbacks lazy de mapas/feeds seguem o mesmo padrão, e a Edição Rápida usa o loader modal bloqueante em vez de uma espera silenciosa.
  • Ações assíncronas inline que não usam os botões compartilhados publicam aria-busy, bloqueiam nova ativação e escondem ícones decorativos. O gate cobre série recorrente, configurações do chat, teste WhatsApp, análise de incidente/conflito e respostas rápidas.
  • Os controles compartilhados de seleção mantêm um contrato único: CFSelect associa/alerta erros; CFCheckbox usa um input checkbox nativo dentro do card visual; CFTabs usa tablist com roving tab stop e setas/Home/End; CFPillSelector publica estado pressed. Ícones permanecem decorativos sem alterar touch ou haptics.
  • Coleções específicas preservam o contexto do grupo: frequências permitidas usam fieldset/legend; modos de resposta do WhatsApp usam grupo nomeado e cartões com aria-pressed. O mesmo destaque visual continua servindo touch, mas a seleção passa a ser explícita em VoiceOver, TalkBack e teclado desktop.
  • ProgressBar separa semântica e pintura: conclusão/tempo usa progressbar; margem, capacidade, fidelidade e receita usam <meter> nativo assistivo; o preenchimento customizado é decorativo. O componente limita valores e exige nome/texto contextual. Barras empilhadas publicam um resumo único, e segmentos de gráficos com legenda completa são ocultados para não repetir valores em leitores de tela.
  • AdaptiveBottomSheet e o lightbox de chat compartilham o contrato modal: diálogo nomeado, scroll lock, foco inicial/confinado/restaurado e Escape limitado ao topo da pilha que atende também o back nativo. A abertura externa do lightbox bloqueia repetição e publica busy/texto de progresso.
  • O Sidebar assume o mesmo isolamento apenas como drawer móvel: foco entra no primeiro destino, Tab fica contido, Escape respeita a profundidade modal, body fica travado e o gatilho recebe foco ao fechar. No desktop continua landmark complementar sem comportamento modal.
  • ToastCard centraliza a prioridade dos avisos: prompts comuns usam status educado, negações e falhas críticas usam alerta assertivo. PromptToast bloqueia repetição, publica busy e mantém o nome original da ação; banners operacionais do WhatsApp e comunicados globais seguem a mesma severidade, com animações decorativas e dismiss nomeado.
  • PDFs, payslips, invoices, relatórios, gráficos, exports CSV e templates passam pelo provider. O payslip não espera um timer de render: seu documento já montado permanece presente e o disclosure fica bloqueado durante a captura; lock síncrono e epoch por slip.id impedem export duplicado e feedback tardio depois de troca/teardown. A fatura serializa PDF e WhatsApp na mesma trava, mantém o modal aberto enquanto o alvo é capturado, valida status HTTP + resultado da entrega e usa epoch por invoice.id para não concluir na fatura errada. Nenhuma permissão de armazenamento compartilhado é solicitada, e exportToBase64 continua separado para entregas server-side como WhatsApp.
  • O despacho de invoice não é mais uma atualização fire-and-forget com sucesso antecipado. Ele aguarda RequestsApiClient somente para os serviços incluídos e InvoicesApiClient para o documento, passa cada resposta por assertMutationOk e então publica stores, auditoria e notificações. A ação visual possui lock antes do render, bloqueia fechamento/exports, libera loading em finally e confere o epoch antes de háptico ou erro.
  • E-mail de invoice separa entrega externa de reconciliação interna. A resposta do endpoint é validada antes do dispatch; falha real mantém o editor aberto, enquanto entrega confirmada seguida de falha de status gera warning de sincronização sem sugerir reenvio. O composer trava antes do próximo render, mantém draft/inputs durante erro, impede fechamento em voo e invalida callbacks no teardown.

erDiagram
    auth_accounts ||--o{ auth_providers : "possui"
    auth_accounts ||--o{ refresh_tokens : "gera"
    clients ||--o{ service_requests : "solicita"
    clients ||--o{ invoices : "recebe"
    employees ||--o{ service_requests : "executa"
    service_requests ||--o{ invoices : "gera"
    leads ||--o{ chat_channels : "inicia"
    chat_channels ||--o{ chat_messages : "contém"