1<!DOCTYPE html> 2<html lang="pt-BR"> 3 4<head>
4<script>window.__TENANT__={"kind":"portal","match":true,"source":"portal_domains","status":"active","hostname":"cbnamazonia.com","launched":true,"org_name":"Rede Amazônica","org_slug":"rede-amazonica","portal_id":"cbn-amazonia","dns_target":null,"is_primary":true,"product_line":"platform","access_blocked":false,"organization_id":"b1eebc99-9c0b-4ef8-bb6d-6bb9bd380a22","legacy_full_platform":true};</script>
4 5 <meta charset="UTF-8" /> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> 7 <!-- iPad/iOS PWA â Add to Home Screen --> 8 <meta name="mobile-web-app-capable" content="yes" /> 9 <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> 10 <meta name="apple-mobile-web-app-title" content="" /> 11 <link rel="apple-touch-icon" sizes="180x180" id="apple-touch-icon" href="" /> 12 <title>Ãltimas NotÃcias - CBN Amazônia</title> 13 <meta name="description" content="As últimas notÃcias de CBN Amazônia. Cobertura jornalÃstica completa e atualizada sobre Amazônia Legal com credibilidade e agilidade." /> 14 <!-- Bing Webmaster verification is injected per-portal by 15 netlify/edge-functions/seo-meta-injection.ts (reading 16 portal_seo_config.verification.bing from the DB, landing in PR2). 17 The single hardcoded JB key that used to live here served all 18 tenants the same value â leaking JB ownership into Bing for every 19 portal under the same Netlify deploy. --> 20 <link id="favicon" rel="icon" type="image/png" href="" /> 21 <!-- Preconnect â kept under 4 (PSI guideline). Only origins on the critical path. --> 22 <!-- api.cattive.com handles 100% of data + storage requests on every page. 23 O preconnect ao host cru do projeto (iefppahyskbnsaogvzem.supabase.co) foi 24 removido: o cliente usa VITE_SUPABASE_URL=api.cattive.com, então nada 25 conecta ao host cru â o hint era desperdÃcio E expunha o ref do projeto. --> 26 <link rel="preconnect" href="https://api.cattive.com" crossorigin /> 27 <!-- AgoraTicker external APIs (inmet, open-meteo, espn, brasilapi) were 28 previously dns-prefetched. Removed: AgoraBar mounts via 29 requestIdleCallback well after FCP, so the DNS lookup happens then 30 anyway. Six dns-prefetch entries also exceeded PSI's budget. --> 31 <link rel="dns-prefetch" href="https://www.googletagmanager.com" /> 32 <link rel="dns-prefetch" href="https://www.google-analytics.com" /> 33 <!-- Self-hosted Inter Variable (latin subset, ~47KB woff2) â inline @font-face + 34 preload means the font request fires immediately (parallel with CSS bundle) 35 and the swap is non-blocking. Previous `@fontsource-variable/inter` import 36 moved the @font-face into the render-blocking CSS chunk (PS
36I: FCP +600ms). --> 37 <link rel="preload" href="/fonts/inter-variable-latin.woff2" as="font" type="font/woff2" crossorigin /> 38 <style> 39 @font-face{ 40 font-family:'Inter'; 41 font-style:normal; 42 font-weight:100 900; 43 font-display:swap; 44 src:url(/fonts/inter-variable-latin.woff2) format('woff2-variations'); 45 unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; 46 } 47 </style> 48 <style> 49 /* Edge above-the-fold skeleton (#app-shell, injected by 50 netlify/edge-functions/seo-meta-injection.ts). It is a SIBLING of #root, 51 overlaid above the (empty) #root, and removed once React paints real 52 content (removal observer near the end of <body>). Self-contained so it 53 renders correctly regardless of the main render-blocking CSS. */ 54 #app-shell{position:absolute;top:0;left:0;right:0;z-index:30;background:#f8fafc;overflow:hidden} 55 #app-shell .sk-box{background:#e2e8f0;animation:sk-pulse 1.5s ease-in-out infinite} 56 #app-shell .sk-on-dark{background:rgba(255,255,255,.16)} 57 #app-shell .sk-cards{display:grid;grid-template-columns:repeat(2,1fr);gap:16px} 58 @media(min-width:1024px){#app-shell .sk-cards{grid-template-columns:repeat(4,1fr)}} 59 @keyframes sk-pulse{0%,100%{opacity:1}50%{opacity:.5}} 60 @media(prefers-reduced-motion:reduce){#app-shell .sk-box{animation:none}} 61 /* ââ Shell de ARTIGO (`#app-shell[data-shell="article"] .ea`) ââ 62 Mesmo mecanismo do esqueleto de home (irmão de #root, removido pelo 63 observer no fim do <body>), mas o conteúdo é REAL: manchete, linha fina, 64 imagem e o corpo serializado pela edge (_shared/article-body.ts), para o 65 crawler que não executa JavaScript encontrar a matéria inteira. 66 Um leitor humano vê isto por alguns instantes, então tem de parecer 67 matéria: coluna de leitura, tipografia legÃvel, imagem com caixa 68 reservada. Auto-contido de propósito â a tipografia canônica
69 (src/styles/articleProse.css) só chega no chunk lazy do ArticlePage, bem 70 depois deste primeiro paint. Valores espelham articleProse.css. */ 71 #app-shell .ea{background:#fff;min-height:100vh;padding:20px 0 48px;font-family:'Inter',system-ui,-apple-system,sans-serif;-webkit-font-smoothing:antialiased} 72 #app-shell .ea-col{max-width:44rem;margin:0 auto;padding:0 20px} 73 #app-shell .ea-kicker{display:block;color:#c00;font-weight:700;font-size:.875rem;letter-spacing:.04em;text-transform:uppercase;margin:0 0 10px} 74 #app-shell .ea-title{font-size:2rem;line-height:1.15;font-weight:800;letter-spacing:-.02em;color:#0f172a;margin:0 0 12px} 75 #app-shell .ea-sub{font-size:1.25rem;line-height:1.5;color:#475569;margin:0 0 14px;font-weight:400} 76 #app-shell .ea-byline{font-size:.8125rem;color:#64748b;margin:0 0 20px} 77 #app-shell .ea-fig{max-width:60rem;margin:0 auto 24px;padding:0} 78 /* width/height no <img> + aspect-ratio aqui = caixa reservada antes do byte 79 da imagem chegar. Sem isso o corpo salta quando ela carrega (CLS). */ 80 #app-shell .ea-img{display:block;width:100%;height:auto;aspect-ratio:16/9;-o-object-fit:cover;object-fit:cover;background:#e2e8f0} 81 #app-shell .ea-cap{font-size:.8125rem;color:#64748b;margin:8px 20px 0;line-height:1.5} 82 #app-shell .ea-prose{color:#334155;overflow-wrap:break-word} 83 #app-shell .ea-prose p{font-size:1.125rem;line-height:1.8;margin:0 0 1.25rem} 84 #app-shell .ea-prose h2{font-size:1.5rem;line-height:1.3;font-weight:700;color:#0f172a;margin:2rem 0 .75rem} 85 #app-shell .ea-prose h3{font-size:1.25rem;line-height:1.35;font-weight:700;color:#0f172a;margin:1.75rem 0 .625rem} 86 #app-shell .ea-prose h4,#app-shell .ea-prose h5,#app-shell .ea-prose h6{font-size:1.0625rem;font-weight:700;color:#0f172a;margin:1.5rem 0 .5rem} 87 #app-shell .ea-prose a{color:#1d4ed8;text-decoration:underline;text-underline-offset:2px} 88 #app-shell .ea-prose strong{font-weight:700;color:#1e293b} 89 #app-shell .ea-prose ul{list-style:disc;padding-left:1.5rem;margin:0 0 1.25rem} 90 #app-shell .ea-prose ol{list-style:decimal;padding-left:1.5rem;margin:0 0 1.25rem} 91 #app-shell .ea-prose li{font-size:1.125rem;line-height:1.75;margin:0 0 .5rem} 92 #app-shell .ea-prose blockquote{margin:1.5rem 0;padding:0 0 0 1.25rem;border-left:3px solid #c00;color:#475569;font-size:1.1875rem;line-height:1.7} 93 #app-shell .ea-prose figure{margin:1.5rem 0} 94 #app-shell .ea-prose img{display:block;max-width:100%;height:auto;margin:0 auto} 95 #app-shell .ea-prose figcaption{font-size:.8125rem;color:#64748b;margin-top:8px;line-height:1.5} 96 #app-shell .ea-prose hr{border:0;border-top:1px solid #e2e8f0;margin:2rem 0} 97 #app-shell .ea-prose table{width:100%;border-collapse:collapse;margin:1.5rem 0;font-size:.9375rem;display:block;overflow-x:auto} 98 #app-shell .ea-prose th,#app-shell .ea-prose td{border:1px solid #e2e8f0;padding:8px 10px;text-align:left} 99 #app-shell .ea-prose pre{background:#0f172a;color:#e2e8f0;padding:14px;border-radius:8px;overflow-x:auto;font-size:.875rem;margin:1.5rem 0} 100 @media(min-width:768px){ 101 #app-shell .ea{padding:32px 0 64px} 102 #app-shell .ea-title{font-size:2.75rem} 103 #app-shell .ea-sub{font-size:1.375rem} 104 } 105 </style> 106 107 <!-- ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 108 FOTO DE CAMPANHA â guarda de scripts de terceiro injetados PELA BORDA 109 110 ð´ POR QUE ISTO PRECISA SER INLINE, NO <head>, E NÃO PODE VIVER NO BUNDLE. 111 A zona do Cloudflare injeta o beacon de Web Analytics 112 (`
112<script type="module" src="static.cloudflareinsights.com/...">`) no fim 113 do <body> de todo documento HTML do domÃnio. Isso acontece DEPOIS do 114 Netlify (o Cloudflare é o proxy reverso na frente dele), então nenhuma 115 edge function nossa consegue removê-lo, e o bundle é um módulo diferido: 116 quando ele executa, o beacon já rodou. Só um script sÃncrono no topo do 117 <head> chega antes. 118 119 ð´ O QUE ELE FAZ, E O QUE COMPROVADAMENTE NÃO FAZ. Medido em produção em 120 15/08/2026 contra o beacon real: 121 ⢠sem guarda: GET static.cloudflareinsights.com/beacon.min.js 122 + POST fotodecampanha.com/cdn-cgi/rum â a COLETA 123 ⢠com guarda: só o GET do arquivo. O POST não acontece. 124 Ou seja: o guarda impede a COLETA, que é o tratamento. O GET do arquivo 125 estático continua saindo (a busca já começou quando o observador acorda) 126 e não há como cancelá-la do cliente; ela vai para o mesmo CDN que já 127 termina o TLS de toda requisição deste domÃnio, então não revela a ele 128 nada que ele ainda não saiba. 129 130 ð´ OBSERVA `document`, NÃO `document.documentElement`. Este script roda 131 antes de o <html> existir em alguns caminhos de boot, e observar um nó 132 nulo falha em silêncio: o observador nunca dispara e o guarda vira 133 decoração. Custou uma rodada de medição descobrir isso. 134 135 ð´ NÃO à UM BLOQUEADOR GENÃRICO. A lista é fechada e só tem o que a 136 BORDA injeta sozinha. O que o nosso próprio código carrega (GA4, pixel) 137 já nasce atrás do consentimento em `medicao.ts` e não passa por aqui. 138 139 Espelho da chave: `CHAVE_DO_CONSENTIMENTO` em 140 src/features/foto-campanha/public/medicao/consentimento.ts. 141 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ --> 142
142<script> 143 (function () { 144 var h = (location.hostname || '').toLowerCase(); 145 if (h.indexOf('fotodecampanha.com') === -1 && h.indexOf('fotodecampanha.localhost') === -1) { 146 return; 147 } 148 // Só o que a borda injeta por conta própria. 149 var DA_BORDA = /cloudflareinsights\.com/i; 150 var CHAVE = 'fc_consentimento_v2'; 151 152 // ð´ ESTE NÃMERO ESPELHA `RegistroDeConsentimento.versao` DE 153 // `consentimento.ts`, E DIVERGIR QUEBRA NOS DOIS SENTIDOS. Medido em 154 // produção em 17/08/2026, quando a pergunta subiu de 2 para 3 e este 155 // literal ficou para trás: 156 // 157 // ⢠registro da versão VELHA: `lerConsentimento()` o descarta (a pessoa 158 // volta ao estado "não decidiu"), mas este guarda o aceitava e 159 // LIBERAVA o beacon da borda â terceiro rodando sem decisão válida, 160 // que é exatamente o que este arquivo existe para impedir; 161 // ⢠registro da versão NOVA: o inverso e pior, porque é silencioso â 162 // este guarda não reconheceria o aceite e reteria o beacon em todo 163 // documento seguinte, para sempre, mesmo com a pessoa tendo aceitado. 164 // 165 // A paridade é travada por teste (`isolamentoDeMedicao.test.ts`): ele lê 166 // o número dos DOIS arquivos e exige igualdade. Subir a versão da 167 // pergunta é mudar os dois no mesmo commit. 168 var VERSAO_DA_PERGUNTA = 3; 169 170 var liberado = false; 171 try { 172 var bruto = localStorage.getItem(CHAVE); 173 if (bruto) { 174 var reg = JSON.parse(bruto); 175 liberado = reg && reg.versao === VERSAO_DA_PERGUNTA && reg.medicao === true; 176 } 177 } catch (e) { 178 /* storage indisponÃvel: trata como não decidido, que é o lado seguro */ 179 } 180 181 // Já consentiu com medição: a borda injeta e nada é retido. 182 window.__FC_BORDA_LIBERADA__ = liberado; 183 var retidos = (window.__FC_SCRIPTS_RETIDOS__ = []); 184 if (liberado) return; 185 186 function reter(no) { 187 if (!no || no.nodeName !== 'SCRIPT' || !no.getAttribute) return; 188 var src = no.getAttribute('src'); 189 if (!src || !DA_BORDA.test(src)) return; 190 retidos.push({ src: src, dados: no.getAttribute('data-cf-beacon') || null }); 191 // Os três de uma vez: sem src não há o que avaliar, o type inválido 192 // impede o navegador de tratá-lo como script, e a remoção tira o nó da 193 // lista de execução diferida dos módulos. 194 no.removeAttribute('src'); 195 no.setAttribute('type', 'text/fc-retido'); 196 if (no.parentNode) no.parentNode.removeChild(no); 197 } 198 199 var obs = new MutationObserver(function (lotes) { 200 for (var i = 0; i < lotes.length; i++) { 201 var novos = lotes[i].addedNodes; 202 for (var j = 0; j < novos.length; j++) reter(novos[j]); 203 } 204 }); 205 obs.observe(document, { childList: true, subtree: true }); 206 207 // Rede de segurança: o observador pode ser desconectado por engano numa 208 // refatoração futura, e uma varredura no fim do parse custa nada. 209 document.addEventListener('DOMContentLoaded', function () { 210 var restantes = document.querySelectorAll('script[src*="cloudflareinsights"]'); 211 for (var k = 0; k < restantes.length; k++) reter(restantes[k]); 212 }); 213 214 // Chamado por `medicao.ts` quando a pessoa aceita medição, para o que foi 215 // retido passar a valer sem obrigar a um recarregamento. 216 window.__FC_LIBERAR_BORDA__ = function () { 217 obs.disconnect(); 218 window.__FC_BORDA_LIBERADA__ = true; 219 for (var i = 0; i < retidos.length; i++) { 220 var item = retidos[i]; 221 if (document.querySelector('script[data-fc-liberado="' + item.src + '"]')) continue; 222 var s = document.createElement('script'); 223 s.src = item.src; 224 s.defer = true; 225 s.setAttribute('data-fc-liberado', item.src); 226 if (item.dados) s.setAttribute('data-cf-beacon', item.dados); 227 document.head.appendChild(s); 228 } 229 retidos.length = 0; 230 }; 231 })(); 232 </script>
232 233 234 <!-- Speculation Rules API â Chrome 121+ pre-renders article-page links when 235 the user hovers or scrolls near them. Navigation to an article from the 236 home becomes instant (page already rendered in background). W3C spec, 237 Google-promoted â NOT cloaking. Eagerness "moderate" balances 238 prefetch cost vs hit rate. --> 239
239<script type="speculationrules"> 240 { 241 "prerender": [{ 242 "where": { 243 "and": [ 244 { "href_matches": "/noticias/*" }, 245 { "not": { "href_matches": "/*/admin/*" } } 246 ] 247 }, 248 "eagerness": "moderate" 249 }] 250 } 251 </script>
251 252 253 <!-- AI Discoverability & Agent-Ready --> 254 <link rel="manifest" href="/.well-known/ai-plugin.json" /> 255 <link rel="author" href="/llms.txt" type="text/plain" /> 256 <link rel="api-catalog" href="/.well-known/api-catalog" type="application/linkset+json" /> 257 <link rel="service-desc" href="/openapi.yaml" type="application/yaml" /> 258 <link rel="alternate" type="application/rss+xml" title="RSS Feed" href="/feed.xml" /> 259 260
260<script> 261 // Set title, description & favicon BEFORE React boots to prevent flash. 262 // Registry mirrors SITE_REGISTRY in src/utils/resolvePortalIdentity.ts. 263 // To add a new portal: add ONE entry here AND in resolvePortalIdentity.ts 264 // AND in netlify/edge-functions/_shared/strip-inline-registry.ts. 265 // 266 // â ï¸ PRIVACY / CROSS-TENANT INFO DISCLOSURE 267 // In production, the Netlify edge function `seo-meta-injection.ts` 268 // rewrites the inline portals registry array below to contain ONLY the 269 // matching entry for the request hostname (same for the GA registry). 270 // Without that strip, view-source on every apex exposes the full 271 // tenant list â fixed in PR addressing 2026-05-17 disclosure report. 272 // Localhost/dev keeps the full array (no edge function runs locally). 273 // Tests: netlify/edge-functions/_shared/__tests__/strip-inline-registry.test.ts 274 // 275 // ð¨ Do NOT write the literal token "var sites = [" or "var gaRegistry = {" 276 // anywhere except the real declarations below. The edge regex is anchored
277 // to line start (^\s*) for defense in depth, but it's still safer to 278 // avoid the literals in comments to prevent any future bypass risk. 279 (function () { 280 var h = location.hostname; 281 var p = location.pathname; 282 var isLocal = h === 'localhost' || h === '127.0.0.1' || h.indexOf('192.168.') === 0; 283 284 // Registry: [domainMatch[], localPaths[], title, description, favicon, shortName] 285 // The publicidade-legal entry MUST come first because the match below 286 // uses substring (hostname.indexOf), so a publicidade-legal subdomain 287 // of any parent tenant would otherwise inherit the parent's branding. 288 // Both the canonical no-hyphen prefix ('publicidadelegal.') and the legacy 289 // hyphen form ('publicidade-legal.') are listed so either matches first. 290 // See netlify/edge-functions/_shared/strip-inline-registry.ts. 291 var sites = [[["cbnamazonia"],["/cbn-amazonia"],"CBN Amazônia","CBN Amazônia - Seu portal regional de notÃcias com credibilidade.","https://api.cattive.com/storage/v1/object/public/news_assets/cbn-amazonia/ndrbimj7p9i_1775536554032.webp","CBN Amazônia"]]; 292 293 // System routes (admin, console) â use Cattive branding 294 var systemPaths = ['/admin', '/console']; 295 296 var matched = null; 297 298 // 0. Prévias (<portal>.cattive.app) e demo antiga (demo-<portal>.cattive.com) 299 // â o slug hifenizado não casa as keys de substring e 'cattive.com' 300 // venceria. Casa pelo localPath ('/<slug>'). Mirror de findInlineEntry 301 // em netlify/edge-functions/_shared/strip-inline-registry.ts e de 302 // PORTAL_PREVIEW_HOST_RE / PORTAL_DEMO_HOST_RE em src/config/domains.ts 303 // (sync!). Prévia de portal fora do registry: fica sem entrada e o 304 // tÃtulo/favicon padrão da página valem até o React. 305 var demoM = !isLocal && (h.match(/^demo-([a-z0-9-]+)\.cattive\.com$/) || h.match(/^([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)\.cattive\.app$/)); 306 if (demoM) { 307 for (var i = 0; i < sites.length; i++) { 308 for (var lp = 0; lp < sites[i][1].length; lp++) { 309 if (sites[i][1][lp] === '/' + demoM[1]) { matched = sites[i]; break; } 310 } 311 if (matched) break; 312 } 313 } 314 315 // 1. Production: match by domain 316 if (!isLocal && !matched) { 317 for (var i = 0; i < sites.length; i++) { 318 for (var d = 0; d < sites[i][0].length; d++) { 319 if (h.indexOf(sites[i][0][d]) !== -1) { matched = sites[i]; break; } 320 } 321 if (matched) break; 322 } 323 } else if (isLocal) { 324 // 2. Localhost: match by path prefix 325 for (var i = 0; i < sites.length; i++) { 326 for (var lp = 0; lp < sites[i][1].length; lp++) { 327 if (p === sites[i][1][lp] || p.indexOf(sites[i][1][lp] + '/') === 0) { matched = sites[i]; break; } 328 } 329 if (matched) break; 330 } 331 // 3. Localhost system routes â Cattive branding 332 if (!matched) { 333 for (var s = 0; s < systemPaths.length; s++) { 334 if (p.indexOf(systemPaths[s]) === 0) { 335 matched = [[], [], 'Cattive Platform', 'Cattive Platform', 'https://api.cattive.com/storage/v1/object/public/portal-assets/cattive/favicon.png', 'Cattive']; 336 break; 337 } 338 } 339 } 340 } 341 342 // 4. Fallback: localhost root â Cattive Platform, production unknown â JB 343 if (!matched) { 344 matched = isLocal 345 ? [[], [], 'Cattive Platform', 'Cattive Platform â Ambiente de Desenvolvimento Local', 'https://api.cattive.com/storage/v1/object/public/portal-assets/cattive/favicon.png', 'Cattive'] 346 : [[], [], 'Cattive', 'Plataforma AI-First para Portais de NotÃcias', 'https://api.cattive.com/storage/v1/object/public/portal-assets/cattive/favicon.png', 'Cattive']; 347 } 348 349 // Apply 350 // 351 // â ï¸ NÃO sobrescrever title/description que já vieram preenchidos. 352 // `netlify/edge-functions/seo-meta-injection.ts` substitui a tag title e a 353 // meta description VAZIAS do topo deste arquivo pelos valores REAIS da 354 // página (tÃtulo da matéria, meta 355 // description do artigo) antes do HTML sair do edge. Este script rodava 356 // logo depois e escrevia por cima o branding genérico do portal. 357 // Googlebot e Bingbot renderizam: se o bundle React demora, o snapshot 358 // que eles guardam é o genérico, e toda matéria do portal vira o mesmo 359 // tÃtulo. Agora só preenche o que estiver VAZIO â em localhost e em host 360 // sem edge function os campos vêm vazios do arquivo e o comportamento 361 // antigo continua idêntico. 362 // 363 // ð´ Não citar essas tags LITERALMENTE neste comentário. Ele sai no HTML 364 // servido de todo portal, e quem lê o HTML por expressão regular (a 365 // auditoria de 29/09/2026 e boa parte das ferramentas de SEO) contava a
366 // citação como segunda meta description vazia e segundo title. Travado 367 // em netlify/edge-functions/__tests__/meta-slots.test.ts. 368 var descEl = document.querySelector('meta[name="description"]'); 369 var appleTitle = document.querySelector('meta[name="apple-mobile-web-app-title"]'); 370 var appleTouchIcon = document.getElementById('apple-touch-icon'); 371 var faviconEl = document.getElementById('favicon'); 372 if (!document.title || !document.title.trim()) document.title = matched[2]; 373 if (descEl && (!descEl.content || !descEl.content.trim())) descEl.content = matched[3]; 374 if (faviconEl && !faviconEl.getAttribute('href')) faviconEl.href = matched[4]; 375 if (appleTitle && (!appleTitle.content || !appleTitle.content.trim())) appleTitle.content = matched[5]; 376 if (appleTouchIcon && !appleTouchIcon.getAttribute('href')) appleTouchIcon.href = matched[4]; 377 378 // ââ Google Analytics Boot (with Consent Mode v2) ââ 379 // GA loads synchronously for Google's verification bot to detect. 380 // Consent Mode v2 ensures NO data is collected until user accepts cookies. 381 // Multi-tenant: cada portal tem a SUA propriedade, e todos mandam uma 382 // cópia para a propriedade de roll-up da plataforma (ver `gaPlataforma`). 383 // Admin/console subdomains are always excluded. 384 (function() { 385 // ââ Roll-up da plataforma ââââââââââââââââââââââââââââââââââââââââââ 386 // UMA propriedade GA4 que recebe uma cópia do tráfego de TODOS os 387 // sites, para a Cattive enxergar o conjunto sem depender de somar 388 // relatório por relatório. Ela NÃO substitui a propriedade do tenant: 389 // o gtag.js aceita vários `config` no mesmo documento e cada `config` 390 // é uma propriedade independente. 391 // 392 // ð´ FORA DO REGISTRY DE TENANTS, E ISSO à O PONTO. A borda 393 // (`strip-inline-registry.ts`) reescreve aquele objeto para conter só 394 // a entrada do tenant servido â é o que impede o `view-source` de um 395 // apex de listar os outros clientes. O roll-up é o MESMO id para todo 396 // mundo, então não revela nada sobre a lista de tenants, e por isso 397 // vive numa var própria que a borda não toca. Se entrasse no registry, 398 // ou seria apagado pelo strip (roll-up morto em produção) ou o strip 399 // teria de preservá-lo â e aà a regra de "só a entrada do tenant" 400 // deixaria de ser verdadeira ao pé da letra. 401 // 402 // Valor reescrito no build por `vite-plugins/inject-ga-registry.ts` a 403 // partir de `inline-ga-registry.generated.json` (chave `platform`), a 404 // mesma fonte única do registry abaixo. Não editar só aqui. 405 var gaPlataforma = 'G-QXHDT8VS2D'; 406 407 // Per-tenant GA4 measurement IDs. Hostname substring => measurement ID. 408 // Subdominios admin/console/auth/myaccount sao excluidos abaixo (sem 409 // tracking de back-office). O matching loop usa early break, entao 410 // cada tenant carrega APENAS o proprio GA, nunca cross-tenant. 411 // IMPORTANTE: ao adicionar uma chave aqui, atualize tambem 412 // netlify/edge-functions/_shared/strip-inline-registry.ts â 413 // a edge function reescreve este objeto para conter SOMENTE a 414 // entrada do tenant atual antes de servir o HTML. 415 var gaRegistry = {}; 416 417 // Skip admin/console/auth/painel and demo-* subdomains (back-office e 418 // demos não poluem o GA de marketing do tenant/da Cattive â nem o 419 // roll-up, que é relatório de AUDIÃNCIA, não de uso de ferramenta) 420 var sub = h.split('.')[0]; 421 if (sub === 'admin' || sub === 'console' || sub === 'auth' || sub === 'myaccount' || sub === 'painel' || sub.indexOf('demo-') === 0) return; 422 423 // Prévias de portal (<portal>.cattive.app): o cliente testando o próprio 424 // portal não é audiência de ninguém. ANTES do registry: a key 'cattive' 425 // casaria o host por substring e mediria a prévia na propriedade da Cattive. 426 if (h === 'cattive.app' || h.slice(-12) === '.cattive.app') return; 427 428 // Skip localhost 429 if (isLocal) return; 430 431 // ð´ FOTO DE CAMPANHA NUNCA POR AQUI â nem o tenant, nem o roll-up. 432 // Aquele produto tem caminho próprio de medição 433 // (`src/features/foto-campanha/public/medicao/medicao.ts`), com 434 // consentimento por categoria em `fc_consentimento_v2` e o aviso do 435 // art. 11 na rota eleitoral. Este bloco decide por 436 // `portal_cookie_consent`, chave que aquele domÃnio NUNCA escreve: 437 // ligar o roll-up aqui mediria quem recusou e ignoraria quem aceitou, 438 // e o gtag entraria no documento antes de qualquer pergunta. O roll-up 439 // daquele produto entra por `ligarMedicao`, depois do "sim". 440 // Travado em `medicao/__tests__/isolamentoDeMedicao.test.ts`. 441 if (h.indexOf('fotodecampanha') !== -1 || h.indexOf('fotocampanha') !== -1) return; 442 443 // Deploy previews (*.netlify.app) não poluem propriedade real. O host 444 // de preview contém 'cattive' (cattiveportal.netlify.app) e casava a 445 // chave do registry por substring â cada preview virava sessão real. 446 if (h.indexOf('.netlify.app') !== -1) return; 447 448 // Find matching GA ID by domain 449 var gaId = null; 450 for (var key in gaRegistry) { 451 if (h.indexOf(key) !== -1) { gaId = gaRegistry[key]; break; } 452 } 453 454 // As propriedades DESTE documento: a do tenant (quando existe) mais a 455 // da plataforma, sem repetir. O dedupe importa: em cattive.com as duas 456 // são o mesmo id, e dois `config` iguais no mesmo documento dobram o 457 // page_view e inflam a sessão. 458 var gaIds = []; 459 if (gaId) gaIds.push(gaId); 460 if (gaPlataforma && gaIds.indexOf(gaPlataforma) === -1) gaIds.push(gaPlataforma); 461 if (!gaIds.length) return; 462 463 // Initialize gtag 464 window.dataLayer = window.dataLayer || []; 465 window.gtag = function() { window.dataLayer.push(arguments); }; 466 467 // ââ Consent Mode v2, modo AVANÃADO (LGPD-307: opt-in) ââ 468 // O armazenamento continua opt-in: nada é gravado no aparelho de quem 469 // não aceitou, que é o que a LGPD-307 decidiu. O que muda é o ENVIO. 470 // 471 // ð´ MODO AVANÃADO à ENVIAR O EVENTO SEMPRE, E ISSO NÃO à "MEDIR SEM 472 // CONSENTIMENTO". Com `analytics_storage: denied` o gtag.js manda o hit 473 // SEM cookie e SEM identificador â não há como reconhecer a pessoa, nem 474 // agora nem depois â e o GA4 usa esses hits para modelar quem recusou. 475 // Até 17/08/2026 o código fazia o contrário e ficava no pior par 476 // possÃvel: baixava o gtag.js em TODA visita (o custo de rede e de 477 // terceiro já pago) e depois suprimia o evento, então não colhia nem o 478 // dado sem cookie nem a modelagem. Suprimir o envio não protege 479 // ninguém a mais do que o `denied` já protege; só apaga o número. 480 // 481 // â ï¸ A modelagem tem piso de volume (1.000 eventos/dia com `denied` por 482 // 7 dias, e 1.000 usuários/dia com `granted` em 7 dos últimos 28). Um 483 // portal sozinho dificilmente bate; a propriedade de roll-up, que soma 484 // todos, é onde isso passa a ser alcançável. 485 // 486 // ð´ Là O GRANULAR PRIMEIRO. `portal_cookie_consent` é a chave GROSSA e 487 // vale 'all' só quando as TRÃS categorias estão ligadas â então quem 488 // aceitava estatÃstica e recusava publicidade era gravado como 489 // 'essential' e, da segunda visita em diante, não era medido apesar de 490 // ter consentido. O mapa por categoria é a fonte precisa; a chave grossa 491 // fica como fallback de quem decidiu antes de ele existir. 492 var granular = null; 493 try { granular = JSON.parse(localStorage.getItem('portal_cookie_consent_granular') || 'null'); } catch(e) {} 494 var priorConsent = null; 495 try { priorConsent = localStorage.getItem('portal_cookie_consent'); } catch(e) {} 496 var aceitouTudo = priorConsent === 'all'; 497 var okAnalytics = granular ? granular.analytics === true : aceitouTudo; 498 var okAnuncio = granular ? granular.advertising === true : aceitouTudo; 499 var okFuncional = granular ? granular.functionality === true : aceitouTudo; 500 501 // Set consent defaults BEFORE config (required by Google) 502 window.gtag('consent', 'default', { 503 analytics_storage: okAnalytics ? 'granted' : 'denied', 504 ad_storage: okAnuncio ? 'granted' : 'denied', 505 ad_user_data: okAnuncio ? 'granted' : 'denied', 506 ad_personalization: okAnuncio ? 'granted' : 'denied', 507 functionality_storage: okFuncional ? 'granted' : 'denied', 508 personalization_storage: okFuncional ? 'granted' : 'denied', 509 url_passthrough: true 510 }); 511 // Sem consentimento de anúncio, o gtag redige os identificadores de 512 // publicidade que ainda assim iriam no hit. à o par recomendado do modo 513 // avançado: enviar, mas enviar limpo. 514 if (!okAnuncio) window.gtag('set', 'ads_data_redaction', true); 515 516 window.gtag('js', new Date()); 517 // Rotas tokenizadas (ex.: /parceria/<token>) nunca podem chegar cruas 518 // ao GA â o token é bearer de acesso sem login. E /candidato/<nome>-<sq> 519 // carrega o NOME do candidato, que permite inferir opinião polÃtica 520 // (dado sensÃvel, LGPD art. 11): o sq numérico fica, o nome sai. 521 // Espelho vanilla de src/lib/sanitizeTrackedPath.ts (manter em sync). 522 var safeBootPath = (location.pathname + location.search) 523 .replace(/^(\/parceria)\/[^/?#]+/i, '$1/:token') 524 .replace(/^(\/candidato)\/[^/?#]*?(\d{6,})(?=[/?#]|$)/i, '$1/:nome-$2') 525 .replace(/^(\/candidato)\/(?!:nome-)[^/?#]+/i, '$1/:handle') 526 .replace(/^(\/eleicoes\/candidato\/\d{4})\/[^/?#]*?-(\d+)(?=[/?#]|$)/i, '$1/:nome-$2'); 527 // Dimensão de tenant do roll-up. O GA4 já coleta `hostname`, mas um 528 // portal pode ter mais de um host (apex, www, domÃnio legado) e o 529 // relatório do roll-up tem de somar os dois na mesma linha. O slug sai 530 // do localPath do registry, que é o id canônico do portal (o mesmo que 531 // `demo-<slug>.cattive.com` usa). Sem entrada casada, cai no hostname. 532 var portalSlug = (matched && matched[1] && matched[1][0]) ? matched[1][0].replace(/^\//, '') : h; 533 for (var gi = 0; gi < gaIds.length; gi++) { 534 window.gtag('config', gaIds[gi], { 535 // ð´ SEMPRE `true`. Ver o bloco do modo avançado acima: quem decide 536 // se o hit leva cookie é o `analytics_storage`, não este campo. 537 send_page_view: true, 538 page_path: safeBootPath, 539 page_location: location.origin + safeBootPath, 540 portal: portalSlug 541 }); 542 } 543 544 // Mark as injected for the React hook to detect. 545 // ð´ `__GA_MEASUREMENT_ID` continua sendo Sà o do tenant, e fica 546 // indefinido quando o tenant não tem propriedade própria. O hook usa
547 // essa ausência para ir buscar o id no Supabase â se o roll-up 548 // preenchesse esta chave, todo portal configurado só no banco perderia 549 // o GA próprio silenciosamente. 550 window.__GA_MEASUREMENT_ID = gaId || undefined; 551 window.__GA_PLATFORM_ID = gaPlataforma; 552 window.__GA_SEND_TO = gaIds; 553 window.__GA_CONSENT_GRANTED = okAnalytics; 554 555 // Um loader só serve todas as propriedades: o gtag.js é genérico e 556 // cada `config` acima já registrou a sua. 557 var s = document.createElement('script'); 558 s.async = true; 559 s.src = 'https://www.googletagmanager.com/gtag/js?id=' + gaIds[0]; 560 document.head.appendChild(s); 561 })(); 562 563 // Generate dynamic Web Manifest for PWA 564 var isAdminPage = location.hostname.startsWith('admin.') || location.hostname.startsWith('console.') || location.pathname.startsWith('/admin') || location.pathname.startsWith('/console'); 565 // Flash subdomain (`flash.<tenant>`) installs as its own PWA with the 566 // Flash branding so users see "Flash" on the home screen, not the 567 // generic portal name. 568 var isFlashSubdomain = location.hostname.startsWith('flash.'); 569 var portalDisplayName = matched[2]; 570 // O manifest descreve o APP (a marca do portal), não a página atual. Usa 571 // os valores do registry em vez de ler document.title / meta description: 572 // desde que o bloco acima passou a preservar o que o edge injetou, esses 573 // dois campos podem conter o tÃtulo e a descrição de UMA matéria â e o 574 // PWA instalado ficaria com o nome de uma notÃcia na tela inicial. 575 // Resultado idêntico ao anterior, quando title e description eram sempre 576 // sobrescritos com matched[2] e matched[3]. 577 var manifestName = isFlashSubdomain ? ('Flash · ' + portalDisplayName) : portalDisplayName; 578 var manifestShortName = isFlashSubdomain ? 'Flash' : (matched[5] || portalDisplayName); 579 var manifestDescription = isFlashSubdomain 580 ? ('NotÃcias em tela cheia do ' + portalDisplayName + ': clima, trânsito, cinema, eventos e mais.') 581 : matched[3]; 582 // `.href` (não getAttribute) de propósito: resolve para URL absoluta, que 583 // é o que o manifest gerado como blob precisa â path relativo como 584 // '/favicon-sp.ico' não resolveria contra a origem do blob. 585 var logoSrc = document.getElementById('favicon').href; 586 var manifest = { 587 name: manifestName, 588 short_name: manifestShortName, 589 description: manifestDescription, 590 start_url: isAdminPage ? "/admin" : "/", 591 display: "standalone", 592 background_color: isFlashSubdomain ? "#000000" : "#ffffff", 593 theme_color: isFlashSubdomain 594 ? "#000000" 595 : (matched[2] === 'Cattive' || matched[2] === 'Cattive Platform') ? "#0f172a" 596 : matched[2] === 'Publicidade.Legal' ? "#1e293b" 597 : "#2563eb", 598 icons: [ 599 { 600 src: logoSrc, 601 sizes: "192x192", 602 type: "image/png", 603 purpose: "any maskable" 604 }, 605 { 606 src: logoSrc, 607 sizes: "512x512", 608 type: "image/png", 609 purpose: "any maskable" 610 } 611 ] 612 }; 613 614 // Web Share Target API for Admin 615 if (isAdminPage) { 616 manifest.shortcuts = [ 617 { name: "Novo Artigo", short_name: "Artigo", url: "/admin/posts/new", icons: [{ src: logoSrc, sizes: "192x192" }] }, 618 { name: "Nova Imagem (DAM)", short_name: "MÃdia", url: "/admin/media", icons: [{ src: logoSrc, sizes: "192x192" }] } 619 ]; 620 621 manifest.share_target = { 622 action: "/admin/media", 623 method: "POST", 624 enctype: "multipart/form-data", 625 params: { 626 title: "title", 627 text: "text", 628 url: "url", 629 files: [ 630 { 631 name: "file", 632 accept: ["image/jpeg", "image/png", "image/webp", "video/mp4"] 633 } 634 ] 635 } 636 }; 637 } 638 639 // Encode manifest as Data URI to avoid CORS and Blob destruction issues 640 var manifestJson = encodeURIComponent(JSON.stringify(manifest)); 641 var link = document.createElement('link'); 642 link.rel = 'manifest'; 643 link.href = 'data:application/manifest+json;charset=utf-8,' + manifestJson; 644 document.head.appendChild(link); 645 })(); 646 </script>
646 647
647<script type="module" crossorigin src="/assets/index-s_BTi5XF.js"></script>
647 648 <link rel="modulepreload" crossorigin href="/assets/vendor-react-BPk9u-w4.js"> 649 <link rel="stylesheet" crossorigin href="/assets/index-GYMwT1An.css"> 650
650<script>(function(){try{var h=location.hostname.toLowerCase();var d=["fotodecampanha.com","fotodecampanha.com.br","cattive.com","cattive.me","publicidade.legal"],p=["painel","auth","admin","console","myaccount","business","anuncie","suporte","flash","tv","publicidade-legal"];for(var i=0;i<d.length;i++){if(h===d[i]||h==="www."+d[i])return;}for(var j=0;j<p.length;j++){if(h.indexOf(p[j]+".")===0)return;}var f=["/assets/PortalApp-CaflchGF.js","/assets/PortalHome-BjWw0OZK.js","/assets/vendor-icons-XRoHuOlC.js"];for(var k=0;k<f.length;k++){var l=document.createElement("link");l.rel="modulepreload";l.crossOrigin="";l.href=f[k];document.head.appendChild(l);}}catch(e){}})()</script>
650 651 <link rel="canonical" href="https://cbnamazonia.com/noticias" /> 652 <link rel="alternate" hreflang="pt-BR" href="https://cbnamazonia.com/noticias" /> 653 <link rel="alternate" hreflang="x-default" href="https://cbnamazonia.com/noticias" /> 654 <meta property="og:title" content="Ãltimas NotÃcias - CBN Amazônia" /> 655 <meta property="og:description" content="As últimas notÃcias de CBN Amazônia. Cobertura jornalÃstica completa e atualizada sobre Amazônia Legal com credibilidade e agilidade." /> 656 <meta property="og:url" content="https://cbnamazonia.com/noticias" /> 657 <meta property="og:type" content="website" /> 658 <meta property="og:site_name" content="CBN Amazônia" /> 659 <meta property="og:locale" content="pt_BR" /> 660 <meta property="og:image" content="https://cbnamazonia.com/.netlify/images?url=https%3A%2F%2Fcbnamazonia.com%2Fog%2Fcategory%2Fnoticias.svg&w=1200&h=630&fit=cover&fm=png&q=90" /> 661 <meta property="og:image:secure_url" content="https://cbnamazonia.com/.netlify/images?url=https%3A%2F%2Fcbnamazonia.com%2Fog%2Fcategory%2Fnoticias.svg&w=1200&h=630&fit=cover&fm=png&q=90" /> 662 <meta property="og:image:width" content="1200" /> 663 <meta property="og:image:height" content="630" /> 664 <meta property="og:image:alt" content="Ãltimas NotÃcias - CBN Amazônia" /> 665 <meta name="twitter:card" content="summary_large_image" /> 666 <meta name="twitter:title" content="Ãltimas NotÃcias - CBN Amazônia" /> 667 <meta name="twitter:description" content="As últimas notÃcias de CBN Amazônia. Cobertura jornalÃstica completa e atualizada sobre Amazônia Legal com credibilidade e agilidade." /> 668 <meta name="twitter:image" content="https://cbnamazonia.com/.netlify/images?url=https%3A%2F%2Fcbnamazonia.com%2Fog%2Fcategory%2Fnoticias.svg&w=1200&h=630&fit=cover&fm=png&q=90" /> 669 <meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1" /> 670
670<script type="application/ld+json">[{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"InÃcio","item":"https://cbnamazonia.com"},{"@type":"ListItem","position":2,"name":"Ãltimas NotÃcias","item":"https://cbnamazonia.com/noticias"}]},{"@context":"https://schema.org","@type":"WebPage","@id":"https://cbnamazonia.com/noticias","name":"Ãltimas NotÃcias - CBN Amazônia","description":"As últimas notÃcias de CBN Amazônia. Cobertura jornalÃstica completa e atualizada sobre Amazônia Legal com credibilidade e agilidade.","url":"https://cbnamazonia.com/noticias","isPartOf":{"@id":"https://cbnamazonia.com/#website"},"publisher":{"@id":"https://cbnamazonia.com/#organization"},"inLanguage":"pt-BR"}]</script>
670 671</head> 672 673<body class="bg-slate-50 text-slate-900 antialiased selection:bg-blue-100 selection:text-blue-900"><h1 style="position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0">Ãltimas NotÃcias - CBN Amazônia</h1> 674 <!-- #A11 â Skip Navigation Link 675 Inline style removed: Tailwind `sr-only` already hides the link until focused, 676 and the inline style was overriding `focus:not-sr-only` (PSI: "skip link not focusable"). --> 677 <a href="#main-content" class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[9999] focus:bg-blue-600 focus:text-white focus:px-4 focus:py-2 focus:rounded-lg focus:text-sm focus:font-medium focus:shadow-lg">Pular para o conteúdo</a> 678 <div id="global-admin-preloader-container"></div> 679 <div id="root"></div> 680 <!-- Crawler / no-JS fallback. 681 Bots that don't execute JS (or pause before React mounts) used to see 682 "JavaScript Required" here and surface it as the page description in 683 Google Alerts / WhatsApp / FB previews. We now render a semantic 684 fallback that mirrors the page <title> and <meta description> 685 (populated either by the inline boot script above or by the 686 seo-meta-injection edge function). Discoverability links point 687 crawlers to the structured feeds. The whole block is hidden when JS 688 runs (React replaces #root), so end users never see it. 689 â2026-05-18 SEO incident fix. --> 690 <noscript> 691 <article style="max-width:720px;margin:60px auto;padding:24px;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;line-height:1.5;color:#0f172a"> 692 <h1 id="ns-title" style="font-size:32px;font-weight:700;margin:0 0 12px;line-height:1.2">Ãltimas NotÃcias - CBN Amazônia</h1> 693 <p id="ns-desc" style="font-size:18px;margin:0 0 24px;color:#475569">As últimas notÃcias de CBN Amazônia. Cobertura jornalÃstica completa e atualizada sobre Amazônia Legal com credibilidade e agilidade.</p> 694 <nav aria-label="Recursos para crawlers"> 695 <h2 style="font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:#64748b;margin:0 0 8px">Recursos</h2> 696 <ul style="list-style:none;padding:0;margin:0;display:flex;flex-wrap:wrap;gap:12px"> 697 <li><a href="/feed.xml" style="color:#2563eb;text-decoration:underline">RSS Feed</a></li> 698 <li><a href="/sitemap.xml" style="color:#2563eb;text-decoration:underline">Mapa do site</a></li> 699 <li><a href="/llms.txt" style="color:#2563eb;text-decoration:underline">AI agents (llms.txt)</a></li> 700 <li><a href="/noticias" style="color:#2563eb;text-decoration:underline">Ãltimas notÃcias</a></li> 701 </ul> 702 </nav> 703 <p style="margin:32px 0 0;padding-top:16px;border-top:1px solid #e2e8f0;color:#94a3b8;font-size:14px">Para a experiência completa, habilite JavaScript no seu navegador.</p> 704 </article> 705 </noscript> 706 <!-- Platform Health: Blank Screen Auto-Recovery V4 707 - Robô (navigator.webdriver ou UA de robô): nunca recarrega nem mostra aviso; só registra 708 - App vivo (#root com filhos ou window.__APP_BOOT) sem falha provada: não recarrega; 709 aos 15 s visÃveis registra slow_boot e mostra um aviso discreto com "Recarregar" 710 (aos 30 s se o esqueleto da borda está na tela) 711 - Falha provada: SCRIPT/LINK de /assets/ ou chunk que falhou, ou #root SEM filhos 712 depois do DOMContentLoaded (entrada que não rodou, ou erro JS do próprio host que 713 derrubou a árvore). Recarrega, 1ª simples, 2ª com limpeza só com evidência de 714 service worker; a mesma falha depois de uma recarga vai para o overlay (com o app 715 montado, para o aviso) 716 - Erro de módulo acessório com o app montado não é queda; promessa rejeitada só conta 717 se for chunk; SyntaxError e erro de ICU/Intl nunca recarregam; "Script error.", erro 718 de outro host e de script inline não contam 719 - Navegação SPA desde a carga: nunca recarrega (#3198) 720 - Aba oculta: só conta o tempo visÃvel 721 - Sends beacon to log-incident for every detected incident --> 722
722<script> 723 (function() { 724 // Todo erro visto, para diagnóstico (beacon e overlay). Só `ownErrors`, 725 // `resErrors` e `fatalError` pesam na decisão. 726 var errors = []; 727 var ownErrors = []; 728 var resErrors = []; 729 var fatalError = null; 730 var RELOAD_KEY = '_pf_recovery_count'; 731 var RELOAD_TS_KEY = '_pf_recovery_ts'; 732 var SIG_KEY = '_pf_recovery_sig'; 733 var SID_KEY = '_pf_sid'; 734 var MAX_RETRIES = 2; 735 var SESSION_WINDOW_MS = 60000; // Reset counter after 60s of no issues 736 737 // Caminho da carga. Uma navegação SPA para rota lazy deixa o #root sem 738 // texto enquanto o Suspense espera o chunk; se isso coincide com uma 739 // checagem, o boot NÃO falhou. Visto em produção em 28/09/2026: SPA 740 // /empresas â /guia-educacao aos ~10 s, recarga aos 12 s, e a limpeza 741 // "nuclear" na segunda. Com navegação, o app está vivo por definição e 742 // nunca é recarregado daqui (ver decideBlankAction). 743 var BOOT_PATH = location.pathname; 744 745 // Relógio. Tudo é contado em tempo VISÃVEL desde o parse deste script: 746 // aba aberta em segundo plano não é boot lento (o navegador estrangula 747 // timers e rede de aba oculta). 748 var FIRST_CHECK_MS = 5000; 749 var RECHECK_MS = 3500; // 5 s, 8,5 s, 12 s, 15,5 s... 750 var OVERLAY_MIN_MS = 12000; // overlay nunca antes dos 12 s 751 var SLOW_NOTICE_MS = 15000; // slow_boot no beacon e aviso "demorando" para app vivo 752 // Com o esqueleto da borda na tela o leitor já vê as manchetes: o aviso 753 // espera mais (no Slow 4G com CPU 4x ele aparecia em 6 de 6 cargas por 754 // cima do esqueleto, e "Recarregar" reiniciava um boot de 40 s). 755 var SLOW_NOTICE_SHELL_MS = 30000; 756 var DCL_GRACE_MS = 2000; // React leva um instante entre a entrada rodar e o 1º commit 757 var WATCH_MAX_MS = 180000; // depois disso o detector para de olhar 758 var START = Date.now(); 759 var recovering = false; 760 var finished = false; 761 762 // Robô não é leitor: recarregar ou cobrir a tela de um renderizador de 763 // busca só atrapalha (e cerca de 90% dos incidentes do JB em 29/09/2026 764 // eram robô). `navigator.webdriver` pega Playwright/Selenium/Puppeteer. A 765 // lista NÃO usa /render/ solto. "cubot" é marca de celular Android, não 766 // robô. Google-InspectionTool (inspeção de URL do Search Console), 767 // BingPreview e Chrome-Lighthouse (PageSpeed) não têm "bot" no UA. 768 var UA = String(navigator.userAgent || ''); 769 var IS_BOT = navigator.webdriver === true || 770 (/bot|spider|crawl|slurp|headless|googlebot|bingbot|yandex|baiduspider|duckduckbot|facebookexternalhit|petalbot|ahrefs|semrush|inspectiontool|bingpreview|lighthouse/i.test(UA) && 771 !/cubot/i.test(UA)); 772 773 // Estado entre recargas. Com o sessionStorage bloqueado (SecurityError, 774 // 51 eventos em 6 dias no Sentry), o contador voltava a 0 a cada carga e 775 // a entrada morta recarregava em loop, sem teto. Reserva: window.name, 776 // que sobrevive à recarga na mesma aba, usado só quando está vazio ou já 777 // é nosso. Sem nenhum dos dois não há teto confiável, e o detector não 778 // recarrega sozinho (ver getReloadCount). 779 var NAME_PREFIX = '__pf:'; 780 function nameState() { 781 try { 782 var n = String(window.name || ''); 783 if (n === '') return {}; 784 if (n.indexOf(NAME_PREFIX) === 0) return JSON.parse(n.slice(NAME_PREFIX.length)) || {}; 785 } catch(e) {} 786 return null; 787 } 788 function nameWrite(o) { 789 var empty = true; 790 for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) { empty = false; break; } 791 try { window.name = empty ? '' : NAME_PREFIX + JSON.stringify(o); } catch(e) {} 792 } 793 function sGet(k) { 794 try { return sessionStorage.getItem(k); } catch(e) {} 795 var o = nameState(); 796 return o && Object.prototype.hasOwnProperty.call(o, k) ? String(o[k]) : null; 797 } 798 function sSet(k, v) { 799 try { sessionStorage.setItem(k, v); return; } catch(e) {} 800 var o = nameState(); 801 if (o) { o[k] = String(v); nameWrite(o); } 802 } 803 function sDel(k) { 804 try { sessionStorage.removeItem(k); return; } catch(e) {} 805 var o = nameState(); 806 if (o && Object.prototype.hasOwnProperty.call(o, k)) { delete o[k]; nameWrite(o); } 807 } 808 var CAN_PERSIST = (function() { 809 try { sessionStorage.setItem('_pf_probe', '1'); sessionStorage.removeItem('_pf_probe'); return true; } catch(e) {} 810 return nameState() !== null; 811 })(); 812 813 // Destino do beacon. O caminho relativo dava 404 no domÃnio do portal (o 814 // netlify.toml não encaminha /functions/v1): até 28/09/2026 a tabela 815 // platform_incidents nunca tinha recebido uma linha de produção, só de 816 // localhost. O Vite troca o marcador pela VITE_SUPABASE_URL no build (em 817 // produção, https://api.cattive.com, o mesmo host do resto do frontend; 818 // o proxy preserva o IP do leitor, que o rate limit da função usa). Build 819 // sem a variável deixa o marcador cru, e aà fica o relativo de antes. 820 var INCIDENT_URL = (function() { 821 var base = 'https://api.cattive.com'; 822 if (!/^https?:\/\/[^%\s]+$/.test(base)) return '/functions/v1/log-incident'; 823 return base.replace(/\/+$/, '') + '/functions/v1/log-incident'; 824 })(); 825 826 // Id da aba (sessionStorage), para ligar recarga â carga seguinte â 827 // overlay no platform_incidents. Aleatório, sem nada do leitor. 828 var SESSION_ID = (function() { 829 var id = sGet(SID_KEY) || ''; 830 if (!id) { 831 id = Math.random().toString(36).slice(2, 10) + Date.now().toString(36); 832 sSet(SID_KEY, id); 833 } 834 return id; 835 })(); 836 837 window.__diag = { errors: errors, phase: 'init' }; 838 window.__platformHealth = { incidents: [] }; 839 840 function perfNow() { 841 try { return Math.round(performance.now()); } catch(e) { return Date.now() - START; } 842 } 843 844 // DOMContentLoaded só dispara depois que a entrada (módulo, que é defer) 845 // e os imports estáticos dela executaram ou falharam. "DCL disparou e 846 // nada do app" prova que a entrada morreu; "DCL ainda não disparou" 847 // prova que ainda está baixando. document.readyState==='c
847omplete' não 848 // serve: espera imagens e anúncio. 849 var dclAt = document.readyState === 'loading' ? null : Date.now(); 850 document.addEventListener('DOMContentLoaded', function() { if (dclAt === null) dclAt = Date.now(); }); 851 852 var ORIGIN = String(location.origin || ''); 853 function isSameOrigin(u) { 854 u = String(u || ''); 855 return u.charAt(0) === '/' || (ORIGIN !== '' && u.indexOf(ORIGIN + '/') === 0); 856 } 857 function pathOf(u) { 858 u = String(u || ''); 859 return ORIGIN !== '' && u.indexOf(ORIGIN) === 0 ? u.slice(ORIGIN.length) : u; 860 } 861 function isOwnAsset(u) { return isSameOrigin(u) && pathOf(u).indexOf('/assets/') === 0; } 862 863 var CHUNK_RE = /Failed to fetch dynamically imported module|Importing a module script failed|error loading dynamically imported module|Unable to preload CSS|is not a valid JavaScript MIME type|Expected a JavaScript(?:-or-Wasm)? module script/i; 864 // HTML servido no lugar de JS: o service worker velho (ou um cache de 865 // borda) devolvendo index.html em /assets/*.js. 866 var MIME_RE = /is not a valid JavaScript MIME type|Expected a JavaScript(?:-or-Wasm)? module script but the server responded with a MIME type of ["']?text\/html|Unexpected token\s*'?</i; 867 868 // Erro que recarregar não conserta: sintaxe que o navegador não entende 869 // (navegador antigo) e ICU/Intl quebrado. A exceção é "Unexpected token 870 // '<'", que é HTML no lugar de JS e aà a recarga (ou a limpeza) resolve. 871 function isFatal(msg, err) { 872 if (/Unexpected token\s*'?</i.test(msg)) return false; 873 if ((err && err.name === 'SyntaxError') || /SyntaxError|Unexpected token|Invalid regular expression/i.test(msg)) return true; 874 return /\bICU\b|Icu error|\bIntl\b|Incorrect locale information|Invalid language tag|Invalid time zone/i.test(msg); 875 } 876 877 function noteError(kind, msg, file, line, err) { 878 msg = String(msg || (err && err.message) || ''); 879 file = String(file || ''); 880 var entry = kind === 'ERR' 881 ? '[ERR] ' + msg + ' @ ' + (file.split('/').pop() || '?') + ':' + (line || '?') 882 : '[REJ] ' + msg.substring(0, 200); 883 errors.push(entry); 884 // "Script error." é erro de script de outro host sem CORS: nada a ver 885 // com o boot. Idem para erro com arquivo de outro host (anúncio, 886 // métrica, extensão do navegador). 887 if (/^\s*(Uncaught\s+)?Script error\.?\s*$/i.test(msg)) return; 888 if (file && !isSameOrigin(file)) return; 889 // Script inline (arquivo = a própria página): GA, anúncio, estes 890 // scripts de boot. Quebrar um deles não impede a entrada de rodar. 891 if (file && file.split('#')[0].split('?')[0] === ORIGIN + location.pathname) return; 892 // Promessa rejeitada à toa (fetch que falhou, AbortError, JSON.parse de 893 // resposta ruim, que é SyntaxError) não prova que o boot caiu; import 894 // dinâmico que falhou prova. Este filtro vem ANTES do isFatal: um 895 // "Unexpected end of JSON input" rejeitado virava erro fatal e cobria 896 // com o overlay um app vivo e só lento. 897 if (kind === 'REJ' && !CHUNK_RE.test(msg)) return; 898 if (kind === 'ERR' && !file && !CHUNK_RE.test(msg) && 899 !(err && err.stack && ORIGIN !== '' && String(err.stack).indexOf(ORIGIN) >= 0)) return; 900 if (isFatal(msg, err)) { if (!fatalError) fatalError = entry; return; } 901 ownErrors.push(entry); 902 } 903 904 // Fase de captura: é o único jeito de ver a falha de
904<script>/<link> 905 // (404, bloqueio, MIME), porque erro de recurso não borbulha até o 906 // window. Antes disso, entrada 404 deixava o #root vazio com errors=[]. 907 window.addEventListener('error', function(e) { 908 var t = e && e.target; 909 if (t && t !== window && t.tagName) { 910 var tag = String(t.tagName).toUpperCase(); 911 var rel = String((t.getAttribute && t.getAttribute('rel')) || '').toLowerCase(); 912 var url = t.src || t.href || ''; 913 if ((tag === 'SCRIPT' || (tag === 'LINK' && /stylesheet|modulepreload/.test(rel))) && isOwnAsset(url)) { 914 resErrors.push(pathOf(url)); 915 errors.push('[RES] ' + tag.toLowerCase() + ' ' + pathOf(url).substring(0, 160)); 916 } 917 return; 918 } 919 noteError('ERR', e && e.message, e && e.filename, e && e.lineno, e && e.error); 920 }, true); 921 window.addEventListener('unhandledrejection', function(e) { 922 var r = e && e.reason; 923 noteError('REJ', r && r.message ? r.message : String(r), '', null, r); 924 }); 925 926 // Get reload attempt count (with stale window check) 927 // Sem onde guardar o contador, conta como tentativas esgotadas: melhor o 928 // overlay com o botão do que recarregar em loop. 929 function getReloadCount() { 930 if (!CAN_PERSIST) return MAX_RETRIES; 931 var ts = parseInt(sGet(RELOAD_TS_KEY) || '0', 10); 932 if (Date.now() - ts > SESSION_WINDOW_MS) { 933 sDel(RELOAD_KEY); 934 sDel(RELOAD_TS_KEY); 935 return 0; 936 } 937 return parseInt(sGet(RELOAD_KEY) || '0', 10) || 0; 938 } 939 940 function incrementReloadCount() { 941 var count = getReloadCount() + 1; 942 sSet(RELOAD_KEY, count.toString()); 943 sSet(RELOAD_TS_KEY, Date.now().toString()); 944 return count; 945 } 946 947 function clearReloadCount() { 948 sDel(RELOAD_KEY); 949 sDel(RELOAD_TS_KEY); 950 sDel(SIG_KEY); 951 } 952 953 // Assinatura da falha, sem hash de build, query nem linha: a mesma falha 954 // que sobrevive a uma recarga é determinÃstica (bloqueio, navegador, 955 // robô) e recarregar de novo só repete o branco. 956 function normalizeSig(s) { 957 return String(s) 958 .replace(/[?#][^\s'"]*/g, '') 959 .replace(/-[A-Za-z0-9_-]{8}\.(js|css)\b/g, '.$1') 960 .replace(/:\d+(:\d+)?/g, '') 961 .substring(0, 160); 962 } 963 function currentSig() { 964 if (fatalError) return 'fatal:' + normalizeSig(fatalError); 965 if (resErrors.length) return 'res:' + normalizeSig(resErrors[0]); 966 if (ownErrors.length) return 'err:' + normalizeSig(ownErrors[0]); 967 return 'entry_dead'; 968 } 969 function readLastReload() { 970 try { 971 var o = JSON.parse(sGet(SIG_KEY) || 'null'); 972 if (!o || Date.now() - (o.ts || 0) > SESSION_WINDOW_MS) return null; 973 return o; 974 } catch(e) { return null; } 975 } 976 function writeLastReload(sig, method) { 977 sSet(SIG_KEY, JSON.stringify({ sig: sig, method: method, ts: Date.now() })); 978 } 979 980 // O src/lib/chunkErrorRecovery.ts recarrega sozinho em erro de chunk 981 // (chave `_chunk_reload_ts`). Marca desta carga: ele está recarregando 982 // agora, não disputar. Marca da carga anterior (até 60 s): a falha de 983 // chunk de agora já sobreviveu a uma recarga. 984 function chunkReloadTs() { 985 try { return parseInt(sessionStorage.getItem('_chunk_reload_ts') || '0', 10) || 0; } catch(e) { return 0; } 986 } 987 988 function hasSwController() { 989 try { return !!(navigator.serviceWorker && navigator.serviceWorker.controller); } catch(e) { return false; } 990 } 991 992 // Limpeza "nuclear": o caso que ela resolve é o service worker velho 993 // servindo HTML ou chunk 404 em /assets/. Só roda com evidência (ver 994 // decideBlankAction) e apaga só os caches do SW que guardam página ou 995 // JS. Imagens do flipbook/Flash e fontes ficam. Desregistrar o SW derruba 996 // a inscrição de push do leitor, então só acontece com a assinatura de 997 // HTML servido como JS: aà o próprio fetch do SW velho é o defeito e 998 // apagar cache não basta (mesma regra do chunkErrorRecovery). 999 function nuclearEscape(unregister, done) { 1000 var called = false;
1001 function fin() { if (called) return; called = true; try { done(); } catch(e) {} } 1002 try { 1003 var tasks = []; 1004 if (unregister && navigator.serviceWorker && navigator.serviceWorker.getRegistrations) { 1005 tasks.push(navigator.serviceWorker.getRegistrations() 1006 .then(function(rs){ return Promise.all(rs.map(function(r){ return r.unregister(); })); }) 1007 .catch(function(){})); 1008 } 1009 if (typeof caches !== 'undefined') { 1010 tasks.push(caches.keys() 1011 .then(function(ks){ 1012 return Promise.all(ks.filter(function(k){ 1013 return !/^(flipbook-images|fonts|flash-images|flash-weather)/.test(k); 1014 }).map(function(k){ return caches.delete(k); })); 1015 }) 1016 .catch(function(){})); 1017 } 1018 Promise.all(tasks).then(fin, fin); 1019 } catch(e) { fin(); } 1020 // Never hang the recovery on a wedged CacheStorage/SW call. 1021 setTimeout(fin, 1200); 1022 } 1023 1024 // Só erro do app: um JSON.parse rejeitado com "Unexpected token '<'" 1025 // (API devolvendo HTML) não é HTML servido no lugar de JS. 1026 function hasMimeEvidence() { 1027 for (var i = 0; i < ownErrors.length; i++) if (MIME_RE.test(ownErrors[i])) return true; 1028 return false; 1029 } 1030 // Falha de ativo de /assets/ ou de chunk: prova de boot quebrado mesmo com 1031 // o app montado (é a rota que não vem). 1032 function hasHardError() { 1033 if (resErrors.length) return true; 1034 for (var i = 0; i < ownErrors.length; i++) if (CHUNK_RE.test(ownErrors[i])) return true; 1035 return false; 1036 } 1037 1038 function recoverAndReload(metrics, phase, method, sig) { 1039 recovering = true; 1040 incrementReloadCount(); 1041 writeLastReload(sig, method); 1042 logIncident('blank_screen', phase, metrics, true, method === 'nuclear' ? 'nuclear_reload' : 'auto_reload', { sig: sig }); 1043 var go = function() { location.reload(); }; 1044 if (method === 'nuclear') { nuclearEscape(hasMimeEvidence(), function() { setTimeout(go, 150); }); } 1045 else { setTimeout(go, 150); } 1046 } 1047 1048 // Manual recovery from the overlay button: o leitor já passou das 1049 // tentativas automáticas, então limpa os caches do SW antes. 1050 window.__pfNuclearReload = function() { 1051 clearReloadCount(); 1052 nuclearEscape(hasMimeEvidence(), function() { location.reload(); }); 1053 setTimeout(function() { location.reload(); }, 1300); 1054 }; 1055 1056 // Texto VISÃVEL do #root: sem <style>/
1056<script>/<noscript>/<template>. O 1057 // GlobalAudioPlayer (montado fora da rota) injeta um <style> de 749 1058 // caracteres de @keyframes, e o textContent contava isso como conteúdo. 1059 function visibleTextLength(root) { 1060 var n = 0; 1061 try { 1062 var walker = document.createTreeWalker(root, 4 /* NodeFilter.SHOW_TEXT */, null); 1063 var node; 1064 while ((node = walker.nextNode())) { 1065 var p = node.parentNode; 1066 var skip = false; 1067 while (p && p !== root) { 1068 var tag = p.nodeName; 1069 if (tag === 'STYLE' || tag === 'SCRIPT' || tag === 'NOSCRIPT' || tag === 'TEMPLATE') { skip = true; break; } 1070 p = p.parentNode; 1071 } 1072 if (!skip) n += String(node.nodeValue || '').trim().length; 1073 } 1074 } catch(e) { n = (root.textContent || '').trim().length; } 1075 return n; 1076 } 1077 1078 // DOM visibility check. O esqueleto da borda (#app-shell) é IRMÃO do 1079 // #root, então não entra aqui: o #root só tem conteúdo quando o React 1080 // pinta. 1081 function checkBlank() { 1082 var root = document.getElementById('root'); 1083 if (!root) return { isBlank: true, info: 'NO_ROOT', metrics: {} }; 1084 var h = root.scrollHeight; 1085 var kids = root.children.length; 1086 var txt = visibleTextLength(root); 1087 var imgs = root.querySelectorAll('img').length; 1088 var buttons = root.querySelectorAll('button,a').length; 1089 var visEls = root.querySelectorAll('header,nav,main,article,section,h1,h2,h3,p').length; 1090 var isBlank = (txt < 10 && imgs === 0 && buttons < 2 && visEls < 2); 1091 return { 1092 isBlank: isBlank, 1093 info: 'scrollH=' + h + ', children=' + kids + ', text=' + txt + ', imgs=' + imgs + ', btns=' + buttons + ', semantic=' + visEls, 1094 metrics: { scrollHeight: h, children: kids, text: txt, imgs: imgs, btns: buttons, semantic: visEls } 1095 }; 1096 } 1097 // Para o bloco do #app-shell: a rede de segurança dele não tira o 1098 // esqueleto enquanto o #root estiver em branco. 1099 window.__pfRootIsBlank = function() { return checkBlank().isBlank; }; 1100 1101 // Walk DOM tree for diagnostics 1102 function walkDOM(root) { 1103 try { 1104 var walk = function(el, depth) { 1105 if (depth > 4 || !el) return ''; 1106 var tag = el.tagName ? el.tagName.toLowerCase() : '#text'; 1107 var cls = el.className ? (' class="' + String(el.className).substring(0, 60) + '"') : ''; 1108 var s = new Array(depth + 1).join(' ') + '<' + tag + cls + '>\n'; 1109 for (var i = 0; i < Math.min(el.children.length, 5); i++) { 1110 s += walk(el.children[i], depth + 1); 1111 } 1112 return s; 1113 }; 1114 return walk(root, 0); 1115 } catch(e) { return 'walk error: ' + e.message; } 1116 } 1117 1118 // Rotas tokenizadas (/parceria/<token>) nunca podem ser persistidas 1119 // cruas: o token é bearer de acesso sem login e o DB só guarda o 1120 // SHA-256 dele. Mesma coisa para /candidato/<nome>-<sq>: o nome do 1121 // candidato permite inferir opinião polÃtica (dado sensÃvel, LGPD 1122 // art. 11), então o sq numérico fica e o nome sai. Espelho vanilla de 1123 // src/lib/sanitizeTrackedPath.ts (manter em sync), mesmos regexes do 1124 // boot do GA acima. 1125 function safePath(p) { 1126 return String(p) 1127 .replace(/^(\/parceria)\/[^/?#]+/i, '$1/:token') 1128 .replace(/^(\/candidato)\/[^/?#]*?(\d{6,})(?=[/?#]|$)/i, '$1/:nome-$2') 1129 .replace(/^(\/candidato)\/(?!:nome-)[^/?#]+/i, '$1/:handle') 1130 .replace(/^(\/eleicoes\/candidato\/\d{4})\/[^/?#]*?-(\d+)(?=[/?#]|$)/i, '$1/:nome-$2'); 1131 } 1132 1133 function appBootMs() { 1134 var b = window.__APP_BOOT; 1135 return typeof b === 'number' ? Math.round(b) : null; 1136 } 1137 1138 // Send incident to logging edge function via beacon (fire-and-forget) 1139 function logIncident(type, phase, metrics, autoResolved, resolutionMethod, extra) { 1140 try { 1141 var root = document.getElementById('root'); 1142 var meta = { 1143 phase: phase, 1144 boot_path: safePath(BOOT_PATH), 1145 metrics: metrics, 1146 errors: errors.slice(0, 10), 1147 dom_tree: type === 'blank_screen' ? walkDOM(root) : null, 1148 reload_attempts: getReloadCount(), 1149 timestamp: new Date().toISOString(), 1150 ua_class: IS_BOT ? 'bot' : 'human', 1151 session_id: SESSION_ID, 1152 t_ms: perfNow(), 1153 visible_ms: Math.round(visibleElapsed()), 1154 dcl: dclAt !== null, 1155 app_boot: appBootMs() === null ? false : appBootMs(), 1156 app_mounted: !!(root && root.children.length > 0), 1157 shell_present: !!document.getElementById('app-shell'), 1158 res_errors: resErrors.slice(0, 5) 1159 }; 1160 if (extra) for (var k in extra) if (Object.prototype.hasOwnProperty.call(extra, k)) meta[k] = extra[k]; 1161 var payload = JSON.stringify({ 1162 incident_type: type, 1163 severity: autoResolved ? 'medium' : 'critical', 1164 url: location.origin + safePath(location.pathname + location.search), 1165 path: safePath(location.pathname), 1166 user_agent: navigator.userAgent, 1167 error_message: fatalError || ownErrors[0] || errors[0] || null, 1168 auto_resolved: autoResolved, 1169 resolution_method: resolutionMethod, 1170 session_id: SESSION_ID, 1171 metadata: meta 1172 }); 1173 // Use sendBeacon for reliability (survives page unload/reload
1173) 1174 if (navigator.sendBeacon) { 1175 navigator.sendBeacon(INCIDENT_URL, payload); 1176 } 1177 // Also store locally for the health service to pick up 1178 window.__platformHealth.incidents.push(JSON.parse(payload)); 1179 } catch(e) { /* silent */ } 1180 } 1181 1182 // O que fazer com uma checagem. Pura: o teste roda este bloco no jsdom 1183 // (src/config/__tests__/blankScreenWatchdog.test.ts). 1184 // 'ok' â #root com conteúdo visÃvel: zera o contador de recargas. 1185 // 'wait' â app vivo ou entrada ainda baixando, sem falha provada. 1186 // Página lenta não é página quebrada: recarregar joga 1187 // fora o download em andamento e mostra outro branco. 1188 // 'spa_wait' â idem, com navegação SPA desde a carga (#3198). 1189 // 'slow' â 'wait' passado dos 15 s visÃveis: aviso discreto com 1190 // "Recarregar" e slow_boot no beacon, sem recarga. 1191 // 'recover' â falha provada, recarga simples. Falha provada é: 1192 // - SCRIPT/LINK de /assets/ que falhou ou erro de chunk 1193 // (hardError), com ou sem app montado; 1194 // - #root SEM filhos depois do DCL e da carência, com a 1195 // entrada que não rodou (sem __APP_BOOT), erro fatal ou 1196 // erro JS do próprio host. O React 19 desmonta a árvore 1197 // em erro não tratado, então #root vazio com erro é 1198 // queda; #root com filhos e erro é um módulo acessório 1199 // (eleições, NPS, anúncio) que falhou enquanto a rota 1200 // ainda baixa, e isso não é queda. 1201 // 'nuclear' â recarga com limpeza dos caches do SW. Só com evidência: 1202 // SW controlando a página E falha de ativo ou de chunk. 1203 // 'give_up' â erro determinÃstico (sintaxe, ICU), mesma falha depois 1204 // de uma recarga, ou tentativas esgotadas: overlay (a 1205 // partir dos 12 s). Com o app montado, o aviso não 1206 // bloqueante no lugar do overlay. 1207 // 'bot' â a falha é real, mas é robô: só registra. 1208 function decideBlankAction(s) { 1209 if (!s.isBlank) return 'ok'; 1210 if (s.chunkReloading) return 'wait'; 1211 var settled = s.dcl && s.dclAgoMs >= DCL_GRACE_MS; 1212 var dead = !s.navigated && (s.hardError || 1213 (!s.appMounted && settled && (s.fatal || s.ownError || !s.appBooted))); 1214 if (!dead) { 1215 if (s.elapsed >= SLOW_NOTICE_MS && !s.bot) return 'slow'; 1216 return s.navigated ? 'spa_wait' : 'wait'; 1217 } 1218 if (s.bot) return 'bot'; 1219 if (s.fatal || s.attempts >= MAX_RETRIES) return 'give_up'; 1220 if (s.repeat) return (s.nuclearEvidence && s.lastMethod !== 'nuclear') ? 'nuclear' : 'give_up'; 1221 return (s.attempts >= 1 && s.nuclearEvidence) ? 'nuclear' : 'recover'; 1222 } 1223 1224 // ââ Tempo visÃvel ââ 1225 var visibleMs = 0; 1226 function isHidden() { 1227 try { return document.visibilityState === 'hidden' || document.hidden === true; } catch(e) { return false; } 1228 } 1229 var visibleSince = isHidden() ? null : Date.now(); 1230 function visibleElapsed() { 1231 return visibleMs + (visibleSince === null ? 0 : Date.now() - visibleSince); 1232 } 1233 var pending = null; 1234 var timer = null; 1235 function arm() { 1236 if (timer) { clearTimeout(timer); timer = null; } 1237 if (!pending || isHidden()) return; // retoma no visibilitychange 1238 timer = setTimeout(function() { 1239 timer = null; 1240 if (!pending) return; 1241 if (isHidden() || visibleElapsed() < pending.target - 25) { arm(); return; } 1242 var p = pending; pending = null; p.fn(); 1243 }, Math.max(0, pending.target - visibleElapsed())); 1244 } 1245 function at(target, fn) { pending = { target: target, fn: fn }; arm(); } 1246 document.addEventListener('visibilitychange', function() { 1247 if (isHidden()) { 1248 if (visibleSince !== null) { visibleMs += Date.now() - visibleSince; visibleSince = null; } 1249 if (timer) { clearTimeout(timer); timer = null; } 1250 } else { 1251 if (visibleSince === null) visibleSince = Date.now(); 1252 arm(); 1253 } 1254 }); 1255 1256 var loggedSpa = false;
1257 var loggedSlow = false; 1258 var loggedGiveUp = false; 1259 1260 function shellVisible() { 1261 return !!document.getElementById('app-shell') && window.__APP_SHELL_REMOVED !== true; 1262 } 1263 1264 function runCheck(target) { 1265 if (recovering || finished) return; 1266 var phase = Math.round(target / 1000) + 's'; 1267 window.__diag.phase = phase; 1268 var result = checkBlank(); 1269 var root = document.getElementById('root'); 1270 var sig = currentSig(); 1271 var last = readLastReload(); 1272 var chunkTs = chunkReloadTs(); 1273 var hardError = hasHardError(); 1274 var appMounted = !!(root && root.children.length > 0); 1275 var repeat = !!(last && last.sig === sig) || 1276 (hardError && chunkTs > 0 && chunkTs < START && START - chunkTs < SESSION_WINDOW_MS); 1277 var elapsed = visibleElapsed(); 1278 var action = decideBlankAction({ 1279 isBlank: result.isBlank, 1280 bot: IS_BOT, 1281 navigated: location.pathname !== BOOT_PATH, 1282 appMounted: appMounted, 1283 appBooted: appBootMs() !== null, 1284 dcl: dclAt !== null, 1285 dclAgoMs: dclAt === null ? 0 : Date.now() - dclAt, 1286 hardError: hardError, 1287 ownError: ownErrors.length > 0, 1288 fatal: fatalError !== null, 1289 repeat: repeat, 1290 lastMethod: last ? last.method : null, 1291 nuclearEvidence: hasSwController() && (hardError || hasMimeEvidence()), 1292 attempts: getReloadCount(), 1293 elapsed: elapsed, 1294 chunkReloading: chunkTs >= START 1295 }); 1296 var next = function() { 1297 if (target + RECHECK_MS <= WATCH_MAX_MS) at(target + RECHECK_MS, function() { runCheck(target + RECHECK_MS); }); 1298 }; 1299 1300 // Conteúdo na tela. Até os 12 s segue olhando, como o V3 (checagens de 1301 // 5 s e 12 s): é o que pega a navegação SPA logo depois da carga. 1302 if (action === 'ok') { clearReloadCount(); hideRecoveryUi(); if (target < OVERLAY_MIN_MS) next(); return; } 1303 if (action === 'wait') { next(); return; } 1304 if (action === 'spa_wait') { 1305 // Registra só o primeiro adiamento: é o dado que mede quantas 1306 // recargas falsas a guarda evitou. Robô só lento não gera linha. 1307 if (!loggedSpa && !IS_BOT) { loggedSpa = true; logIncident('blank_screen', phase, result.metrics, true, 'spa_recheck'); } 1308 next(); 1309 return; 1310 } 1311 if (action === 'slow') { 1312 if (!loggedSlow) { loggedSlow = true; logIncident('blank_screen', phase, result.metrics, true, 'slow_boot'); } 1313 if (!shellVisible() || elapsed >= SLOW_NOTICE_SHELL_MS) showSlowNotice(); 1314 next(); 1315 return; 1316 } 1317 if (action === 'bot') { 1318 // Robô com a página quebrada de verdade: uma linha, sem recarga, 1319 // aviso ou overlay. Robô só lento não gera linha nenhuma. 1320 finished = true; 1321 logIncident('blank_screen', phase, result.metrics, false, 'bot_no_action', { sig: sig }); 1322 return; 1323 } 1324 if (action === 'recover' || action === 'nuclear') { 1325 recoverAndReload(result.metrics, phase, action === 'nuclear' ? 'nuclear' : 'plain', sig); 1326 return; 1327 } 1328 // give_up: o overlay espera os 12 s; até lá, o leitor ainda pode ver 1329 // a rota pintar. 1330 if (elapsed < OVERLAY_MIN_MS) { next(); return; } 1331 if (!loggedGiveUp) { loggedGiveUp = true; logIncident('blank_screen', phase, result.metrics, false, null, { sig: sig }); } 1332 // App montado (só chega aqui com falha de ativo ou de chunk que se 1333 // repete): o aviso não bloqueante, e segue olhando. Um overlay por 1334 // cima de um app que ainda pode pintar a rota é pior que o branco. 1335 if (appMounted) { showSlowNotice(); next(); return; } 1336 finished = true; 1337 hideSlowNotice(); 1338 if (typeof window.__removeAppShell === 'function') { try { window.__removeAppShell(); } catch(e) {} } 1339 showRecoveryOverlay(result); 1340 watchPaint(); 1341 } 1342 1343 at(FIRST_CHECK_MS, function() { runCheck(FIRST_CHECK_MS); }); 1344 1345 // Nó com estilo inline e texto via textContent (nada de HTML montado a 1346 // partir de mensagem de erro, UA ou URL). 1347 function node(tag, css, text) { 1348 var el = document.createElement(tag); 1349 if (css) el.style.cssText = css; 1350 if (text !== undefined) el.textContent = text; 1351 return el; 1352 } 1353 1354 // Aviso de boot lento: discreto, não bloqueia a página e não recarrega 1355 // sozinho. Some quando a rota pinta. 1356 function showSlowNotice() { 1357 if (IS_BOT || document.getElementById('pf-slow-notice')) return; 1358 var d = node('div', 'position:fixed;left:50%;bottom:16px;transform:translateX(-50%);z-index:99998;box-sizing:border-box;width:max-content;max-width:calc(100% - 32px);display:flex;align-items:center;gap:12px;padding:10px 10px 10px 16px;background:#0f172a;color:#f8fafc;border-radius:12px;box-shadow:0 8px 24px rgba(15,23,42,.25);font-family:Inter,-apple-system,system-ui,sans-serif;font-size:14px;font-weight:500;line-height:1.4'); 1359 d.id = 'pf-slow-notice'; 1360 d.setAttribute('role', 'status'); 1361 d.setAttribute('aria-live', 'polite'); 1362 d.appendChild(node('span', '', 'A página está demorando mais que o normal.')); 1363 var b = node('button', 'flex:none;padding:8px 14px;background:#fff;color:#0f172a;border:none;border-radius:8px;font-family:inherit;font-size:14px;font-weight:600;cursor:pointer', 'Recarregar'); 1364 b.type = 'button'; 1365 b.onclick = function() { clearReloadCount(); location.reload(); }; 1366 d.appendChild(b); 1367 (document.body || document.documentElement).appendChild(d); 1368 watchPaint(); 1369 } 1370 function hideSlowNotice() { 1371 var d = document.getElementById('pf-slow-notice'); 1372 if (d && d.parentNode) d.parentNode.removeChild(d); 1373 } 1374 function hideRecoveryUi() { 1375 hideSlowNotice(); 1376 var o = document.getElementById('ios-diag-overlay'); 1377 if (o && o.parentNode) o.parentNode.removeChild(o); 1378 } 1379 // Com aviso ou overlay na tela, olha a cada meio segundo e tira os dois 1380 // assim que a rota pinta. A checagem de 3,5 s deixava o aviso por cima 1381 // da página pronta, e depois dos 180 s (ou do overlay) ninguém olhava.
1382 var paintTimer = null; 1383 function watchPaint() { 1384 if (paintTimer) return; 1385 paintTimer = setInterval(function() { 1386 if (checkBlank().isBlank) return; 1387 clearInterval(paintTimer); 1388 paintTimer = null; 1389 hideRecoveryUi(); 1390 }, 500); 1391 } 1392 1393 function showRecoveryOverlay(result) { 1394 if (document.getElementById('ios-diag-overlay')) return; // Already showing 1395 var tried = getReloadCount() > 0; 1396 var d = node('div', 'position:fixed;inset:0;z-index:99999;background:#fafafa;padding:0;font-family:Inter,-apple-system,system-ui,sans-serif;overflow:auto;display:flex;flex-direction:column;align-items:center;justify-content:center'); 1397 d.id = 'ios-diag-overlay'; 1398 var box = node('div', 'max-width:420px;width:100%;padding:32px 24px;text-align:center'); 1399 var icon = node('div', 'width:56px;height:56px;border-radius:16px;background:#fee2e2;display:flex;align-items:center;justify-content:center;margin:0 auto 20px'); 1400 try { 1401 var NS = 'http://www.w3.org/2000/svg'; 1402 var svg = document.createElementNS(NS, 'svg'); 1403 var attrs = { width: '28', height: '28', viewBox: '0 0 24 24', fill: 'none', stroke: '#dc2626', 'stroke-width': '2', 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'aria-hidden': 'true' }; 1404 for (var a in attrs) svg.setAttribute(a, attrs[a]); 1405 var shapes = [['circle', { cx: '12', cy: '12', r: '10' }], ['line', { x1: '12', y1: '8', x2: '12', y2: '12' }], ['line', { x1: '12', y1: '16', x2: '12.01', y2: '16' }]]; 1406 for (var i = 0; i < shapes.length; i++) { 1407 var sh = document.createElementNS(NS, shapes[i][0]); 1408 for (var sa in shapes[i][1]) sh.setAttribute(sa, shapes[i][1][sa]); 1409 svg.appendChild(sh); 1410 } 1411 icon.appendChild(svg); 1412 } catch(e) {} 1413 box.appendChild(icon); 1414 box.appendChild(node('h2', 'font-size:20px;font-weight:700;color:#0f172a;margin:0 0 8px;letter-spacing:-0.02em', 'Algo deu errado')); 1415 box.appendChild(node('p', 'font-size:14px;color:#64748b;margin:0 0 24px;line-height:1.5', tried 1416 ? 'A página não carregou corretamente. Tentamos recarregar automaticamente, mas o problema persiste.' 1417 : 'A página não carregou corretamente. Recarregue ou volte para a página inicial.')); 1418 var reload = node('button', 'width:100%;padding:14px;background:#0f172a;color:#fff;border:none;border-radius:12px;font-size:15px;font-weight:600;cursor:pointer;letter-spacing:-0.01em', 'Recarregar Página'); 1419 reload.type = 'button'; 1420 reload.onclick = function() { window.__pfNuclearReload(); }; 1421 reload.onmouseover = function() { reload.style.background = '#1e293b'; }; 1422 reload.onmouseout = function() { reload.style.background = '#0f172a'; }; 1423 box.appendChild(reload); 1424 var home = node('button', 'width:100%;padding:12px;background:transparent;color:#64748b;border:1px solid #e2e8f0;border-radius:12px;font-size:14px;font-weight:500;cursor:pointer;margin-top:10px', 'Ir para a página inicial'); 1425 home.type = 'button'; 1426 home.onclick = function() { location.href = '/'; }; 1427 home.onmouseover = function() { home.style.borderColor = '#94a3b8'; home.style.color = '#334155'; }; 1428 home.onmouseout = function() { home.style.borderColor = '#e2e8f0'; home.style.color = '#64748b'; }; 1429 box.appendChild(home); 1430 // Collapsible technical details (dev-friendly, user-hidden) 1431 var det = node('details', 'margin-top:24px;text-align:left;width:100%'); 1432 det.appendChild(node('summary', 'font-size:12px;color:#94a3b8;cursor:pointer;font-weight:500;user-select:none', 'Informações técnicas')); 1433 det.appendChild(node('pre', 'background:#0f172a;color:#4ade80;padding:14px;border-radius:10px;font-size:10px;overflow:auto;white-space:pre-wrap;word-break:break-all;max-height:200px;margin-top:8px;line-height:1.5', 1434 result.info 1435 + '\n\nErros (' + errors.length + '):\n' + (errors.length ? errors.join('\n') : '(nenhum)') 1436 + '\n\nUA: ' + navigator.userAgent 1437 + '\n\nURL: ' + location.origin + safePath(location.pathname + location.search) 1438 + '\n\nTentativas: ' + getReloadCount())); 1439 box.appendChild(det); 1440 d.appendChild(box); 1441 document.body.appendChild(d); 1442 } 1443 })(); 1444 </script>
1444 1445
1445<script> 1446 // ââ Edge skeleton (#app-shell) removal ââ 1447 // The edge (seo-meta-injection.ts) injects a full above-the-fold skeleton as 1448 // a SIBLING overlay of #root for portal homes. Remove it the instant React 1449 // paints REAL page content into #root: a <main>, <article> or <h1>, or the 1450 // explicit window.__removeAppShell() (MainLayout.tsx e edgeArticleShell.ts). 1451 // NOT on #root.firstElementChild: App renders an empty <div class="min-h- 1452 // screen bg-white"> wrapper on React's very first commit, long before the 1453 // lazy PortalApp->PortalHome chunks paint. And NOT on text length: o 1454 // GlobalAudioPlayer e o PortalCookieBanner, montados fora da rota, injetam 1455 // <style> no #root (749 caracteres de @keyframes), e o Toaster do sonner 1456 // monta uma <section>. Com "texto > 30", o esqueleto saÃa aos ~1 s e o 1457 // leitor via 2 a 3 s de branco até a rota pintar (medido em 29/09/2026, 1458 // 5 de 5 cargas da home do JB; até 10 s no celular lento). 1459 (function () { 1460 var root = document.getElementById('root'); 1461 var shell = document.getElementById('app-shell'); 1462 var removed = false; 1463 var mo = null; 1464 var iv = null; 1465 // Shell de MATÃRIA: guarda a referência para a página de matéria mostrar 1466 // esse conteúdo se a consulta dela falhar depois de o shell ter saÃdo 1467 // (src/features/news/edgeArticleShell.ts). Só guarda; a remoção é a mesma. 1468 if (shell && shell.getAttribute('data-shell') === 'article') { 1469 window.__EDGE_ARTICLE_SHELL = { node: shell, path: location.pathname }; 1470 } 1471 function hasRealContent() { 1472 return !!root && !!root.querySelector('main, article, h1'); 1473 } 1474 function removeShell() { 1475 if (removed) return; 1476 removed = true; 1477 window.__APP_SHELL_REMOVED = true; // watchdog readiness flag 1478 if (mo) mo.disconnect(); 1479 if (iv) clearInterval(iv); 1480 var s = document.getElementById('app-shell'); 1481 if (!s) return; 1482 s.style.transition = 'opacity .25s ease'; 1483 s.style.opacity = '0'; 1484 setTimeout(function () { if (s.parentNode) s.parentNode.removeChild(s); }, 280); 1485 } 1486 // Secondary trigger: MainLayout's mount effect calls this after real content. 1487 window.__removeAppShell = removeShell; 1488 if (shell && root) { 1489 if (hasRealContent()) { 1490 removeShell(); 1491 } else if ('MutationObserver' in window) { 1492 mo = new MutationObserver(function () { 1493 if (hasRealContent()) removeShell(); 1494 }); 1495 mo.observe(root, { childList: true, subtree: true }); 1496 } else { 1497 iv = setInterval(function () { 1498 if (hasRealContent()) removeShell(); 1499 }, 80); 1500 } 1501 // Rede de segurança, aos 25 s (era 8 s incondicional, e aos 8 s uma 1502 // página só lenta perdia o esqueleto e mostrava branco). Cobre a rota 1503 // que pinta sem <main>/<article>/<h1>: com qualquer conteúdo visÃvel no 1504 // #root, o esqueleto sai. Com o #root ainda em branco, fica, e o 1505 // detector de tela branca decide: boot morto recarrega ou vai para o 1506 // overlay (que tira o esqueleto); boot só lento mostra o aviso com 1507 // "Recarregar" por cima do esqueleto. Sem o detector, sai aos 25 s. 1508 var safety = function () { 1509 if (removed) return; 1510 var isBlank = window.__pfRootIsBlank; 1511 if (typeof isBlank === 'function') { 1512 var blank = true; 1513 try { blank = isBlank() === true; } catch (e) { blank = false; } 1514 if (blank) { setTimeout(safety, 4000); return; } 1515 } 1516 removeShell(); 1517 }; 1518 setTimeout(safety, 25000); 1519 } else { 1520 // No skeleton on this page (dev / non-portal / logged-in): mark ready, 1521 // como antes (o detector V4 não depende mais desta flag). 1522 window.__APP_SHELL_REMOVED = true; 1523 } 1524 })(); 1525 </script>
1525 1526
1526<script> 1527 (function() { 1528 var h = location.hostname; 1529 var p = location.pathname; 1530 var isAdminPage = h.startsWith('admin.') || h.startsWith('console.') || p.startsWith('/admin') || p.startsWith('/console'); 1531 if (isAdminPage) { 1532 var preloaderContainer = document.getElementById('global-admin-preloader-container'); 1533 if (preloaderContainer) { 1534 preloaderContainer.innerHTML = '<style>.uni-loader{position:fixed;top:0;left:0;width:100%;height:100%;z-index:99999;background:#fafafa;transition:opacity .4s ease}.uni-loader-bar{position:absolute;top:0;left:0;width:100%;height:2px;overflow:hidden}.uni-loader-bar::after{content:"";position:absolute;top:0;left:-40%;width:40%;height:100%;background:linear-gradient(90deg,transparent,rgba(0,0,0,.12),transparent);animation:uni-shimmer 1.4s ease-in-out infinite}@keyframes uni-shimmer{0%{left:-40%}100%{left:100%}}@media(prefers-color-scheme:dark){.uni-loader{background:#0a0a0a}.uni-loader-bar::after{background:linear-gradient(90deg,transparent,rgba(255,255,255,.08),transparent)}}</style><div id="global-admin-preloader" class="uni-loader"><div class="uni-loader-bar"></div></div>'; 1535 } 1536 } 1537 })(); 1538 </script>
1538 1539 <!-- WebMCP: Expose site tools to AI agents via browser --> 1540
1540<script> 1541 (function() { 1542 // WebMCP API â navigator.modelContext.provideContext() 1543 // Spec: https://webmachinelearning.github.io/webmcp/ 1544 // No-op if browser doesn't support it (progressive enhancement) 1545 if (typeof navigator !== 'undefined' && 'modelContext' in navigator && navigator.modelContext && typeof navigator.modelContext.provideContext === 'function') { 1546 try { 1547 navigator.modelContext.provideContext({ 1548 tools: [ 1549 { 1550 name: 'search_articles', 1551 description: 'Search published news articles on this portal. Returns headlines, summaries, categories, and direct URLs.', 1552 inputSchema: { 1553 type: 'object', 1554 properties: { 1555 query: { type: 'string', description: 'Search query text' }, 1556 category: { type: 'string', description: 'Category filter (politica, educacao, saude, seguranca, cultura, esportes, economia, transporte, meio-ambiente)' }, 1557 limit: { type: 'number', description: 'Maximum results to return (default: 10)' } 1558 }, 1559 required: ['query'] 1560 } 1561 }, 1562 { 1563 name: 'get_portal_info', 1564 description: 'Get structured information about the current portal including editorial policies, coverage areas, contact details, and citation format.', 1565 inputSchema: { 1566 type: 'object', 1567 properties: { 1568 format: { type: 'string', enum: ['full', 'summary'], description: 'Response detail level' } 1569 } 1570 } 1571 }, 1572 { 1573 name: 'get_latest_news', 1574 description: 'Get the most recent published news articles from this portal with headlines, summaries, and publication timestamps.', 1575 inputSchema: { 1576 type: 'object', 1577 properties: { 1578 count: { type: 'number', description: 'Number of articles to return (default: 5, max: 20)' }, 1579 category: { type: 'string', description: 'Optional category filter' } 1580 } 1581 } 1582 }, 1583 { 1584 name: 'get_weather', 1585 description: 'Get current weather conditions and forecast for the local region.', 1586 inputSchema: { 1587 type: 'object', 1588 properties: { 1589 days: { type: 'number', description: 'Forecast days (1-7, default: 1)' } 1590 } 1591 } 1592 } 1593 ] 1594 }); 1595 } catch(e) { 1596 // Silently fail â WebMCP not supported 1597 } 1598 } 1599 })(); 1600 </script>
1600 1601</body> 1602 1603</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.