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.
Visão Geral de Componentes
Section titled “Visão Geral de Componentes”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
Runtimes Web e Android
Section titled “Runtimes Web e Android”npm run buildenpm run build:webproduzem o mesmo PWA emdist/; o deploy Cloudflare existente não depende do Android.npm run pwa:doctoraudita 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 oViewStatecompartilhadoLIVE_TRACKERe continua sujeito ao mesmo RBAC das demais entradas externas.npm run android:syncproduz o build web e sincronizadist/com o projeto nativo emandroid/.src/shared/platform/runtime.tsé a fonte central de detecção do runtime. Componentes não devem consultar diretamentewindow.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-loginno 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 seguem100dvhe 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.ukcomo 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 em401/403e em toda troca de identidade (login, logout e impersonação), inclusive chaves antigas com query string. Odist/sincronizado pelo Capacitor continua sendo artefato de build, não uma base de dados nativa independente. - As entradas públicas
/quote,/pay,/statement,/proposale/rateusam correspondência de segmento completo. O caminho-base e seus descendentes entram no fluxo público, mas prefixos coincidentes como/payrollpermanecem 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 distingue404de indisponibilidade/rede, oferece retry real, arredondastarsda URL para 1–5, normaliza o feedback e usaCFInput,ButtoneIconBadgecom 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
UPDATED1 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.randomUUIDquando disponível e RFC 4122 v4 sobrecrypto.getRandomValuesnas 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. Opwa:doctorpercorre todosrcde produção e impede queMath.randomvolte a gerar identidade operacional. vite.config.tssubstitui o token__BUILD_ID__do worker por um hash de 12 caracteres calculado a partir doindex.htmlemitido 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.useAppUpdatedetecta tanto o evento de instalação quanto um worker que já esteja emregistration.waitingao 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.createControllerChangeHandlernã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 usamReact.lazysem fallback de reload próprio. A primeira falha atualiza/promove o worker e recarrega network-first; uma repetição propaga para oErrorBoundaryem 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 emtextContent, seminnerHTML/handler inline, safe areas,role=alerte 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 noErrorFallback, enquanto stack e árvore de componentes ficam restritas ao desenvolvimento.telemetrySanitizerremove 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-logsrepete o sanitizer e não confia emuserId/timestampdo 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 retorna429+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 emsystem_logs, e todas as plataformas recebem um único contrato de retry. Respostas funcionais4xxpermanecem 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 retornam409em 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
closeDisabledpode 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 usamlaunchExternalUrl, 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
refantes do primeiroawait; 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 emfinally. - 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 tarefasteam; 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-dbnão integra mais a superfície publicada. NativeBridgeintegra pause/resume aofocusManagerdo TanStack Query, trata o botão voltar e converte App Links em intenções de navegação existentes (view,reqIdechannelId). Ao montar, ele reconcilia o estado autoritativo deApp.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 porimport()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.appInfoProvideradiaApp.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 emfinally; 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
pcViewepcDepth. 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. initializeNativePushregistra 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 comallSettled, 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 acompleteFlexibleUpdate()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/*eprimecrown://*.src/shared/platform/androidAppLinks.tsmantém os certificados da upload key e da app signing key do Google Play; a rotafunctions/.well-known/assetlinks.json.tspublica esse contrato mesmo quando o Pages ignora diretórios estáticos ocultos, epublic/.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
notificationPermissionProviderentre 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 umsingleFlightpor 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 umsingleFlightkeyed 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. OAbortSignaldo 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 emnative_push_tokenspor/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 usaAbortSignale é 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 aoreference_iddo 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 convertedata.urlpela 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
SearchIntelligenceServiceformam uma fronteira lazy do layout autenticado. O atalhoCtrl/Cmd+Kpublica 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
mustChangePasswordestá 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 paraADMIN/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 emInstalledContextMenuGuard, 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
maxTouchPointsou(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. networkProvidermultiplexa conectividade em todas as plataformas: Android/iOS compartilham um listener do plugin Capacitor e browser/PWA/desktop compartilham um único paronline/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/clearAppBadgeexiste. 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 comrole=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:localStoragecontinua 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 comoexpectedUserId. 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.
unregisterNativePushtambé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 aensureNativePushRegistrationou 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-busye 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 atualCamera.takePhotocom orientação corrigida, limite de 1920 px e arquivo temporário fora da galeria. Cancelar retornanull; falhas de captura continuam distintas. O fluxo não depende mais doCamera.getPhotodepreciado 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/0017recebem 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.permissione 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. Obeforeinstallpromptcapturado 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
nativeSettingsProviderpara 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,/termse/account-deletionsã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.tsusa@capacitor/cameraapenas no runtime nativo. Evidências, checklist e chat recebem umFilecomum e mantêm os inputscapture="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 +
getUserMediano navegador/PWA/desktop. O stream web de prova é encerrado assim que a permissão é confirmada; browsers sem consulta padronizada preservam estadounknowne continuam com o input nativo como fallback. src/shared/platform/locationProvider.tscentraliza posição, permissão, observação e watch. Android usa@capacitor/geolocationcom localização precisa e intervalos compatíveis com bateria; navegador/PWA preservanavigator.geolocatione observa a Permissions API quando disponível. Watches aceitamAbortSignal: 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.useGeoPermissioninicia 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çãosingleFlight, 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 emunknown, semunhandledrejection. 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()euserChoicesã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
singleFlightindependente, 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 emcheckingantes 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
singleFlightcompartilhado 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 eDOWNLOADEDsomente conclui apóscompleteFlexibleUpdate(). 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 enviarSKIP_WAITINGem 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
setTimeoutdecorativo 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/bootsegue o mesmo boundary noServiceContext: cada mount, retry ou refresh de foco/rede propagaAbortSignalporDataServiceeCloudClient(inclusive durante backoff) e só hidrata stores/query cache se a identidade ainda for a proprietária. Sinais simultâneos deresume, visibilidade e rede da mesma conta compartilham uma única leitura porsingleFlight; 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 um401tardio do boot antigo acione reload na tela pública. CloudClientaplica 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: corpos4xxde escrita continuam disponíveis ao consumidor, enquanto5xxpreservam 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;
onclosesó manipula a própria instância, portanto um socket antigo não remove nem ressuscita o novo. - Todo push emitido por
sendPushToUserreceberecipientUserIdno 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 declaraRECORD_AUDIOpara 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.tscentraliza arquivos gerados e anexos remotos autenticados. Android/iOS gravam temporariamente no cache privado com@capacitor/filesysteme 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 aceitamAbortSignal; 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 informashared,downloadedoucancelled, e o serviço de PDF acrescentafailed; tanto oAbortErrorweb quanto a rejeiçãoShare canceled/cancelleddo 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, aguardaonImport, bloqueia fechamento e só finaliza se o epoch ainda pertencer ao modal; não existe delay cosmético.runSequentialBatchaguarda cada save antes do próximo para preservar snapshots de rollback, expõecompletedCountquando 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-hoverdentro 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 earia-hidden. O gate AST também exige texto ou nome ARIA em cada uso deButtoneCFButton. titlepermanece somente como tooltip desktop. Botões e links icon-only publicamaria-labelcontextual 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. Variantesgroup-hover/msgtambém entram no gate; somente decoraçãoaria-hiddene setas desktop com scroll touch equivalente podem permanecer hover-only.HapticsServiceusa@capacitor/hapticsno Android para impactos e resultados semânticos; o PWA preservanavigator.vibratee ambos respeitam a mesma preferência do usuário.src/shared/platform/clipboardProvider.tscentraliza 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 emfinally, 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.tsenvia HTTPS externo ao@capacitor/browserno Android/iOS, entrega schemes de sistema comowebcal://ao@capacitor/app-launchere mantémwindow.openprotegido 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:esms:. 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 nolocalStorageda 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 emlocalStorage. 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,
AuthContextprocessa essa barreira antes de consultar/api/auth/me, repete/api/auth/logoute 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 emsessionStorage; preferências estritamente visuais continuam nolocalStorage. - 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-initiatecomissueGuestToken: false: cria/atualiza lead, canal operacional elead_quoteaberta, 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 permanecetruepara widgets de chat existentes. A confirmação administrativa da cotação é a única fronteira que materializa cliente eservice_request. chat-initiateecheck-numbercompartilham uma allowlist CORS: domínios Prime Crown, previews HTTPS do projeto Pages, desenvolvimento local e origins Capacitor. Origens de navegador desconhecidas recebem403antes de tocar o banco; integrações servidor-a-servidor semOriginpermanecem compatíveis e as respostas variáveis usamVary: Origin.- Os mesmos endpoints consomem um orçamento persistente em
public_api_attemptsantes 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 retorna429comRetry-Afterpara web, PWA e shells nativos tratarem igualmente. src/shared/platform/networkProvider.tsusa@capacitor/networkcomo 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 denavigator.onLineno WebView.src/shared/platform/statusBarProvider.tssincroniza ícones do sistema e--status-chrome. O PWA usatheme-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 dobodycombackground-clip: content-box, sem rolagem no chrome), sem APIs legadas de background/overlay. O shell iOS emios/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.entitlementse/.well-known/apple-app-site-association. A resposta é construída comAPPLE_TEAM_IDno Cloudflare e associa o bundleuk.co.primecrowncleaning.app; o scheme privadoprimecrown://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.htmlna 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./slidesusa a resolução natural doindex.htmldo diretório, enquantoassets/404.htmlisola somente chunks inexistentes e devolve 404 real em vez do HTML da aplicação. /slidesé uma rota permanente de treinamento. Os scripts versionadoscapture-slide-*.mjsecapture-wizard-shots.mjsmantê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
DashboardRelationshipIndexantes 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.classifyRequestsdistribui 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 dePENDINGentre alocação e confirmação.useDashboardControlleré o único proprietário das queries, stores e ações operacionais consumidas pela página;Dashboardnão cria um segundo conjunto de observers para os mesmos dados.useDashboardDataprepara 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
fitBoundsuma 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/PAIDe 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.
DRAFTpermanece privado, somentePAIDquita 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
isDefaultalterado 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 comrole=option,aria-selectede nome que anuncia adicionar/remover. Pagamentos sem fatura deixam de anunciar clique; os expansíveis publicamaria-expanded. O doctor deriva esse inventário diretamente da AST TSX e rejeita qualquer elemento estático comonClicksem 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 rendererPrimeCrownMessagingService; iOS envia o device token diretamente ao APNs e recebe alerta visível com som, badge ethread-id. OAppDelegateencaminha 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
NativeBridgefornece umAbortSignalà 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 guardactivedentro 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
AbortSignaljá 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,warningouerrorno 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 sobrenavigator.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 legadofcm: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"oudata-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. privacyScreenProviderno Android fica sempre desligado (FLAG_SECUREbloqueava 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/preferencespara 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 umsetantigo 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.geolocationdentro da WebView; PWA mantémfocusevisibilitychangecomo 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 usamtype="search",inputMode="search",enterKeyHint="search"e autofill desligado; o postcode preserva seu autocomplete de endereço. Os 17 campos temporais permanecemdate,timeoudatetime-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,adjustResizeeresizeOnFullScreen; o provider publica altura/visibilidade no elemento raiz para que telas em100dvhe modais respeitem a área realmente disponível.keyboardWillHidee o próprio disposer usam a mesma restauração de--safe-bottom,--content-safe-bottom,--native-bottom-lifte 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: ummediaQueryStoremultiplexa uma única assinatura dematchMediapor consulta e notifica somente quando o resultado cruza o breakpoint. Não usewindow.resizepara 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
80dvhe 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.foregroundLifecyclemultiplexa 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 únicouseNowMinute, 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
startForegroundIntervalcomrunImmediately: false: preservam o primeiro intervalo, removem o timer em background e reconciliam imediatamente na retomada. Não mantenha umsetIntervalativo apenas para abortar seu callback quandodocument.visibilityStateestiver oculto. - Listeners de
pointer/touchque não chamampreventDefaultdevem ser{ passive: true }. Otouchmovedo 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 criesetTimeout(() => setState(...))solto em componentes. - Menus ancorados usam
usePopoverMenucomrole="menu"e itensmenuitem*: setas circulam, Escape restaura o gatilho e pointer fora fecha preservando o foco do alvo tocado. No mobile, prefiraAdaptiveBottomSheetquando a mesma escolha precisa de uma superfície de alcance do polegar. - Utilities
overflow-y-auto,overflow-x-autoeoverflow-autocontêm overscroll no eixo que possuem; no iOS todas recebem momentum nativo. Não apliquetouch-action: pan-xa rails/tabelas: isso impediria a mesma região de participar da rolagem vertical da página. foregroundLifecyclecombinadocument.visibilityStatecom 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,ChatInboxfornece o mesmomd/768 px ao controlador e aoClientChatView, 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 reintroduzirmin-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 paronline/offlinena web e um único@capacitor/networkno nativo; Auth, boot, canais, atualização e fila offline recebem o mesmo sinal. Não recrie eventosprimecrown:network-*noNativeBridgenem adicione listeners diretos deonlineem 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.tsmantém o contador de mensagens não lidas no ícone. Android usa@capawesome/capacitor-badgequando 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.tsconsulta 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-targetpreserva 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õesh/w/min-h/min-winclusive 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 usamtablist/tab+aria-selected. Painéis recolhíveis publicamaria-expanded. O mesmo DOM atende mouse, teclado, PWA, TalkBack e VoiceOver, etests/selection_state_semantics.test.tsprotege 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 pisotouch-targetde 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.CFTabse o dashboard específico do chat obedecem ao mesmo contrato. Cada aba publicaaria-controlse cada painel montado usatabpanel+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 entreoptions, setas/Home/End para percorrer e Enter/Espaço para alternararia-selected. Assim listas grandes não criam uma parada de Tab por item em nenhuma plataforma. - Menus ancorados usam
usePopoverMenupara 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. Sidebaralterna semanticamente entre navegação complementar permanente no desktop e drawer modal no mobile. Fechada, a versão mobile usainert+aria-hidden, portanto itens traduzidos para fora da tela não recebem Tab nem aparecem antes do conteúdo em VoiceOver/TalkBack.BottomNavmantémaria-expandede o ciclo de foco Menu → primeiro item → Menu; acima delg,MainLayoutnão o monta, evitando uma segunda assinatura de canais e umResizeObserverinvisível no desktop.- Ações icon-only que exibem
titleno desktop também expõemaria-labelpara VoiceOver/TalkBack; toggles publicam o estado comaria-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 indicamdone/sende 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, e100dvhpermanece contido quando o teclado redimensiona a WebView. Limites de agenda, menus de conversa/configuração e modais de manual/QuickBooks também usamdvh, nunca uma alturavhcongelada 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
selectbrutos mantêm o picker nativo de cada runtime e publicamaria-labelcontextual; os demais seletores seguem a associação automáticaid/labeldeCFSelect. 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.
ButtoneCFButtoncompartilham o contrato de processamento: loading desabilita nova ativação, publicaaria-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
typeexplicitamente; os componentes compartilhados usambuttonpor padrão e aceitamsubmitsomente 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
captionassistivo (ou nome ARIA explícito), e todo cabeçalho HTML tem conteúdo/nome próprio e declarascope="col"ouscope="row". Colunas visualmente vazias de ações, opções ou expansão usamaria-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-sortno<th>e usam um botão nativo interno para a ação; o cabeçalho estrutural nunca recebeonClick. 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 noreferrere passam pelo provider externo no runtime nativo;tel:,mailto:ewebcal:são encaminhados ao app do sistema. O gate AST protege novas janelas contraopenere impede que protocolos externos permaneçam presos à WebView privilegiada. PageLoadereModalLoaderformam 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:
CFSelectassocia/alerta erros;CFCheckboxusa um input checkbox nativo dentro do card visual;CFTabsusa tablist com roving tab stop e setas/Home/End;CFPillSelectorpublica 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 comaria-pressed. O mesmo destaque visual continua servindo touch, mas a seleção passa a ser explícita em VoiceOver, TalkBack e teclado desktop. ProgressBarsepara semântica e pintura: conclusão/tempo usaprogressbar; 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.AdaptiveBottomSheete 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
Sidebarassume 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. ToastCardcentraliza a prioridade dos avisos: prompts comuns usam status educado, negações e falhas críticas usam alerta assertivo.PromptToastbloqueia 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.idimpedem 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 porinvoice.idpara não concluir na fatura errada. Nenhuma permissão de armazenamento compartilhado é solicitada, eexportToBase64continua 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
RequestsApiClientsomente para os serviços incluídos eInvoicesApiClientpara o documento, passa cada resposta porassertMutationOke então publica stores, auditoria e notificações. A ação visual possui lock antes do render, bloqueia fechamento/exports, libera loading emfinallye 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.
Modelo ER do Banco D1
Section titled “Modelo ER do Banco D1”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"