1/** 2 * Catellix Detector â catálogo (/api/detector/catalog) + resumo (/api/detector/summary) + modal com histórico. 3 */ 4(function () { 5 /** WiâFi MAP (e outras páginas leves): só inicia o pack de anúncios, sem o resto do Detector. */ 6 var isMapaAdSlotsPage = 7 document.documentElement && document.documentElement.getAttribute('data-catellix-mapa-ad-slots') === '1'; 8 9 var state = { 10 view: 'list', 11 filter: 'all', 12 /** Filtro DNS: all | v4 | dual | v6 */ 13 ipStackFilter: 'all', 14 rankSort: 'live_problems', 15 catalog: null, 16 summary: null, 17 chart: null, 18 /** Janela do gráfico no modal (horas); alinhada com GET .../latency?hours= */ 19 chartRangeHours: 168, 20 modalSvcId: null, 21 modalDetailData: null, 22 probeIntervalSec: 60, 23 communityWindowDays: 7, 24 /** Janela do ranking comunidade: 1/7/15/30 dias civis desde 0h (BrasÃlia); padrão 1 dia. */ 25 complaintsRankingDays: 1, 26 communityDisclaimerPt: '', 27 crSvcId: null, 28 /** ID do comentário público a que se responde (um nÃvel); null = relato normal. */ 29 crParentReportId: null, 30 crVerifyToken: null, 31 communityFeedFilter: 'all', 32 communityFeedReplyParentId: null, 33 communityFeedVerifyToken: null, 34 communityFeedAnonToken: null, 35 rankExpanded: { 36 latFast: false, 37 latSlow: false, 38 incidents: false, 39 trends: false, 40 games: false, 41 community: false, 42 }, 43 /** Lista completa do ranking comunidade (após último GET). undefined = ainda não carregado; null = erro. */ 44 communityRankingRows: undefined, 45 /** Posição anterior por service_id (1-based) para setas ââ no ranking comunidade. */ 46 communityRankPrev: {}, 47 /** Snapshot antes do último `communityRankPrev` (mobile + sidebar usam nas setas). */ 48 _complaintsRankTrendPrev: {}, 49 /** Motivos de relato (/api/detector/report-reasons). */ 50 reportReasons: [], 51 /** Google OAuth client id (relatos com verificação Google). */ 52 googleClientId: '', 53 /** Rankings: 'bad' = incidentes / latência alta / tendências;
53 'good' = latência baixa + jogos (ordenados OK primeiro). */ 54 rankMood: 'bad', 55 /** eco de cat.probe_profile (ICMP paralelo, POPs, etc.) */ 56 probeProfile: {}, 57 /** Limites e tiers (eco de /api/detector/catalog latency_class_config) */ 58 latencyClassConfig: {}, 59 /** Filtro de texto no catálogo (nome, id, descrição, domÃnio) */ 60 catalogSearch: '', 61 /** true enquanto loadAll() está em curso (contagem de auto-refresh em pausa). */ 62 detectorLoadInProgress: false, 63 /** Atualização automática da página (ms); padrão 1 min */ 64 refreshIntervalMs: 60000, 65 /** Ãltima janela (dias) para a qual `communityRankingRows` foi obtida do servidor; evita GET duplicado após prefetch paralelo. */ 66 _fetchedCommunityRankingDays: null, 67 /** Limite de linhas pedido na última resposta de community-ranking (alinhado a RANK_FULL_MAX). */ 68 _fetchedCommunityRankingLimit: null, 69 /** 70 * Mapa canónico logo_domain â URL (GET /api/detector/logo-manifest). 71 * Prioriza o mesmo Ãcone correcto no PDF e no cartão social. 72 */ 73 logoManifestByDomain: null, 74 _logoManifestPromise: null, 75 /** Eco de cat.ad_slots { total, available } para UI (faixa + modal). */ 76 detectorAdSlots: null, 77 /** Tamanho do conjunto de anúncios em destaque (API local-ad/next). */ 78 localAdPoolSize: 0, 79 /** Slots da faixa com criativo fixo (catálogo â local_ads_carousel). */ 80 detectorCarouselSlotAds: [], 81 _adZoneDismissedL: false, 82 _adZoneDismissedR: false, 83 _adZoneSupportUiL: false, 84 _adZoneSupportUiR: false, 85 _topBarDismissed: false, 86 }; 87 88 /** Tradução UI (PT padrão / EN) â chaves em `detector-i18n.js`. */ 89 function __(k) { 90 if (typeof window.detectorT === 'function') return window.detectorT(k); 91 return k; 92 } 93 94 /** Carrega a base canónica de logos (espelho de LOGO_URL_BY_DOMAIN no servidor). */ 95 function fetchDetectorLogoManifest() { 96 if (state._logoManifestPromise) return state._logoManifestPromise; 97 var origin = ''; 98 try { 99 origin = window.location.origin || ''; 100 } catch (eO) {} 101 var manifestUrl = origin + '/api/detector/logo-manifest'; 102 var manifestFetch = fetch(manifestUrl, { credentials: 'omit' }); 103 var manifestDeadline = new Promise(function (_, reject) { 104 setTimeout(function () { 105 reject(new Error('manifest-timeout')); 106 }, DET_T10_SOCIAL_MANIFEST_MS); 107 }); 108 state._logoManifestPromise = Promise.race([manifestFetch, manifestDeadline]) 109 .then(function (r) { 110 if (!r || !r.ok) throw new Error('manifest-http'); 111 return r.json(); 112 }) 113 .then(function (j) { 114 if (j && j.success && j.by_domain && typeof j.by_domain === 'object') { 115 state.logoManifestByDomain = j.by_domain; 116 } else { 117 state.logoManifestByDomain = {}; 118 } 119 return state.logoManifestByDomain; 120 }) 121 .catch(function () { 122 state.logoManifestByDomain = {}; 123 return state.logoManifestByDomain; 124 }); 125 return state._logoManifestPromise; 126 } 127 128 function catLabel(cat) { 129 var id = cat || 'core'; 130 var k = 'cat_' + id; 131 var s = __(k); 132 return s !== k ? s : id; 133 } 134 135 function categoryBlurb(cat) { 136 var id = cat || 'core'; 137 var k = 'blurb_' + id; 138 var s = __(k); 139 return s !== k ? s : ''; 140 } 141 142 function detectorCommunityAnonToken() { 143 if (state.communityFeedAnonToken) return state.communityFeedAnonToken; 144 var key = 'detector_public_anon_token_v1'; 145 try { 146 var cur = localStorage.getItem(key); 147 if (cur && cur.length >= 12) { 148 state.communityFeedAnonToken = cur; 149 return cur; 150 } 151 } catch (e0) {} 152 var made = 'anon_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 12); 153 state.communityFeedAnonToken = made; 154 try { 155 localStorage.setItem(key, made); 156 } catch (e1) {} 157 return made; 158 } 159 160 function i18nSortLocale() { 161 return typeof window.detectorGetLocale === 'function' && window.detectorGetLocale() === 'en' 162 ? 'en' 163 : 'pt'; 164 } 165 166 var DETECTOR_REFRESH_MS_KEY = 'detector_refresh_interval_ms'; 167 var COMPLAINTS_RANKING_DAYS_KEY = 'detector_complaints_ranking_days'; 168 var DETECTOR_THEME_KEY = 'detector_theme';
169 /** Stale-while-revalidate: mostrar último catálogo da sessão enquanto /catalog refetch (TTL de exibição). */ 170 var DETECTOR_CATALOG_SWR_KEY = 'detector_catalog_swr_v2'; 171 var CATALOG_SWR_MAX_DISPLAY_AGE_MS = 2 * 60 * 60 * 1000; 172 /** Ranking comunidade (localStorage): mostrar último snapshot à entrada; GET atualiza sem skeleton. */ 173 var COMMUNITY_RANKING_SWR_KEY = 'detector_community_ranking_swr_v1'; 174 var COMMUNITY_RANKING_SWR_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; 175 var detReportPickerEscBound = false; 176 var detComplaintsHourlyEscBound = false; 177 var detComplaintsHourlyFetchGen = 0; 178 /** Ãltimo relatório aberto no modal (top10 | top20 | top30) â partilha/PDF coerentes. */ 179 var detReportModalScope = 'top30'; 180 /** Logo Catellix para o cartão PNG do Top 10 (carregado uma vez). */ 181 var detTop10SocialBrandLogoImg = null; 182 var detTop10SocialBrandLogoPromise = null; 183 /** Timeouts do cartão social Top 10 â evita modal preso em rede lenta ou fetch pendente. */ 184 /** Manifest: rede lenta não deve abortar antes da resposta (antes: 8s cortava e esvaziava o mapa de logos). */ 185 var DET_T10_SOCIAL_MANIFEST_MS = 22000; 186 /** Cada URL de Ãcone (via proxy); várias tentativas em cadeia precisam caber em ROW_LOGO_MS. */ 187 var DET_T10_SOCIAL_ICON_FETCH_MS = 4000; 188 /** Uma linha do Top 10: DOM + várias URLs; 8s esgotava antes do proxy tentar Wikimedia/favicons. */ 189 var DET_T10_SOCIAL_ROW_LOGO_MS = 52000; 190 var DET_T10_SOCIAL_BRAND_IMG_MS = 8000; 191 192 /* ââ Logo Blob Cache (IndexedDB) âââââââââââââââââââââââââââââââââââââ 193 Persiste blobs de logos entre sessões para que o cartão social abra 194 instantaneamente na segunda vez. TTL de 48h por entrada. */ 195 var _logoCacheDB = null; 196 var _logoCacheDBReady = false; 197 var _LOGO_CACHE_DB = 'det_logo_cache_v1'; 198 var _LOGO_CACHE_STORE = 'blobs'; 199 var _LOGO_CACHE_TTL = 48 * 60 * 60 * 1000; 200 201 function _openLogoCacheDB() { 202 if (_logoCacheDB) return Promise.resolve(_logoCacheDB); 203 if (typeof indexedDB === 'undefined') return Promise.reject(); 204 return new Promise(function (resolve, reject) { 205 try { 206 var req = indexedDB.open(_LOGO_CACHE_DB, 1); 207 req.onupgradeneeded = function (e) { 208 var db = e.target.result; 209 if (!db.objectStoreNames.contains(_LOGO_CACHE_STORE)) { 210 db.createObjectStore(_LOGO_CACHE_STORE, { keyPath: 'url' }); 211 } 212 }; 213 req.onsuccess = function (e) { 214 _logoCacheDB = e.target.result; 215 _logoCacheDBReady = true; 216 resolve(_logoCacheDB); 217 }; 218 req.onerror = reject; 219 } catch (ex) { reject(ex); } 220 }); 221 } 222 223 function logoCacheGet(url) { 224 return _openLogoCacheDB().then(function (db) { 225 return new Promise(function (resolve, reject) { 226 try { 227 var tx = db.transaction(_LOGO_CACHE_STORE, 'readonly'); 228 var req = tx.objectStore(_LOGO_CACHE_STORE).get(url); 229 req.onsuccess = function () { 230 var row = req.result; 231 if (!row || !row.blob) return resolve(null); 232 if (Date.now() - (row.ts || 0) > _LOGO_CACHE_TTL) return resolve(null); 233 resolve(row.blob); 234 }; 235 req.onerror = function () { resolve(null); }; 236 } catch (ex) { resolve(null); } 237 }); 238 }).catch(function () { return null; }); 239 } 240 241 function logoCachePut(url, blob) { 242 _openLogoCacheDB().then(function (db) { 243 try { 244 var tx = db.transaction(_LOGO_CACHE_STORE, 'readwrite'); 245 tx.objectStore(_LOGO_CACHE_STORE).put({ url: url, blob: blob, ts: Date.now() }); 246 } catch (ex) {} 247 }).catch(function () {}); 248 } 249 250 function blobToImage(blob) { 251 return new Promise(function (resolve, reject) { 252 var ou = URL.createObjectURL(blob); 253 var im = new Image(); 254 im.onload = function () { URL.revokeObjectURL(ou);
254 im.naturalWidth > 0 ? resolve(im) : reject(); }; 255 im.onerror = function () { URL.revokeObjectURL(ou); reject(); }; 256 im.src = ou; 257 }); 258 } 259 260 function getDetTheme() { 261 var a = document.documentElement.getAttribute('data-det-theme'); 262 return a === 'dark' ? 'dark' : 'light'; 263 } 264 265 function setDetTheme(theme) { 266 if (theme !== 'dark' && theme !== 'light') theme = 'light'; 267 document.documentElement.setAttribute('data-det-theme', theme); 268 try { 269 localStorage.setItem(DETECTOR_THEME_KEY, theme); 270 } catch (e) { 271 /* ignore */ 272 } 273 updateDetThemeToggleUi(); 274 refreshDetectorChartsAfterThemeChange(); 275 } 276 277 /** Recria o gráfico do modal de serviço para aplicar cores do tema (Chart.js). */ 278 function refreshDetectorChartsAfterThemeChange() { 279 try { 280 if (state.chart && state.modalSvcId) { 281 reloadModalChartFromControls(); 282 } 283 } catch (e) { 284 /* ignore */ 285 } 286 } 287 288 function updateDetThemeToggleUi() { 289 var btn = document.getElementById('detThemeToggle'); 290 if (!btn) return; 291 var dark = getDetTheme() === 'dark'; 292 btn.setAttribute('aria-pressed', dark ? 'true' : 'false'); 293 btn.textContent = dark ? 'â' : 'ð'; 294 var goDark = __('theme_goto_dark'); 295 var goLight = __('theme_goto_light'); 296 btn.setAttribute('title', dark ? goLight : goDark); 297 btn.setAttribute('aria-label', dark ? goLight : goDark); 298 } 299 300 function wireDetThemeToggle() { 301 var btn = document.getElementById('detThemeToggle'); 302 if (!btn || btn.dataset.detectorThemeWired === '1') return; 303 btn.dataset.detectorThemeWired = '1'; 304 btn.addEventListener('click', function () { 305 setDetTheme(getDetTheme() === 'dark' ? 'light' : 'dark'); 306 }); 307 updateDetThemeToggleUi(); 308 } 309 310 /** Em viewports estreitas: Top 10 comunidade no hero, tendências inline, quatro rankings (top10Boards) antes do catálogo; painel «Rankings rápidos» do details fica só no desktop. */ 311 var DET_COMPLAINTS_VIEWPORT_MQ = window.matchMedia('(max-width: 1023px)'); 312 var _detTop10CardsViewportNarrow = DET_COMPLAINTS_VIEWPORT_MQ.matches; 313 314 /* ââ Lazy-load por IntersectionObserver ââ 315 * Seções pesadas só renderizam quando perto do viewport. 316 * Se os dados chegam mas a seção não está visÃvel, marcamos "dirty". 317 * Quando o observer dispara, renderiza se dirty. 318 */ 319 var _lazyRegistry = {}; 320 var _hasIO = typeof IntersectionObserver === 'function'; 321 function registerLazySection(hostId, renderFn) { 322 if (_lazyRegistry[hostId]) return; 323 var entry = { dirty: false, rendered: false, fn: renderFn, observer: null }; 324 _lazyRegistry[hostId] = entry; 325 if (!_hasIO) return; 326 var el = document.getElementById(hostId); 327 if (!el) return; 328 entry.observer = new IntersectionObserver(function (entries) { 329 for (var i = 0; i < entries.length; i++) { 330 if (entries[i].isIntersecting && entry.dirty) { 331 entry.dirty = false; 332 entry.rendered = true; 333 try { entry.fn(); } catch (e) { console.warn('[det-lazy]', hostId, e); } 334 } 335 } 336 }, { rootMargin: '400px 0px' }); 337 entry.observer.observe(el); 338 } 339 function markLazySectionDirty(hostId) { 340 var entry = _lazyRegistry[hostId]; 341 if (!entry) return false; 342 entry.dirty = true; 343 if (!_hasIO) { try { entry.fn(); } catch (e) {} return true; } 344 var el = document.getElementById(hostId); 345 if (el) { 346 var rect = el.getBoundingClientRect(); 347 var near = rect.top < (window.innerHeight + 400); 348 if (near) { 349 entry.dirty = false; 350 entry.rendered = true; 351 try { entry.fn(); } catch (e) { console.warn('[det-lazy]', hostId, e); } 352 } 353 } 354 return true; 355 } 356 357 /** Antigo: movia a secção inteira para dentro de Rankings. Agora o Top 10 mobile está só no hero. */ 358 function relocateDetComplaintsSectionForViewport() {} 359 360 function relocateComplaintsToolbarForViewport() { 361 var tb = document.getElementById('detSidebarComplaintsToolbar'); 362 var host = document.getElementById('detMobileComplaintsToolbarHost'); 363 var sec = document.getElementById('detSidebarComplaintsSection'); 364 if (!tb || !host || !sec) return; 365 try { 366 if (DET_COMPLAINTS_VIEWPORT_MQ.matches) { 367 if (tb.parentElement !== host) { 368 host.appendChild(tb); 369 host.removeAttribute('hidden'); 370 } 371 } else { 372 var head = sec.querySelector('.det-sidebar-section-head'); 373 if (tb.parentElement === host && head) { 374 if (head.nextSibling) { 375 sec.insertBefore(tb, head.nextSibling); 376 } else { 377 sec.appendChild(tb); 378 } 379 } 380 host.setAttribute('hidden', ''); 381 } 382 } catch (e) { 383 /* ignore */ 384 } 385 } 386 387 function relocateRankPanelsForViewport() { 388 var wrap = document.getElementById('rankSectionWrap'); 389 var mount = document.getElementById('detMobileRankPanelsMount'); 390 var detailsBody = document.querySelector('#detDashRank .det-dash-body'); 391 if (!wrap || !detailsBody) return; 392 try { 393 if (DET_COMPLAINTS_VIEWPORT_MQ.matches) { 394 if (mount && wrap.parentElement === mount) { 395 detailsBody.insertBefore(wrap, detailsBody.firstChild); 396 } 397 return; 398 } 399 if (wrap.parentElement !== detailsBody) { 400 detailsBody.insertBefore(wrap, detailsBody.firstChild); 401 } 402 } catch (e2) { 403 /* ignore */ 404 } 405 } 406 407 function relocateCatalogSearchForViewport() { 408 var bar = document.getElementById('catalogSearchBar'); 409 var head = document.querySelector('.block-head--catalog'); 410 var shelfSlot = document.getElementById('catalogSearchShelfSlot'); 411 if (!bar || !head) return; 412 try { 413 if (DET_COMPLAINTS_VIEWPORT_MQ.matches) { 414 if (bar.parentElement !== head) head.appendChild(bar); 415 } else if (shelfSlot) { 416 if (bar.parentElement !== shelfSlot) shelfSlot.appendChild(bar); 417 } else if (bar.parentElement !== head) { 418 head.appendChild(bar); 419 } 420 } catch (e3) { 421 /* ignore */ 422 } 423 } 424 425 function relocateTop10BoardsForViewport() { 426 var boards = document.getElementById('top10Boards'); 427 var slot = document.getElementById('detMobileTopFiveSlot'); 428 var shelves = document.getElementById('secShelves'); 429 var cards = document.getElementById('top10Cards'); 430 if (!boards || !shelves || !cards) return; 431 try { 432 if (DET_COMPLAINTS_VIEWPORT_MQ.matches && slot) { 433 if (boards.parentElement !== slot) { 434 slot.appendChild(boards); 435 } 436 } else { 437 if (boards.parentElement !== shelves) { 438 shelves.insertBefore(boards, cards); 439 } else if (boards.nextElementSibling !== cards) { 440 shelves.insertBefore(boards, cards); 441 } 442 } 443 } catch (eB) { 444 /* ignore */ 445 } 446 } 447 448 function syncDetectorMobileFlowLayout() { 449 relocateComplaintsToolbarForViewport(); 450 relocateRankPanelsForViewport(); 451 relocateCatalogSearchForViewport(); 452 relocateTop10BoardsForViewport(); 453 placeRefreshToolbarForViewport(); 454 try { 455 initDetectorTopBarLocalSpin(); 456 } catch (eSpinVp) { 457 /* ignore */ 458 } 459 try { 460 mountMobileGamesBoardClone(); 461 } catch (eMg) { 462 /* ignore */ 463 } 464 } 465 466 function wireDetComplaintsViewportRelocate() { 467 if (!document.getElementById('detSidebarComplaintsSection')) return; 468 syncDetectorMobileFlowLayout(); 469 if (typeof DET_COMPLAINTS_VIEWPORT_MQ.addEventListener === 'function') { 470 DET_COMPLAINTS_VIEWPORT_MQ.addEventListener('change', syncDetectorMobileFlowLayout); 471 } else if (typeof DET_COMPLAINTS_VIEWPORT_MQ.addListener === 'function') { 472 DET_COMPLAINTS_VIEWPORT_MQ.addListener(syncDetectorMobileFlowLayout); 473 } 474 window.addEventListener('resize', syncDetectorMobileFlowLayout, { passive: true }); 475 } 476 477 /** Em â¤1023px, coloca intervalo + â» acima da barra de carregamento e do Top 10 comunidade. */ 478 function placeRefreshToolbarForViewport() { 479 var slot = document.getElementById('detRefreshToolbarSlot'); 480 var tb = document.getElementById('toolbarCompact'); 481 var hero = document.getElementById('detMobileCommunityHero'); 482 var loading = document.getElementById('detectorLoading'); 483 if (!slot || !tb || !hero) return; 484 try { 485 if (DET_COMPLAINTS_VIEWPORT_MQ.matches) { 486 var anchor = 487 loading && loading.parentNode === hero.parentNode ? loading : hero; 488 if (slot.nextElementSibling !== anchor) { 489 anchor.parentNode.insertBefore(slot, anchor); 490 } 491 } else if (slot.parentNode !== tb || slot !== tb.lastElementChild) { 492 tb.appendChild(slot); 493 } 494 } catch (e) { 495 /* ignore */ 496 } 497 } 498 499 function wireDetRefreshToolbarPlacement() { 500 if (!document.getElementById('detRefreshToolbarSlot')) return; 501 placeRefreshToolbarForViewport(); 502 if (typeof DET_COMPLAINTS_VIEWPORT_MQ.addEventListener === 'function') { 503 DET_COMPLAINTS_VIEWPORT_MQ.addEventListener('change', placeRefreshToolbarForViewport); 504 } else if (typeof DET_COMPLAINTS_VIEWPORT_MQ.addListener === 'function') { 505 DET_COMPLAINTS_VIEWPORT_MQ.addListener(placeRefreshToolbarForViewport); 506 } 507 window.addEventListener('resize', placeRefreshToolbarForViewport, { passive: true }); 508 } 509 510 window.detectorRefreshThemeToggleI18n = updateDetThemeToggleUi; 511 512 var VALID_RANKING_DAYS = [1, 7, 15, 30]; 513 /** Linhas pedidas ao GET community-ranking (Top 10/30 embed + painel até «ranking completo»). */ 514 var COMMUNITY_RANKING_API_LIMIT = 60; 515 function communityRankingApiQuery(days) { 516 var d = VALID_RANKING_DAYS.indexOf(days) !== -1 ? days : 1; 517 return ( 518 'days=' + 519 encodeURIComponent(String(d)) + 520 '&limit=' + 521 encodeURIComponent(String(COMMUNITY_RANKING_API_LIMIT)) 522 ); 523 } 524 525 function readCommunityRankingSwrCache(days, limit) { 526 try { 527 var raw = localStorage.getItem(COMMUNITY_RANKING_SWR_KEY); 528 if (!raw) return null; 529 var o = JSON.parse(raw);
530 if (!o || o.v !== 1 || typeof o.t !== 'number' || !o.payload) return null; 531 if (Date.now() - o.t > COMMUNITY_RANKING_SWR_MAX_AGE_MS) return null; 532 var p = o.payload; 533 if (!p || p.success !== true || !Array.isArray(p.ranking)) return null; 534 if (p.days !== days || p.limit !== limit) return null; 535 return p; 536 } catch (e) { 537 return null; 538 } 539 } 540 541 function writeCommunityRankingSwrCache(data) { 542 if (!data || data.success !== true || !Array.isArray(data.ranking)) return; 543 var d = data.days; 544 var lim = data.limit; 545 if (typeof d !== 'number' || typeof lim !== 'number') return; 546 try { 547 localStorage.setItem( 548 COMMUNITY_RANKING_SWR_KEY, 549 JSON.stringify({ 550 v: 1, 551 t: Date.now(), 552 payload: { 553 success: true, 554 days: d, 555 limit: lim, 556 ranking: data.ranking, 557 disclaimer_pt: data.disclaimer_pt != null ? String(data.disclaimer_pt) : '', 558 next_reset_utc: data.next_reset_utc != null ? String(data.next_reset_utc) : '', 559 window_timezone: data.window_timezone, 560 hours_window: data.hours_window, 561 }, 562 }) 563 ); 564 } catch (e) { 565 /* quota */ 566 } 567 } 568 569 /** Aplica último ranking da storage para pintura imediata (antes do catálogo). Retorna true se havia dados. */ 570 function hydrateCommunityRankingFromSwrCache(days) { 571 var d = VALID_RANKING_DAYS.indexOf(days) !== -1 ? days : 1; 572 var disk = readCommunityRankingSwrCache(d, COMMUNITY_RANKING_API_LIMIT); 573 if (!disk) return false; 574 state.communityRankingRows = disk.ranking; 575 state._fetchedCommunityRankingDays = d; 576 state._fetchedCommunityRankingLimit = COMMUNITY_RANKING_API_LIMIT; 577 if (disk.disclaimer_pt) state.communityDisclaimerPt = disk.disclaimer_pt; 578 if (disk.next_reset_utc) scheduleCommunityRankingMidnightRefresh(disk.next_reset_utc); 579 var discEl = document.getElementById('rankCommunityDisclaimer'); 580 if (discEl && disk.disclaimer_pt) discEl.textContent = disk.disclaimer_pt; 581 return true; 582 } 583 584 function readComplaintsRankingDays() { 585 try { 586 var x = parseInt(localStorage.getItem(COMPLAINTS_RANKING_DAYS_KEY), 10); 587 if (VALID_RANKING_DAYS.indexOf(x) !== -1) return x; 588 } catch (e) { 589 /* ignore */ 590 } 591 return 1; 592 } 593 594 function persistComplaintsRankingDays(d) { 595 try { 596 localStorage.setItem(COMPLAINTS_RANKING_DAYS_KEY, String(d)); 597 } catch (e) { 598 /* ignore */ 599 } 600 } 601 602 function updateHeroTagline() { 603 var el = document.getElementById('heroTagline'); 604 if (!el) return; 605 var days = VALID_RANKING_DAYS.indexOf(state.complaintsRankingDays) !== -1 ? state.complaintsRankingDays : 1; 606 var periodMap = { 607 1: __('js_comm_period_24h'), 608 7: __('js_comm_period_7d'), 609 15: __('js_comm_period_15d'), 610 30: __('js_comm_period_30d'), 611 }; 612 var p = periodMap[days] || ''; 613 el.textContent = __('hero_tagline_tpl').replace(/\{p\}/g, p); 614 } 615 616 function applyCommunityRankingToCatalog(rows) { 617 if (!state.catalog || !state.catalog.length || !Array.isArray(rows)) return; 618 var map = {}; 619 for (var i = 0; i < rows.length; i++) { 620 var r = rows[i]; 621 if (r && r.service_id) map[String(r.service_id)] = r; 622 } 623 for (var j = 0; j < state.catalog.length; j++) { 624 var s = state.catalog[j]; 625 var row = map[String(s.id)]; 626 if (!row) continue; 627 var vc = Number(row.vote_count) || 0; 628 s.community_vote_count = vc; 629 var vuRaw = row.vote_count_verified; 630 var vsRaw = row.vote_count_signals; 631 var vu = vuRaw != null && vuRaw !== '' && !isNaN(Number(vuRaw)) ? Number(vuRaw) : vc; 632 var vs = vsRaw != null && vsRaw !== '' && !isNaN(Number(vsRaw)) ? Number(vsRaw) : 0; 633 s.community_vote_count_verified = vu; 634 s.community_vote_count_signals = vs; 635 var vah = row.vote_avg_per_hour; 636 if (vah != null && vah !== '' && !isNaN(Number(vah))) { 637 s.community_vote_avg_per_hour = Number(vah); 638 } else { 639 s.community_vote_avg_per_hour = null; 640 } 641 var vl = row.volume_last_30m; 642 var vp = row.volume_prev_30m; 643 s.community_volume_last_30m = 644 vl != null && vl !== '' && !isNaN(Number(vl)) ? Number(vl) : null; 645 s.community_volume_prev_30m = 646 vp != null && vp !== '' && !isNaN(Number(vp)) ? Number(vp) : null; 647 } 648 } 649 650 function refreshCommunityRankingSilently() { 651 if (!document.getElementById('detComplaintsTop10')) return; 652 if (state.detectorLoadInProgress) return; 653 var origin = window.location.origin || ''; 654 var d = VALID_RANKING_DAYS.indexOf(state.complaintsRankingDays) !== -1 ? state.complaintsRankingDays : 1; 655 fetch(origin + '/api/detector/community-ranking?' + communityRankingApiQuery(d), { credentials: 'omit' }) 656 .then(function (r) { 657 return r.json().then(function (data) { 658 return { ok: r.ok, data: data }; 659 }); 660 }) 661 .then(function (pack) { 662 var data = pack.data || {}; 663 if (!pack.ok || !data.success) return; 664 writeCommunityRankingSwrCache(data); 665 state.communityRankingRows = data.ranking || []; 666 scheduleCommunityRankingMidnightRefresh(data.next_reset_utc); 667 if (data.disclaimer_pt) state.communityDisclaimerPt = data.disclaimer_pt; 668 applyCommunityRankingToCatalog(state.communityRankingRows); 669 var disc = document.getElementById('rankCommunityDisclaimer'); 670 if (disc && data.disclaimer_pt) disc.textContent = data.disclaimer_pt; 671 renderCommunityRankingOl(); 672 syncComplaintsTop10BoardsAndCards(); 673 refreshComplaintsTrendSparklines(document); 674 try { prefetchTop10Logos(); } catch (ePf) {} 675 try { 676 renderTop30List(); 677 } catch (eT30s) {} 678 }) 679 .catch(function () {}); 680 } 681
682 function startCommunityRankingPoll() { 683 if (communityRankingPollTimer != null) clearInterval(communityRankingPollTimer); 684 communityRankingPollTimer = setInterval(function () { 685 refreshCommunityRankingSilently(); 686 }, COMMUNITY_RANKING_POLL_MS); 687 } 688 689 function syncSidebarComplaintsToolbarLabels() { 690 var sel = document.getElementById('selSidebarComplaintsRankingDays'); 691 if (sel) { 692 var labelMap = { '1': __('js_window_1d'), '7': __('js_window_7d'), '15': __('js_window_15d'), '30': __('js_window_30d') }; 693 for (var oi = 0; oi < sel.options.length; oi++) { 694 var lbl = labelMap[sel.options[oi].value]; 695 if (lbl) sel.options[oi].textContent = lbl; 696 } 697 } 698 var live = document.getElementById('detComplaintsSyncHint'); 699 if (live) { 700 live.textContent = __('sidebar_rank_live'); 701 live.setAttribute('title', __('sidebar_rank_live_title')); 702 } 703 } 704 705 function wireSidebarComplaintsRanking() { 706 var sel = document.getElementById('selSidebarComplaintsRankingDays'); 707 if (!sel || sel.dataset.detWired === '1') return; 708 sel.dataset.detWired = '1'; 709 syncSidebarComplaintsToolbarLabels(); 710 sel.value = String(VALID_RANKING_DAYS.indexOf(state.complaintsRankingDays) !== -1 ? state.complaintsRankingDays : 1); 711 sel.addEventListener('change', function () { 712 var v = parseInt(sel.value, 10); 713 if (VALID_RANKING_DAYS.indexOf(v) === -1) v = 1; 714 state.complaintsRankingDays = v; 715 state.communityRankPrev = {}; 716 state._complaintsRankTrendPrev = {}; 717 updateHeroTagline(); 718 persistComplaintsRankingDays(v); 719 mountCommunityRanking(); 720 }); 721 } 722 var detectorLoadAllGen = 0; 723 var AUTO_REFRESH_ALLOWED_MS = [30000, 45000, 60000, 180000, 300000]; 724 /** Timestamp (ms) em que deve correr a próxima tentativa de auto-refresh. */ 725 var refreshDeadlineAt = 0; 726 var countdownTimerId = null; 727 /** Atualização leve só do ranking comunidade (lateral + contagens no catálogo), sem recarregar todo o catálogo. Alinhado ao ciclo ~20 min do feed. */ 728 var COMMUNITY_RANKING_POLL_MS = 1200000; 729 var communityRankingPollTimer = null; 730 var communityRankingMidnightTimer = null; 731 732 /** Após a API devolver next_reset_utc, agenda um refresh na meia-noite BrasÃlia (novo dia civil = ranking do «1 dia» recomeça). */ 733 function scheduleCommunityRankingMidnightRefresh(nextResetIso) { 734 if (communityRankingMidnightTimer != null) { 735 clearTimeout(communityRankingMidnightTimer); 736 communityRankingMidnightTimer = null; 737 } 738 if (!nextResetIso || typeof nextResetIso !== 'string') return; 739 var t = Date.parse(nextResetIso); 740 if (isNaN(t)) return; 741 var delay = t - Date.now() + 1200; 742 if (delay < 2000) delay = 2000; 743 if (delay > 172800000) delay = 86400000; 744 communityRankingMidnightTimer = setTimeout(function () { 745 communityRankingMidnightTimer = null; 746 if (!document.getElementById('detComplaintsTop10')) return; 747 refreshCommunityRankingSilently(); 748 }, delay); 749 } 750 /** Evita aplicar resposta antiga se outro fetch do mesmo serviço foi disparado depois. */ 751 var complaintsTrendFetchGen = {}; 752 753 function communityTop10ServiceIds() { 754 var rows = state.communityRankingRows; 755 if (!rows || !rows.length) return []; 756 var out = []; 757 for (var i = 0; i < rows.length && out.length < 10; i++) { 758 if ((Number(rows[i].vote_count) || 0) < 1) continue; 759 if (rows[i].service_id) out.push(rows[i].service_id); 760 } 761 return out; 762 } 763 764 function refreshComplaintsTrendSparklines(root) { 765 var scope = root && root.querySelectorAll ? root : document; 766 var top = communityTop10ServiceIds(); 767 var topSet = {}; 768 for (var ti = 0; ti < top.length; ti++) topSet[top[ti]] = true; 769 var days = VALID_RANKING_DAYS.indexOf(state.complaintsRankingDays) !== -1 ? state.complaintsRankingDays : 1; 770 var origin = window.location.origin || ''; 771 var bySid = {}; 772 var nodes = scope.querySelectorAll('.tile-complaints-trend-wrap[data-complaints-trend]'); 773 for (var ni = 0; ni < nodes.length; ni++) { 774 var el = nodes[ni]; 775 var sid = el.getAttribute('data-complaints-trend'); 776 if (!sid) continue; 777 if (!topSet[sid]) { 778 el.innerHTML = ''; 779 el.setAttribute('hidden', ''); 780 el.setAttribute('aria-hidden', 'true'); 781 continue; 782 } 783 if (!bySid[sid]) bySid[sid] = []; 784 bySid[sid].push(el); 785 }
786 Object.keys(bySid).forEach(function (sid) { 787 var els = bySid[sid]; 788 var gen = (complaintsTrendFetchGen[sid] || 0) + 1; 789 complaintsTrendFetchGen[sid] = gen; 790 var url = 791 origin + 792 '/api/detector/service/' + 793 encodeURIComponent(sid) + 794 '/community-hourly?days=' + 795 encodeURIComponent(String(days)) + 796 '&hours=72&require_top10=true'; 797 fetch(url, { credentials: 'omit' }) 798 .then(function (r) { 799 return r.json().then(function (data) { 800 return { ok: r.ok, data: data }; 801 }); 802 }) 803 .then(function (pack) { 804 if (complaintsTrendFetchGen[sid] !== gen) return; 805 var data = pack.data || {}; 806 var html = ''; 807 if ( 808 pack.ok && 809 data.success && 810 data.in_top10 && 811 communityTop10ServiceIds().indexOf(sid) >= 0 812 ) { 813 var pts = (data.points || []).map(function (p) { 814 return Number(p.n); 815 }); 816 html = 817 '<div class="tile-complaints-trend-inner" title="' + 818 esc(__('js_tile_complaints_trend_title')) + 819 '">' + 820 '<span class="tile-complaints-trend-label">' + 821 esc(__('js_tile_complaints_trend_label')) + 822 '</span>' + 823 complaintsSparklineSvg(pts, null) + 824 '</div>'; 825 } 826 for (var ei = 0; ei < els.length; ei++) { 827 var node = els[ei]; 828 if (!node || !node.parentNode) continue; 829 if (!html) { 830 node.innerHTML = ''; 831 node.setAttribute('hidden', ''); 832 node.setAttribute('aria-hidden', 'true'); 833 } else { 834 node.innerHTML = html; 835 node.removeAttribute('hidden'); 836 node.setAttribute('aria-hidden', 'false'); 837 } 838 } 839 }) 840 .catch(function () { 841 if (complaintsTrendFetchGen[sid] !== gen) return; 842 for (var ej = 0; ej < els.length; ej++) { 843 var node2 = els[ej]; 844 if (node2 && node2.parentNode) { 845 node2.innerHTML = ''; 846 node2.setAttribute('hidden', ''); 847 node2.setAttribute('aria-hidden', 'true'); 848 } 849 } 850 }); 851 }); 852 } 853 854 function readStoredRefreshMs() { 855 try { 856 var raw = localStorage.getItem(DETECTOR_REFRESH_MS_KEY); 857 var v = parseInt(raw, 10); 858 if (AUTO_REFRESH_ALLOWED_MS.indexOf(v) >= 0) return v; 859 } catch (e) { /* ignore */ } 860 return 60000; 861 } 862 863 function persistRefreshMs(ms) { 864 try {
865 localStorage.setItem(DETECTOR_REFRESH_MS_KEY, String(ms)); 866 } catch (e) { /* ignore */ } 867 } 868 869 function shouldSkipAutoRefresh() { 870 if (document.visibilityState !== 'visible') return true; 871 var m1 = document.getElementById('svcModal'); 872 var m2 = document.getElementById('communityReportModal'); 873 var m3 = document.getElementById('starRatingModal'); 874 var m4 = document.getElementById('det-contact-overlay'); 875 var m5 = document.getElementById('communityStatsModal'); 876 if (m1 && m1.classList.contains('open')) return true; 877 if (m2 && m2.classList.contains('open')) return true; 878 if (m3 && m3.classList.contains('open')) return true; 879 if (m4 && m4.classList.contains('visible')) return true; 880 if (m5 && m5.classList.contains('open')) return true; 881 return false; 882 } 883 884 function tickAutoRefresh() { 885 if (shouldSkipAutoRefresh()) return; 886 loadAll(); 887 } 888 889 function formatRefreshCountdownClock(sec) { 890 sec = Math.max(0, Math.floor(sec)); 891 var m = Math.floor(sec / 60); 892 var s = sec % 60; 893 return m + ':' + (s < 10 ? '0' : '') + s; 894 } 895 896 function scheduleNextRefreshDeadline() { 897 refreshDeadlineAt = Date.now() + (state.refreshIntervalMs || 60000); 898 } 899 900 function updateRefreshCountdownUi() { 901 var sel = document.getElementById('selRefreshInterval'); 902 var cd = document.getElementById('detRefreshCountdown'); 903 if (!sel) return; 904 var loadingLab = 905 typeof window.detectorT === 'function' 906 ? window.detectorT('refresh_countdown_loading') 907 : 'â¦'; 908 if (state.detectorLoadInProgress) { 909 if (cd) cd.textContent = loadingLab; 910 sel.setAttribute('title', loadingLab); 911 return; 912 } 913 var secLeft = Math.max(0, Math.ceil((refreshDeadlineAt - Date.now()) / 1000)); 914 var clock = formatRefreshCountdownClock(secLeft); 915 var line = clock; 916 if (typeof window.detectorT === 'function') { 917 var tpl = window.detectorT('refresh_countdown_title'); 918 if (tpl && tpl.indexOf('{t}') >= 0) { 919 line = tpl.replace(/\{t\}/g, clock); 920 } 921 } 922 if (cd) cd.textContent = line; 923 sel.setAttribute('title', line); 924 } 925 926 function onRefreshCountdownTick() { 927 if (state.detectorLoadInProgress) { 928 updateRefreshCountdownUi(); 929 return; 930 } 931 updateRefreshCountdownUi(); 932 if (Date.now() < refreshDeadlineAt) return; 933 if (shouldSkipAutoRefresh()) { 934 scheduleNextRefreshDeadline(); 935 updateRefreshCountdownUi(); 936 return; 937 } 938 tickAutoRefresh(); 939 } 940 941 function setAutoRefreshInterval(ms) { 942 if (countdownTimerId != null) { 943 clearInterval(countdownTimerId); 944 countdownTimerId = null; 945 } 946 if (AUTO_REFRESH_ALLOWED_MS.indexOf(ms) < 0) ms = 60000; 947 state.refreshIntervalMs = ms; 948 persistRefreshMs(ms); 949 var sel = document.getElementById('selRefreshInterval'); 950 if (sel && String(sel.value) !== String(ms)) sel.value = String(ms); 951 scheduleNextRefreshDeadline(); 952 countdownTimerId = setInterval(onRefreshCountdownTick, 1000); 953 updateRefreshCountdownUi(); 954 } 955 956 function wireRefreshIntervalSelect() { 957 var sel = document.getElementById('selRefreshInterval'); 958 if (!sel) return; 959 var initial = readStoredRefreshMs(); 960 state.refreshIntervalMs = initial; 961 sel.value = String(initial); 962 sel.addEventListener('change', function () { 963 var n = parseInt(sel.value, 10); 964 if (AUTO_REFRESH_ALLOWED_MS.indexOf(n) < 0) n = 60000; 965 setAutoRefreshInterval(n); 966 if (typeof requestAnimationFrame === 'function') { 967 requestAnimationFrame(function () { 968 try { 969 sel.blur(); 970 } catch (eBl) { 971 /* ignore */ 972 } 973 updateRefreshCountdownUi(); 974 }); 975 } else { 976 updateRefreshCountdownUi(); 977 } 978 }); 979 setAutoRefreshInterval(initial); 980 } 981 982 /** 983 * «Menor latência»: basear-se na sonda principal (HTTPS/TLS). Só excluir por ICMP quando isso 984 * acompanha falha da sonda principal â CDN sem ping continua elegÃvel se last_ok. 985 */ 986 function passesIcmpForFastestRank(s) { 987 if (!s || typeof s !== 'object') return true; 988 if (s.icmp_responds === false && s.last_ok === true) return true; 989 if (s.icmp_responds === false) return false;
990 return true; 991 } 992 993 /** Latência TCP na porta (catálogo tcp_ports), só com handshake OK. */ 994 function tcpPortOpenLatencyMs(rows, port) { 995 if (!Array.isArray(rows)) return null; 996 var p = Number(port); 997 if (!isFinite(p)) return null; 998 for (var i = 0; i < rows.length; i++) { 999 var r = rows[i]; 1000 if (Number(r.port) !== p) continue; 1001 if (r.ok === true && r.latency_ms != null) { 1002 var v = Number(r.latency_ms); 1003 if (!isNaN(v) && v >= 0) return v; 1004 } 1005 } 1006 return null; 1007 } 1008 1009 /** 1010 * Média HTTP+HTTPS quando ambas têm amostra; se só uma estiver aberta, usa essa. 1011 * Alinha ranking com a experiência web real quando ICMP está degradado (ex.: AliExpress). 1012 */ 1013 function tcpHttpHttpsAvgLatencyMs(svc) { 1014 if (!svc || !Array.isArray(svc.tcp_ports)) return null; 1015 var a = tcpPortOpenLatencyMs(svc.tcp_ports, 80); 1016 var b = tcpPortOpenLatencyMs(svc.tcp_ports, 443); 1017 var parts = []; 1018 if (a != null) parts.push(a); 1019 if (b != null) parts.push(b); 1020 if (parts.length === 0) return null; 1021 var sum = 0; 1022 for (var j = 0; j < parts.length; j++) sum += parts[j]; 1023 return sum / parts.length; 1024 } 1025 1026 function icmpCatalogLatencyMs(svc) { 1027 if (!svc || svc.icmp_responds !== true || svc.icmp_latency_ms == null) return null; 1028 var v = Number(svc.icmp_latency_ms); 1029 return !isNaN(v) ? v : null; 1030 } 1031 1032 function isVeroInternetRankingService(svc) { 1033 if (!svc || typeof svc !== 'object') return false; 1034 var id = String(svc.id || '').toLowerCase(); 1035 if (id === 'ext_verointernet_com_br') return true; 1036 var dom = String(svc.logo_domain || '') 1037 .toLowerCase() 1038 .trim(); 1039 return dom === 'verointernet.com.br'; 1040 } 1041 1042 /** 1043 * Vero Internet: ranking sempre pelo menor ms entre cartão, destinos ao vivo, TCP, ICMP 1044 * e mÃnimo recente em `latency_metrics` (alinha ao cálculo do servidor). 1045 */ 1046 function veroRankingLatencyMinMs(svc) { 1047 var cands = []; 1048 var base = svc.last_latency_ms != null ? Number(svc.last_latency_ms) : null; 1049 if (base != null && !isNaN(base)) cands.push(base); 1050 if (Array.isArray(svc.targets_live)) { 1051 for (var i = 0; i < svc.targets_live.length; i++) { 1052 var t = svc.targets_live[i]; 1053 if (t && t.last_ok === true && t.last_latency_ms != null) { 1054 var tv = Number(t.last_latency_ms); 1055 if (!isNaN(tv) && tv >= 0) cands.push(tv); 1056 } 1057 } 1058 } 1059 var icmpL = icmpCatalogLatencyMs(svc); 1060 if (icmpL != null) cands.push(icmpL); 1061 if (Array.isArray(svc.tcp_ports)) { 1062 for (var j = 0; j < svc.tcp_ports.length; j++) { 1063 var p = svc.tcp_ports[j]; 1064 if (p && p.ok === true && p.latency_ms != null) { 1065 var pv = Number(p.latency_ms); 1066 if (!isNaN(pv) && pv >= 0) cands.push(pv); 1067 } 1068 } 1069 } 1070 var met = svc.latency_metrics || {}; 1071 var mm = met.min_ms != null ? Number(met.min_ms) : null; 1072 if (mm != null && !isNaN(mm) && mm >= 0) cands.push(mm); 1073 if (cands.length === 0) return null; 1074 return Math.min.apply(null, cands); 1075 } 1076 1077 /** 1078 * Latência para rankings / ordenação / médias no resumo: 1079 * se a média HTTP+HTTPS (TCP) for claramente melhor que ICMP ou que last_latency_ms, 1080 * usa essa média; caso contrário mantém a sonda principal do catálogo. 1081 * Vero Internet: sempre o mÃnimo entre todas as sondas relevantes (ver `veroRankingLatencyMinMs`). 1082 */ 1083 function rankingLatencyMs(svc) { 1084 if (!svc || svc.last_ok !== true) return null; 1085 if (isVeroInternetRankingService(svc)) { 1086 return veroRankingLatencyMinMs(svc); 1087 } 1088 var base = svc.last_latency_ms != null ? Number(svc.last_latency_ms) : null; 1089 if (base != null && isNaN(base)) base = null; 1090 1091 var tcpAvg = tcpHttpHttpsAvgLatencyMs(svc); 1092 if (tcpAvg == null || isNaN(tcpAvg)) return base; 1093 1094 var icmpL = icmpCatalogLatencyMs(svc); 1095 var delta = 25; 1096 if (icmpL != null && tcpAvg < icmpL - delta) return tcpAvg; 1097 if (base != null && tcpAvg < base - delta) return tcpAvg; 1098 if (base == null) return tcpAvg; 1099 return base; 1100 } 1101 1102 function hasRankingLatencySample(svc) { 1103 return svc && svc.last_ok === true && rankingLatencyMs(svc) != null; 1104 } 1105 1106 /**
1107 * Desempate nos tops de latência: em empate (ms arredondados iguais), priorizar marcas com maior 1108 * relevância global (alinhado às «mega-marcas» do detector + sinais do catálogo). 1109 */ 1110 var MARKET_PROM_DOMAIN_WEIGHT = (function () { 1111 var o = {}; 1112 function add(list, w) { 1113 var parts = String(list).split(','); 1114 for (var i = 0; i < parts.length; i++) { 1115 var k = parts[i].trim().toLowerCase(); 1116 if (k) o[k] = w; 1117 } 1118 } 1119 add( 1120 'google.com,youtube.com,gmail.com,android.com,chrome.com,chromium.org,' + 1121 'facebook.com,instagram.com,whatsapp.com,messenger.com,meta.com,' + 1122 'microsoft.com,office.com,outlook.com,live.com,azure.com,skype.com,xbox.com,' + 1123 'apple.com,icloud.com,apps.apple.com,music.apple.com,' + 1124 'amazon.com,amazonaws.com,aws.amazon.com,primevideo.com,' + 1125 'netflix.com,spotify.com,tiktok.com,linkedin.com,twitter.com,x.com,' + 1126 'discord.com,reddit.com,snapchat.com,pinterest.com,telegram.org,' + 1127 'paypal.com,visa.com,mastercard.com,stripe.com,shopify.com,squareup.com,' + 1128 'openai.com,chatgpt.com,anthropic.com,claude.ai,' + 1129 'deepseek.com,grok.com,x.ai,mistral.ai,perplexity.ai,' + 1130 'midjourney.com,character.ai,meta.ai,copilot.microsoft.com,' + 1131 'huggingface.co,elevenlabs.io,runwayml.com,suno.com,poe.com,' + 1132 'cohere.com,stability.ai,cursor.com,' + 1133 'baidu.com,qq.com,wechat.com,weixin.qq.com,tencent.com,alibaba.com,aliexpress.com,' + 1134 'bytedance.com,cloud.google.com,adobe.com,salesforce.com,oracle.com,ibm.com,' + 1135 'uber.com,airbnb.com,booking.com,expedia.com,doordash.com,' + 1136 'walmart.com,target.com,ebay.com,bestbuy.com,costco.com,' + 1137 'samsung.com,sony.com,nvidia.com,intel.com,' + 1138 'github.com,gitlab.com,bitbucket.org,stackoverflow.com,' + 1139 'zoom.us,slack.com,dropbox.com,notion.so,atlassian.com,' + 1140 'bbc.com,cnn.com,nytimes.com,reuters.com,bloomberg.com,' + 1141 'nubank.com.br,bb.com.br,itau.com.br,bradesco.com.br,santander.com.br,caixa.gov.br,' + 1142 'mercadolivre.com.br,magazineluiza.com.br,americanas.com.br,shopee.com.br,ifood.com.br,' + 1143 'gov.br,correios.com.br,esocial.gov.br,nfe.fazenda.gov.br,bcb.gov.br,dados.gov.br,' + 1144 'drogasil.com.br,drogaraia.com.br,paguemenos.com.br,unimed.coop.br,hapvida.com.br,' + 1145 'fleury.com.br,serasa.com.br,reclameaqui.com.br,' + 1146 'usp.br,unicamp.br,ufrj.br,ufmg.br,' + 1147 'latamairlines.com,voegol.com.br,voeazul.com.br,decolar.com,cvc.com.br,' + 1148 'carrefour.com.br,casasbahia.com.br,quintoandar.com.br,' + 1149 'portoseguro.com.br,hotmart.com,gupy.io,globoplay.globo.com,' + 1150 'g1.globo.com,folha.uol.com.br,valor.globo.com,infomoney.com.br,' + 1151 'dazn.com,espn.com.br,sofascore.com,flashscore.com.br,' + 1152 'cbf.com.br,premierleague.com,uefa.com,conmebol.com,fifa.com,' + 1153 'flamengo.com.br,corinthians.com.br,palmeiras.com.br,' + 1154 'ufc.com.br,nba.com,formula1.com,lance.com.br', 1155 1000, 1156 ); 1157 add( 1158 'epicgames.com,steampowered.com,steamcommunity.com,riotgames.com,roblox.com,' + 1159 'playstation.com,ubisoft.com,ea.com,blizzard.com,battle.net,activision.com,' + 1160 'hoyoverse.com,mihoyo.com,nintendo.com,twitch.tv,' + 1161 'fortnite.com,callofduty.com,rockstargames.com,rocketleague.com,' + 1162 'ff.garena.com,supercell.com,tibia.com,pokemongolive.com,' + 1163 'huawei.com,zte.com,cisco.com,juniper.net,cloudflare.com,fastly.com,akamai.com', 1164 720, 1165 ); 1166 add('fiberhome.com,fiberhome.com.cn', 420); 1167 return o; 1168 })(); 1169 1170 function domainMarketProminenceWeight(host) { 1171 if (!host) return 0; 1172 var d = String(host).toLowerCase().trim(); 1173 if (!d) return 0; 1174 for (;;) { 1175 var w = MARKET_PROM_DOMAIN_WEIGHT[d]; 1176 if (w) return w; 1177 var ix = d.indexOf('.'); 1178 if (ix < 0) break; 1179 d = d.slice(ix + 1); 1180 } 1181 return 0; 1182 } 1183 1184 function marketProminenceForLatencyRank(svc) { 1185 if (!svc || typeof svc !== 'object') return 0; 1186 var w = domainMarketProminenceWeight(svc.logo_domain); 1187 var ts = Number(svc.trust_score); 1188 if (!isNaN(ts) && ts > 0) w += Math.min(ts, 100) * 0.15; 1189 var vc = Number(svc.star_rating_count) || 0; 1190 w += Math.min(vc, 150) * 0.05; 1191 var comm = Number(svc.community_vote_count) || 0; 1192 w += Math.min(comm, 80) * 0.12; 1193 return w; 1194 } 1195 1196 function ratingBoardCompositeParts(s) { 1197 var votes = Number(s.star_rating_count) || 0; 1198 var stars = votes > 0 ? Number(s.avg_star_rating) || 5 : 0; 1199 var sc = Number(s.trust_score) || 0; 1200 if (isNaN(sc)) sc = 0; 1201 1202 var met = s.latency_metrics || {}; 1203 var avgMs = met.avg_ms != null ? Number(met.avg_ms) : null; 1204 var jitter = met.jitter_ms != null ? Number(met.jitter_ms) : null; 1205 var pLoss = met.packet_loss_pct != null ? Number(met.packet_loss_pct) : null; 1206 var isUp = s.last_ok === true; 1207 var reports = Number(s.community_vote_count) || 0; 1208
1209 var latBonus = 0; 1210 if (avgMs != null && !isNaN(avgMs) && avgMs > 0) { 1211 latBonus = avgMs < 100 ? 15 : avgMs < 300 ? 10 : avgMs < 600 ? 5 : avgMs < 1000 ? 2 : 0; 1212 } 1213 1214 var lossBonus = 0; 1215 if (pLoss != null && !isNaN(pLoss)) { 1216 lossBonus = pLoss <= 0 ? 10 : pLoss < 1 ? 7 : pLoss < 5 ? 3 : 0; 1217 } 1218 1219 var jitterBonus = 0; 1220 if (jitter != null && !isNaN(jitter)) { 1221 jitterBonus = jitter < 10 ? 5 : jitter < 50 ? 3 : jitter < 100 ? 1 : 0; 1222 } 1223 1224 var uptimeBonus = isUp ? 5 : 0; 1225 1226 var reportPenalty = reports <= 0 ? 5 : reports <= 5 ? 3 : reports <= 20 ? 1 : 0; 1227 1228 return { 1229 composite: sc + stars * 4 + Math.min(votes, 50) * 0.5 1230 + latBonus + lossBonus + jitterBonus + uptimeBonus + reportPenalty, 1231 votes: votes, 1232 }; 1233 } 1234 1235 /** 1236 * Top «pior / melhor avaliação»: ordena pela métrica composta 1237 * (nota + estrelas + votos + latência + perda + jitter + uptime + relatos). 1238 * Empates: peso de mercado pelo domÃnio; empate final aleatório. 1239 */ 1240 function sortCatalogForRatingBoard(items, worst) { 1241 var decorated = items.map(function (s) { 1242 var p = ratingBoardCompositeParts(s); 1243 return { 1244 s: s, 1245 c: p.composite, 1246 w: domainMarketProminenceWeight(s.logo_domain), 1247 r: Math.random(), 1248 }; 1249 }); 1250 decorated.sort(function (a, b) { 1251 if (a.c !== b.c) return worst ? a.c - b.c : b.c - a.c; 1252 if (a.w !== b.w) return worst ? a.w - b.w : b.w - a.w; 1253 return a.r - b.r; 1254 }); 1255 return decorated.map(function (x) { 1256 return x.s; 1257 }); 1258 } 1259 1260 function cmpRankingLatencyWorst(a, b) { 1261 var la = latencyMs(a); 1262 var lb = latencyMs(b); 1263 if (la == null && lb == null) { 1264 return String(a.name || '').localeCompare(String(b.name || ''), i18nSortLocale()); 1265 } 1266 if (la == null) return 1; 1267 if (lb == null) return -1; 1268 var ra = Math.round(la); 1269 var rb = Math.round(lb); 1270 if (ra !== rb) return rb - ra; 1271 var pa = marketProminenceForLatencyRank(a); 1272 var pb = marketProminenceForLatencyRank(b); 1273 if (pb !== pa) return pb - pa; 1274 if (lb !== la) return lb - la; 1275 return String(a.name || '').localeCompare(String(b.name || ''), i18nSortLocale()); 1276 } 1277 1278 function cmpRankingLatencyBest(a, b) { 1279 var la = latencyMs(a); 1280 var lb = latencyMs(b); 1281 if (la == null && lb == null) { 1282 return String(a.name || '').localeCompare(String(b.name || ''), i18nSortLocale()); 1283 } 1284 if (la == null) return 1; 1285 if (lb == null) return -1; 1286 var ra = Math.round(la); 1287 var rb = Math.round(lb); 1288 if (ra !== rb) return ra - rb; 1289 var pa = marketProminenceForLatencyRank(a); 1290 var pb = marketProminenceForLatencyRank(b); 1291 if (pb !== pa) return pb - pa; 1292 if (la !== lb) return la - lb; 1293 return String(a.name || '').localeCompare(String(b.name || ''), i18nSortLocale()); 1294 } 1295 1296 /** Itens por coluna nos painéis de ranking (página mais curta). */ 1297 var RANK_TOP = 5; 1298 /** Máximo ao expandir «ranking completo». */ 1299 var RANK_FULL_MAX = 60; 1300 /** Top N no catálogo de problemas (global e por categoria). */ 1301 var PROB_TOP = 10; 1302 /** Alinhar resumo «jogos» a entradas ext_* do catálogo pelo domÃnio do logo. */ 1303 var GAME_SLUG_LOGO = { 1304 steam: 'steampowered.com', 1305 epicgames: 'epicgames.com', 1306 playstation: 'playstation.com', 1307 xbox: 'xbox.com', 1308 roblox: 'roblox.com', 1309 riotgames: 'riotgames.com', 1310 discord: 'discord.com', 1311 twitch: 'twitch.tv', 1312 }; 1313 1314 /** Statuspage «sp_*» alinhados ao resumo Jogos & live do Detector. */ 1315 var GAME_SP_STATUS_IDS = { 1316 steam: 1, 1317 epicgames: 1, 1318 playstation: 1, 1319 xbox: 1, 1320 roblox: 1, 1321 riotgames: 1, 1322 discord: 1, 1323 twitch: 1, 1324 }; 1325 1326 var detTop10SocialSession = { kind: 'community', rows: null }; 1327 1328 function gameCatalogDomainRoots() { 1329 var roots = []; 1330 var seen = {}; 1331 function add(d) { 1332 var x = String(d || '') 1333 .trim() 1334 .toLowerCase(); 1335 if (!x || seen[x]) return; 1336 seen[x] = 1; 1337 roots.push(x); 1338 } 1339 var k; 1340 for (k in GAME_SLUG_LOGO) { 1341 if (Object.prototype.hasOwnProperty.call(GAME_SLUG_LOGO, k)) add(GAME_SLUG_LOGO[k]); 1342 } 1343 var extras = [ 1344 'steamcommunity.com', 1345 'battle.net', 1346 'blizzard.com', 1347 'ea.com', 1348 'electronicarts.com', 1349 'origin.com', 1350 'ubisoft.com', 1351 'nintendo.com', 1352 'minecraft.net', 1353 'facepunch.com', 1354 'pubg.com', 1355 'mihoyo.com', 1356 'hoyoverse.com', 1357 'wargaming.net', 1358 'deadbydaylight.com', 1359 'bhvr.com', 1360 'garena.com', 1361 'krafton.com', 1362 'leagueoflegends.com', 1363 'playvalorant.com', 1364 'rockstargames.com', 1365 'activision.com', 1366 'callofduty.com', 1367 'bethesda.net', 1368 'zenimax.com', 1369 'cdprojektred.com', 1370 'gog.com', 1371 'nexusmods.com', 1372 'curseforge.com', 1373 ]; 1374 for (var ei = 0; ei < extras.length; ei++) add(extras[ei]); 1375 return roots; 1376 } 1377 1378 var _gameCatalogDomainRootsCache = null; 1379 function domainMatchesGameCatalog(dom) { 1380 var d = String(dom || '') 1381 .trim() 1382 .toLowerCase(); 1383 if (!d) return false;
1384 if (!_gameCatalogDomainRootsCache) _gameCatalogDomainRootsCache = gameCatalogDomainRoots(); 1385 var roots = _gameCatalogDomainRootsCache; 1386 var ri; 1387 for (ri = 0; ri < roots.length; ri++) { 1388 var r = roots[ri]; 1389 if (d === r || d.endsWith('.' + r)) return true; 1390 } 1391 return false; 1392 } 1393 1394 function isDetectorGameCatalogService(svc) { 1395 if (!svc) return false; 1396 var id = String(svc.id || '').toLowerCase(); 1397 if (id.indexOf('sp_') === 0) { 1398 var rest = id.slice(3); 1399 if (GAME_SP_STATUS_IDS[rest]) return true; 1400 } 1401 if (String(svc.category || '').toLowerCase() === 'gaming') return true; 1402 if (domainMatchesGameCatalog(svc.logo_domain)) return true; 1403 1404 var bag = ( 1405 String(svc.id || '') + 1406 ' ' + 1407 String(svc.name || '') + 1408 ' ' + 1409 String(svc.logo_domain || '') 1410 ).toLowerCase(); 1411 var hints = [ 1412 'game', 1413 'gaming', 1414 'jogo', 1415 'jogos', 1416 'steam', 1417 'epic', 1418 'riot', 1419 'valorant', 1420 'league of legends', 1421 'playstation', 1422 'psn', 1423 'xbox', 1424 'nintendo', 1425 'roblox', 1426 'minecraft', 1427 'twitch', 1428 'discord', 1429 'battle.net', 1430 'blizzard', 1431 ]; 1432 for (var hi = 0; hi < hints.length; hi++) { 1433 if (bag.indexOf(hints[hi]) !== -1) return true; 1434 } 1435 return false; 1436 } 1437 1438 function top10GamesCatalogServices(limit) { 1439 if (!state.catalog || !state.catalog.length) return []; 1440 var pool = state.catalog.filter(function (s) { 1441 return isDetectorGameCatalogService(s) && gameRankingLatencyMs(s) != null; 1442 }); 1443 pool.sort(cmpGamesRankingLatencyWorst); 1444 var lim = Number(limit); 1445 if (!isNaN(lim) && lim > 0) return pool.slice(0, lim); 1446 return pool.slice(0, 10); 1447 } 1448 1449 function gamesLatencyQuantileMs(svc, q) { 1450 var src = svc && Array.isArray(svc.latency_sparkline_ms) ? svc.latency_sparkline_ms : []; 1451 var vals = []; 1452 var i; 1453 for (i = 0; i < src.length; i++) { 1454 var n = Number(src[i]); 1455 if (!isNaN(n) && n >= 0) vals.push(n); 1456 } 1457 if (!vals.length) return null; 1458 vals.sort(function (a, b) { 1459 return a - b; 1460 }); 1461 var qq = Number(q); 1462 if (isNaN(qq)) qq = 0.5; 1463 if (qq < 0) qq = 0; 1464 if (qq > 1) qq = 1; 1465 var pos = Math.round((vals.length - 1) * qq); 1466 return vals[pos]; 1467 } 1468 1469 /** 1470 * Candidatos de latência para o Top 10 jogos: mesma ideia do ranking «Vero» â várias sondas 1471 * (HTTPS/TCP, sparkline, métricas, ICMP, destinos ao vivo) e escolha robusta (min vs mediana). 1472 */ 1473 function gameRankingLatencyCandidateMs(svc) { 1474 if (!svc || svc.last_ok !== true) return []; 1475 var out = []; 1476 var b = rankingLatencyMs(svc); 1477 if (b != null && !isNaN(b) && b >= 0) out.push(b); 1478 var tcp = tcpHttpHttpsAvgLatencyMs(svc); 1479 if (tcp != null && !isNaN(tcp) && tcp >= 0) out.push(tcp); 1480 var p50 = gamesLatencyQuantileMs(svc, 0.5); 1481 if (p50 != null && !isNaN(p50) && p50 >= 0) out.push(p50); 1482 var met = svc.latency_metrics || {}; 1483 var mn = met.min_ms != null ? Number(met.min_ms) : null; 1484 if (mn != null && !isNaN(mn) && mn >= 0) out.push(mn); 1485 var ic = icmpCatalogLatencyMs(svc); 1486 if (ic != null && !isNaN(ic) && ic >= 0) out.push(ic); 1487 if (Array.isArray(svc.targets_live)) { 1488 var ti; 1489 for (ti = 0; ti < svc.targets_live.length; ti++) { 1490 var tl = svc.targets_live[ti]; 1491 if (tl && tl.last_ok === true && tl.last_latency_ms != null) { 1492 var tv = Number(tl.last_latency_ms); 1493 if (!isNaN(tv) && tv >= 0) out.push(tv); 1494 } 1495 } 1496 } 1497 return out; 1498 } 1499 1500 function gameRankingLatencyMs(svc) { 1501 var cands = gameRankingLatencyCandidateMs(svc); 1502 if (!cands.length) return null; 1503 cands.sort(function (a, b) { 1504 return a - b; 1505 }); 1506 var lo = cands[0]; 1507 var hi = cands[cands.length - 1]; 1508 if (cands.length === 1) return lo; 1509 if (cands.length === 2) return Math.min(cands[0], cands[1]); 1510 if (hi - lo > 500) { 1511 return cands[Math.floor(cands.length / 2)]; 1512 } 1513 return lo; 1514 } 1515 1516 function cmpGamesRankingLatencyWorst(a, b) { 1517 var la = gameRankingLatencyMs(a); 1518 var lb = gameRankingLatencyMs(b); 1519 if (la == null && lb == null) { 1520 return String(a.name || '').localeCompare(String(b.name || ''), i18nSortLocale()); 1521 } 1522 if (la == null) return 1; 1523 if (lb == null) return -1; 1524 var ra = Math.round(la); 1525 var rb = Math.round(lb); 1526 if (ra !== rb) return rb - ra; 1527 var pa = marketProminenceForLatencyRank(a); 1528 var pb = marketProminenceForLatencyRank(b); 1529 if (pb !== pa) return pb - pa; 1530 if (lb !== la) return lb - la; 1531 return String(a.name || '').localeCompare(String(b.name || ''), i18nSortLocale()); 1532 } 1533 1534 /** 1535 * Mesma ordenação que o painel «Top 10 games» à direita: relatos de games primeiro, 1536 * depois lugares até 10 por latência (catálogo, piores primeiro). Serviço único para UI e cartão/copy de partilha. 1537 */ 1538 function sidebarTop10GamesHybridParts() { 1539 var commRows = state.communityRankingRows; 1540 var gameCommRows = []; 1541 if (commRows && commRows.length) { 1542 for (var ri = 0; ri < commRows.length; ri++) { 1543 var rw = commRows[ri]; 1544 if (!rw || (Number(rw.vote_count) || 0) < 1) continue; 1545 var svcCheck = findSvc(rw.service_id); 1546 if (svcCheck && isDetectorGameCatalogService(svcCheck)) { 1547 gameCommRows.push(rw); 1548 } 1549 } 1550 }
1551 gameCommRows = gameCommRows.slice(0, 10); 1552 var latFillSvcs = []; 1553 if (gameCommRows.length < 10) { 1554 var commIds = {}; 1555 for (var ci = 0; ci < gameCommRows.length; ci++) { 1556 commIds[gameCommRows[ci].service_id] = true; 1557 } 1558 var allLatSvcs = top10GamesCatalogServices(200); 1559 for (var li2 = 0; li2 < allLatSvcs.length && latFillSvcs.length < (10 - gameCommRows.length); li2++) { 1560 var ls = allLatSvcs[li2]; 1561 if (ls && !commIds[ls.id]) latFillSvcs.push(ls); 1562 } 1563 } 1564 return { communityRankingRows: commRows, gameCommRows: gameCommRows, latFillSvcs: latFillSvcs }; 1565 } 1566 1567 /** Linhas no formato esperado pelo modal social / canvas (iguais ao painel direito). */ 1568 function rankingRowsForTop10GamesSocialShare() { 1569 var hy = sidebarTop10GamesHybridParts(); 1570 var gameCommRows = hy.gameCommRows; 1571 var latFillSvcs = hy.latFillSvcs; 1572 var merged = []; 1573 var gi; 1574 for (gi = 0; gi < gameCommRows.length; gi++) { 1575 merged.push(gameCommRows[gi]); 1576 } 1577 var fj; 1578 for (fj = 0; fj < latFillSvcs.length; fj++) { 1579 var s = latFillSvcs[fj]; 1580 if (!s || !s.id) continue; 1581 merged.push({ 1582 service_id: s.id, 1583 name: s.name || s.id, 1584 vote_count: 0, 1585 logo_domain: s.logo_domain, 1586 logo_url: s.logo_url, 1587 logo_icons: s.logo_icons, 1588 }); 1589 } 1590 return merged; 1591 } 1592 1593 function getTop10SocialShareRowsForModal() { 1594 if (detTop10SocialSession && detTop10SocialSession.rows && detTop10SocialSession.rows.length) { 1595 return detTop10SocialSession.rows.slice(0, 10); 1596 } 1597 return communityRowsWithVotesForReport().slice(0, 10); 1598 } 1599 1600 function top10SocialCardPngBasename() { 1601 return detTop10SocialSession && detTop10SocialSession.kind === 'games' 1602 ? 'catellix-top10-jogos.png' 1603 : 'catellix-top10-comunidade.png'; 1604 } 1605 1606 function rankLimit(panelKey) { 1607 return state.rankExpanded[panelKey] ? RANK_FULL_MAX : RANK_TOP; 1608 } 1609 1610 function updateRankMoreBtn(btnId, panelKey, totalAvailable) { 1611 var btn = document.getElementById(btnId); 1612 if (!btn) return; 1613 var n = Number(totalAvailable) || 0; 1614 if (n <= RANK_TOP) { 1615 btn.hidden = true; 1616 return; 1617 } 1618 btn.hidden = false; 1619 btn.textContent = state.rankExpanded[panelKey] ? __('js_rank_more_top5') : __('js_rank_more_full'); 1620 btn.setAttribute('aria-expanded', state.rankExpanded[panelKey] ? 'true' : 'false'); 1621 } 1622 1623 var RANK_OPTION_KEYS = [ 1624 ['live_problems', 'rank_sort_live_problems'], 1625 ['latency_desc', 'rank_sort_latency_desc'], 1626 ['latency_asc', 'rank_sort_latency_asc'], 1627 ['rank_global', 'rank_sort_rank_global'], 1628 ['fail_first', 'rank_sort_fail_first'], 1629 ['incident_first', 'rank_sort_incident_first'], 1630 ['name', 'rank_sort_name'], 1631 ['name_desc', 'rank_sort_name_desc'], 1632 ]; 1633 1634 function enrichDescription(svc) { 1635 function clip(t, max) { 1636 var s = (t || '').trim(); 1637 if (s.length <= max) return s; 1638 return s.slice(0, max - 1) + '\u2026'; 1639 } 1640 var base = (svc && svc.description) ? String(svc.description).trim() : ''; 1641 if (base.length >= 100) return clip(base, 190); 1642 var extra = categoryBlurb(svc && svc.category); 1643 if (!extra) return clip(base || __('desc_fallback'), 190); 1644 if (!base) return clip(extra, 190); 1645 return clip(base + ' â ' + extra, 190); 1646 } 1647 1648 function esc(s) { 1649 if (s == null) return ''; 1650 return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/"/g, '"'); 1651 } 1652 1653 function faviconForDomain(dom) { 1654 if (!dom) return 'https://www.google.com/s2/favicons?domain=catellix.com&sz=128'; 1655 return 'https://www.google.com/s2/favicons?domain=' + encodeURIComponent(dom) + '&sz=128'; 1656 } 1657 1658 function trendFaviconDomain(x) { 1659 if (!x) return 'catellix.com'; 1660 var d = x.hint_domain != null ? String(x.hint_domain).trim() : ''; 1661 if (d) return d; 1662 var s = x.slug != null ? String(x.slug).trim() : ''; 1663 if (s.indexOf('.') >= 0) return s; 1664 return s ? s + '.com' : 'catellix.com'; 1665 } 1666 1667 function trendHref(x) { 1668 var u = x && x.url != null ? String(x.url).trim() : ''; 1669 return u || '/detector'; 1670 } 1671 1672 function trendIsExternal(href) { 1673 return /^https?:\/\//i.test(href) && href.indexOf('catellix.com') < 0; 1674 } 1675 1676 function trendLinkTargetAttrs(href) { 1677 if (trendIsExternal(href)) return ' target="_blank" rel="noopener noreferrer"'; 1678 return ''; 1679 } 1680 1681 function faviconDdg(dom) { 1682 if (!dom) return ''; 1683 return 'https://icons.duckduckgo.com/ip3/' + encodeURIComponent(dom) + '.ico'; 1684 } 1685 1686 /** 1687 * Fallbacks em `<img>`: **não** usar unavatar como 1º passo (devolve «cara» genérica, não logo de marca). 1688 * Ordem: proxy DDG â proxy Google favicon do domÃnio «pai» (.com.br â .com). 1689 * Requer `detectorIconProxyWrapUrl` (definido mais abaixo; declaração hoisted no IIFE). 1690 */ 1691 function imgLogoOnErrorAttr(logoDomain) { 1692 var dom = (logoDomain || '').trim(); 1693 if (!dom) return ''; 1694 var ddg = faviconDdg(dom); 1695 var u1 = ddg ? detectorIconProxyWrapUrl(ddg) : ''; 1696 var u2 = ''; 1697 if (/\.com\.(br|ar|mx)$/i.test(dom)) { 1698 u2 = detectorIconProxyWrapUrl(faviconForDomain(dom.replace(/\.com\.(br|ar|mx)$/i, '.com'))); 1699 } 1700 if (!u1 && !u2) return ''; 1701 if (!u1) { 1702 return ( 1703 ' onerror="if(this.dataset.lx===\'1\'){this.onerror=null;return;}this.dataset.lx=\'1\';this.src=\'' + 1704 esc(u2) + 1705 '\'"' 1706 ); 1707 } 1708 if (!u2) { 1709 return ( 1710 ' onerror="if(this.dataset.lx===\'1\'){this.onerror=null;return;}this.dataset.lx=\'1\';this.src=\'' + 1711 esc(u1) + 1712 '\'"' 1713 ); 1714 } 1715 return ( 1716 ' onerror="(function(el){var s=parseInt(el.dataset.lx,10)||0;var u1=\'' + 1717 esc(u1) + 1718 '\';var u2=\'' + 1719 esc(u2) + 1720 '\';if(s===0){el.dataset.lx=\'1\';el.src=u1;return;}if(s===1){el.dataset.lx=\'2\';el.src=u2;return;}el.onerror=null;})(this)"' 1721 ); 1722 } 1723 1724 /** 1725 * `<img>` com cadeia vinda do backend (`logo_icons`): mesmo primário que `logo_url`, depois Logo.dev, 1726 * Clearbit, Simple Icons, `/static/detector-logos/â¦`, favicon Google, unavatar, DDG. 1727 */ 1728 function detLogoImgHtml(svc, extraClass, dimAttrs) { 1729 var dom = (svc.logo_domain || '').trim(); 1730 var chain = Array.isArray(svc.logo_icons) && svc.logo_icons.length ? svc.logo_icons : null; 1731 var src = chain ? chain[0] : svc.logo_url || faviconForDomain(dom); 1732 var baseCls = (extraClass || '').trim(); 1733 var cls = (baseCls + (chain ? ' det-logo-fallback' : '')).trim(); 1734 var dim = dimAttrs || ''; 1735 if (chain) { 1736 var enc = encodeURIComponent(JSON.stringify(chain)); 1737 return ( 1738 '<img class="' + 1739 esc(cls) + 1740 '"' + 1741 dim + 1742 ' src="' + 1743 esc(src) + 1744 '" alt="" loading="lazy" decoding="async" data-logo-chain="' + 1745 esc(enc) + 1746 '" data-logo-i="0" data-logo-domain="' + 1747 esc(dom) + 1748 '">' 1749 ); 1750 } 1751 return ( 1752 '<img class="' + 1753 esc(cls) + 1754 '"' + 1755 dim + 1756 ' src="' + 1757 esc(src) + 1758 '" alt="" loading="lazy" decoding="async" data-logo-domain="' + 1759 esc(dom) + 1760 '"' + 1761 imgLogoOnErrorAttr(dom) + 1762 '>' 1763 ); 1764 } 1765 1766 function attachLogoChainOnError(img) { 1767 if (!img) return; 1768 var raw = img.getAttribute('data-logo-chain'); 1769 if (!raw) return; 1770 var chain; 1771 try { 1772 chain = JSON.parse(decodeURIComponent(raw)); 1773 } catch (e) { 1774 return; 1775 } 1776 if (!Array.isArray(chain) || chain.length < 1) return; 1777 img.onerror = function () { 1778 var i = parseInt(img.getAttribute('data-logo-i'), 10) || 0; 1779 var n = i + 1; 1780 if (n >= chain.length) { 1781 img.onerror = null; 1782 return; 1783 } 1784 img.setAttribute('data-logo-i', String(n)); 1785 img.src = chain[n]; 1786 }; 1787 } 1788 1789 function bindDetLogoFallbacks(root) { 1790 if (!root || !root.querySelectorAll) return; 1791 root.querySelectorAll('img.det-logo-fallback[data-logo-chain]').forEach(attachLogoChainOnError); 1792 } 1793 1794 function updateOneShelfHint(wrap) { 1795 if (!wrap) return; 1796 var row = wrap.querySelector('.row-scroll'); 1797 var hint = wrap.querySelector('.shelf-scroll-hint'); 1798 if (!row || !hint) return; 1799 var cs = window.getComputedStyle(row); 1800 if (cs.display === 'none' || cs.visibility === 'hidden') { 1801 hint.classList.remove('visible'); 1802 return; 1803 } 1804 var max = row.scrollWidth - row.clientWidth; 1805 if (max <= 6) { 1806 hint.classList.remove('visible'); 1807 return; 1808 } 1809 var atEnd = row.scrollLeft >= max - 6; 1810 hint.classList.toggle('visible', !atEnd); 1811 } 1812 1813 /** 1814 * Roda do rato vertical â scroll horizontal no carrossel; arrastar com o rato (desktop). 1815 * Snap CSS é desligado durante o arrasto para o movimento ficar contÃnuo. 1816 */ 1817 function bindShelfRowSmoothUx(row) { 1818 if (!row || row.dataset.detShelfUx === '1') return; 1819 row.dataset.detShelfUx = '1'; 1820 1821 row.addEventListener( 1822 'wheel', 1823 function (e) { 1824 var max = row.scrollWidth - row.clientWidth; 1825 if (max <= 1) return; 1826 var absY = Math.abs(e.deltaY); 1827 var absX = Math.abs(e.deltaX); 1828 if (absX >= absY) return; 1829 if (absY < 0.5) return; 1830 var delta = e.deltaY; 1831 var next = row.scrollLeft + delta; 1832 var atStart = row.scrollLeft <= 0; 1833 var atEnd = row.scrollLeft >= max - 1; 1834 if (delta < 0 && atStart) return; 1835 if (delta > 0 && atEnd) return; 1836 row.scrollLeft = Math.max(0, Math.min(max, next)); 1837 e.preventDefault(); 1838 }, 1839 { passive: false }, 1840 ); 1841 1842 var drag = { 1843 active: false, 1844 ptrId: null, 1845 startX: 0, 1846 startScroll: 0, 1847 moved: false, 1848 }; 1849 1850 function isInteractiveTarget(t) { 1851 if (!t || !t.closest) return false;
1852 return !!t.closest( 1853 'button, a[href], input, select, textarea, [role="button"], label[for]', 1854 ); 1855 } 1856 1857 function endDrag(e) { 1858 if (!drag.active) return; 1859 if (e && e.pointerId != null && drag.ptrId != null && e.pointerId !== drag.ptrId) return; 1860 var didMove = drag.moved; 1861 drag.active = false; 1862 drag.ptrId = null; 1863 row.classList.remove('is-shelf-dragging'); 1864 try { 1865 if (e && e.pointerId != null) row.releasePointerCapture(e.pointerId); 1866 } catch (err) { 1867 /* ignore */ 1868 } 1869 if (didMove) { 1870 row.addEventListener( 1871 'click', 1872 function suppressClick(ev) { 1873 ev.preventDefault(); 1874 ev.stopPropagation(); 1875 row.removeEventListener('click', suppressClick, true); 1876 }, 1877 true, 1878 ); 1879 } 1880 drag.moved = false; 1881 } 1882 1883 row.addEventListener('pointerdown', function (e) { 1884 if (e.pointerType !== 'mouse' || e.button !== 0) return; 1885 if (isInteractiveTarget(e.target)) return; 1886 drag.active = true; 1887 drag.ptrId = e.pointerId; 1888 drag.startX = e.clientX; 1889 drag.startScroll = row.scrollLeft; 1890 drag.moved = false; 1891 try { 1892 row.setPointerCapture(e.pointerId); 1893 } catch (err2) { 1894 /* ignore */ 1895 } 1896 }); 1897 row.addEventListener('pointermove', function (e) { 1898 if (!drag.active || e.pointerId !== drag.ptrId) return; 1899 var dx = e.clientX - drag.startX; 1900 if (Math.abs(dx) > 4) { 1901 drag.moved = true; 1902 row.classList.add('is-shelf-dragging'); 1903 } 1904 row.scrollLeft = drag.startScroll - dx; 1905 }); 1906 row.addEventListener('pointerup', endDrag); 1907 row.addEventListener('pointercancel', endDrag); 1908 row.addEventListener('lostpointercapture', function (e) { 1909 if (drag.active && drag.ptrId === e.pointerId) endDrag(e); 1910 }); 1911 } 1912 1913 function bindShelfScrollHint(wrap) { 1914 if (!wrap) return; 1915 var row = wrap.querySelector('.row-scroll'); 1916 if (!row) return; 1917 bindShelfRowSmoothUx(row); 1918 function update() { 1919 updateOneShelfHint(wrap); 1920 } 1921 row.addEventListener('scroll', update, { passive: true }); 1922 window.addEventListener('resize', update, { passive: true }); 1923 if (typeof ResizeObserver !== 'undefined') { 1924 var ro = new ResizeObserver(update); 1925 ro.observe(row); 1926 } 1927 update(); 1928 } 1929 1930 function refreshShelfHints() { 1931 document.querySelectorAll('.shelf-row-wrap').forEach(updateOneShelfHint); 1932 } 1933 1934 var ddRowHintBound = false; 1935 1936 function badgeForService(svc) { 1937 if (svc.last_ok === true) return { cls: 'ok', label: __('js_status_ok') }; 1938 if (svc.last_ok === false) return { cls: 'bad', label: __('js_status_fail') }; 1939 return { cls: 'unknown', label: __('js_status_no_data') }; 1940 } 1941 1942 /** 1943 * Classificação global de latência (servidor: `latency_class` + métricas; cliente: DetectorLatencyClassify). 1944 */ 1945 function latencyTierMeta(svc) { 1946 if (svc.last_ok === false) { 1947 return { 1948 cls: 'lat-tier-fail', 1949 pill: __('js_status_fail'), 1950 hint: __('js_lat_tier_fail_hint'), 1951 }; 1952 } 1953 if (svc.last_ok !== true || svc.last_latency_ms == null) { 1954 return { 1955 cls: 'lat-tier-unk', 1956 pill: __('js_pill_no_data'), 1957 hint: __('js_lat_tier_unk_hint'), 1958 }; 1959 } 1960 var lc = svc.latency_class; 1961 if (lc && lc.ui_class) { 1962 var hint = (lc.description_pt || '').trim(); 1963 var rules = lc.rules_applied || []; 1964 if (rules.length) { 1965 hint += (hint ? ' · ' : '') + __('js_lat_rules_label') + ' ' + rules.join(', '); 1966 } 1967 return { 1968 cls: lc.ui_class, 1969 pill: (lc.short_pt || lc.label_pt || 'â').trim(), 1970 hint: hint || (lc.label_pt || '').trim() || __('js_lat_tier_fallback_hint'), 1971 }; 1972 } 1973 if (typeof DetectorLatencyClassify !== 'undefined' && DetectorLatencyClassify.classifyFromSvc) { 1974 var fb = DetectorLatencyClassify.classifyFromSvc(svc); 1975 var h2 = (fb.description_pt || '').trim(); 1976 if (fb.rules_applied && fb.rules_applied.length) { 1977 h2 += (h2 ? ' · ' : '') + __('js_lat_rules_label') + ' ' + fb.rules_applied.join(', '); 1978 } 1979 return { 1980 cls: fb.ui_class, 1981 pill: (fb.short_pt || 'â').trim(), 1982 hint: h2 || __('js_lat_tier_client_hint'), 1983 }; 1984 } 1985 return { cls: 'lat-tier-unk', pill: __('js_pill_no_data'), hint: __('js_lat_no_class') }; 1986 } 1987 1988 /** Tags IPv4 (azul) / IPv6 (dourado) conforme registos DNS A / AAAA do hostname sondado. */ 1989 function ipStackTagsHtml(svc) { 1990 var h4 = svc.has_ipv4 === true; 1991 var h6 = svc.has_ipv6 === true; 1992 if (!h4 && !h6) return ''; 1993 var parts = []; 1994 if (h4) { 1995 parts.push( 1996 '<span class="ip-tag ip-tag--v4" title="' + 1997 esc(__('js_ip_tag_v4_title')) + 1998 '">IPv4</span>', 1999 ); 2000 } 2001 if (h6) { 2002 parts.push( 2003 '<span class="ip-tag ip-tag--v6" title="' + 2004 esc(__('js_ip_tag_v6_title')) + 2005 '">IPv6</span>', 2006 ); 2007 } 2008 return ( 2009 '<div class="tile-ip-tags" role="group" aria-label="' + 2010 esc(__('js_ip_stack_aria')) + 2011 '">' + 2012 parts.join('') + 2013 '</div>' 2014 ); 2015 } 2016 2017 function ipStackTagsHtmlCompact(svc) { 2018 var h4 = svc.has_ipv4 === true; 2019 var h6 = svc.has_ipv6 === true; 2020 if (!h4 && !h6) return ''; 2021 var p = []; 2022 if (h4) p.push('<span class="ip-tag ip-tag--v4 ip-tag--compact">IPv4</span>'); 2023 if (h6) p.push('<span class="ip-tag ip-tag--v6 ip-tag--compact">IPv6</span>'); 2024 return '<span class="rank-ip-tags">' + p.join('') + '</span>'; 2025 } 2026 2027 /** Ãcone «i» (Wikimedia Commons Info.svg), via https â CSP permite img https:. */ 2028 var INFO_ICON_HTML = 2029 '<img class="tile-info-ico" src="https://upload.wikimedia.org/wikipedia/commons/e/e1/Info.svg" width="17" height="17" alt="" decoding="async" loading="lazy" referrerpolicy="no-referrer" />'; 2030 2031 function sparklineSvg(pts, chartW) { 2032 var W = chartW != null ? chartW : 112; 2033 var H = 34; 2034 var pad = 2; 2035 var arr = Array.isArray(pts) 2036 ? pts 2037 .map(function (x) { 2038 return Number(x); 2039 }) 2040 .filter(function (x) { 2041 return !isNaN(x) && isFinite(x); 2042 }) 2043 : []; 2044 if (arr.length === 0) { 2045 return ( 2046 '<svg class="tile-spark-svg tile-spark-svg--latency" viewB
2046ox="0 0 ' + 2047 W + 2048 ' ' + 2049 H + 2050 '" preserveAspectRatio="none" aria-hidden="true"><line class="tile-spark-empty" x1="' + 2051 pad + 2052 '" y1="' + 2053 H / 2 + 2054 '" x2="' + 2055 (W - pad) + 2056 '" y2="' + 2057 H / 2 + 2058 '" stroke-width="1.75" stroke-linecap="round" stroke-dasharray="4 3"/></svg>' 2059 ); 2060 } 2061 if (arr.length === 1) { 2062 var cx = W / 2; 2063 var cy = H / 2; 2064 return ( 2065 '<svg class="tile-spark-svg tile-spark-svg--latency" viewBox="0 0 ' + 2066 W + 2067 ' ' + 2068 H + 2069 '" preserveAspectRatio="none" aria-hidden="true"><circle class="tile-spark-dot" cx="' + 2070 cx + 2071 '" cy="' + 2072 cy + 2073 '" r="4"/></svg>' 2074 ); 2075 } 2076 var mn = Math.min.apply(null, arr); 2077 var mx = Math.max.apply(null, arr); 2078 if (mx - mn < 1e-6) { 2079 mn -= 1; 2080 mx += 1; 2081 } 2082 var n = arr.length; 2083 var yB = H - pad; 2084 var xy = []; 2085 var i; 2086 for (i = 0; i < n; i++) { 2087 var x = pad + ((W - 2 * pad) * i) / (n - 1); 2088 var y = pad + (H - 2 * pad) * (1 - (arr[i] - mn) / (mx - mn)); 2089 xy.push({ x: x, y: y }); 2090 } 2091 var dLine = ''; 2092 for (i = 0; i < n; i++) { 2093 dLine += (i === 0 ? 'M' : 'L') + xy[i].x.toFixed(2) + ',' + xy[i].y.toFixed(2) + ' '; 2094 } 2095 dLine = dLine.trim(); 2096 var dArea = 2097 'M ' + 2098 xy[0].x.toFixed(2) + 2099 ',' + 2100 yB.toFixed(2) + 2101 ' L ' + 2102 xy[0].x.toFixed(2) + 2103 ',' + 2104 xy[0].y.toFixed(2) + 2105 ' '; 2106 for (i = 1; i < n; i++) { 2107 dArea += 'L ' + xy[i].x.toFixed(2) + ',' + xy[i].y.toFixed(2) + ' '; 2108 } 2109 dArea += 'L ' + xy[n - 1].x.toFixed(2) + ',' + yB.toFixed(2) + ' Z'; 2110 return ( 2111 '<svg class="tile-spark-svg tile-spark-svg--latency" viewBox="0 0 ' + 2112 W + 2113 ' ' + 2114 H + 2115 '" preserveAspectRatio="none" aria-hidden="true">' + 2116 '<path class="tile-spark-area" d="' + 2117 dArea + 2118 '"/>' + 2119 '<path class="tile-spark-glow" fill="none" d="' + 2120 dLine + 2121 '"/>' + 2122 '<path class="tile-spark-line" fill="none" d="' + 2123 dLine + 2124 '"/>' + 2125 '</svg>' 2126 ); 2127 } 2128 2129 /** Sparkline de relatos (comunidade) â cor quente, distinta da latência. */ 2130 function complaintsSparklineSvg(pts, chartW) { 2131 var W = chartW != null ? chartW : 112; 2132 var H = 34; 2133 var pad = 2; 2134 var arr = Array.isArray(pts) 2135 ? pts 2136 .map(function (x) { 2137 return Number(x); 2138 }) 2139 .filter(function (x) { 2140 return !isNaN(x) && isFinite(x) && x >= 0; 2141 }) 2142 : []; 2143 if (arr.length === 0) { 2144 return ( 2145 '<svg class="tile-spark-svg tile-spark-svg--complaints" viewBox="0 0 ' + 2146 W + 2147 ' ' + 2148 H + 2149 '" preserveAspectRatio="none" aria-hidden="true"><line class="tile-spark-empty" x1="' + 2150 pad + 2151 '" y1="' + 2152 H / 2 + 2153 '" x2="' + 2154 (W - pad) + 2155 '" y2="' + 2156 H / 2 + 2157 '" stroke-width="1.75" stroke-linecap="round" stroke-dasharray="4 3"/></svg>' 2158 ); 2159 } 2160 if (arr.length === 1) { 2161 var cx = W / 2; 2162 var cy = H / 2; 2163 return ( 2164 '<svg class="tile-spark-svg tile-spark-svg--complaints" viewBox="0 0 ' + 2165 W + 2166 ' ' + 2167 H + 2168 '" preserveAspectRatio="none" aria-hidden="true"><circle class="tile-spark-dot" cx="' + 2169 cx + 2170 '" cy="' + 2171 cy + 2172 '" r="4"/></svg>' 2173 ); 2174 } 2175 var mn = Math.min.apply(null, arr); 2176 var mx = Math.max.apply(null, arr); 2177 if (mx - mn < 1e-6) { 2178 mn -= 1; 2179 mx += 1; 2180 } 2181 var n = arr.length; 2182 var yB = H - pad; 2183 var xy = []; 2184 var i; 2185 for (i = 0; i < n; i++) { 2186 var x = pad + ((W - 2 * pad) * i) / (n - 1);
2187 var y = pad + (H - 2 * pad) * (1 - (arr[i] - mn) / (mx - mn)); 2188 xy.push({ x: x, y: y }); 2189 } 2190 var dLine = ''; 2191 for (i = 0; i < n; i++) { 2192 dLine += (i === 0 ? 'M' : 'L') + xy[i].x.toFixed(2) + ',' + xy[i].y.toFixed(2) + ' '; 2193 } 2194 dLine = dLine.trim(); 2195 var dArea = 2196 'M ' + 2197 xy[0].x.toFixed(2) + 2198 ',' + 2199 yB.toFixed(2) + 2200 ' L ' + 2201 xy[0].x.toFixed(2) + 2202 ',' + 2203 xy[0].y.toFixed(2) + 2204 ' '; 2205 for (i = 1; i < n; i++) { 2206 dArea += 'L ' + xy[i].x.toFixed(2) + ',' + xy[i].y.toFixed(2) + ' '; 2207 } 2208 dArea += 'L ' + xy[n - 1].x.toFixed(2) + ',' + yB.toFixed(2) + ' Z'; 2209 return ( 2210 '<svg class="tile-spark-svg tile-spark-svg--complaints" viewBox="0 0 ' + 2211 W + 2212 ' ' + 2213 H + 2214 '" preserveAspectRatio="none" aria-hidden="true">' + 2215 '<path class="tile-spark-area" d="' + 2216 dArea + 2217 '"/>' + 2218 '<path class="tile-spark-glow" fill="none" d="' + 2219 dLine + 2220 '"/>' + 2221 '<path class="tile-spark-line" fill="none" d="' + 2222 dLine + 2223 '"/>' + 2224 '</svg>' 2225 ); 2226 } 2227 2228 var TCP_PORT_SVC_LABEL = { 2229 20: 'FTP-D', 2230 21: 'FTP', 2231 22: 'SSH', 2232 23: 'Telnet', 2233 25: 'SMTP', 2234 53: 'DNS', 2235 80: 'HTTP', 2236 443: 'HTTPS', 2237 3306: 'MySQL', 2238 3389: 'RDP', 2239 8080: 'HTTP·alt', 2240 8443: 'HTTPS·alt', 2241 }; 2242 2243 function tcpPortServiceLabel(port) { 2244 var p = Number(port); 2245 if (!isFinite(p)) return 'TCP'; 2246 return TCP_PORT_SVC_LABEL[p] || 'TCP'; 2247 } 2248 2249 function tcpPortsRowHtml(svc) { 2250 var pp = state.probeProfile || {}; 2251 if (pp.tcp_port_status_enabled === false) return ''; 2252 var rows = svc.tcp_ports; 2253 if (!rows || !rows.length) return ''; 2254 var parts = rows 2255 .map(function (r) { 2256 var p = Number(r.port); 2257 if (!isFinite(p)) return ''; 2258 var ok = r.ok === true; 2259 var cls = ok 2260 ? 'tcp-port-pill tcp-port-pill--open' 2261 : r.ok === false 2262 ? 'tcp-port-pill tcp-port-pill--closed' 2263 : 'tcp-port-pill tcp-port-pill--unk'; 2264 var svcLb = tcpPortServiceLabel(p); 2265 var title = ok 2266 ? __('js_tcp_pill_open_tpl').replace(/\{p\}/g, String(p)).replace(/\{svc\}/g, svcLb) 2267 : r.ok === false 2268 ? __('js_tcp_pill_closed_tpl').replace(/\{p\}/g, String(p)).replace(/\{svc\}/g, svcLb) 2269 : __('js_tcp_pill_wait_tpl').replace(/\{p\}/g, String(p)).replace(/\{svc\}/g, svcLb); 2270 var ms = r.latency_ms != null ? Math.round(Number(r.latency_ms)) + ' ms' : ''; 2271 return ( 2272 '<span class="' + 2273 cls + 2274 '" title="' + 2275 esc(title) + 2276 '"><span class="tcp-port-num">' + 2277 esc(String(p)) + 2278 '</span>' + 2279 (ms ? '<span class="tcp-port-ms">' + esc(ms) + '</span>' : '') + 2280 '</span>' 2281 ); 2282 }) 2283 .join(''); 2284 if (!parts.trim()) return ''; 2285 return ( 2286 '<div class="tile-tcp-ports" role="group" aria-label="' + 2287 esc(__('js_tcp_group_aria')) + 2288 '">' + 2289 parts + 2290 '</div>' 2291 ); 2292 } 2293 2294 function renderProbeLayersBlock(svc, detail) { 2295 var el = document.getElementById('svcProbeLayersHost'); 2296 if (!el) return; 2297 var prof = (detail && detail.probe_profile) || state.probeProfile || {}; 2298 var pp = detail && detail.primary_probe; 2299 var icmpD = detail && detail.icmp_echo; 2300 var notePt = 2301 (detail && detail.availability_note_pt) || __('js_prob_hint_tls'); 2302 2303 var primaryOk = pp && pp.ok !== undefined && pp.ok !== null ? pp.ok : svc.last_ok; 2304 var primaryMs = 2305 pp && pp.latency_ms != null 2306 ? Number(pp.latency_ms) 2307 : svc.last_latency_ms != null 2308 ? Number(svc.last_latency_ms) 2309 : null; 2310 var primaryLabel = (pp && pp.mode_label_pt) || prof.mode_label_pt || prof.mode || 'Sonda principal'; 2311 var primaryEp = (pp && pp.probe_endpoint) || svc.card_probe_url || svc.probe_url || ''; 2312 var targetKey = (pp && pp.target_key) || svc.card_target_key || 'primary'; 2313 2314 var primaryCardCls = 'probe-layer-card'; 2315 if (primaryOk === true) primaryCardCls += ' probe-layer-card--ok'; 2316 else if (primaryOk === false) primaryCardCls += ' probe-layer-card--bad'; 2317 else primaryCardCls += ' probe-layer-card--unk'; 2318 2319 var primaryStateTxt; 2320 if (primaryOk === true) { 2321 primaryStateTxt = 2322 primaryMs != null && !isNaN(primaryMs) 2323 ? __('js_probe_operational_ms').replace( 2324 /\{ms\}/g, 2325 String(Math.round(primaryMs)), 2326 ) 2327 : __('js_probe_operational'); 2328 } else if (primaryOk === false) { 2329 primaryStateTxt = __('js_probe_main_timeout'); 2330 } else { 2331 primaryStateTxt = __('js_primary_no_measure'); 2332 } 2333 2334 var icmpOn = prof.icmp_status_enabled !== false; 2335 var icmpCardCls = 'probe-layer-card'; 2336 var icmpBody = ''; 2337 if (!icmpOn) { 2338 icmpCardCls += ' probe-layer-card--off'; 2339 icmpBody = __('js_probe_disabled'); 2340 } else if (icmpD && icmpD.enabled === false) { 2341 icmpCardCls += ' probe-layer-card--off'; 2342 icmpBody = __('js_probe_disabled'); 2343 } else if (icmpD && icmpD.ok === true) { 2344 icmpCardCls += ' probe-layer-card--ok'; 2345 var il = icmpD.latency_ms != null ? Math.round(Number(icmpD.latency_ms)) + ' ms' : ''; 2346 var pl = 2347 icmpD.packet_loss_pct != null 2348 ? __('js_probe_icmp_loss').replace( 2349 /\{pct\}/g, 2350 Number(icmpD.packet_loss_pct).toFixed(1), 2351 ) 2352 : ''; 2353 icmpBody = __('js_icmp_ok') + (il ? ' · ' + il : '') + pl; 2354 } else if (icmpD && icmpD.ok === false) { 2355 icmpCardCls += ' probe-layer-card--bad'; 2356 icmpBody = __('js_icmp_no'); 2357 } else if (icmpD) { 2358 icmpCardCls += ' probe-layer-card--unk'; 2359 icmpBody = __('js_icmp_wait'); 2360 } else if (svc.icmp_responds === true) { 2361 icmpCardCls += ' probe-layer-card--ok'; 2362 var il0 = svc.icmp_latency_ms != null ? Math.round(Number(svc.icmp_latency_ms)) + ' ms' : ''; 2363 icmpBody = __('js_icmp_ok') + (il0 ? ' · ' + il0 : ''); 2364 } else if (svc.icmp_responds === false) { 2365 icmpCardCls += ' probe-layer-card--bad'; 2366 icmpBody = __('js_icmp_no'); 2367 } else { 2368 icmpCardCls += ' probe-layer-card--unk'; 2369 icmpBody = __('js_icmp_wait'); 2370 } 2371 2372 var httpHint = ''; 2373 if (detail && Array.isArray(detail.tcp_ports)) { 2374 var p80 = detail.tcp_ports.filter(function (x) { return Number(x.port) === 80; })[0]; 2375 var p443 = detail.tcp_ports.filter(function (x) { return Number(x.port) === 443; })[0]; 2376 if (p80 || p443) { 2377 var bits = []; 2378 var st80 = 2379 p80 && (p80.ok === true 2380 ? __('js_tcp_state_open') 2381 : p80.ok === false 2382 ? __('js_tcp_state_closed') 2383 : __('js_tcp_state_wait')); 2384 var st443 = 2385 p443 && (p443.ok === true 2386 ? __('js_tcp_state_open') 2387 : p443.ok === false 2388 ? __('js_tcp_state_closed') 2389 : __('js_tcp_state_wait')); 2390 if (p80) bits.push(__('js_probe_line_80').replace(/\{state\}/g, st80)); 2391 if (p443) bits.push(__('js_probe_line_443').replace(/\{state\}/g, st443)); 2392 httpHint = 2393 '<div class="probe-layer-sub" style="margin-top:10px"><strong>' + 2394 esc(__('js_probe_tcp_vs')) + 2395 '</strong> ' + 2396 esc(bits.join(' · ')) + 2397 '</div>'; 2398 } 2399 } 2400 2401 el.style.display = 'block'; 2402 el.innerHTML = 2403 '<h4 class="svc-probe-layers-title">' + 2404 esc(__('js_probe_layers_title')) + 2405 '</h4>' + 2406 '<div class="probe-layer-grid">' + 2407 '<div class="' + 2408 esc(primaryCardCls) + 2409 '">' + 2410 '<div class="probe-layer-k">' + 2411 esc(__('js_probe_main_key').replace(/\{key\}/g, String(targetKey))) + 2412 '</div>' + 2413 '<div class="probe-layer-v">' + 2414 esc(primaryStateTxt) + 2415 '</div>' + 2416 '<div class="probe-layer-sub">' + 2417 esc(primaryLabel) + 2418 '</div>' + 2419 (primaryEp 2420 ? '<div class="probe-layer-sub probe-layer-sub--ep">' + 2421 esc( 2422 String(primaryEp).length > 72 2423 ? String(primaryEp).slice(0, 69) + '\u2026' 2424 : String(primaryEp), 2425 ) + 2426 '</div>' 2427 : '') + 2428 '</div>' + 2429 '<div class="' + 2430 esc(icmpCardCls) + 2431 '">' + 2432 '<div class="probe-layer-k">' + 2433 esc(__('js_probe_icmp')) + 2434 '</div>' + 2435 '<div class="probe-layer-v">' + 2436 esc(icmpBody) + 2437 '</div>' + 2438 '<div class="probe-layer-sub">' + 2439 esc(__('js_probe_icmp_sub')) + 2440 '</div>' + 2441 '</div>' + 2442 '</div>' + 2443 httpHint + 2444 (notePt && String(notePt).trim() 2445 ? '<p class="probe-avail-note" role="note">' + 2446 esc( 2447 String(notePt).length > 100 2448 ? String(notePt).trim().slice(0, 97) + '\u2026' 2449 : String(notePt).trim(), 2450 ) + 2451 '</p>' 2452 : ''); 2453 } 2454
2455 function renderSvcTcpPortsBlock(rows, prof) { 2456 var el = document.getElementById('svcTcpPortsHost'); 2457 if (!el) return; 2458 var pp = prof && typeof prof === 'object' ? prof : state.probeProfile || {}; 2459 if (pp.tcp_port_status_enabled === false) { 2460 el.innerHTML = ''; 2461 el.style.display = 'none'; 2462 return; 2463 } 2464 el.style.display = 'block'; 2465 if (!rows || !rows.length) { 2466 el.innerHTML = 2467 '<div class="svc-tcp-ports-wrap-inner"><h4 class="svc-tcp-ports-title">' + 2468 esc(__('js_tcp_title')) + 2469 '</h4><p class="svc-tcp-ports-note">' + 2470 esc(__('js_tcp_disabled_note')) + 2471 '</p></div>'; 2472 return; 2473 } 2474 var cells = rows 2475 .map(function (r) { 2476 var p = Number(r.port); 2477 if (!isFinite(p)) return ''; 2478 var ok = r.ok === true; 2479 var cellCls = ok 2480 ? 'tcp-port-cell tcp-port-cell--open' 2481 : r.ok === false 2482 ? 'tcp-port-cell tcp-port-cell--closed' 2483 : 'tcp-port-cell tcp-port-cell--unk'; 2484 var ms = r.latency_ms != null ? Math.round(Number(r.latency_ms)) + ' ms' : 'â'; 2485 var svcLb = tcpPortServiceLabel(p); 2486 var stLab = ok 2487 ? __('js_tcp_cell_open') 2488 : r.ok === false 2489 ? __('js_tcp_cell_closed') 2490 : __('js_tcp_cell_wait'); 2491 return ( 2492 '<div class="' + 2493 cellCls + 2494 '" title="' + 2495 esc( 2496 __('js_tcp_port_title_tpl') 2497 .replace(/\{p\}/g, String(p)) 2498 .replace(/\{svc\}/g, svcLb) 2499 .replace(/\{st\}/g, stLab), 2500 ) + 2501 '">' + 2502 '<div class="tcp-port-cell-num">' + 2503 esc(String(p)) + 2504 '</div>' + 2505 '<div class="tcp-port-cell-svc">' + 2506 esc(svcLb) + 2507 '</div>' + 2508 '<div class="tcp-port-cell-st">' + 2509 esc(stLab) + 2510 '</div>' + 2511 '<div class="tcp-port-cell-ms">' + 2512 esc(ms) + 2513 '</div></div>' 2514 ); 2515 }) 2516 .join(''); 2517 el.innerHTML = 2518 '<div class="svc-tcp-ports-wrap-inner"><h4 class="svc-tcp-ports-title">' + 2519 esc(__('js_tcp_title')) + 2520 '</h4>' + 2521 '<div class="tcp-port-grid">' + 2522 cells + 2523 '</div></div>'; 2524 } 2525 2526 function tileHtml(svc) { 2527 var bd = badgeForService(svc); 2528 var rankHtml = svc.latency_rank ? '<span class="poster-rank">#' + esc(svc.latency_rank) + '</span>' : ''; 2529 var tsNum = Number(svc.trust_score); 2530 var trustHtml = ''; 2531 if (!isNaN(tsNum) && tsNum > 0) { 2532 var tCls = tsNum >= 90 ? '' : tsNum >= 80 ? ' warn' : ' bad'; 2533 trustHtml = 2534 '<span class="tile-trust-score' + 2535 tCls + 2536 '" title="' + 2537 esc(__('js_tile_trust_title')) + 2538 '">' + 2539 esc(tsNum.toFixed(1)) + 2540 '</span>'; 2541 } 2542 var latCol; 2543 var tileLatMs = rankingLatencyMs(svc); 2544 if (svc.last_ok === true && tileLatMs != null && !isNaN(tileLatMs)) { 2545 latCol = 2546 '<div class="poster-lat"><span class="lat-val">' + 2547 esc(Math.round(tileLatMs)) + 2548 '</span><span class="lat-unit">ms</span></div>'; 2549 } else if (svc.last_ok === false) { 2550 latCol = 2551 '<div class="poster-lat bad"><span class="lat-val">â</span><span class="lat-unit">' + 2552 esc(__('js_tile_lat_error')) + 2553 '</span></div>'; 2554 } else { 2555 latCol = 2556 '<div class="poster-lat unk"><span class="lat-val">â¦</span><span class="lat-unit">ms</span></div>'; 2557 } 2558 var stCls = svc.last_ok === true ? 'state-ok' : (svc.last_ok === false ? 'state-bad' : 'state-unknown'); 2559 var lt = latencyTierMeta(svc); 2560 var cv = Number(svc.community_vote_count) || 0; 2561 var cvSig = Number(svc.community_vote_count_signals) || 0; 2562 var cvVer = Number(svc.community_vote_count_verified); 2563 if (isNaN(cvVer)) cvVer = Math.max(0, cv - cvSig); 2564 var vAvgField = svc.community_vote_avg_per_hour; 2565 var useAvgDisp = 2566 vAvgField != null && vAvgField !== '' && !isNaN(Number(vAvgField)); 2567 var cvDispStr = useAvgDisp ? Number(vAvgField).toFixed(1) : String(cv); 2568 var cvW = cv === 1 ? __('js_relato_word') : __('js_relato_plural'); 2569 var cvTitleBase = useAvgDisp 2570 ? __('js_tile_comm_avg_hint_tpl') 2571 .replace(/\{avg\}/g, cvDispStr) 2572 .replace(/\{total\}/g, String(cv)) 2573 : __('js_tile_comm_title_tpl').replace(/\{n\}/g, String(cv)).replace(/\{w\}/g, cvW); 2574 var cvTitle = cvTitleBase; 2575 var showCvBadge = cv > 0 || useAvgDisp; 2576 var cvBadge = 2577 showCvBadge 2578 ? '<button type="button" class="tile-community-cnt" data-cs-sid="' + 2579 esc(svc.id) + 2580 '" data-cs-name="' + 2581 esc(svc.name || svc.id) + 2582 '" title="' + 2583 esc(cvTitle) + 2584 '">' + 2585 esc(cvDispStr) + 2586 '</button>' 2587 : ''; 2588 var avgRating = Number(svc.avg_star_rating) || 0; 2589 var starBadge = 2590 '<button type="button" class="tile-star-btn" data-star-sid="' + 2591 esc(svc.id) + 2592 '" data-star-name="' + 2593 esc(svc.name || svc.id) + 2594 '" title="' + 2595 esc(__('js_tile_rate_title')) + 2596 '">' + 2597 '<span class="star-ic">★</span>' + 2598 '<span class="star-avg">' + 2599 (avgRating > 0 ? avgRating.toFixed(1) : esc(__('js_tile_rate_cta'))) + 2600 '</span></button>'; 2601 var a11yName = esc(svc.name || svc.id || __('js_tile_service_fallback')); 2602 return ( 2603 '<div tabindex="0" role="group" class="tile tile-3d ' + 2604 stCls + 2605 ' ' + 2606 lt.cls + 2607 '" data-svc-id="' + 2608 esc(svc.id) + 2609 '" aria-label="' + 2610 a11yName + 2611 esc(__('js_tile_aria_hub')) + 2612 '">' + 2613 '<button type="button" class="tile-report-tag" title="' + 2614 esc(__('js_tile_report_title')) + 2615 '">' + 2616 esc(__('js_tile_report')) + 2617 '</button>' + 2618 cvBadge + 2619 '<div class="poster">' + 2620 starBadge + 2621 trustHtml + 2622 rankHtml + 2623 '<div class="poster-logo-wrap">' + 2624 detLogoImgHtml(svc, '', ' width="96" height="96"') + 2625 '</div>' + 2626 latCol + 2627 '</div>' + 2628 '<div class="meta">' + 2629 ipStackTagsHtml(svc) + 2630 '<span class="tile-lat-pill ' + 2631 lt.cls + 2632 '" title="' + 2633 esc(lt.hint) + 2634 '">' + 2635 esc(lt.pill) + 2636 '</span><div class="name">' + 2637 esc(svc.name) + 2638 '</div><span class="badge ' + 2639 bd.cls + 2640 '">' + 2641 esc(bd.label) + 2642 '</span></div>' + 2643 tcpPortsRowHtml(svc) + 2644 '<div class="tile-sparkline-wrap" title="Tendência recente da latência (destino do cartão)">' + 2645 sparklineSvg(svc.latency_sparkline_ms, null) + 2646 '</div>' + 2647 '<div class="tile-complaints-trend-wrap" data-complaints-trend="' + 2648 esc(svc.id) + 2649 '" hidden aria-hidden="true"></div>' + 2650 '<button type="button" class="tile-more-btn" data-more-sid="' + 2651 esc(svc.id) + 2652 '" aria-label="+ info: mais detalhes, hub e gráfico"><span class="tile-info-row">' + 2653 INFO_ICON_HTML + 2654 '<span class="tile-info-txt">+ info</span></span></button></div>' 2655 ); 2656 } 2657 2658 function bindCommunityReportButtons(root) { 2659 if (!root) return; 2660 root.querySelectorAll('.tile-report-tag').forEach(function (btn) { 2661 btn.addEventListener('click', function (e) { 2662 e.preventDefault(); 2663 e.stopPropagation(); 2664 var tile = btn.closest('.tile'); 2665 var id = tile && tile.getAttribute('data-svc-id'); 2666 if (id) openCommunityReportModal(id); 2667 }); 2668 }); 2669 root.querySelectorAll('.list-report-btn').forEach(function (btn) { 2670 btn.addEventListener('click', function (e) { 2671 e.preventDefault(); 2672 e.stopPropagation(); 2673 var id = btn.getAttribute('data-report-sid'); 2674 if (id) openCommunityReportModal(id); 2675 }); 2676 }); 2677 } 2678 2679 /* ââ Community Stats Modal (score click) ââ */ 2680 var _csChart = null; 2681 2682 function bindCommunityStatsButtons(root) { 2683 if (!root) return; 2684 root.querySelectorAll('.tile-community-cnt[data-cs-sid]').forEach(function (btn) { 2685 btn.addEventListener('click', function (e) { 2686 e.preventDefault(); 2687 e.stopPropagation(); 2688 var sid = btn.getAttribute('data-cs-sid'); 2689 var sname = btn.getAttribute('data-cs-name') || sid; 2690 if (sid) openCommunityStatsModal(sid, sname); 2691 }); 2692 }); 2693 } 2694 2695 function bindStarRatingButtons(root) { 2696 if (!root) return; 2697 root.querySelectorAll('.tile-star-btn[data-star-sid]').forEach(function (btn) { 2698 btn.addEventListener('click', function (e) { 2699 e.preventDefault(); 2700 e.stopPropagation(); 2701 var sid = btn.getAttribute('data-star-sid'); 2702 var sname = btn.getAttribute('data-star-name') || sid; 2703 if (sid) openStarRatingModal(sid, sname); 2704 }); 2705 }); 2706 } 2707 2708 /** Só suprime o clique se o dedo deslizou claramente em horizontal (carrossel). touchcancel não marca como arrasto. */ 2709 function bindTouchAwareOpen(el, openFn) { 2710 var ptr = { x0: 0, y0: 0, horizontalScroll: false }; 2711 el.addEventListener( 2712 'touchstart', 2713 function (e) { 2714 if (e.touches.length !== 1) return; 2715 ptr.x0 = e.touches[0].clientX; 2716 ptr.y0 = e.touches[0].clientY; 2717 ptr.horizontalScroll = false;
2718 }, 2719 { passive: true }, 2720 ); 2721 el.addEventListener( 2722 'touchmove', 2723 function (e) { 2724 if (!e.touches.length) return; 2725 var dx = e.touches[0].clientX - ptr.x0; 2726 var dy = e.touches[0].clientY - ptr.y0; 2727 if (Math.abs(dx) > 26 && Math.abs(dx) > Math.abs(dy) * 1.2) ptr.horizontalScroll = true; 2728 }, 2729 { passive: true }, 2730 ); 2731 el.addEventListener('touchcancel', function () { 2732 ptr.horizontalScroll = false; 2733 }, { passive: true }); 2734 el.addEventListener('click', function (e) { 2735 if (ptr.horizontalScroll) { 2736 e.preventDefault(); 2737 e.stopPropagation(); 2738 ptr.horizontalScroll = false; 2739 return; 2740 } 2741 if ( 2742 e.target.closest( 2743 'button.tile-report-tag, button.tile-more-btn, button.list-report-btn, button.list-more-btn', 2744 ) 2745 ) { 2746 return; 2747 } 2748 openFn(el); 2749 }); 2750 } 2751 2752 /** Liga clique/toque nos `.tile` e `.list-row` dentro de um contentor (carrosséis Top 10, ICMP, prateleiras). */ 2753 function attachTileInteractions(container, openCallback) { 2754 if (!container || typeof openCallback !== 'function') return; 2755 container.querySelectorAll('.tile[data-svc-id], .list-row[data-svc-id]').forEach(function (el) { 2756 function doOpen() { 2757 openCallback(el); 2758 } 2759 bindTouchAwareOpen(el, doOpen); 2760 el.addEventListener('keydown', function (e) { 2761 if (e.key === 'Enter' || e.key === ' ') { 2762 e.preventDefault(); 2763 doOpen(); 2764 } 2765 }); 2766 }); 2767 } 2768 2769 /* ââ Star Rating Modal ââ */ 2770 var _starState = { svcId: null, svcName: '', stars: 0, verifyToken: null }; 2771 var STAR_LABELS = ['', 'Péssimo', 'Ruim', 'Regular', 'Bom', 'Excelente']; 2772 2773 function starRatingParseDetail(detail) { 2774 if (detail == null || detail === '') return ''; 2775 if (typeof detail === 'string') return detail; 2776 if (Array.isArray(detail)) { 2777 return detail 2778 .map(function (item) { 2779 if (typeof item === 'string') return item; 2780 if (item && item.msg) return String(item.msg); 2781 return ''; 2782 }) 2783 .filter(Boolean) 2784 .join(' '); 2785 } 2786 return String(detail); 2787 } 2788 2789 function starRatingUserMessage(status, data) { 2790 var d = data || {}; 2791 if (d.message_pt) return d.message_pt; 2792 var det = starRatingParseDetail(d.detail); 2793 if (det) return det; 2794 if (status === 429 && d.retry_after_sec != null) { 2795 var sec = Math.max(0, Number(d.retry_after_sec) || 0); 2796 var h = Math.floor(sec / 3600); 2797 var m = Math.floor((sec % 3600) / 60); 2798 if (h > 0) { 2799 return ( 2800 'Você já votou neste serviço nas últimas 24 horas. Aguarde mais ' + 2801 h + 2802 (h === 1 ? ' hora' : ' horas') + 2803 (m > 0 ? ' e ' + m + ' min.' : '.') 2804 ); 2805 } 2806 if (m > 0) return 'Aguarde mais ' + m + ' min. para votar de novo.'; 2807 } 2808 if (status >= 500) return 'Erro no servidor. Tente mais tarde.'; 2809 if (status === 400 || status === 404) return 'Pedido inválido. Atualize a página e tente de novo.'; 2810 return 'Erro ao processar o pedido (código ' + status + ').'; 2811 } 2812 2813 function setStarMsgNeutral(text) { 2814 var msg = document.getElementById('starMsg'); 2815 if (!msg) return; 2816 msg.className = 'star-msg'; 2817 msg.textContent = text || ''; 2818 } 2819 2820 function setStarMsgOk(text) { 2821 var msg = document.getElementById('starMsg'); 2822 if (!msg) return; 2823 msg.className = 'star-msg ok'; 2824 msg.innerHTML = 2825 '<span class="star-ok-tick" aria-hidden="true">â</span><span class="star-ok-text">' + 2826 esc(text || 'ConcluÃdo.') + 2827 '</span>'; 2828 } 2829 2830 function setStarMsgErr(text) { 2831 var msg = document.getElementById('starMsg'); 2832 if (!msg) return; 2833 msg.className = 'star-msg err'; 2834 msg.textContent = text || 'Erro.'; 2835 } 2836 2837 function starRatingPostJson(url, body) { 2838 return fetch(url, { 2839 method: 'POST', 2840 headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, 2841 /* same-origin: envia cookie CSRF; csrf.js acrescenta X-CSRF-Token (double-submit) */ 2842 credentials: 'same-origin', 2843 body: JSON.stringify(body), 2844 }).then(function (r) { 2845 return r.text().then(function (t) { 2846 var data = {}; 2847 if (t && String(t).trim()) { 2848 try { 2849 data = JSON.parse(t); 2850 } catch (e) {
2851 data = { message_pt: 'Resposta inválida do servidor.' }; 2852 } 2853 } 2854 return { status: r.status, okHttp: r.ok, data: data }; 2855 }); 2856 }); 2857 } 2858 2859 function openStarRatingModal(svcId, svcName, presetStars) { 2860 _starState = { svcId: svcId, svcName: svcName, stars: 0, verifyToken: null }; 2861 var modal = document.getElementById('starRatingModal'); 2862 if (!modal) return; 2863 document.getElementById('starSvcName').textContent = svcName || svcId; 2864 document.getElementById('starLabel').textContent = 'Toque numa estrela'; 2865 document.getElementById('starAuthSection').style.display = 'none'; 2866 document.getElementById('starCodeRow').style.display = 'none'; 2867 var msg = document.getElementById('starMsg'); 2868 if (msg) { 2869 msg.textContent = ''; 2870 msg.innerHTML = ''; 2871 msg.className = 'star-msg'; 2872 } 2873 var emailIn = document.getElementById('starEmail'); 2874 if (emailIn) emailIn.value = ''; 2875 var codeIn = document.getElementById('starCode'); 2876 if (codeIn) codeIn.value = ''; 2877 document.querySelectorAll('#starPicker button').forEach(function (b) { 2878 b.classList.remove('active'); 2879 }); 2880 var ps = Number(presetStars); 2881 if (!isNaN(ps) && ps >= 1 && ps <= 5) { 2882 _starState.stars = ps; 2883 document.querySelectorAll('#starPicker button').forEach(function (b) { 2884 b.classList.toggle('active', Number(b.getAttribute('data-star')) <= ps); 2885 }); 2886 document.getElementById('starLabel').textContent = STAR_LABELS[ps] || ''; 2887 document.getElementById('starAuthSection').style.display = ''; 2888 } 2889 modal.classList.add('open'); 2890 document.body.style.overflow = 'hidden'; 2891 _initStarGoogleButton(); 2892 } 2893 2894 function closeStarRatingModal() { 2895 var modal = document.getElementById('starRatingModal'); 2896 if (modal) modal.classList.remove('open'); 2897 document.body.style.overflow = ''; 2898 _starState = { svcId: null, svcName: '', stars: 0, verifyToken: null }; 2899 } 2900 2901 function _initStarGoogleButton() { 2902 var host = document.getElementById('starGoogleHost'); 2903 if (!host) return; 2904 var cid = state.googleClientId || ''; 2905 if (!cid) { host.innerHTML = '<p style="color:var(--muted);font-size:0.72rem;">Google não configurado.</p>'; return; } 2906 if (typeof google === 'undefined' || !google.accounts || !google.accounts.id) { 2907 setTimeout(_initStarGoogleButton, 250); 2908 return; 2909 } 2910 host.innerHTML = ''; 2911 try { 2912 google.accounts.id.initialize({ 2913 client_id: cid, 2914 callback: _onStarGoogleCredential, 2915 auto_select: false, 2916 }); 2917 google.accounts.id.renderButton(host, { 2918 type: 'standard', theme: 'filled_blue', size: 'large', text: 'continue_with', width: 280, 2919 }); 2920 } catch (e) { 2921 host.innerHTML = '<p style="color:var(--bad);font-size:0.72rem;">Erro ao carregar Google.</p>'; 2922 } 2923 } 2924 2925 function _onStarGoogleCredential(response) { 2926 var cred = response && response.credential; 2927 if (!cred || !_starState.svcId || !_starState.stars) { 2928 setStarMsgErr('Selecione estrelas primeiro.'); 2929 return; 2930 } 2931 setStarMsgNeutral('A registar avaliaçãoâ¦'); 2932 var origin = window.location.origin || ''; 2933 starRatingPostJson(origin + '/api/detector/star-rating/start', { 2934 service_id: _starState.svcId, 2935 stars: _starState.stars, 2936 channel: 'google', 2937 google_id_token: cred, 2938 }) 2939 .then(function (pack) { 2940 var d = pack.data || {}; 2941 if (pack.okHttp && d.ok) { 2942 setStarMsgOk(d.message_pt || 'Avaliação registada com sucesso!'); 2943 setTimeout(closeStarRatingModal, 2200); 2944 } else { 2945 setStarMsgErr(starRatingUserMessage(pack.status, d)); 2946 } 2947 }) 2948 .catch(function () { 2949 setStarMsgErr('Erro de rede.'); 2950 }); 2951 } 2952 2953 (function _starModalInit() { 2954 var modal = document.getElementById('starRatingModal'); 2955 if (!modal) return; 2956 modal.addEventListener('click', function (e) { if (e.target === modal) closeStarRatingModal(); }); 2957 document.getElementById('starClose').addEventListener('click', closeStarRatingModal); 2958 document.addEventListener('keydown', function (e) { 2959 if (e.key === 'Escape' && modal.classList.contains('open')) closeStarRatingModal(); 2960 }); 2961 document.querySelectorAll('#starPicker button').forEach(function (btn) { 2962 btn.addEventListener('click', function () { 2963 var val = Number(btn.getAttribute('data-star')); 2964 _starState.stars = val; 2965 document.querySelectorAll('#starPicker button').forEach(function (b) { 2966 b.classList.toggle('active', Number(b.getAttribute('data-star')) <= val); 2967 }); 2968 document.getElementById('starLabel').textContent = STAR_LABELS[val] || ''; 2969 document.getElementById('starAuthSection').style.display = ''; 2970 }); 2971 }); 2972 2973 document.getElementById('starSendEmail').addEventListener('click', function () { 2974 var emailEl = document.getElementById('starEmail'); 2975 var email = emailEl ? emailEl.value.trim() : ''; 2976 if (!email || email.indexOf('@') < 1) { 2977 setStarMsgErr('Informe um e-mail válido.'); 2978 return; 2979 } 2980 if (!_starState.svcId || !_starState.stars) return; 2981 setStarMsgNeutral('A enviar códigoâ¦'); 2982 var origin = window.location.origin || ''; 2983 starRatingPostJson(origin + '/api/detector/star-rating/start', { 2984 service_id: _starState.svcId, 2985 stars: _starState.stars, 2986 channel: 'email', 2987 email: email, 2988 }) 2989 .then(function (pack) { 2990 var d = pack.data || {}; 2991 if (pack.okHttp && d.ok && d.verify_token) { 2992 _starState.verifyToken = d.verify_token; 2993 document.getElementById('starCodeRow').style.display = ''; 2994 setStarMsgOk( 2995 d.message_pt || 2996 'Código enviado com sucesso! Verifique o seu e-mail e introduza os 4 dÃgitos.', 2997 ); 2998 var cin = document.getElementById('starCode'); 2999 if (cin) setTimeout(function () { cin.focus(); }, 80); 3000 } else { 3001 setStarMsgErr(starRatingUserMessage(pack.status, d)); 3002 } 3003 }) 3004 .catch(function () { 3005 setStarMsgErr('Erro de rede.'); 3006 }); 3007 }); 3008 3009 document.getElementById('starConfirmCode').addEventListener('click', function () { 3010 var codeEl = document.getElementById('starCode'); 3011 var code = codeEl ? codeEl.value.replace(/\D/g, '').slice(0, 4) : ''; 3012 if (code.length !== 4) { 3013 setStarMsgErr('Introduza os 4 dÃgitos.'); 3014 return; 3015 } 3016 if (!_starState.verifyToken) return; 3017 setStarMsgNeutral('A confirmarâ¦'); 3018 var origin = window.location.origin || ''; 3019 starRatingPostJson(origin + '/api/detector/star-rating/confirm', { 3020 verify_token: _starState.verifyToken, 3021 code: code, 3022 }) 3023 .then(function (pack) { 3024 var d = pack.data || {}; 3025 if (pack.okHttp && d.ok) { 3026 setStarMsgOk(d.message_pt || 'Avaliação registada com sucesso!'); 3027 setTimeout(closeStarRatingModal, 2200); 3028 } else { 3029 setStarMsgErr(starRatingUserMessage(pack.status, d)); 3030 } 3031 }) 3032 .catch(function () { 3033 setStarMsgErr('Erro de rede.'); 3034 }); 3035 }); 3036 })(); 3037 3038 function openCommunityStatsModal(serviceId, serviceName) { 3039 var modal = document.getElementById('communityStatsModal'); 3040 if (!modal) return; 3041 modal.classList.add('open'); 3042 document.body.style.overflow = 'hidden'; 3043 3044 var title = document.getElementById('csTitle'); 3045 if (title) title.textContent = (serviceName || serviceId) + ' â Reclamações'; 3046 3047 document.getElementById('csTotal').textContent = 'â¦'; 3048 document.getElementById('csAnon').textContent = 'â¦'; 3049 document.getElementById('csReg').textContent = 'â¦'; 3050 var pill = document.getElementById('csScorePill'); 3051 if (pill) { pill.textContent = 'â¦'; pill.className = 'cs-score-pill cs-score-pill--good'; } 3052 var fbList = document.getElementById('csFeedbackList'); 3053 if (fbList) fbList.innerHTML = '<div class="cs-empty">Carregandoâ¦</div>'; 3054 3055 if (_csChart) { _csChart.destroy(); _csChart = null; } 3056 3057 fetch('/api/detector/service/' + encodeURIComponent(serviceId) + '/community-stats?days=30') 3058 .then(function (r) { return r.json(); }) 3059 .then(function (data) { 3060 if (!data || !data.success) { 3061 if (fbList) fbList.innerHTML = '<div class="cs-empty">Erro ao carregar dados.</div>'; 3062 return; 3063 } 3064 document.getElementById('csTotal').textContent = String(data.total_reports || 0); 3065 document.getElementById('csAnon').textContent = String(data.anonymous_count || 0); 3066 document.getElementById('csReg').textContent = String(data.registered_count || 0); 3067 3068 var ts = Number(data.trust_score); 3069 if (pill && !isNaN(ts)) { 3070 pill.textContent = ts.toFixed(1) + ' / 100'; 3071 pill.className = 'cs-score-pill ' + (ts >= 90 ? 'cs-score-pill--good' : ts >= 80 ? 'cs-score-pill--warn' : 'cs-score-pill--bad'); 3072 } 3073 3074 renderCsChart(data.daily_counts || []); 3075 renderCsFeedback(data.public_feedback || [], serviceId); 3076 }) 3077 .catch(function () { 3078 if (fbList) fbList.innerHTML = '<div class="cs-empty">Erro de rede.</div>'; 3079 }); 3080 } 3081 3082 function renderCsChart(daily) { 3083 var canvas = document.getElementById('csChart'); 3084 if (!canvas) return; 3085 var ctx = canvas.getContext('2d'); 3086 3087 var today = new Date(); 3088 var labels = []; 3089 var counts = {};
3090 daily.forEach(function (d) { counts[d.date] = d.count; }); 3091 var data = []; 3092 for (var i = 29; i >= 0; i--) { 3093 var dt = new Date(today); 3094 dt.setDate(dt.getDate() - i); 3095 var key = dt.toISOString().slice(0, 10); 3096 labels.push(key.slice(5)); 3097 data.push(counts[key] || 0); 3098 } 3099 3100 var gradient = ctx.createLinearGradient(0, 0, 0, canvas.parentElement.clientHeight || 160); 3101 gradient.addColorStop(0, 'rgba(239,68,68,0.5)'); 3102 gradient.addColorStop(1, 'rgba(239,68,68,0.02)'); 3103 3104 _csChart = new Chart(ctx, { 3105 type: 'bar', 3106 data: { 3107 labels: labels, 3108 datasets: [{ 3109 label: 'Reclamações', 3110 data: data, 3111 backgroundColor: gradient, 3112 borderColor: 'rgba(239,68,68,0.8)', 3113 borderWidth: 1, 3114 borderRadius: 3, 3115 hoverBackgroundColor: 'rgba(239,68,68,0.7)', 3116 }] 3117 }, 3118 options: { 3119 responsive: true, 3120 maintainAspectRatio: false, 3121 animation: { duration: 400 }, 3122 plugins: { 3123 legend: { display: false }, 3124 tooltip: { 3125 backgroundColor: 'rgba(255,255,255,0.97)', 3126 titleColor: '#0369a1', 3127 bodyColor: '#0f172a', 3128 borderColor: 'rgba(239,68,68,0.4)', 3129 borderWidth: 1, 3130 cornerRadius: 8, 3131 padding: 10, 3132 callbacks: { 3133 title: function (items) { return items[0] ? items[0].label : ''; }, 3134 label: function (item) { return item.raw + ' reclamação' + (item.raw !== 1 ? 'ões' : ''); } 3135 } 3136 } 3137 }, 3138 scales: { 3139 x: { 3140 grid: { display: false }, 3141 ticks: { color: 'rgba(71,85,105,0.85)', font: { size: 9 }, maxRotation: 45 } 3142 }, 3143 y: { 3144 beginAtZero: true, 3145 grace: '10%', 3146 ticks: { color: 'rgba(71,85,105,0.85)', font: { size: 10 }, precision: 0 }, 3147 grid: { color: 'rgba(148,163,184,0.2)' } 3148 } 3149 } 3150 } 3151 }); 3152 } 3153 3154 function renderCsFeedback(fb, serviceId) { 3155 var el = document.getElementById('csFeedbackList'); 3156 if (!el) return; 3157 var titleEl = document.getElementById('csFeedbackTitle'); 3158 if (!fb || !fb.length) { 3159 el.innerHTML = 3160 '<div class="svc-feedback-empty">' + 3161 '<span class="svc-feedback-empty__ico">ð¬</span>' + 3162 '<span class="svc-feedback-empty__msg">Nenhum comentário público ainda para este serviço.</span>' + 3163 '</div>'; 3164 if (titleEl) titleEl.innerHTML = '<span class="svc-feed-ico">ð¬</span> Comentários públicos'; 3165 return; 3166 } 3167 var sid = serviceId || ''; 3168 if (titleEl) titleEl.innerHTML = '<span class="svc-feed-ico">ð¬</span> Comentários públicos <span style="font-weight:400;font-size:.82em;color:var(--muted)">(' + fb.length + ')</span>'; 3169 var html = fb.map(function (it) { 3170 var when = it.created_at ? esc(it.created_at.slice(0, 16).replace('T', ' ')) : 'â'; 3171 var auth = esc(it.author_label || 'Anónimo'); 3172 var reason = esc(it.reason_label_pt || ''); 3173 var com = it.comment ? '<div class="svc-feed-comment">' + esc(it.comment) + '</div>' : ''; 3174 var reasonTag = reason ? '<span style="display:inline-block;padding:2px 8px;border-radius:999px;background:rgba(251,191,36,.12);color:#b45309;font-size:.76rem;font-weight:600;margin-top:4px">' + reason + '</span>' : ''; 3175 var replyBtn = ''; 3176 if (it.id && sid) { 3177 replyBtn = 3178 '<div class="svc-fb-actions"><button type="button" class="det-fb-reply-btn svc-feed-open-report" data-det-reply-svc="' + 3179 esc(String(sid)) + 3180 '" data-det-reply-parent="' + 3181 esc(String(it.id)) + 3182 '" aria-label="' + 3183 esc(__('js_feedback_reply_aria')) + 3184 '">â© ' + 3185 esc(__('js_feedback_reply')) + 3186 '</button></div>'; 3187 } 3188 return ( 3189 '<article class="svc-feedback-item svc-feed-card">' + 3190 '<div class="svc-feed-head"><div class="svc-feed-avatar">' + 3191 esc((auth || 'A').slice(0, 1).toUpperCase()) + 3192 '</div><div class="svc-feed-head__txt"><div class="svc-feed-meta"><strong>' + 3193 auth + 3194 '</strong> · ' + 3195 when + 3196 '</div>
3196' + reasonTag + '</div></div>' + 3197 com + 3198 renderReactionButtons(it) + 3199 renderFeedRepliesHtml(it.replies) + 3200 replyBtn + 3201 '</article>' 3202 ); 3203 }).join(''); 3204 el.innerHTML = '<div class="svc-feed-list">' + html + '</div>'; 3205 } 3206 3207 function closeCommunityStatsModal() { 3208 var modal = document.getElementById('communityStatsModal'); 3209 if (!modal) return; 3210 modal.classList.remove('open'); 3211 document.body.style.overflow = ''; 3212 if (_csChart) { _csChart.destroy(); _csChart = null; } 3213 } 3214 3215 (function _csModalInit() { 3216 var modal = document.getElementById('communityStatsModal'); 3217 if (!modal) return; 3218 var closeBtn = document.getElementById('csClose'); 3219 if (closeBtn) closeBtn.addEventListener('click', closeCommunityStatsModal); 3220 modal.addEventListener('click', function (e) { 3221 if (e.target === modal) closeCommunityStatsModal(); 3222 }); 3223 document.addEventListener('keydown', function (e) { 3224 if (e.key === 'Escape' && modal.classList.contains('open')) closeCommunityStatsModal(); 3225 }); 3226 })(); 3227 3228 function bindTileMoreButtons(root) { 3229 if (!root) return; 3230 root.querySelectorAll('.tile-more-btn').forEach(function (btn) { 3231 btn.addEventListener('click', function (e) { 3232 e.preventDefault(); 3233 e.stopPropagation(); 3234 var id = 3235 btn.getAttribute('data-more-sid') || 3236 (function () { 3237 var tile = btn.closest('.tile'); 3238 return tile && tile.getAttribute('data-svc-id'); 3239 })(); 3240 if (id) openModal(id); 3241 }); 3242 }); 3243 root.querySelectorAll('.list-more-btn').forEach(function (btn) { 3244 btn.addEventListener('click', function (e) { 3245 e.preventDefault(); 3246 e.stopPropagation(); 3247 var id = btn.getAttribute('data-more-sid'); 3248 if (id) openModal(id); 3249 }); 3250 }); 3251 } 3252 3253 function getCrMetaPayload() { 3254 var checked = document.querySelector('#crReasonList input[name="crReason"]:checked'); 3255 var rc = checked ? String(checked.value || 'other') : 'other'; 3256 var ta = document.getElementById('crComment'); 3257 var cmt = ta ? String(ta.value || '').trim() : ''; 3258 var pubEl = document.getElementById('crPublic'); 3259 var anonEl = document.getElementById('crAnonymous'); 3260 var dnEl = document.getElementById('crDisplayName'); 3261 var bcastEl = document.getElementById('crBroadcast'); 3262 var bcastIdEl = document.getElementById('crBroadcastIdentify'); 3263 var pub = !!(pubEl && pubEl.checked); 3264 var anon = anonEl ? !!anonEl.checked : true; 3265 var dn = dnEl ? String(dnEl.value || '').trim() : ''; 3266 var bcast = bcastEl ? !!bcastEl.checked : true; 3267 var bcastId = bcastIdEl ? !!bcastIdEl.checked : false; 3268 var out = { 3269 reason_code: rc, 3270 comment: cmt || null, 3271 is_public: pub, 3272 is_anonymous: anon, 3273 display_name: dn || null, 3274 broadcast_notify: bcast, 3275 broadcast_identify: bcastId, 3276 parent_report_id: state.crParentReportId || null, 3277 }; 3278 if (state.crParentReportId) { 3279 out.reason_code = 'other'; 3280 out.is_public = true; 3281 out.broadcast_notify = false; 3282 } 3283 return out; 3284 } 3285 3286 var _crModTimer = null; 3287 var _crModSeq = 0; 3288 3289 function crModerationPayload() { 3290 var p = getCrMetaPayload(); 3291 var dn = null; 3292 if (!p.is_anonymous || p.broadcast_identify) { 3293 dn = p.display_name || null; 3294 } 3295 return { 3296 comment: p.comment || null, 3297 display_name: dn, 3298 is_reply: !!p.parent_report_id, 3299 }; 3300 } 3301 3302 function setCrModerationHint(text, extraClass) { 3303 var el = document.getElementById('crModerationHint'); 3304 if (!el) return; 3305 if (!text) { 3306 el.textContent = ''; 3307 el.className = 'cr-mod-hint';
3308 el.hidden = true; 3309 return; 3310 } 3311 el.textContent = text; 3312 el.className = 'cr-mod-hint' + (extraClass ? ' ' + extraClass : ''); 3313 el.hidden = false; 3314 } 3315 3316 function scheduleCommunityCommentModeration() { 3317 var sm = document.getElementById('crStepMeta'); 3318 if (!sm || sm.style.display === 'none') return; 3319 if (_crModTimer) clearTimeout(_crModTimer); 3320 setCrModerationHint(__('js_cr_mod_analyzing'), ''); 3321 var seq = ++_crModSeq; 3322 _crModTimer = setTimeout(function () { 3323 _crModTimer = null; 3324 var origin = ''; 3325 try { 3326 origin = window.location.origin || ''; 3327 } catch (eO) {} 3328 fetch(origin + '/api/detector/community-comment/moderate', { 3329 method: 'POST', 3330 headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, 3331 body: JSON.stringify(crModerationPayload()), 3332 credentials: 'same-origin', 3333 }) 3334 .then(function (r) { 3335 return r.json(); 3336 }) 3337 .then(function (data) { 3338 if (seq !== _crModSeq) return; 3339 if (!data || !data.ok || !data.moderation) { 3340 setCrModerationHint('', ''); 3341 return; 3342 } 3343 var loc = typeof window.detectorGetLocale === 'function' ? window.detectorGetLocale() : 'pt'; 3344 var m = data.moderation; 3345 var txt = loc === 'en' && m.summary_en ? m.summary_en : m.summary_pt || ''; 3346 setCrModerationHint(txt, m.allowed ? 'cr-mod-hint--ok' : 'cr-mod-hint--bad'); 3347 }) 3348 .catch(function () { 3349 if (seq !== _crModSeq) return; 3350 setCrModerationHint('', ''); 3351 }); 3352 }, 520); 3353 } 3354 3355 function runCommunityCommentModerationSync() { 3356 return new Promise(function (resolve) { 3357 var origin = ''; 3358 try { 3359 origin = window.location.origin || ''; 3360 } catch (e1) {} 3361 fetch(origin + '/api/detector/community-comment/moderate', { 3362 method: 'POST', 3363 headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, 3364 body: JSON.stringify(crModerationPayload()), 3365 credentials: 'same-origin', 3366 }) 3367 .then(function (r) { 3368 return r.json(); 3369 }) 3370 .then(function (data) { 3371 if (!data || !data.ok) { 3372 resolve({ allowed: true, moderation: null }); 3373 return; 3374 } 3375 resolve({ allowed: !!data.allowed, moderation: data.moderation || null }); 3376 }) 3377 .catch(function () { 3378 resolve({ allowed: true, moderation: null }); 3379 }); 3380 }); 3381 } 3382 3383 function renderCrReasonList() { 3384 var host = document.getElementById('crReasonList'); 3385 if (!host) return; 3386 if (state.crParentReportId) { 3387 host.innerHTML = 3388 '<input type="radio" name="crReason" value="other" checked tabindex="-1" aria-hidden="true" style="position:absolute;opacity:0;width:0;height:0;pointer-events:none;" />'; 3389 return; 3390 } 3391 var list = state.reportReasons || []; 3392 if (!list.length) { 3393 host.innerHTML = 3394 '<label class="cr-reason-item"><input type="radio" name="crReason" value="other" checked />' + 3395 '<span>Outro motivo<small>Descreva no comentário, se quiser.</small></span></label>'; 3396 return; 3397 } 3398 host.innerHTML = list 3399 .map(function (r, i) { 3400 return ( 3401 '<label class="cr-reason-item"><input type="radio" name="crReason" value="' + 3402 esc(r.id) + 3403 '" ' + 3404 (i === 0 ? 'checked' : '') + 3405 ' /><span>' + 3406 esc(r.label_pt || r.id) + 3407 '<small>' + 3408 esc(r.hint_pt || '') + 3409 '</small></span></label>' 3410 ); 3411 }) 3412 .join(''); 3413 } 3414 3415 function initCrGoogleButton() { 3416 var host = document.getElementById('crGoogleBtnHost'); 3417 if (!host || !state.googleClientId) { 3418 if (host) 3419 host.innerHTML = 3420 '<p style="color:var(--muted);font-size:0.75rem;">Google não configurado no servidor.</p>'; 3421 return; 3422 } 3423 if (typeof google === 'undefined' || !google.accounts || !google.accounts.id) { 3424 setTimeout(initCrGoogleButton, 250); 3425 return; 3426 } 3427 host.innerHTML = ''; 3428 try { 3429 google.accounts.id.initialize({ 3430 client_id: state.googleClientId, 3431 callback: onCrGoogleCredential,
3432 auto_select: false, 3433 }); 3434 google.accounts.id.renderButton(host, { 3435 type: 'standard', 3436 theme: 'filled_blue', 3437 size: 'large', 3438 text: 'continue_with', 3439 width: 320, 3440 }); 3441 } catch (e) { 3442 host.innerHTML = '<p style="color:var(--bad);font-size:0.75rem;">Não foi possÃvel carregar o botão Google.</p>'; 3443 } 3444 } 3445 3446 function onCrGoogleCredential(response) { 3447 var msg = document.getElementById('crMsg'); 3448 var sid = state.crSvcId; 3449 var cred = response && response.credential; 3450 if (!sid || !cred) return; 3451 var origin = window.location.origin || ''; 3452 var body = Object.assign( 3453 { 3454 service_id: sid, 3455 channel: 'google', 3456 google_id_token: cred, 3457 }, 3458 getCrMetaPayload(), 3459 ); 3460 if (msg) { 3461 msg.textContent = 'A enviarâ¦'; 3462 msg.className = 'cr-msg'; 3463 } 3464 fetch(origin + '/api/detector/community-report/start', { 3465 method: 'POST', 3466 headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, 3467 body: JSON.stringify(body), 3468 credentials: 'same-origin', 3469 }) 3470 .then(function (r) { 3471 return r.json().then(function (data) { 3472 return { r: r, data: data }; 3473 }); 3474 }) 3475 .then(function (pack) { 3476 var d = pack.data || {}; 3477 if (msg) { 3478 msg.className = 'cr-msg ' + (pack.r.ok && d.ok ? 'ok' : 'err'); 3479 var locG = 3480 typeof window.detectorGetLocale === 'function' ? window.detectorGetLocale() : 'pt'; 3481 var apiG = 3482 locG === 'en' && d.message_en 3483 ? d.message_en 3484 : d.message_pt || (pack.r.ok ? 'OK.' : 'Falhou.'); 3485 msg.textContent = apiG; 3486 } 3487 if (pack.r.ok && d.ok) { 3488 if (!state.crParentReportId && !d.moderation_pending) { 3489 bumpCommunityVoteLocal(sid); 3490 mountShelves(); 3491 mountProblemsCatalog(); 3492 mountCommunityRanking(true); 3493 } 3494 setTimeout(closeCommunityReportModal, 2000); 3495 } 3496 }) 3497 .catch(function () { 3498 if (msg) { 3499 msg.className = 'cr-msg err'; 3500 msg.textContent = 'Erro de rede.'; 3501 } 3502 }); 3503 } 3504 3505 function reactionLabel(reaction) { 3506 var map = { like: 'Curtir', love: 'Amei', wow: 'Uau', fire: 'Top' }; 3507 return map[reaction] || reaction; 3508 } 3509 3510 function renderReactionButtons(item) { 3511 var reactions = ['like', 'love', 'wow', 'fire']; 3512 var counts = item && item.reactions ? item.reactions : {}; 3513 var active = item && item.viewer_reaction ? String(item.viewer_reaction) : ''; 3514 return ( 3515 '<div class="svc-feed-reactions">' + 3516 reactions 3517 .map(function (key) { 3518 var n = Number(counts[key]) || 0; 3519 return ( 3520 '<button type="button" class="svc-feed-react-btn' + 3521 (active === key ? ' is-active' : '') + 3522 '" data-det-react-report="' + 3523 esc(String(item.id || '')) + 3524 '" data-det-react-kind="' + 3525 esc(key) + 3526 '" aria-label="' + 3527 esc(reactionLabel(key)) + 3528 '">' + 3529 '<span class="svc-feed-react-ico" aria-hidden="true">' + 3530 (key === 'like' ? 'ð' : key === 'love' ? 'â¤ï¸' : key === 'wow' ? 'ð®' : 'ð¥') + 3531 '</span>' + 3532 '<span class="svc-feed-react-lbl">' +
3533 esc(reactionLabel(key)) + 3534 '</span>' + 3535 '<span class="svc-feed-react-n">' + 3536 esc(String(n)) + 3537 '</span>' + 3538 '</button>' 3539 ); 3540 }) 3541 .join('') + 3542 '</div>' 3543 ); 3544 } 3545 3546 function renderFeedRepliesHtml(replies) { 3547 var list = Array.isArray(replies) ? replies : []; 3548 if (!list.length) return ''; 3549 return ( 3550 '<div class="det-fb-replies svc-feed-replies">' + 3551 list 3552 .map(function (rp) { 3553 var w2 = rp.created_at ? esc(rp.created_at.slice(0, 16).replace('T', ' ')) : ''; 3554 var au = esc(rp.author_label || 'Anónimo'); 3555 var cm = rp.comment ? '<div class="svc-feed-comment">' + esc(rp.comment) + '</div>' : ''; 3556 return ( 3557 '<div class="det-fb-reply svc-feed-reply">' + 3558 '<div class="svc-feed-meta">' + 3559 au + 3560 (w2 ? ' · ' + w2 : '') + 3561 '</div>' + 3562 cm + 3563 renderReactionButtons(rp) + 3564 '</div>' 3565 ); 3566 }) 3567 .join('') + 3568 '</div>' 3569 ); 3570 } 3571 3572 function filteredCommunityFeedRows(fb) { 3573 var rows = Array.isArray(fb) ? fb.slice() : []; 3574 if (state.communityFeedFilter === 'with_comments') { 3575 rows = rows.filter(function (it) { 3576 return !!((it && it.comment) || ((it.replies || []).length > 0)); 3577 }); 3578 } 3579 return rows; 3580 } 3581 3582 function bindPublicFeedbackComposer(root, sid) { 3583 if (!root || !sid) return; 3584 var ta = root.querySelector('#svcFeedComposerInput'); 3585 var sendBtn = root.querySelector('#svcFeedComposerSend'); 3586 var hint = root.querySelector('#svcFeedComposerHint'); 3587 var chips = root.querySelectorAll('.svc-feed-filter-btn'); 3588 chips.forEach(function (btn) { 3589 btn.addEventListener('click', function () { 3590 state.communityFeedFilter = btn.getAttribute('data-feed-filter') || 'all'; 3591 renderPublicFeedbackBlock(state.modalDetailData, sid); 3592 }); 3593 }); 3594 root.querySelectorAll('.svc-feed-open-report,.det-fb-reply-btn').forEach(function (btn) { 3595 btn.addEventListener('click', function () { 3596 var parentId = btn.getAttribute('data-det-reply-parent'); 3597 openCommunityReportModal( 3598 sid, 3599 parentId ? parseInt(parentId, 10) : null, 3600 ); 3601 }); 3602 }); 3603 root.querySelectorAll('.svc-feed-react-btn').forEach(function (btn) { 3604 btn.addEventListener('click', function () { 3605 var reportId = btn.getAttribute('data-det-react-report'); 3606 var kind = btn.getAttribute('data-det-react-kind'); 3607 if (!reportId || !kind) return; 3608 fetch((window.location.origin || '') + '/api/detector/community-reaction', { 3609 method: 'POST', 3610 headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, 3611 credentials: 'same-origin', 3612 body: JSON.stringify({ 3613 report_id: parseInt(reportId, 10), 3614 reaction: kind, 3615 anon_token: detectorCommunityAnonToken(), 3616 }), 3617 }) 3618 .then(function (r) { return r.json().then(function (d) { return { ok: r.ok, data: d || {} }; }); }) 3619 .then(function (pack) { 3620 if (!pack.ok || !state.modalDetailData || !Array.isArray(state.modalDetailData.public_feedback)) return; 3621 state.modalDetailData.public_feedback.forEach(function (it) { 3622 function applyOne(row) { 3623 if (!row || String(row.id) !== String(reportId)) return; 3624 row.reactions = pack.data.reactions || {}; 3625 row.reaction_total = pack.data.reaction_total || 0; 3626 row.viewer_reaction = pack.data.viewer_reaction || null; 3627 } 3628 applyOne(it);
3629 (it.replies || []).forEach(applyOne); 3630 }); 3631 renderPublicFeedbackBlock(state.modalDetailData, sid); 3632 }) 3633 .catch(function () {}); 3634 }); 3635 }); 3636 function triggerSend() { 3637 var text = ta ? String(ta.value || '').trim() : ''; 3638 if (!text) return; 3639 state.crParentReportId = null; 3640 openCommunityReportModal(sid, null); 3641 setTimeout(function () { 3642 var c = document.getElementById('crComment'); 3643 var pub = document.getElementById('crPublic'); 3644 if (c) c.value = text; 3645 if (pub) pub.checked = true; 3646 if (hint) hint.textContent = 'Para publicar, confirme seu e-mail ou Google.'; 3647 }, 30); 3648 } 3649 if (sendBtn) sendBtn.addEventListener('click', triggerSend); 3650 if (ta) { 3651 ta.addEventListener('keydown', function (e) { 3652 if (e.key === 'Enter' && !e.shiftKey) { 3653 e.preventDefault(); 3654 triggerSend(); 3655 } 3656 }); 3657 ta.addEventListener('input', function () { 3658 ta.style.height = 'auto'; 3659 ta.style.height = Math.min(ta.scrollHeight, 120) + 'px'; 3660 }); 3661 } 3662 } 3663 3664 function renderPublicFeedbackBlock(data, modalSvcId) { 3665 var el = document.getElementById('svcPublicFeedback'); 3666 if (!el) return; 3667 if (!data || !data.success) { 3668 el.style.display = 'none'; 3669 el.innerHTML = ''; 3670 return; 3671 } 3672 state.modalDetailData = data; 3673 var fb = filteredCommunityFeedRows(data && data.public_feedback); 3674 var ts = data && data.trust_score; 3675 var sid = modalSvcId || state.modalSvcId || ''; 3676 var tsStr = 3677 ts != null && !isNaN(Number(ts)) 3678 ? '<p class="svc-feedback-meta">Nota comunidade: <strong>' + esc(String(Number(ts).toFixed(1))) + '</strong> / 100 · relatos verificados com pesos por tipo</p>' 3679 : ''; 3680 var fbCount = fb ? fb.length : 0; 3681 var headBlock = 3682 '<div class="svc-feed-head-block">' + 3683 '<div class="svc-feed-composer__top">' + 3684 '<div><h4 class="svc-feed-section-title"><span class="svc-feed-ico">ð¬</span> ' + 3685 esc(__('js_comments_app_title')) + 3686 (fbCount > 0 3687 ? ' <span style="font-weight:400;font-size:.82em;color:var(--muted)">(' + fbCount + ')</span>' 3688 : '') + 3689 '</h4>' + 3690 tsStr + 3691 '</div>' + 3692 (fbCount > 0 3693 ? '<div class="svc-feed-filters">' + 3694 '<button type="button" class="svc-feed-filter-btn' + 3695 (state.communityFeedFilter === 'all' ? ' is-active' : '') + 3696 '" data-feed-filter="all">ð Tudo</button>' + 3697 '<button type="button" class="svc-feed-filter-btn' + 3698 (state.communityFeedFilter === 'with_comments' ? ' is-active' : '') + 3699 '" data-feed-filter="with_comments">ð¬ Comentários</button>' + 3700 '</div>' 3701 : '') + 3702 '</div></div>'; 3703 var composerFoot = 3704 '<div class="svc-feed-composer">' + 3705 '<div class="svc-feed-composer__box">' + 3706 '<textarea id="svcFeedComposerInput" class="svc-feed-composer__input" rows="1" maxlength="2000" placeholder="Escreva um comentário..."></textarea>' + 3707 '<button type="button" id="svcFeedComposerSend" class="svc-feed-composer__send svc-feed-composer__send--pulse" aria-label="Enviar">â¤</button>' + 3708 '</div>' + 3709 '<span id="svcFeedComposerHint" class="svc-feed-composer__hint">ð Reaja sem login · Para comentar, pedimos verificação rápida por e-mail ou Google</span>' + 3710 '</div>'; 3711 if (!fb || !fb.length) { 3712 el.style.display = 'block'; 3713 el.innerHTML = 3714 headBlock + 3715 '<div class="svc-feedback-empty">' + 3716 '<span class="svc-feedback-empty__ico">ð¬</span>' + 3717 '<span class="svc-feedback-empty__msg">Nenhum comentário ainda. Seja o primeiro a relatar!</span>' + 3718 '<span class="svc-feedback-empty__cta">ð Use a caixa abaixo para partilhar a sua experiência</span>' + 3719 '</div>' + 3720 composerFoot; 3721 bindPublicFeedbackComposer(el, sid); 3722 return; 3723 } 3724 var rows = fb 3725 .map(function (it) { 3726 var when = it.created_at ? esc(it.created_at.slice(0, 16).replace('T', ' ')) : 'â'; 3727 var auth = esc(it.author_label || 'Anónimo'); 3728 var reason = esc(it.reason_label_pt || ''); 3729 var com = it.comment ? '<div class="svc-feed-comment">' + esc(it.comment) + '</div>' : ''; 3730 var reasonTag = reason ? '<span style="display:inline-block;padding:2px 8px;border-radius:999px;background:rgba(251,191,36,.12);color:#b45309;font-size:.76rem;font-weight:600;margin-top:4px">' + reason + '</span>' : ''; 3731 var replyBtn = ''; 3732 if (it.id && sid) { 3733 replyBtn = 3734 '<div class="svc-fb-actions">' + 3735 '<button type="button" class="det-fb-reply-btn svc-feed-open-report" data-det-reply-svc="' + 3736 esc(String(sid)) + 3737 '" data-det-reply-parent="' + 3738 esc(String(it.id)) + 3739 '" aria-label="' + 3740 esc(__('js_feedback_reply_aria')) + 3741 '">â© ' + 3742 esc(__('js_feedback_reply')) + 3743 '</button></div>'; 3744 } 3745 return ( 3746 '<article class="svc-feedback-item svc-feed-card">' + 3747 '<div class="svc-feed-head"><div class="svc-feed-avatar">' + 3748 esc((auth || 'A').slice(0, 1).toUpperCase()) + 3749 '</div><div class="svc-feed-head__txt"><div class="svc-feed-meta"><strong>' + 3750 auth + 3751 '</strong> · ' + 3752 when + 3753 '</div>
3753' + reasonTag + '</div></div>' + 3754 com + 3755 renderReactionButtons(it) + 3756 renderFeedRepliesHtml(it.replies) + 3757 replyBtn + 3758 '</article>' 3759 ); 3760 }) 3761 .join(''); 3762 el.style.display = 'block'; 3763 el.innerHTML = headBlock + '<div class="svc-feed-list">' + rows + '</div>' + composerFoot; 3764 bindPublicFeedbackComposer(el, sid); 3765 } 3766 3767 function applyCrExplainerLinkIntoParagraph(pEl) { 3768 if (!pEl) return; 3769 pEl.appendChild(document.createTextNode(' ')); 3770 var a = document.createElement('a'); 3771 a.href = '#'; 3772 a.className = 'cr-open-det-explainer'; 3773 a.textContent = __('cr_explainer_fab_link'); 3774 pEl.appendChild(a); 3775 } 3776 3777 function applyCrQuestionLead(isReply) { 3778 var q = document.getElementById('crQuestion'); 3779 if (!q) return; 3780 if (isReply) { 3781 q.textContent = __('cr_reply_step_question'); 3782 return; 3783 } 3784 q.textContent = ''; 3785 var span = document.createElement('span'); 3786 span.textContent = __('cr_question_main'); 3787 q.appendChild(span); 3788 applyCrExplainerLinkIntoParagraph(q); 3789 } 3790 3791 function applyCrVerifyLeadText() { 3792 var p = document.getElementById('crVerifyLead'); 3793 if (!p) return; 3794 p.textContent = __('cr_verify_lead_main'); 3795 } 3796 3797 function resetCommunityReportModalUi() { 3798 var s0 = document.getElementById('crStepConfirm'); 3799 var sm = document.getElementById('crStepMeta'); 3800 var s2 = document.getElementById('crStepVerify'); 3801 var msg = document.getElementById('crMsg'); 3802 var ph = document.getElementById('crPhone'); 3803 var em = document.getElementById('crEmail'); 3804 var cd = document.getElementById('crCode'); 3805 var panel = document.getElementById('crCodePanel'); 3806 var ch = document.getElementById('crChannel'); 3807 var pw = document.getElementById('crPhoneWrap'); 3808 var ew = document.getElementById('crEmailWrap'); 3809 var gw = document.getElementById('crGoogleWrap'); 3810 var sendBtn = document.getElementById('crSendCode'); 3811 var cmt = document.getElementById('crComment'); 3812 var pub = document.getElementById('crPublic'); 3813 var anon = document.getElementById('crAnonymous'); 3814 var dn = document.getElementById('crDisplayName'); 3815 var dnw = document.getElementById('crDisplayNameWrap'); 3816 var gHost = document.getElementById('crGoogleBtnHost'); 3817 state.crVerifyToken = null; 3818 if (s0) s0.style.display = '';
3819 if (sm) sm.style.display = 'none'; 3820 if (s2) s2.style.display = 'none'; 3821 if (msg) { 3822 msg.textContent = ''; 3823 msg.className = 'cr-msg'; 3824 } 3825 if (ph) ph.value = ''; 3826 if (em) em.value = ''; 3827 if (cd) cd.value = ''; 3828 if (cmt) cmt.value = ''; 3829 if (pub) pub.checked = false; 3830 if (anon) anon.checked = true; 3831 if (dn) dn.value = ''; 3832 if (dnw) dnw.style.display = 'none'; 3833 if (panel) panel.style.display = 'none'; 3834 if (ch) ch.value = 'google'; 3835 if (gHost) gHost.innerHTML = ''; 3836 syncCrChannelUi(); 3837 var crInline = document.getElementById('crMetaInlineErr'); 3838 if (crInline) { 3839 crInline.textContent = ''; 3840 crInline.style.display = 'none'; 3841 } 3842 if (_crModTimer) { 3843 clearTimeout(_crModTimer); 3844 _crModTimer = null; 3845 } 3846 _crModSeq += 1; 3847 setCrModerationHint('', ''); 3848 var bcast = document.getElementById('crBroadcast'); 3849 var bcastId = document.getElementById('crBroadcastIdentify'); 3850 var bcastWrap = document.getElementById('crBroadcastNameWrap'); 3851 if (bcast) bcast.checked = true; 3852 if (bcastId) bcastId.checked = false; 3853 if (bcastWrap) bcastWrap.style.display = 'none'; 3854 if (pub) pub.disabled = false; 3855 var bcastRow = _crBroadcastRow(); 3856 if (bcastRow) bcastRow.style.display = ''; 3857 var lead = document.getElementById('crMetaReasonLead'); 3858 var list = document.getElementById('crReasonList'); 3859 if (lead) lead.style.display = ''; 3860 if (list) list.style.display = ''; 3861 ensureCrUiDefaults(); 3862 applyCrQuestionLead(false); 3863 applyCrVerifyLeadText(); 3864 var mcl = document.getElementById('crMetaCommentLabel'); 3865 if (mcl) mcl.textContent = _crUiDefaults.commentLbl; 3866 state.crParentReportId = null; 3867 } 3868 3869 function syncCrChannelUi() { 3870 var sel = document.getElementById('crChannel'); 3871 var pw = document.getElementById('crPhoneWrap'); 3872 var ew = document.getElementById('crEmailWrap'); 3873 var gw = document.getElementById('crGoogleWrap'); 3874 var sendBtn = document.getElementById('crSendCode'); 3875 if (!sel || !ew) return; 3876 var v = sel.value || 'google'; 3877 if (pw) pw.style.display = 'none'; 3878 ew.style.display = v === 'email' ? '' : 'none'; 3879 if (gw) gw.style.display = v === 'google' ? '' : 'none'; 3880 if (sendBtn) sendBtn.style.display = v === 'google' ? 'none' : ''; 3881 if (v !== 'google') { 3882 var cp = document.getElementById('crCodePanel'); 3883 if (cp) cp.style.display = 'none'; 3884 } else { 3885 initCrGoogleButton(); 3886 } 3887 } 3888 3889 function resetCommunityReportModal() { 3890 resetCommunityReportModalUi(); 3891 state.crSvcId = null; 3892 state.crParentReportId = null; 3893 } 3894 3895 function closeCommunityReportModal() { 3896 var m = document.getElementById('communityReportModal'); 3897 if (m) m.classList.remove('open'); 3898 resetCommunityReportModal(); 3899 } 3900 3901 var _crUiDefaults = { commentLbl: '' }; 3902 function ensureCrUiDefaults() { 3903 if (_crUiDefaults.commentLbl) return; 3904 var mcl = document.getElementById('crMetaCommentLabel'); 3905 _crUiDefaults.commentLbl = mcl ? mcl.textContent : ''; 3906 } 3907 function _crBroadcastRow() { 3908 var b = document.getElementById('crBroadcast'); 3909 return b && b.closest ? b.closest('.cr-check-row') : null; 3910 } 3911 function applyCrReplyMode(isReply) { 3912 ensureCrUiDefaults(); 3913 var lead = document.getElementById('crMetaReasonLead'); 3914 var list = document.getElementById('crReasonList'); 3915 var pub = document.getElementById('crPublic'); 3916 var bcastRow = _crBroadcastRow(); 3917 var mcl = document.getElementById('crMetaCommentLabel'); 3918 if (lead) lead.style.display = isReply ? 'none' : ''; 3919 if (list) list.style.display = isReply ? 'none' : ''; 3920 if (pub) { 3921 pub.disabled = !!isReply; 3922 if (isReply) pub.checked = true; 3923 } 3924 if (bcastRow) bcastRow.style.display = isReply ? 'none' : ''; 3925 var bcastIdRow = document.getElementById('crBroadcastNameWrap'); 3926 if (bcastIdRow) bcastIdRow.style.display = 'none'; 3927 applyCrQuestionLead(!!isReply); 3928 if (mcl) mcl.textContent = isReply ? __('cr_reply_comment_label') : _crUiDefaults.commentLbl; 3929 } 3930 3931 function openCommunityReportModal(svcId, parentReportId) { 3932 var svc = findSvc(svcId); 3933 if (!svc) return;
3934 resetCommunityReportModalUi(); 3935 state.crSvcId = svcId; 3936 var pr = 3937 parentReportId != null && parentReportId !== undefined && parentReportId !== '' 3938 ? parseInt(parentReportId, 10) 3939 : NaN; 3940 state.crParentReportId = isFinite(pr) && pr > 0 ? pr : null; 3941 applyCrReplyMode(!!state.crParentReportId); 3942 var m = document.getElementById('communityReportModal'); 3943 var t = document.getElementById('crTitle'); 3944 var leg = document.getElementById('crLegal'); 3945 if (t) { 3946 t.textContent = state.crParentReportId 3947 ? __('js_feedback_reply') + ': ' + (svc.name || svcId) 3948 : 'Relato: ' + (svc.name || svcId); 3949 } 3950 if (leg) leg.textContent = state.communityDisclaimerPt || ''; 3951 if (m) m.classList.add('open'); 3952 } 3953 3954 function openReportPickerModal() { 3955 var m = document.getElementById('detReportPickerModal'); 3956 if (!m) return; 3957 m.classList.add('open'); 3958 var inp = document.getElementById('detReportPickerSearch'); 3959 if (inp) inp.value = ''; 3960 renderReportPickerList(''); 3961 if (inp) { 3962 setTimeout(function () { 3963 inp.focus(); 3964 }, 80); 3965 } 3966 } 3967 3968 function closeReportPickerModal() { 3969 var m = document.getElementById('detReportPickerModal'); 3970 if (m) m.classList.remove('open'); 3971 } 3972 3973 function reportPickerMatchesQuery(svc, q) { 3974 if (!q) return true; 3975 q = String(q).trim().toLowerCase(); 3976 if (!q) return true; 3977 var name = (svc.name && String(svc.name).toLowerCase()) || ''; 3978 var id = String(svc.id || '').toLowerCase(); 3979 var dom = (svc.logo_domain && String(svc.logo_domain).toLowerCase()) || ''; 3980 return name.indexOf(q) >= 0 || id.indexOf(q) >= 0 || dom.indexOf(q) >= 0; 3981 } 3982 3983 function renderReportPickerList(filterText) { 3984 var listEl = document.getElementById('detReportPickerList'); 3985 var emptyEl = document.getElementById('detReportPickerEmpty'); 3986 if (!listEl) return; 3987 var cat = state.catalog; 3988 if (!Array.isArray(cat) || !cat.length) { 3989 listEl.innerHTML = 3990 '<div class="det-report-picker-row" role="presentation"><span class="det-rp-name" style="flex:1;">A carregar catálogoâ¦</span></div>'; 3991 if (emptyEl) emptyEl.hidden = true; 3992 return; 3993 } 3994 var q = filterText != null ? String(filterText) : ''; 3995 var rows = []; 3996 for (var i = 0; i < cat.length; i++) { 3997 if (reportPickerMatchesQuery(cat[i], q)) rows.push(cat[i]); 3998 } 3999 rows.sort(function (a, b) { 4000 var an = (a.name || a.id || '').toLowerCase(); 4001 var bn = (b.name || b.id || '').toLowerCase(); 4002 if (an < bn) return -1; 4003 if (an > bn) return 1; 4004 return 0; 4005 }); 4006 var lim = Math.min(rows.length, 200); 4007 var h = ''; 4008 for (var j = 0; j < lim; j++) { 4009 var s = rows[j]; 4010 h += 4011 '<div class="det-report-picker-row" role="listitem">' + 4012 detLogoImgHtml(s, '', ' width="36" height="36"') + 4013 '<span class="det-rp-name">' + 4014 esc(s.name || s.id) + 4015 '</span>' + 4016 '<button type="button" class="det-rp-report" data-det-rp-sid="' + 4017 esc(String(s.id)) + 4018 '">Reportar</button></div>'; 4019 } 4020 if (rows.length > lim) { 4021 h += 4022 '<p class="det-report-picker-lead" style="text-align:center;margin:8px 0 0;">Mostrando ' + 4023 lim + 4024 ' de ' + 4025 rows.length + 4026 '. Refine a pesquisa para encontrar mais.</p>'; 4027 } 4028 listEl.innerHTML = h; 4029 bindDetLogoFallbacks(listEl); 4030 if (emptyEl) emptyEl.hidden = rows.length > 0; 4031 listEl.querySelectorAll('.det-rp-report').forEach(function (btn) { 4032 btn.addEventListener('click', function (ev) { 4033 ev.preventDefault(); 4034 var sid = btn.getAttribute('data-det-rp-sid'); 4035 closeReportPickerModal(); 4036 if (sid) openCommunityReportModal(sid); 4037 }); 4038 }); 4039 } 4040 4041 function wireReportPickerModal() { 4042 var btn = document.getElementById('detBarReportBtn'); 4043 var m = document.getElementById('detReportPickerModal'); 4044 var closeBtn = document.getElementById('detReportPickerClose'); 4045 var search = document.getElementById('detReportPickerSearch'); 4046 if (btn && btn.dataset.detRpWired !== '1') { 4047 btn.dataset.detRpWired = '1'; 4048 btn.addEventListener('click', function () { 4049 openReportPickerModal(); 4050 }); 4051 } 4052 if (closeBtn && closeBtn.dataset.detRpWired !== '1') { 4053 closeBtn.dataset.detRpWired = '1'; 4054 closeBtn.addEventListener('click', function () { 4055 closeReportPickerModal(); 4056 }); 4057 } 4058 if (m && m.dataset.detRpBackdrop !== '1') { 4059 m.dataset.detRpBackdrop = '1'; 4060 m.addEventListener('click', function (e) { 4061 if (e.target === m) closeReportPickerModal(); 4062 }); 4063 } 4064 if (search && search.dataset.detRpWired !== '1') { 4065 search.dataset.detRpWired = '1'; 4066 search.addEventListener('input', function () { 4067 renderReportPickerList(search.value); 4068 }); 4069 } 4070 if (!detReportPickerEscBound) { 4071 detReportPickerEscBound = true; 4072 document.addEventListener('keydown', function (e) { 4073 if (e.key !== 'Escape') return; 4074 var mod = document.getElementById('detReportPickerModal'); 4075 if (mod && mod.classList.contains('open')) closeReportPickerModal(); 4076 }); 4077 } 4078 } 4079 4080 function initDetectorBarMascoteVideo() { 4081 var btn = document.getElementById('aurix-carnaval-mask'); 4082 var v = btn && btn.querySelector('video'); 4083 if (v) v.play().catch(function () {}); 4084 } 4085 4086 /** Meta do ranking comunidade (HTML seguro: texto escapado + seta de tendência de volume). */ 4087 function communityRankingMetaLineHtml(row) { 4088 if (!row) return ''; 4089 var vc = Number(row.vote_count) || 0; 4090 var parts = [ 4091 esc(__('js_sidebar_meta_simple').replace(/\{n\}/g, String(vc))) + volumeTrendMarkHtml(row), 4092 ]; 4093 if (row.probe_down) parts.push(esc(__('js_sidebar_meta_probe_down')));
4094 var vel = Number(row.velocity_30m) || 0; 4095 if (vel >= 2) parts.push(esc(__('js_sidebar_meta_accel'))); 4096 var pz = Number(row.poisson_z) || 0; 4097 if (pz >= 3) parts.push(esc(__('js_sidebar_meta_anomaly'))); 4098 return parts.join(' · '); 4099 } 4100 4101 /** Ãcone de constância (ondas azuis): sempre visÃvel quando não há subida/descida clara. */ 4102 function trendConstancyMarkHtml(titleText) { 4103 var t = esc(titleText); 4104 return ( 4105 '<span class="det-trend-stable" title="' + 4106 t + 4107 '" role="img" aria-label="' + 4108 t + 4109 '"><svg class="det-trend-stable-svg" viewBox="0 0 20 12" width="18" height="11" focusable="false" aria-hidden="true">' + 4110 '<path fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" d="M1 6.5c2.2-1.8 4.1 1.8 6.3 0s4.1-1.8 6.3 0 3.2 2.2 4.7 0"/>' + 4111 '<path fill="none" stroke="currentColor" stroke-width="1.55" stroke-linecap="round" opacity="0.55" d="M1 9c2.2-1.8 4.1 1.8 6.3 0s4.1-1.8 6.3 0 3.2 2.2 4.7 0"/>' + 4112 '</svg></span>' 4113 ); 4114 } 4115 4116 /** 4117 * Seta fina verde â / vermelha â: volume nos últimos 30 min vs 30 min anteriores (humanos + sinais). 4118 * Igual espÃrito ao indicador de uso no Catellix Kids (subiu/desceu vs perÃodo anterior). 4119 */ 4120 function volumeTrendMarkHtml(row) { 4121 // Só faz sentido com pontuação agregada (+N): relatos comunidade ou sinais coletados. 4122 // +0 (sem rede, sem local, sem votos) â não mostrar tendência de volume. 4123 var agg = 4124 row && row.vote_count != null && row.vote_count !== '' 4125 ? Number(row.vote_count) 4126 : 0; 4127 if (isNaN(agg) || agg < 1) return ''; 4128 var aRaw = row && row.volume_last_30m; 4129 var bRaw = row && row.volume_prev_30m; 4130 var a = aRaw != null && aRaw !== '' && !isNaN(Number(aRaw)) ? Number(aRaw) : NaN; 4131 var b = bRaw != null && bRaw !== '' && !isNaN(Number(bRaw)) ? Number(bRaw) : NaN; 4132 if (isNaN(a) || isNaN(b)) return ''; 4133 if (a === b) { 4134 return trendConstancyMarkHtml(__('js_vol_trend_stable_title').replace(/\{cur\}/g, String(a)).replace(/\{prev\}/g, String(b))); 4135 } 4136 var up = a > b; 4137 var cls = 'det-vol-trend ' + (up ? 'det-vol-trend--up' : 'det-vol-trend--down'); 4138 var sym = up ? 'â' : 'â'; 4139 var titleKey = up ? 'js_vol_trend_up_title' : 'js_vol_trend_down_title'; 4140 var title = __(titleKey).replace(/\{cur\}/g, String(a)).replace(/\{prev\}/g, String(b)); 4141 return ( 4142 '<span class="' + 4143 cls + 4144 '" title="' + 4145 esc(title) + 4146 '" role="img" aria-label="' + 4147 esc(title) + 4148 '">' + 4149 sym + 4150 '</span>' 4151 ); 4152 } 4153 4154 /** Seta â (subiu) / â (desceu) vs última atualização do ranking; ondas se estável ou 1.ª leitura na sessão. */ 4155 function rankingTrendMarkHtml(svcId, rank, prevMap) { 4156 if (!svcId) return ''; 4157 var map = prevMap || {}; 4158 var prev = map[svcId]; 4159 if (prev == null || prev === undefined || isNaN(Number(prev))) { 4160 return trendConstancyMarkHtml(__('js_rank_trend_baseline_title')); 4161 } 4162 if (Number(prev) === Number(rank)) { 4163 return trendConstancyMarkHtml(__('js_rank_trend_stable_title')); 4164 } 4165 var up = Number(rank) < Number(prev); 4166 var cls = 'det-rank-trend ' + (up ? 'det-rank-trend--up' : 'det-rank-trend--down'); 4167 var sym = up ? 'â' : 'â'; 4168 var title = up ? __('js_sidebar_rank_up') : __('js_sidebar_rank_down'); 4169 return ( 4170 '<span class="' + 4171 cls + 4172 '" title="' + 4173 esc(title) + 4174 '" role="img" aria-label="' + 4175 esc(title) + 4176 '">' + 4177 sym + 4178 '</span>' 4179 ); 4180 } 4181 4182 function snapshotCommunityRankPositions(rows) { 4183 var m = {}; 4184 if (!rows || !rows.length) return m; 4185 for (var i = 0; i < rows.length; i++) { 4186 if (rows[i].service_id) m[rows[i].service_id] = i + 1; 4187 } 4188 return m; 4189 } 4190 4191 /** Mescla uma linha do ranking comunidade com o serviço do catálogo (votos atualizados). */ 4192 function communityRankingRowToSvc(row) { 4193 if (!row || !row.service_id) return null; 4194 var vc = Number(row.vote_count) || 0; 4195 var vuRaw = row.vote_count_verified; 4196 var vsRaw = row.vote_count_signals; 4197 var vu = vuRaw != null && vuRaw !== '' && !isNaN(Number(vuRaw)) ? Number(vuRaw) : vc; 4198 var vs = vsRaw != null && vsRaw !== '' && !isNaN(Number(vsRaw)) ? Number(vsRaw) : 0; 4199 var vah = row.vote_avg_per_hour; 4200 var avgPerH = 4201 vah != null && vah !== '' && !isNaN(Number(vah)) ? Number(vah) : null; 4202 var patch = { 4203 community_vote_count: vc, 4204 community_vote_count_verified: vu, 4205 community_vote_count_signals: vs, 4206 community_vote_avg_per_hour: avgPerH, 4207 }; 4208 var base = findSvc(row.service_id); 4209 if (base) { 4210 var o = {}; 4211 for (var k in base) { 4212 if (Object.prototype.hasOwnProperty.call(base, k)) o[k] = base[k]; 4213 } 4214 var rowIcons = row.logo_icons; 4215 if ( 4216 (!Array.isArray(o.logo_icons) || !o.logo_icons.length) && 4217 Array.isArray(rowIcons) && 4218 rowIcons.length 4219 ) {
4220 o.logo_icons = rowIcons; 4221 } 4222 if (!(o.logo_domain || '').trim() && row.logo_domain) { 4223 o.logo_domain = String(row.logo_domain).trim(); 4224 } 4225 if ((o.logo_url == null || o.logo_url === '') && row.logo_url) { 4226 o.logo_url = row.logo_url; 4227 } 4228 o.community_vote_count = patch.community_vote_count; 4229 o.community_vote_count_verified = patch.community_vote_count_verified; 4230 o.community_vote_count_signals = patch.community_vote_count_signals; 4231 o.community_vote_avg_per_hour = patch.community_vote_avg_per_hour; 4232 return o; 4233 } 4234 var ld0 = row.logo_domain != null ? String(row.logo_domain).trim() : ''; 4235 var li0 = row.logo_icons; 4236 return { 4237 id: row.service_id, 4238 name: row.name || row.service_id, 4239 logo_domain: ld0, 4240 logo_url: row.logo_url != null && row.logo_url !== '' ? row.logo_url : null, 4241 logo_icons: Array.isArray(li0) && li0.length ? li0 : null, 4242 last_ok: null, 4243 last_latency_ms: null, 4244 category: 'core', 4245 community_vote_count: vc, 4246 community_vote_count_verified: vu, 4247 community_vote_count_signals: vs, 4248 community_vote_avg_per_hour: avgPerH, 4249 }; 4250 } 4251 4252 var MOBILE_COMMUNITY_TOP_N = 10; 4253 4254 function mobileCommunityTop10HeadActionsHtml() { 4255 return ( 4256 '<div class="det-m-t10-head-actions" role="group" aria-label="' + 4257 esc(__('js_t10_actions_aria')) + 4258 '"><button type="button" class="det-m-t10-report-btn">' + 4259 esc(__('js_rank_share_btn_short')) + 4260 '</button><button type="button" class="det-m-t10-social-btn"><span class="det-m-t10-social-ico" aria-hidden="true">â¦</span>' + 4261 esc(__('js_t10_social_btn_short')) + 4262 '</button></div>' 4263 ); 4264 } 4265 4266 function mountMobileCommunityTop10Board() { 4267 var host = document.getElementById('detMobileCommunityBoardHost'); 4268 if (!host) return; 4269 4270 if (!state.catalog || !state.catalog.length) { 4271 host.innerHTML = 4272 '<div class="top10-board top10-board--bad">' + 4273 '<div class="top10-board-head">' + 4274 '<span class="t10-icon">ð£</span>' + 4275 '<div class="t10-title-col"><h4>' + 4276 esc(__('js_m_top10_comm_title')) + 4277 '</h4><small>' + 4278 esc(__('js_m_top10_comm_sub')) + 4279 '</small></div>' + 4280 mobileCommunityTop10HeadActionsHtml() + 4281 '</div>' + 4282 '<div class="top10-empty">' + 4283 esc(__('js_m_top10_comm_loading')) + 4284 '</div></div>'; 4285 var rep0 = host.querySelector('.det-m-t10-report-btn'); 4286 var soc0 = host.querySelector('.det-m-t10-social-btn'); 4287 if (rep0) 4288 rep0.addEventListener('click', function (e) { 4289 e.preventDefault(); 4290 openDetectorRankingReportModal('top10'); 4291 }); 4292 if (soc0) 4293 soc0.addEventListener('click', function (e) { 4294 e.preventDefault(); 4295 openTop10SocialShareModal(); 4296 }); 4297 return; 4298 } 4299 4300 var rankRows = state.communityRankingRows; 4301 var rowsWithVotes = []; 4302 if (rankRows !== undefined && rankRows !== null && rankRows.length) { 4303 for (var wi = 0; wi < rankRows.length; wi++) { 4304 var rw0 = rankRows[wi]; 4305 if (rw0 && (Number(rw0.vote_count) || 0) > 0) rowsWithVotes.push(rw0); 4306 } 4307 } 4308 var globalRankById = {}; 4309 for (var gi = 0; gi < rowsWithVotes.length; gi++) { 4310 var gr = rowsWithVotes[gi]; 4311 if (gr && gr.service_id) globalRankById[gr.service_id] = gi + 1; 4312 } 4313 var trendPrevMap = state._complaintsRankTrendPrev || {}; 4314 4315 var items = []; 4316 if (rankRows !== undefined && rankRows !== null && rankRows.length) { 4317 for (var ri = 0; ri < rankRows.length && items.length < MOBILE_COMMUNITY_TOP_N; ri++) { 4318 var row = rankRows[ri]; 4319 if (!row || (Number(row.vote_count) || 0) < 1) continue; 4320 var s0 = communityRankingRowToSvc(row); 4321 if (!s0 || !passesFilterSansSearch(s0)) continue; 4322 items.push({ svc: s0, row: row }); 4323 } 4324 } 4325 4326 var title = __('js_m_top10_comm_title'); 4327 var sub; 4328 if (items.length && rankRows !== undefined && rankRows !== null) { 4329 var days = VALID_RANKING_DAYS.indexOf(state.complaintsRankingDays) !== -1 ? state.complaintsRankingDays : 1; 4330 var dayLblMap = { 4331 1: __('js_window_1d'), 4332 7: __('js_window_7d'), 4333 15: __('js_window_15d'), 4334 30: __('js_window_30d'), 4335 };
4336 var dayLbl = dayLblMap[days] || __('js_window_1d'); 4337 sub = __('js_t10_complaints_window_top_tpl') 4338 .replace(/\{w\}/g, dayLbl) 4339 .replace(/\{top\}/g, String(MOBILE_COMMUNITY_TOP_N)) 4340 .replace(/\{n\}/g, String(items.length)); 4341 } else { 4342 sub = __('js_m_top10_comm_sub'); 4343 } 4344 4345 var body = ''; 4346 if (!items.length) { 4347 var emptyMsg; 4348 if (rankRows === undefined) { 4349 emptyMsg = __('js_m_top10_comm_loading'); 4350 } else if (rankRows === null) { 4351 emptyMsg = __('js_comm_rank_err_li'); 4352 } else if ( 4353 rankRows.some(function (r) { 4354 return r && (Number(r.vote_count) || 0) > 0; 4355 }) 4356 ) { 4357 emptyMsg = __('js_cards_empty_filter'); 4358 } else { 4359 emptyMsg = __('js_complaints_empty_generic').replace(/\{d\}/g, String(state.complaintsRankingDays)); 4360 } 4361 body = '<div class="top10-empty">' + esc(emptyMsg) + '</div>'; 4362 } else { 4363 var listParts = []; 4364 for (var ii = 0; ii < items.length; ii++) { 4365 var it = items[ii]; 4366 var row = it.row; 4367 var pillDisp = complaintsRankingPillDisplay(row); 4368 var pillTitle = complaintsRankingPillTitle(row, pillDisp); 4369 var globalRnk = globalRankById[row.service_id] || ii + 1; 4370 var idx0 = globalRnk - 1; 4371 var rankCls = 4372 idx0 === 0 4373 ? 'det-sb-rank det-sb-rank--gold' 4374 : idx0 === 1 4375 ? 'det-sb-rank det-sb-rank--silver' 4376 : idx0 === 2 4377 ? 'det-sb-rank det-sb-rank--bronze' 4378 : 'det-sb-rank'; 4379 var trendHtml = rankingTrendMarkHtml(row.service_id, globalRnk, trendPrevMap); 4380 var svc = findSvc(row.service_id); 4381 var dom = svc && svc.logo_domain ? String(svc.logo_domain) : (row.logo_domain ? String(row.logo_domain) : ''); 4382 var logo = detComplaintsRankLogoSrc(svc, row); 4383 var rowA11y = 4384 esc(__('js_open_service_aria')) + ' ' + esc(row.name || row.service_id || ''); 4385 listParts.push( 4386 '<li class="det-sidebar-complaints-item"><button type="button" class="det-sidebar-line det-sidebar-line--rank det-sidebar-line--rank-main" data-svc-id="' + 4387 esc(row.service_id) + 4388 '" aria-label="' + 4389 rowA11y + 4390 '"><span class="det-sb-rank-wrap"><span class="' + 4391 esc(rankCls) + 4392 '">' + 4393 esc(String(globalRnk)) + 4394 '</span>' + 4395 trendHtml + 4396 '</span><img class="det-sb-logo" src="' + 4397 esc(logo) + 4398 '" alt="" width="36" height="36" loading="lazy" decoding="async"' + 4399 imgLogoOnErrorAttr(dom) + 4400 '><span class="det-sb-body"><span class="det-sb-name">' + 4401 esc(row.name) + 4402 '</span><span class="det-sb-meta">' + 4403 communityRankingMetaLineHtml(row) + 4404 '</span></span></button>' + 4405 communityTop10ComplaintsRailHtml(row, pillDisp, pillTitle) + 4406 '</li>' 4407 ); 4408 } 4409 body = 4410 '<div class="det-t10-m-comm-stack">' + 4411 '<div class="det-t10-comm-col-head">' + 4412 esc(__('js_t10_report_now_line')) + 4413 '</div>' + 4414 '<ol class="det-complaints-ol det-complaints-ol--sidebar-rank det-m-community-rank-ol" aria-label="' + 4415 esc(__('sidebar_complaints_aria')) + 4416 '">' + 4417 listParts.join('') + 4418 '</ol></div>'; 4419 } 4420 4421 host.innerHTML = 4422 '<div class="top10-board top10-board--bad">' + 4423 '<div class="top10-board-head">' + 4424 '<span class="t10-icon">ð£</span>' + 4425 '<div class="t10-title-col"><h4>' + 4426 esc(title) + 4427 '</h4><small>' + 4428 esc(sub) + 4429 '</small></div>' + 4430 mobileCommunityTop10HeadActionsHtml() + 4431 '</div>' + 4432 body + 4433 '</div>'; 4434 4435 bindDetLogoFallbacks(host); 4436 wireComplaintsTop10Clicks(host); 4437 var repB = host.querySelector('.det-m-t10-report-btn'); 4438 var socB = host.querySelector('.det-m-t10-social-btn'); 4439 if (repB) 4440 repB.addEventListener('click', function (e) { 4441 e.preventDefault(); 4442 openDetectorRankingReportModal('top10'); 4443 }); 4444 if (socB) 4445 socB.addEventListener('click', function (e) { 4446 e.preventDefault(); 4447 openTop10SocialShareModal(); 4448 }); 4449 } 4450 4451 function mountMobileGamesBoardClone() { 4452 var mobileMount = document.getElementById('detMobileGamesBoardMount'); 4453 if (!mobileMount) return; 4454 if (!DET_COMPLAINTS_VIEWPORT_MQ.matches) { 4455 mobileMount.innerHTML = ''; 4456 return; 4457 } 4458 var desktopBoard = document.querySelector('.ads-right .det-right-games-board'); 4459 if (!desktopBoard) return; 4460 var clone = desktopBoard.cloneNode(true); 4461 clone.removeAttribute('id'); 4462 var ids = clone.querySelectorAll('[id]'); 4463 for (var i = 0; i < ids.length; i++) { 4464 ids[i].removeAttribute('id'); 4465 } 4466 mobileMount.innerHTML = ''; 4467 mobileMount.appendChild(clone); 4468 bindDetLogoFallbacks(clone); 4469 clone.querySelectorAll('.det-sidebar-line--rank-main[data-svc-id]').forEach(function (btn) { 4470 btn.addEventListener('click', function () { 4471 var id = btn.getAttribute('data-svc-id'); 4472 if (id) openModal(id); 4473 }); 4474 }); 4475 wireComplaintsTop10Clicks(clone); 4476 } 4477 4478 function syncComplaintsTop10BoardsAndCards() { 4479 mountMobileCommunityTop10Board(); 4480 if (state.catalog && state.catalog.length) { 4481 if (!markLazySectionDirty('top10Boards')) mountTop10Boards(); 4482 mountTop10Cards(); 4483 try { mountRightTop10GamesBoard(); } catch (eGb) { console.warn('[det] games board err', eGb); } 4484 try { mountMobileGamesBoardClone(); } catch (eMg) { console.warn('[det] mobile games clone err', eMg); } 4485 updateCatalogFilterBanner(); 4486 } 4487 } 4488 4489 /** 4490 * Top 10 Games â mesma lógica do Top 10 comunidade, mas filtrada só a serviços de games. 4491 * 4492 * Fonte primária: communityRankingRows (ranking_sort_score + relatos + sinais â igual ao 4493 * painel esquerdo), filtrado por isDetectorGameCatalogService. 4494 * 4495 * Fallback: quando não há linhas de comunidade com votos para games (dados ainda não 4496 * carregaram ou janela zerada), cai para ordenação por latência (gameRankingLatencyMs) 4497 * nos serviços do catálogo com categoria game. 4498 */ 4499 function mountRightTop10GamesBoard() { 4500 var host = document.getElementById('detRightGamesTop10'); 4501 var sub = document.getElementById('detRightGamesTop10Sub'); 4502 var headEl = document.getElementById('detRightGamesTop10Head'); 4503 var shareBtn = document.getElementById('btnRightTop10GamesSocial'); 4504 if (!host) return; 4505 4506 var hybrid = sidebarTop10GamesHybridParts(); 4507 var commRows = hybrid.communityRankingRows; 4508 var gameCommRows = hybrid.gameCommRows; 4509 var latFillSvcs = hybrid.latFillSvcs;
4510 var prevMap = state._gamesRankTrendPrev || {}; 4511 4512 var totalItems = gameCommRows.length + latFillSvcs.length; 4513 4514 /* ââ 3. Estado de carregamento ââ */ 4515 if (commRows === undefined && totalItems === 0) { 4516 if (sub) sub.textContent = __('js_comm_rank_loading'); 4517 if (headEl) headEl.hidden = true; 4518 var sk = ''; 4519 for (var si2 = 0; si2 < 5; si2++) { 4520 sk += '<li class="det-sidebar-skel" aria-hidden="true"><span class="det-sb-skel-rank"></span><span class="det-sb-skel-logo"></span><span class="det-sb-skel-text"><span class="det-sb-skel-line"></span><span class="det-sb-skel-line det-sb-skel-line--short"></span></span></li>'; 4521 } 4522 host.innerHTML = sk; 4523 if (shareBtn && shareBtn.dataset.detWired !== '1') { 4524 shareBtn.dataset.detWired = '1'; 4525 shareBtn.addEventListener('click', function (e) { e.preventDefault(); openTop10GamesSocialShareModal(); }); 4526 } 4527 return; 4528 } 4529 4530 if (totalItems === 0) { 4531 if (sub) sub.textContent = __('js_t10_games_social_no_data'); 4532 if (headEl) headEl.hidden = true; 4533 host.innerHTML = '<li class="det-sidebar-empty" role="status">' + esc(__('js_t10_games_social_no_data')) + '</li>'; 4534 if (shareBtn && shareBtn.dataset.detWired !== '1') { 4535 shareBtn.dataset.detWired = '1'; 4536 shareBtn.addEventListener('click', function (e) { e.preventDefault(); openTop10GamesSocialShareModal(); }); 4537 } 4538 return; 4539 } 4540 4541 /* ââ 4. Renderização ââ */ 4542 if (sub) sub.textContent = __('js_t10_games_sub'); 4543 if (headEl) { 4544 headEl.hidden = (gameCommRows.length === 0); 4545 headEl.textContent = __('js_t10_report_now_line'); 4546 } 4547 4548 /* Helper: renderiza linha de latência (fill) */ 4549 function renderGameLatRow(s, rnk, trendHtml) { 4550 var ms = gameRankingLatencyMs(s); 4551 var msRound = ms != null && !isNaN(ms) ? Math.round(ms) : null; 4552 var rankCls = rnk === 1 ? 'det-sb-rank det-sb-rank--gold' 4553 : rnk === 2 ? 'det-sb-rank det-sb-rank--silver' 4554 : rnk === 3 ? 'det-sb-rank det-sb-rank--bronze' 4555 : 'det-sb-rank'; 4556 var logo = detComplaintsRankLogoSrc(s, null); 4557 var dom = s.logo_domain || ''; 4558 var rowA11y = esc(__('js_open_service_aria')) + ' ' + esc(s.name || s.id || ''); 4559 return ( 4560 '<li class="det-sidebar-complaints-item det-right-games-board__item det-right-games-board__item--lat">' + 4561 '<button type="button" class="det-sidebar-line det-sidebar-line--rank det-sidebar-line--rank-main" data-svc-id="' + 4562 esc(s.id) + '" aria-label="' + rowA11y + '">' + 4563 '<span class="det-sb-rank-wrap"><span class="' + esc(rankCls) + '">' + 4564 esc(String(rnk)) + '</span>' + trendHtml + '</span>' + 4565 '<img class="det-sb-logo" src="' + esc(logo) + 4566 '" alt="" width="36" height="36" loading="lazy" decoding="async"' + 4567 imgLogoOnErrorAttr(dom) + '>' + 4568 '<span class="det-sb-body"><span class="det-sb-name">' + esc(s.name || s.id) + 4569 '</span><span class="det-sb-meta det-sb-meta--lat">' + 4570 (msRound != null ? 'HTTPS ' + String(msRound) + ' ms · sem relatos ainda' : __('js_games_rank_meta')) + 4571 '</span></span></button>' + 4572 gamesTop10LatencyRailHtml(s, msRound) + 4573 '</li>' 4574 ); 4575 } 4576 4577 var htmlParts = []; 4578 4579 /* Parte A â linhas com relatos (comunidade) */ 4580 for (var ci2 = 0; ci2 < gameCommRows.length; ci2++) { 4581 try { 4582 var row = gameCommRows[ci2]; 4583 if (!row || !row.service_id) continue; 4584 var rnk = ci2 + 1; 4585 var rankCls = rnk === 1 ? 'det-sb-rank det-sb-rank--gold' 4586 : rnk === 2 ? 'det-sb-rank det-sb-rank--silver' 4587 : rnk === 3 ? 'det-sb-rank det-sb-rank--bronze' 4588 : 'det-sb-rank'; 4589 var trendHtml = rankingTrendMarkHtml(row.service_id, rnk, prevMap); 4590 var svc = findSvc(row.service_id); 4591 var logo = detComplaintsRankLogoSrc(svc, row); 4592 var dom = svc && svc.logo_domain ? String(svc.logo_domain) : (row.logo_domain || ''); 4593 var pillDisp = complaintsRankingPillDisplay(row); 4594 var pillTitle = complaintsRankingPillTitle(row, pillDisp);
4595 var rowA11y = esc(__('js_open_service_aria')) + ' ' + esc(row.name || row.service_id || ''); 4596 var metaLine = communityRankingMetaLineHtml(row); 4597 htmlParts.push( 4598 '<li class="det-sidebar-complaints-item det-right-games-board__item">' + 4599 '<button type="button" class="det-sidebar-line det-sidebar-line--rank det-sidebar-line--rank-main" data-svc-id="' + 4600 esc(row.service_id) + '" aria-label="' + rowA11y + '">' + 4601 '<span class="det-sb-rank-wrap"><span class="' + esc(rankCls) + '">' + 4602 esc(String(rnk)) + '</span>' + trendHtml + '</span>' + 4603 '<img class="det-sb-logo" src="' + esc(logo) + 4604 '" alt="" width="36" height="36" loading="lazy" decoding="async"' + 4605 imgLogoOnErrorAttr(dom) + '>' + 4606 '<span class="det-sb-body"><span class="det-sb-name">' + esc(row.name) + 4607 '</span><span class="det-sb-meta">' + metaLine + '</span></span></button>' + 4608 communityTop10ComplaintsRailHtml(row, pillDisp, pillTitle) + 4609 '</li>' 4610 ); 4611 } catch (eComm) { /* skip broken row */ } 4612 } 4613 4614 /* Parte B â fill com latência */ 4615 for (var fi = 0; fi < latFillSvcs.length; fi++) { 4616 try { 4617 if (!latFillSvcs[fi] || !latFillSvcs[fi].id) continue; 4618 var fillRnk = gameCommRows.length + fi + 1; 4619 var fillTrend = rankingTrendMarkHtml(latFillSvcs[fi].id, fillRnk, prevMap); 4620 htmlParts.push(renderGameLatRow(latFillSvcs[fi], fillRnk, fillTrend)); 4621 } catch (eFill) { /* skip broken row */ } 4622 } 4623 4624 var html = htmlParts.join(''); 4625 host.innerHTML = html; 4626 4627 /* Snapshot de posições para a próxima chamada */ 4628 var snapGames = {}; 4629 gameCommRows.forEach(function (row, i) { snapGames[row.service_id] = i + 1; }); 4630 latFillSvcs.forEach(function (s, i) { snapGames[s.id] = gameCommRows.length + i + 1; }); 4631 state._gamesRankTrendPrev = snapGames; 4632 4633 bindDetLogoFallbacks(host); 4634 wireComplaintsTop10Clicks(host); 4635 4636 if (shareBtn && shareBtn.dataset.detWired !== '1') { 4637 shareBtn.dataset.detWired = '1'; 4638 shareBtn.addEventListener('click', function (e) { 4639 e.preventDefault(); 4640 openTop10GamesSocialShareModal(); 4641 }); 4642 } 4643 } 4644 4645 function wireComplaintsTop10Clicks(container) { 4646 if (!container) return; 4647 container.querySelectorAll('.det-sidebar-line--rank-main[data-svc-id]').forEach(function (btn) { 4648 btn.addEventListener('click', function () { 4649 var id = btn.getAttribute('data-svc-id'); 4650 if (id) openModal(id); 4651 }); 4652 }); 4653 container.querySelectorAll('.det-sb-hourly-btn[data-hourly-sid]').forEach(function (btn) { 4654 btn.addEventListener('click', function (e) { 4655 e.preventDefault(); 4656 e.stopPropagation(); 4657 var sid = btn.getAttribute('data-hourly-sid'); 4658 var nm = btn.getAttribute('data-hourly-name') || sid; 4659 if (sid) openComplaintsHourlyMicroModal(sid, nm); 4660 }); 4661 }); 4662 } 4663 4664 function formatRankingAvgPerHour(row) { 4665 if (!row || row.vote_avg_per_hour == null || row.vote_avg_per_hour === '') return '0.0'; 4666 var n = Number(row.vote_avg_per_hour); 4667 return isNaN(n) ? '0.0' : n.toFixed(1); 4668 } 4669 4670 /** 4671 * Escala máxima do Pulso normalizado: o #1 do ranking recebe até este valor. 4672 * Escala 0â100 (metáfora batimento cardÃaco). 4673 */ 4674 var PULSE_SCALE_MAX = 100; 4675 4676 function _communityRankingMaxScore() { 4677 var rows = state.communityRankingRows; 4678 if (!rows || !rows.length) return 1; 4679 var mx = 0; 4680 for (var i = 0; i < rows.length; i++) { 4681 var s = Number(rows[i].ranking_sort_score); 4682 if (!isNaN(s) && s > mx) mx = s; 4683 } 4684 return mx || 1; 4685 } 4686 4687 function complaintsRankingPillDisplay(row) { 4688 var avg = row && row.vote_avg_per_hour != null ? Number(row.vote_avg_per_hour) : NaN; 4689 var ultra = row && row.vote_per_hour_ultra != null ? Number(row.vote_per_hour_ultra) :
4689NaN; 4690 var vel = row ? Number(row.velocity_30m) || 0 : 0; 4691 var pz = row ? Number(row.poisson_z) || 0 : 0; 4692 var down = row && row.probe_down; 4693 4694 if (down) { 4695 return { value: __('js_sidebar_pill_down'), isSpike: true, isDown: true }; 4696 } 4697 var isSpike = false; 4698 if (!isNaN(ultra) && !isNaN(avg) && ultra >= avg * 1.22 && ultra >= 0.35) isSpike = true; 4699 if (vel >= 3 || pz >= 4) isSpike = true; 4700 4701 var sortScore = row && row.ranking_sort_score != null ? Number(row.ranking_sort_score) : NaN; 4702 var displayVal; 4703 if (!isNaN(sortScore) && sortScore > 0) { 4704 var maxScore = _communityRankingMaxScore(); 4705 var pulse = (sortScore / maxScore) * PULSE_SCALE_MAX; 4706 displayVal = pulse.toFixed(1); 4707 } else { 4708 displayVal = formatRankingAvgPerHour(row); 4709 } 4710 return { value: displayVal, isSpike: isSpike, isDown: false }; 4711 } 4712 4713 function complaintsRankingPillTitle(row, pillDisp) { 4714 var avgStr = formatRankingAvgPerHour(row); 4715 var vc = row && (Number(row.vote_count) || 0) > 0 ? Number(row.vote_count) : 0; 4716 var uh = row && row.ultra_window_hours != null ? Number(row.ultra_window_hours) : 2; 4717 var vel = row ? Number(row.velocity_30m) || 0 : 0; 4718 var pz = row ? Number(row.poisson_z) || 0 : 0; 4719 if (pillDisp && pillDisp.isDown) { 4720 return __('js_sidebar_avg_pill_title_down'); 4721 } 4722 if (pillDisp && pillDisp.isSpike) { 4723 var parts = []; 4724 if (row && row.vote_per_hour_ultra != null) { 4725 parts.push(__('js_sidebar_avg_pill_title_spike_tpl') 4726 .replace(/\{uh\}/g, String(!isNaN(uh) && uh > 0 ? uh : 2)) 4727 .replace(/\{short\}/g, pillDisp.value) 4728 .replace(/\{avg\}/g, avgStr) 4729 .replace(/\{total\}/g, String(vc))); 4730 } 4731 if (vel >= 3) parts.push(__('js_sidebar_meta_accel')); 4732 if (pz >= 4) parts.push(__('js_sidebar_meta_anomaly_detail').replace(/\{z\}/g, pz.toFixed(1))); 4733 return parts.join(' â ') || avgStr; 4734 } 4735 return __('js_sidebar_avg_pill_title_tpl').replace(/\{avg\}/g, avgStr).replace(/\{total\}/g, String(vc)); 4736 } 4737 4738 /** 4739 * Um único botão por linha no Top 10 comunidade: abre o gráfico por hora; o relato fica no modal. 4740 */ 4741 function communityTop10ComplaintsRailHtml(row, pillDisp, pillTitle) { 4742 if (!row || !row.service_id) return ''; 4743 var disp = pillDisp || { value: 'â', isSpike: false, isDown: false }; 4744 var sid = row.service_id; 4745 var nm = row.name || sid; 4746 var btnCls = 4747 'det-t10-unified-btn det-sb-hourly-btn' + 4748 (disp.isDown ? ' det-t10-unified--down' : disp.isSpike ? ' det-t10-unified--spike' : ''); 4749 var reportLbl = esc(__('js_t10_btn_report') || 'Reportar'); 4750 var pulseLbl = esc(__('js_rank_pill_label') || 'Pulso'); 4751 return ( 4752 '<div class="det-t10-comm-rail">' + 4753 '<button type="button" class="' + 4754 esc(btnCls) + 4755 '" data-hourly-sid="' + 4756 esc(sid) + 4757 '" data-hourly-name="' + 4758 esc(nm) + 4759 '" title="' + 4760 esc(pillTitle) + 4761 '" aria-label="' + 4762 esc(pillTitle) + 4763 '">' + 4764 '<span class="det-t10-unified-shine" aria-hidden="true"></span>' + 4765 '<span class="det-t10-unified-inner">' + 4766 '<span class="det-t10-report-tag">' + 4767 reportLbl + 4768 '</span>' + 4769 '<span class="det-t10-report-sep" aria-hidden="true"></span>' + 4770 '<span class="det-t10-pulse-mini">' + 4771 '<span class="det-t10-pulse-mini-lbl">' + 4772 pulseLbl + 4773 '</span>' + 4774 '<span class="det-t10-pulse-mini-val">' + 4775 esc(disp.value) + 4776 '</span>' + 4777 '</span></span></button></div>' 4778 ); 4779 } 4780 4781 /** 4782 * Rail do Top 10 jogos (direita): botão Reportar + latência como info secundária. 4783 */ 4784 function gamesTop10LatencyRailHtml(svc, ms) { 4785 if (!svc || !svc.id) return ''; 4786 var sid = svc.id; 4787 var nm = svc.name || sid; 4788 var dispVal = ms != null && !isNaN(ms) ? String(Math.round(ms)) + ' ms' : 'â'; 4789 var isDown = ms == null || isNaN(ms); 4790 var isSpike = !isDown && ms > 500; 4791 var btnCls = 4792 'det-t10-unified-btn det-sb-hourly-btn' + 4793 (isDown ? ' det-t10-unified--down' : isSpike ? ' det-t10-unified--spike' : ''); 4794 var reportLbl = esc(__('js_t10_btn_report') || 'Reportar'); 4795 var httpsLbl = esc(__('js_games_lat_rail_https')); 4796 var aria = reportLbl + ' · ' + esc(nm); 4797 return ( 4798 '<div class="det-t10-comm-rail">' + 4799 '<button type="button" class="' + 4800 esc(btnCls) + 4801 '" data-hourly-sid="' + 4802 esc(sid) + 4803 '" data-hourly-name="' + 4804 esc(nm) + 4805 '" title="' + 4806 esc(aria) + 4807 '" aria-label="' + 4808 esc(aria) + 4809 '">' + 4810 '<span class="det-t10-unified-shine" aria-hidden="true"></span>' + 4811 '<span class="det-t10-unified-inner">' + 4812 '<span class="det-t10-report-tag">' + 4813 reportLbl + 4814 '</span>' + 4815 '<span class="det-t10-report-sep" aria-hidden="true"></span>' + 4816 '<span class="det-t10-pulse-mini">' + 4817 '<span class="det-t10-pulse-mini-lbl">' + 4818 httpsLbl + 4819 '</span>' + 4820 '<span class="det-t10-pulse-mini-val">' + 4821 esc(dispVal) + 4822 '</span>' + 4823 '</span></span></button></div>' 4824 ); 4825 } 4826 4827 function syncDetComplaintsTop10ColumnHead(visible) { 4828 var h = document.getElementById('detComplaintsTop10Head'); 4829 if (!h) return; 4830 h.hidden = !visible; 4831 if (visible) h.textContent = __('js_t10_report_now_line'); 4832 } 4833 4834 function closeComplaintsHourlyMicroModal() { 4835 var m = document.getElementById('detComplaintsHourlyModal'); 4836 var repRow = document.getElementById('detComplaintsHourlyReportRow'); 4837 if (m) { 4838 m.classList.remove('open'); 4839 m.setAttribute('aria-hidden', 'true'); 4840 try { 4841 delete m.dataset.hourlySvcId; 4842 } catch (eD) { 4843 m.removeAttribute('data-hourly-svc-id'); 4844 } 4845 } 4846 if (repRow) repRow.hidden = true; 4847 } 4848 4849 /** 4850 * GET JSON do detector com abort após timeoutMs (evita modal preso em «A carregar gráficoâ¦Â»). 4851 */ 4852 function fetchDetectorApiJson(url, timeoutMs) { 4853 var ms = timeoutMs != null ? timeoutMs : 28000; 4854 var ctrl = typeof AbortController !== 'undefined' ? new AbortController() : null; 4855 var tid = null; 4856 if (ctrl && ms > 0) { 4857 tid = setTimeout(function () { 4858 try { 4859 ctrl.abort(); 4860 } catch (eAbort) { 4861 /* ignore */ 4862 } 4863 }, ms); 4864 } 4865 var opts = { credentials: 'omit' }; 4866 if (ctrl) opts.signal = ctrl.signal; 4867 return fetch(url, opts) 4868 .then(function (r) { 4869 return r.text().then(function (text) { 4870 var data = {}; 4871 if (text != null && String(text).trim()) { 4872 try { 4873 data = JSON.parse(text); 4874 } catch (eParse) { 4875 var err = new Error('bad_json'); 4876 err._httpStatus = r.status; 4877 throw err; 4878 } 4879 } 4880 return { ok: r.ok, status: r.status, data: data }; 4881 }); 4882 }) 4883 .finally(function () { 4884 if (tid) clearTimeout(tid); 4885 }); 4886 } 4887 4888 function openComplaintsHourlyMicroModal(sid, svcName) { 4889 var m = document.getElementById('detComplaintsHourlyModal'); 4890 var title = document.getElementById('detComplaintsHourlyTitle'); 4891 var host = document.getElementById('detComplaintsHourlyChartHost'); 4892 var foot = document.getElementById('detComplaintsHourlyFoot'); 4893 var repRow = document.getElementById('detComplaintsHourlyReportRow'); 4894 var repBtn = document.getElementById('detComplaintsHourlyReportBtn'); 4895 if (!m || !host) return; 4896 m.dataset.hourlySvcId = sid; 4897 if (repBtn) { 4898 repBtn.textContent = __('js_t10_report_now_line'); 4899 var nm = svcName || sid; 4900 repBtn.setAttribute('aria-label', __('js_t10_report_flow_aria_tpl').replace(/\{name\}/g, nm)); 4901 repBtn.setAttribute('title', __('js_t10_report_flow_aria_tpl').replace(/\{name\}/g, nm)); 4902 } 4903 if (repRow) repRow.hidden = false;
4904 var reqGen = (detComplaintsHourlyFetchGen += 1); 4905 var days = VALID_RANKING_DAYS.indexOf(state.complaintsRankingDays) !== -1 ? state.complaintsRankingDays : 1; 4906 var hours = Math.min(720, Math.max(24, days * 24)); 4907 var origin = window.location.origin || ''; 4908 if (title) { 4909 title.textContent = 4910 (svcName || sid) + ' â ' + __('js_sidebar_hourly_chart_lead'); 4911 } 4912 var lead = document.getElementById('detComplaintsHourlyLead'); 4913 if (lead) { 4914 lead.textContent = __('js_sidebar_hourly_modal_lead') || 4915 'Distribuição horária na janela selecionada (relatos verificados + sinais agregados).'; 4916 } 4917 host.innerHTML = 4918 '<p class="det-hourly-loading">' + esc(__('js_sidebar_hourly_loading')) + '</p>'; 4919 if (foot) foot.textContent = ''; 4920 m.classList.add('open'); 4921 m.setAttribute('aria-hidden', 'false'); 4922 4923 var resolved = false; 4924 var safetyTid = setTimeout(function () { 4925 if (!resolved && reqGen === detComplaintsHourlyFetchGen) { 4926 host.innerHTML = 4927 '<p class="det-hourly-err">' + 4928 esc(__('js_sidebar_hourly_err_timeout') || 'Tempo esgotado ao carregar gráfico.') + 4929 '</p>'; 4930 } 4931 }, 24000); 4932 4933 var url = 4934 origin + 4935 '/api/detector/service/' + 4936 encodeURIComponent(sid) + 4937 '/community-hourly?days=' + 4938 encodeURIComponent(String(days)) + 4939 '&hours=' + 4940 encodeURIComponent(String(hours)) + 4941 '&require_top10=false'; 4942 fetchDetectorApiJson(url, 20000) 4943 .then(function (pack) { 4944 resolved = true; 4945 clearTimeout(safetyTid); 4946 if (reqGen !== detComplaintsHourlyFetchGen) return; 4947 var data = pack.data || {}; 4948 if (!pack.ok || !data.success) { 4949 var emsg = 4950 pack.status === 429 4951 ? __('js_sidebar_hourly_err_429') || __('js_chart_429') 4952 : __('js_sidebar_hourly_err'); 4953 if (pack.status !== 429 && data.detail) { 4954 emsg = String(data.detail); 4955 } 4956 host.innerHTML = '<p class="det-hourly-err">' + esc(emsg) + '</p>'; 4957 return; 4958 } 4959 try { 4960 var pts = (data.points || []).map(function (p) { 4961 return typeof p === 'number' ? p : Number(p.n); 4962 }); 4963 if (!pts.length) { 4964 host.innerHTML = 4965 '<p class="det-hourly-err" style="color:var(--muted)">' + 4966 esc(__('js_sidebar_hourly_no_data') || 'Sem dados de relatos nesta janela.') + 4967 '</p>'; 4968 return; 4969 } 4970 host.innerHTML = 4971 '<div class="det-hourly-spark-wrap" title="' + 4972 esc(__('js_sidebar_hourly_chart_hint')) + 4973 '">' + 4974 complaintsSparklineSvg(pts, 300) + 4975 '</div>'; 4976 var sum = 0; 4977 for (var i = 0; i < pts.length; i++) sum += pts[i]; 4978 if (foot) { 4979 foot.textContent = __('js_sidebar_hourly_foot_tpl').replace(/\{n\}/g, String(sum)); 4980 } 4981 } catch (eRender) { 4982 host.innerHTML = 4983 '<p class="det-hourly-err">' + esc(__('js_sidebar_hourly_err')) + '</p>'; 4984 } 4985 }) 4986 .catch(function (err) { 4987 resolved = true; 4988 clearTimeout(safetyTid); 4989 if (reqGen !== detComplaintsHourlyFetchGen) return; 4990 var msg = __('js_sidebar_hourly_err'); 4991 if (err && err.name === 'AbortError') { 4992 msg = 4993 __('js_sidebar_hourly_err_timeout') || 4994 'Tempo esgotado ao carregar gráfico. Tente de novo dentro de instantes.'; 4995 } 4996 host.innerHTML = '<p class="det-hourly-err">' + esc(msg) + '</p>'; 4997 }); 4998 } 4999 5000 function wireComplaintsHourlyMicroModal() { 5001 var m = document.getElementById('detComplaintsHourlyModal'); 5002 var closeBtn = document.getElementById('detComplaintsHourlyClose'); 5003 if (!m || m.dataset.detHourlyWired === '1') return; 5004 m.dataset.detHourlyWired = '1'; 5005 if (closeBtn) { 5006 closeBtn.addEventListener('click', function () { 5007 closeComplaintsHourlyMicroModal(); 5008 }); 5009 } 5010 var repBtn = document.getElementById('detComplaintsHourlyReportBtn'); 5011 if (repBtn && repBtn.dataset.detHourlyReportWired !== '1') { 5012 repBtn.dataset.detHourlyReportWired = '1'; 5013 repBtn.addEventListener('click', function (e) { 5014 e.preventDefault(); 5015 var mod = document.getElementById('detComplaintsHourlyModal'); 5016 var id = mod && mod.dataset ? mod.dataset.hourlySvcId : ''; 5017 if (!id) return; 5018 closeComplaintsHourlyMicroModal(); 5019 openCommunityReportModal(id); 5020 }); 5021 } 5022 m.addEventListener('click', function (e) { 5023 if (e.target === m) closeComplaintsHourlyMicroModal(); 5024 }); 5025 if (!detComplaintsHourlyEscBound) { 5026 detComplaintsHourlyEscBound = true; 5027 document.addEventListener('keydown', function (e) { 5028 if (e.key !== 'Escape') return; 5029 var mod = document.getElementById('detComplaintsHourlyModal'); 5030 if (mod && mod.classList.contains('open')) closeComplaintsHourlyMicroModal(); 5031 }); 5032 } 5033 } 5034 5035 /* ââ Top 30 tendências (GET /api/detector/top-services) ââ */ 5036 var TOP30_CACHE_MS = 30000; 5037 var TOP30_DEBOUNCE_MS = 280; 5038 var TOP30_POLL_MS = 30000; 5039 var TOP30_LIMIT = 30; 5040 var top30FetchGen = 0; 5041 var top30State = { 5042 cacheAt: 0, 5043 cachePayload: null, 5044 debounceT: null, 5045 pollT: null, 5046 open: false, 5047 }; 5048 5049 /** Tendência de atividade (30 min vs 30 min anteriores), alinhada ao motor do servidor. */ 5050 function communityRowActivityTrend(row) { 5051 if (!row) return 'stable'; 5052 var a = 5053 row.volume_last_30m != null && row.volume_last_30m !== '' && !isNaN(Number(row.volume_last_30m)) 5054 ? Number(row.volume_last_30m) 5055 : NaN; 5056 var b = 5057 row.volume_prev_30m != null && row.volume_prev_30m !== '' && !isNaN(Number(row.volume_prev_30m)) 5058 ? Number(row.volume_prev_30m) 5059 : NaN; 5060 if (isNaN(a) || isNaN(b)) return 'stable'; 5061 var prev = Math.max(b, 1.0); 5062 var ratio = a / prev; 5063 if (ratio > 1.2) return 'up'; 5064 if (ratio < 0.8) return 'down'; 5065 return 'stable'; 5066 } 5067 5068 function communityRowToTop30Item(row, position) { 5069 var st = 'ok'; 5070 if (row && row.probe_down) st = 'unstable'; 5071 return { 5072 position: position, 5073 name: (row && (row.name || row.service_id)) || '', 5074 service_id: row && row.service_id ? String(row.service_id) : '', 5075 norm_key: '', 5076 status: st, 5077 trend: communityRowActivityTrend(row), 5078 sources: Math.max(0, Number(row && row.vote_count_signals) || 0), 5079 }; 5080 } 5081 5082 /** 5083 * Top 30 na UI: prioriza ranking da comunidade na janela JANELA (até 30 linhas); 5084 * se não houver dados, usa agregador externo (GET top-services) em cache. 5085 */ 5086 function getTop30ItemsForUi(externalOverride) { 5087 var filtered = communityRowsWithVotesForReport(); 5088 if (filtered.length) { 5089 var out = []; 5090 for (var i = 0; i < filtered.length && out.length < TOP30_LIMIT; i++) { 5091 out.push(communityRowToTop30Item(filtered[i], out.length + 1)); 5092 } 5093 return out; 5094 } 5095 if (externalOverride && externalOverride.length) return externalOverride; 5096 var ext = top30State.cachePayload && top30State.cachePayload.items; 5097 return ext && ext.length ? ext : []; 5098 } 5099 5100 /** 5101 * Relatório PDF/modal: até 30 linhas â comunidade primeiro, depois top-services em cache, 5102 * depois catálogo (por votos agregados), sem duplicar serviço. 5103 */ 5104 function mergeTop30ItemsForReport() { 5105 var seen = {}; 5106 var out = []; 5107 function pushItem(raw) { 5108 if (!raw) return; 5109 var sid = raw.service_id != null ? String(raw.service_id).trim() : ''; 5110 var nk = raw.norm_key != null ? String(raw.norm_key).trim() : ''; 5111 var nm = raw.name != null ? String(raw.name).trim() : ''; 5112 var key = sid || nk || (nm ? 'n:' + nm : ''); 5113 if (!key) return; 5114 if (seen[key]) return; 5115 seen[key] = true; 5116 out.push(raw); 5117 } 5118 var primary = getTop30ItemsForUi(); 5119 var i; 5120 for (i = 0; i < primary.length; i++) pushItem(primary[i]); 5121 var apiItems = (top30State.cachePayload && top30State.cachePayload.items) || []; 5122 for (i = 0; i < apiItems.length && out.length < TOP30_LIMIT; i++) pushItem(apiItems[i]); 5123 if (out.length < TOP30_LIMIT) { 5124 var fb = buildTop30FallbackFromCatalog(); 5125 for (i = 0; i < fb.length && out.length < TOP30_LIMIT; i++) pushItem(fb[i]); 5126 } 5127 var merged = []; 5128 for (i = 0; i < out.length; i++) { 5129 var it = out[i]; 5130 merged.push({ 5131 position: i + 1, 5132 name: it.name, 5133 service_id: it.service_id != null ? String(it.service_id) : '', 5134 norm_key: it.norm_key != null ? String(it.norm_key) : '', 5135 status: it.status || 'ok', 5136 trend: it.trend || 'stable', 5137 sources: Math.max(0, Number(it.sources) || 0), 5138 }); 5139 } 5140 return merged; 5141 } 5142
5143 function top30PrevRanksKey() { 5144 var d = VALID_RANKING_DAYS.indexOf(state.complaintsRankingDays) !== -1 ? state.complaintsRankingDays : 1; 5145 return 'det_top30_prev_v4_d' + d; 5146 } 5147 5148 function readTop30PrevRanks() { 5149 try { 5150 var raw = sessionStorage.getItem(top30PrevRanksKey()); 5151 if (!raw) return {}; 5152 var o = JSON.parse(raw); 5153 return o && typeof o === 'object' ? o : {}; 5154 } catch (e) { 5155 return {}; 5156 } 5157 } 5158 5159 function writeTop30PrevRanks(map) { 5160 try { 5161 sessionStorage.setItem(top30PrevRanksKey(), JSON.stringify(map)); 5162 } catch (e) {} 5163 } 5164 5165 function updateTop30OpenLabels() { 5166 var b1 = document.getElementById('btnTop30Sidebar'); 5167 var b2 = document.getElementById('btnTop30RankCol'); 5168 var b3 = document.getElementById('btnTop30MobileHero'); 5169 var t = __('js_top30_btn'); 5170 if (b1) b1.textContent = t; 5171 if (b2) b2.textContent = t; 5172 if (b3) { 5173 b3.textContent = __('js_top30_mobile_cta'); 5174 b3.setAttribute('aria-label', __('js_top30_mobile_cta_aria')); 5175 } 5176 var title = document.getElementById('detTop30Title'); 5177 var lead = document.getElementById('detTop30Lead'); 5178 if (title) title.textContent = __('js_top30_modal_title'); 5179 if (lead) lead.textContent = __('js_top30_modal_lead'); 5180 var s10 = document.getElementById('btnSidebarTop10Share'); 5181 if (s10) { 5182 var s10lbl = s10.querySelector('.det-sd-action__lbl'); 5183 if (s10lbl) s10lbl.textContent = __('js_rank_share_btn_short'); 5184 s10.setAttribute('title', __('js_rank_share_btn_top10')); 5185 s10.setAttribute('aria-label', __('js_rank_share_aria_top10')); 5186 } 5187 var s10s = document.getElementById('btnSidebarTop10Social'); 5188 if (s10s) { 5189 var s10sl = s10s.querySelector('.det-sd-action__lbl'); 5190 if (s10sl) s10sl.textContent = __('js_t10_social_btn_short'); 5191 else s10s.textContent = __('js_t10_social_btn_short'); 5192 s10s.setAttribute('title', __('js_t10_social_btn')); 5193 s10s.setAttribute('aria-label', __('js_t10_social_aria')); 5194 } 5195 var t30s = document.getElementById('detTop30Share'); 5196 if (t30s) { 5197 t30s.textContent = __('js_rank_share_btn_short'); 5198 t30s.setAttribute('title', __('js_rank_share_btn')); 5199 t30s.setAttribute('aria-label', __('js_rank_share_aria_top30')); 5200 } 5201 var t20s = document.getElementById('detTop20Share'); 5202 if (t20s) { 5203 t20s.textContent = __('js_rank_share_btn'); 5204 t20s.setAttribute('aria-label', __('js_rank_share_aria_top20')); 5205 } 5206 var rh = document.getElementById('detReportShareTitle'); 5207 if (rh) rh.textContent = __('js_report_modal_title'); 5208 function setDetReportBtnLabel(id, text) { 5209 var b = document.getElementById(id); 5210 if (!b) return; 5211 var lbl = b.querySelector('.det-report-act-lbl'); 5212 if (lbl) lbl.textContent = text; 5213 else b.textContent = text; 5214 } 5215 setDetReportBtnLabel('detReportBtnPrint', __('js_report_btn_print')); 5216 setDetReportBtnLabel('detReportBtnWhatsapp', __('js_report_btn_wa')); 5217 setDetReportBtnLabel('detReportBtnEmail', __('js_report_btn_email')); 5218 setDetReportBtnLabel('detReportBtnNative', __('js_report_btn_native')); 5219 var tsm = document.getElementById('detTop10SocialTitle'); 5220 if (tsm) tsm.textContent = __('js_t10_social_modal_title'); 5221 setDetReportBtnLabel('detTop10SocialCopyCap', __('js_t10_social_act_copy')); 5222 var waIcoBtn = document.getElementById('detTop10SocialWa'); 5223 if (waIcoBtn) { waIcoBtn.title = __('js_t10_social_act_wa'); waIcoBtn.setAttribute('aria-label', __('js_t10_social_act_wa')); } 5224 var igIcoBtn = document.getElementById('detTop10SocialIg'); 5225 if (igIcoBtn) { igIcoBtn.title = 'Instagram'; igIcoBtn.setAttribute('aria-label', 'Instagram'); } 5226 var fbIcoBtn = document.getElementById('detTop10SocialFb'); 5227 if (fbIcoBtn) { fbIcoBtn.title = 'Facebook'; fbIcoBtn.setAttribute('aria-label', 'Facebook'); } 5228 setDetReportBtnLabel('detTop10SocialSave', __('js_t10_social_act_save')); 5229 setDetReportBtnLabel('detTop10SocialNative', __('js_t10_social_act_native')); 5230 var t10Ld = document.getElementById('detTop10SocialLoading'); 5231 if (t10Ld && !t10Ld.hidden) t10Ld.textContent = __('js_t10_social_preparing'); 5232 } 5233
5234 function top30StatusLabel(st) { 5235 if (st === 'down') return __('js_top30_status_down'); 5236 if (st === 'unstable') return __('js_top30_status_unstable'); 5237 return __('js_top30_status_ok'); 5238 } 5239 5240 function top30StatusEmoji(st) { 5241 if (st === 'down') return 'ð´'; 5242 if (st === 'unstable') return 'ð¡'; 5243 return 'ð¢'; 5244 } 5245 5246 function top30TrendSub(t) { 5247 if (t === 'up') return __('js_top30_trend_up'); 5248 if (t === 'down') return __('js_top30_trend_down'); 5249 return __('js_top30_trend_stable'); 5250 } 5251 5252 function buildTop30FallbackFromCatalog() { 5253 if (!state.catalog || !state.catalog.length) return []; 5254 var arr = state.catalog.slice(); 5255 arr.sort(function (a, b) { 5256 return (Number(b.community_vote_count) || 0) - (Number(a.community_vote_count) || 0); 5257 }); 5258 var out = []; 5259 for (var i = 0; i < arr.length && out.length < TOP30_LIMIT; i++) { 5260 var s = arr[i]; 5261 if (!s || s.id == null || s.id === '') continue; 5262 out.push({ 5263 position: out.length + 1, 5264 name: s.display_name || s.name || String(s.id), 5265 service_id: String(s.id), 5266 norm_key: String(s.norm_key || ''), 5267 status: 'ok', 5268 trend: 'stable', 5269 sources: 0, 5270 }); 5271 } 5272 return out; 5273 } 5274 5275 function buildTop30SkeletonInto(skEl, nRows) { 5276 if (!skEl) return; 5277 var n = nRows != null ? nRows : 8; 5278 skEl.innerHTML = ''; 5279 skEl.hidden = false; 5280 for (var i = 0; i < n; i++) { 5281 var d = document.createElement('div'); 5282 d.className = 'det-top30-sk-row'; 5283 skEl.appendChild(d); 5284 } 5285 } 5286 5287 function buildTop30SkeletonRows() { 5288 buildTop30SkeletonInto(document.getElementById('detTop30Skeleton'), 8); 5289 } 5290 5291 function renderTop30ListInto(list, empty, err, sk, items, skipWritePrev) { 5292 if (sk) sk.hidden = true; 5293 if (err) { 5294 err.hidden = true; 5295 err.textContent = ''; 5296 } 5297 if (!list || !empty) return; 5298 5299 var fromApi = items && items.length; 5300 var effective = fromApi ? items : buildTop30FallbackFromCatalog(); 5301 var usedFallback = !fromApi && effective && effective.length; 5302 5303 if (!effective || !effective.length) { 5304 list.hidden = true; 5305 list.innerHTML = ''; 5306 empty.hidden = false; 5307 empty.textContent = __('js_top30_empty'); 5308 return; 5309 } 5310 empty.hidden = true; 5311 list.hidden = false; 5312 5313 var prev = readTop30PrevRanks(); 5314 var nextMap = {}; 5315 var frag = document.createDocumentFragment(); 5316 for (var i = 0; i < effective.length; i++) { 5317 var it = effective[i]; 5318 var pos = it.position != null ? Number(it.position) : i + 1; 5319 var sid = (it.service_id && String(it.service_id).trim()) || ''; 5320 var mapKey = sid || String(it.norm_key || it.name || 'k' + i); 5321 nextMap[mapKey] = pos; 5322 var movedUp = prev[mapKey] != null && pos < prev[mapKey]; 5323 var movedDown = prev[mapKey] != null && pos > prev[mapKey]; 5324 var rankExtra = pos === 1 ? ' det-top30-item--rank1' : pos === 2 ? ' det-top30-item--rank2' : pos === 3 ? ' det-top30-item--rank3' : ''; 5325 var upExtra = movedUp ? ' det-top30-item--moved-up' : ''; 5326 var downExtra = movedDown ? ' det-top30-item--moved-down' : ''; 5327 var row = document.createElement(sid ? 'button' : 'div'); 5328 row.className = 'det-top30-item' + rankExtra + upExtra + downExtra; 5329 if (sid) { 5330 row.setAttribute('type', 'button'); 5331 row.setAttribute('data-top30-sid', sid); 5332 row.setAttribute( 5333 'aria-label', 5334 __('js_open_service_aria') + ' ' + (it.name || sid), 5335 ); 5336 } 5337 var posWrap = document.createElement('span'); 5338 posWrap.className = 'det-top30-pos-wrap'; 5339 posWrap.innerHTML = 5340 '<span class="det-top30-pos">' + 5341 String(pos) + 5342 '</span>' + 5343 rankingTrendMarkHtml(mapKey, pos, prev); 5344 var main = document.createElement('div'); 5345 main.className = 'det-top30-main'; 5346 var nm = document.createElement('div'); 5347 nm.className = 'det-top30-name'; 5348 nm.textContent = it.name || ''; 5349 var meta = document.createElement('div'); 5350 meta.className = 'det-top30-meta'; 5351 var st = it.status || 'ok'; 5352 var tr = it.trend || 'stable'; 5353 var stSpan = document.createElement('span'); 5354 stSpan.className = 'det-top30-status'; 5355 stSpan.textContent = top30StatusEmoji(st) + ' ' + top30StatusLabel(st); 5356 var trSpan = document.createElement('span'); 5357 trSpan.className = 'det-top30-trend'; 5358 var trSub = top30TrendSub(tr); 5359 trSpan.title = trSub; 5360 if (tr === 'up' || tr === 'down') { 5361 trSpan.textContent = (tr === 'up' ? 'â ' : 'â ') + trSub; 5362 } else { 5363 trSpan.textContent = 'â ' + trSub; 5364 } 5365 var srcSpan = document.createElement('span'); 5366 srcSpan.textContent = __('js_top30_sources_tpl').replace(/\{n\}/g, String(Number(it.sources) || 0)); 5367 meta.appendChild(stSpan); 5368 meta.appendChild(trSpan); 5369 meta.appendChild(srcSpan); 5370 main.appendChild(nm); 5371 main.appendChild(meta);
5372 row.appendChild(posWrap); 5373 row.appendChild(main); 5374 frag.appendChild(row); 5375 } 5376 list.innerHTML = ''; 5377 list.appendChild(frag); 5378 if (!skipWritePrev) { 5379 writeTop30PrevRanks(nextMap); 5380 } 5381 5382 list.querySelectorAll('button.det-top30-item[data-top30-sid]').forEach(function (el) { 5383 el.addEventListener('click', function () { 5384 var id = el.getAttribute('data-top30-sid'); 5385 if (!id) return; 5386 closeTop30Modal(); 5387 openModal(id); 5388 }); 5389 }); 5390 } 5391 5392 function renderTop30List(itemsFromFetch) { 5393 var eff = getTop30ItemsForUi(itemsFromFetch); 5394 renderTop30ListInto( 5395 document.getElementById('detTop30List'), 5396 document.getElementById('detTop30Empty'), 5397 document.getElementById('detTop30Error'), 5398 document.getElementById('detTop30Skeleton'), 5399 eff, 5400 false, 5401 ); 5402 } 5403 5404 function clearTop30ErrorState() { 5405 var err = document.getElementById('detTop30Error'); 5406 if (err) { 5407 err.hidden = true; 5408 err.textContent = ''; 5409 } 5410 } 5411 5412 function showTop30ErrorState(msg) { 5413 var err = document.getElementById('detTop30Error'); 5414 var t = msg || __('js_top30_err'); 5415 if (err) { 5416 err.hidden = false; 5417 err.textContent = t; 5418 } 5419 } 5420 5421 function fetchTop30Services(force) { 5422 var origin = window.location.origin || ''; 5423 var now = Date.now(); 5424 if (!force && top30State.cachePayload && now - top30State.cacheAt < TOP30_CACHE_MS) { 5425 return Promise.resolve(top30State.cachePayload); 5426 } 5427 var gen = ++top30FetchGen; 5428 var url = 5429 origin + 5430 '/api/detector/top-services?limit=' + 5431 encodeURIComponent(String(TOP30_LIMIT)); 5432 return fetch(url, { credentials: 'omit' }) 5433 .then(function (r) { 5434 return r.text().then(function (text) { 5435 var data = {}; 5436 try { 5437 data = text ? JSON.parse(text) : {}; 5438 } catch (eJson) { 5439 data = {}; 5440 } 5441 return { ok: r.ok, data: data }; 5442 }); 5443 }) 5444 .then(function (pack) { 5445 if (gen !== top30FetchGen) { 5446 return Promise.reject({ code: 'stale' }); 5447 } 5448 var data = pack.data || {}; 5449 if (!pack.ok || !data.success) { 5450 throw new Error('fail'); 5451 } 5452 top30State.cacheAt = Date.now(); 5453 top30State.cachePayload = data; 5454 return data; 5455 }); 5456 } 5457 5458 function openTop30ModalLoad() { 5459 var sk = document.getElementById('detTop30Skeleton'); 5460 var empty = document.getElementById('detTop30Empty'); 5461 clearTop30ErrorState(); 5462 if (empty) empty.hidden = true; 5463 5464 var hasComm = communityRowsWithVotesForReport().length > 0; 5465 if (hasComm) { 5466 if (sk) { 5467 sk.hidden = true; 5468 sk.innerHTML = ''; 5469 } 5470 renderTop30List(); 5471 return; 5472 } 5473 5474 var now = Date.now(); 5475 if (top30State.cachePayload && now - top30State.cacheAt < TOP30_CACHE_MS) { 5476 if (sk) sk.hidden = true; 5477 renderTop30List(top30State.cachePayload.items || []); 5478 } else { 5479 buildTop30SkeletonRows(); 5480 } 5481 5482 fetchTop30Services(false) 5483 .then(function (data) { 5484 if (!top30State.open) return; 5485 var items = 5486 (data && data.items) || 5487 (top30State.cachePayload && top30State.cachePayload.items) || 5488 []; 5489 renderTop30List(items); 5490 }) 5491 .catch(function (e) { 5492 if (e && e.code === 'stale') return; 5493 if (!top30State.open) return; 5494 if (sk) sk.hidden = true; 5495 var list = document.getElementById('detTop30List'); 5496 var hasList = list && list.children && list.children.length > 0; 5497 if (top30State.cachePayload && top30State.cachePayload.items && top30State.cachePayload.items.length) { 5498 renderTop30List(top30State.cachePayload.items); 5499 return; 5500 } 5501 if (hasList) return; 5502 showTop30ErrorState(); 5503 }); 5504 } 5505 5506 function openTop30Modal() { 5507 var modal = document.getElementById('detTop30Modal'); 5508 if (!modal) return; 5509 top30State.open = true; 5510 modal.classList.add('open'); 5511 modal.setAttribute('aria-hidden', 'false'); 5512 requestAnimationFrame(function () { 5513 modal.classList.add('det-top30-modal--anim'); 5514 }); 5515 try { 5516 document.body.style.overflow = 'hidden'; 5517 } catch (e) {} 5518 updateTop30OpenLabels(); 5519 openTop30ModalLoad(); 5520 5521 if (top30State.pollT) clearInterval(top30State.pollT); 5522 top30State.pollT = setInterval(function () { 5523 if (!top30State.open) return; 5524 if (communityRowsWithVotesForReport().length > 0) { 5525 renderTop30List(); 5526 return; 5527 } 5528 fetchTop30Services(true) 5529 .then(function (data) { 5530 if (!data || !top30State.open) return; 5531 renderTop30List(data.items || []); 5532 }) 5533 .catch(function () {}); 5534 }, TOP30_POLL_MS); 5535 } 5536 5537 function closeTop30Modal() { 5538 var modal = document.getElementById('detTop30Modal'); 5539 if (!modal) return; 5540 top30State.open = false;
5541 modal.classList.remove('open', 'det-top30-modal--anim'); 5542 modal.setAttribute('aria-hidden', 'true'); 5543 try { 5544 document.body.style.overflow = ''; 5545 } catch (e) {} 5546 if (top30State.pollT) { 5547 clearInterval(top30State.pollT); 5548 top30State.pollT = null; 5549 } 5550 var sk = document.getElementById('detTop30Skeleton'); 5551 if (sk) { 5552 sk.hidden = true; 5553 sk.innerHTML = ''; 5554 } 5555 } 5556 5557 function stripDetectorHtmlToText(html) { 5558 var d = document.createElement('div'); 5559 d.innerHTML = String(html || ''); 5560 return String(d.textContent || d.innerText || '') 5561 .replace(/\s+/g, ' ') 5562 .trim(); 5563 } 5564 5565 function communityRowsWithVotesForReport() { 5566 var rows = state.communityRankingRows; 5567 if (!Array.isArray(rows)) return []; 5568 return rows.filter(function (row) { 5569 return row && (Number(row.vote_count) || 0) > 0; 5570 }); 5571 } 5572 5573 function catalogCategoryMixForReport() { 5574 var cat = state.catalog || []; 5575 var map = {}; 5576 for (var i = 0; i < cat.length; i++) { 5577 var c = (cat[i] && cat[i].category) || 'core'; 5578 map[c] = (map[c] || 0) + 1; 5579 } 5580 return Object.keys(map) 5581 .map(function (k) { 5582 return { id: k, n: map[k], label: catLabel(k) }; 5583 }) 5584 .sort(function (a, b) { 5585 return b.n - a.n; 5586 }); 5587 } 5588 5589 function catalogIpStackMixForReport() { 5590 var cat = state.catalog || []; 5591 var map = { v4: 0, dual: 0, v6: 0, unknown: 0 }; 5592 for (var i = 0; i < cat.length; i++) { 5593 var st = (cat[i] && cat[i].ip_stack) || 'unknown'; 5594 if (map[st] == null) st = 'unknown'; 5595 map[st]++; 5596 } 5597 return map; 5598 } 5599 5600 /** 5601 * Preferir URLs same-origin (cache HTTP + CORS previsÃvel) ao carregar Ãcones para canvas / partilha. 5602 */ 5603 function prioritizeSameOriginDetectorUrls(urls) { 5604 if (!urls || !urls.length) return urls || []; 5605 var pageOrigin = ''; 5606 try { 5607 pageOrigin = new URL(window.location.href || 'http://localhost').origin; 5608 } catch (eO) {} 5609 if (!pageOrigin) return urls.slice(); 5610 var same = []; 5611 var other = []; 5612 var i; 5613 for (i = 0; i < urls.length; i++) { 5614 var u = urls[i]; 5615 if (!u) continue; 5616 try { 5617 var o = new URL(u, pageOrigin).origin; 5618 if (o === pageOrigin) same.push(u); 5619 else other.push(u); 5620 } catch (eU) { 5621 other.push(u); 5622 } 5623 } 5624 return same.concat(other); 5625 } 5626 5627 /** Célula «Serviço» no relatório: logo do catálogo + nome. */ 5628 function reportRankingServiceCellHtml(displayName, serviceId, rankingRow) { 5629 var sid = serviceId != null ? String(serviceId).trim() : ''; 5630 var svc = sid ? findSvc(sid) : null; 5631 if ( 5632 !svc && 5633 rankingRow && 5634 sid && 5635 String(rankingRow.service_id != null ? rankingRow.service_id : '').trim() === sid 5636 ) { 5637 var ldR = rankingRow.logo_domain != null ? String(rankingRow.logo_domain).trim() : ''; 5638 var liR = rankingRow.logo_icons; 5639 svc = { 5640 id: sid, 5641 name: rankingRow.name || sid, 5642 logo_domain: ldR, 5643 logo_url: 5644 rankingRow.logo_url != null && rankingRow.logo_url !== '' 5645 ? rankingRow.logo_url 5646 : null, 5647 logo_icons: Array.isArray(liR) && liR.length ? liR : null, 5648 }; 5649 } 5650 var stub = svc || { logo_domain: '', logo_url: null, logo_icons: null }; 5651 var logoHtml = detLogoImgHtml(stub, 'det-rpt-svc-logo', ' width="32" height="32"'); 5652 return ( 5653 '<div class="det-rpt-svc-cell">' + 5654 logoHtml + 5655 '<span class="det-rpt-svc-name">' + 5656 esc(displayName != null ? String(displayName) : '') + 5657 '</span></div>' 5658 ); 5659 } 5660 5661 function buildDetectorRankingReportDocumentHtml(scope) { 5662 var rows = communityRowsWithVotesForReport(); 5663 var lim = scope === 'top10' ? 10 : scope === 'top20' ? 20 : 30; 5664 var slice = rows.slice(0, lim); 5665 var top30items = mergeTop30ItemsForReport(); 5666 var title = 5667 scope === 'top10' 5668 ? __('js_report_title_top10') 5669 : scope === 'top20' 5670 ? __('js_report_title_top20') 5671 : __('js_report_title_top30'); 5672 var sub = __('js_report_subtitle_enterprise'); 5673 var when = new Date().toISOString().slice(0, 19).replace('T', ' ') + ' UTC'; 5674 var url = typeof window !== 'undefined' && window.location ? String(window.location.href) : ''; 5675 var reportLogoSrc = '/static/assets/logo-catellix%202.png'; 5676 try { 5677 if (typeof window !== 'undefined' && window.location && window.location.origin) { 5678 reportLogoSrc = new URL(reportLogoSrc, window.location.origin).href; 5679 } 5680 } catch (eLogo) {} 5681 var catMix = catalogCategoryMixForReport(); 5682 var ipMix = catalogIpStackMixForReport(); 5683 var nCat = (state.catalog || []).length; 5684 var tableRows = ''; 5685 var i; 5686 var rptTop30 = { 5687 h: '#', 5688 svc: __('js_report_th_service'), 5689 st: __('js_report_th_status'), 5690 tr: __('js_report_th_trend'), 5691 src: __('js_report_th_sources'), 5692 }; 5693 var rptComm = { 5694 h: '#', 5695 svc: __('js_report_th_service'), 5696 sig: __('js_report_th_signal'), 5697 ctx: __('js_report_th_context'), 5698 }; 5699 if (scope === 'top30') { 5700 for (i = 0; i < top30items.length; i++) { 5701 var it = top30items[i]; 5702 var pos = it.position != null ? Number(it.position) : i + 1; 5703 tableRows += 5704 '<tr><td data-det-rpt-lbl="' + 5705 esc(rptTop30.h) + 5706 '">' + 5707 esc(String(pos)) + 5708 '</td><td data-det-rpt-lbl="' + 5709 esc(rptTop30.svc) + 5710 '">' + 5711 reportRankingServiceCellHtml(it.name || '', it.service_id) + 5712 '</td><td data-det-rpt-lbl="' + 5713 esc(rptTop30.st) + 5714 '">' + 5715 esc(top30StatusLabel(it.status || 'ok')) + 5716 '</td><td data-det-rpt-lbl="' + 5717 esc(rptTop30.tr) + 5718 '">' + 5719 esc(top30TrendSub(it.trend || 'stable')) + 5720 '</td><td data-det-rpt-lbl="' + 5721 esc(rptTop30.src) + 5722 '">' + 5723 esc(String(Number(it.sources) || 0)) + 5724 '</td></tr>'; 5725 } 5726 } else { 5727 for (i = 0; i < slice.length; i++) { 5728 var rw = slice[i]; 5729 var pill = complaintsRankingPillDisplay(rw); 5730 tableRows += 5731 '<tr><td data-det-rpt-lbl="' + 5732 esc(rptComm.h) + 5733 '">' + 5734 esc(String(i + 1)) + 5735 '</td><td data-det-rpt-lbl="' + 5736 esc(rptComm.svc) + 5737 '">' + 5738 reportRankingServiceCellHtml(rw.name || rw.service_id || '', rw.service_id, rw) + 5739 '</td><td data-det-rpt-lbl="' + 5740 esc(rptComm.sig) + 5741 '">' + 5742 esc(String(pill.value)) + 5743 '</td><td data-det-rpt-lbl="' + 5744 esc(rptComm.ctx) + 5745 '">' + 5746 esc(stripDetectorHtmlToText(communityRankingMetaLineHtml(rw))) + 5747 '</td></tr>'; 5748 } 5749 } 5750 var catBars = ''; 5751 for (i = 0; i < catMix.length && i < 12; i++) { 5752 var pct = nCat > 0 ? Math.round((catMix[i].n / nCat) * 1000) / 10 : 0; 5753 catBars += 5754 '<div class="det-rpt-bar-row"><span class="det-rpt-bar-lbl">' + 5755 esc(catMix[i].label) + 5756 '</span><div class="det-rpt-bar-track"><span class="det-rpt-bar-fill" style="width:' + 5757 esc(String(Math.min(100, pct))) + 5758 '%"></span></div><span class="det-rpt-bar-pct">' + 5759 esc(String(pct)) + 5760 '%</span></div>'; 5761 } 5762 var ipTotal = ipMix.v4 + ipMix.dual + ipMix.v6 + ipMix.unknown; 5763 function ipPct(x) { 5764 return ipTotal > 0 ? Math.round((x / ipTotal) * 1000) / 10 : 0; 5765 } 5766 var thead = 5767 scope === 'top30' 5768 ? '<thead><tr><th>
5768#</th><th>' + 5769 esc(__('js_report_th_service')) + 5770 '</th><th>' + 5771 esc(__('js_report_th_status')) + 5772 '</th><th>' + 5773 esc(__('js_report_th_trend')) + 5774 '</th><th>' + 5775 esc(__('js_report_th_sources')) + 5776 '</th></tr></thead>' 5777 : '<thead><tr><th>#</th><th>' + 5778 esc(__('js_report_th_service')) + 5779 '</th><th>' + 5780 esc(__('js_report_th_signal')) + 5781 '</th><th>' + 5782 esc(__('js_report_th_context')) + 5783 '</th></tr></thead>'; 5784 return ( 5785 '<div class="det-rpt-doc">' + 5786 '<header class="det-rpt-doc-head"><div class="det-rpt-brand"><img class="det-rpt-logo" src="' + 5787 esc(reportLogoSrc) + 5788 '" alt="' + 5789 esc('Catellix') + 5790 '" width="180" height="52" loading="eager" decoding="async" /></div><h1>' + 5791 esc(title) + 5792 '</h1><p class="det-rpt-lead">' + 5793 esc(sub) + 5794 '</p><p class="det-rpt-meta">' + 5795 esc(__('js_report_generated')) + 5796 ' ' + 5797 esc(when) + 5798 '</p></header>' + 5799 '<section class="det-rpt-sec"><h2>' + 5800 esc(__('js_report_sec_ranking')) + 5801 '</h2><table class="det-rpt-table">' + 5802 thead + 5803 '<tbody>' + 5804 tableRows + 5805 '</tbody></table></section>' + 5806 '<section class="det-rpt-sec"><h2>' + 5807 esc(__('js_report_sec_mix_cat')) + 5808 '</h2><p class="det-rpt-note">' + 5809 esc(__('js_report_mix_cat_note')) + 5810 '</p>' + 5811 catBars + 5812 '</section>' + 5813 '<section class="det-rpt-sec"><h2>' + 5814 esc(__('js_report_sec_mix_ip')) + 5815 '</h2><p class="det-rpt-note">' + 5816 esc(__('js_report_mix_ip_note')) + 5817 '</p><ul class="det-rpt-ip-list"><li>IPv4: ' + 5818 esc(String(ipPct(ipMix.v4))) + 5819 '%</li><li>IPv4+IPv6: ' + 5820 esc(String(ipPct(ipMix.dual))) + 5821 '%</li><li>IPv6: ' + 5822 esc(String(ipPct(ipMix.v6))) + 5823 '%</li><li>' + 5824 esc(__('js_report_ip_other')) + 5825 ': ' + 5826 esc(String(ipPct(ipMix.unknown))) + 5827 '%</li></ul></section>' + 5828 '<footer class="det-rpt-foot"><p>' + 5829 esc(__('js_report_footer')) + 5830 '</p><p class="det-rpt-foot-site"><a href="https://catellix.com" class="det-rpt-foot-link">catellix.com</a> · ' + 5831 esc(__('js_report_site_tagline')) + 5832 '</p><p class="det-rpt-url">' + 5833 esc(url) + 5834 '</p></footer></div>' 5835 ); 5836 } 5837 5838 function openDetectorRankingReportModal(scope) { 5839 detReportModalScope = 5840 scope === 'top10' || scope === 'top20' || scope === 'top30' ? scope : 'top30'; 5841 var modal = document.getElementById('detReportShareModal'); 5842 var inner = document.getElementById('detReportPreviewInner'); 5843 if (!modal || !inner) return; 5844 inner.innerHTML = buildDetectorRankingReportDocumentHtml(detReportModalScope); 5845 bindDetLogoFallbacks(inner); 5846 modal.classList.add('open'); 5847 modal.setAttribute('aria-hidden', 'false'); 5848 try { 5849 document.body.style.overflow = 'hidden'; 5850 } catch (e) {} 5851 } 5852 5853 function closeDetectorRankingReportModal() { 5854 var modal = document.getElementById('detReportShareModal'); 5855 if (!modal) return; 5856 modal.classList.remove('open'); 5857 modal.setAttribute('aria-hidden', 'true'); 5858 try { 5859 document.body.style.overflow = ''; 5860 } catch (e2) {} 5861 } 5862 5863 /** Mesmo fluxo do botão «Relatório» do modal Top 30: atualiza cache e abre o relatório. */ 5864 function openTop30RankingReportAfterPrefetch() { 5865 fetchTop30Services(false) 5866 .then(function () { 5867 openDetectorRankingReportModal('top30'); 5868 }) 5869 .catch(function () { 5870 openDetectorRankingReportModal('top30'); 5871 }); 5872 } 5873 5874 function detectorReportShareTitle() { 5875 var s = detReportModalScope || 'top30'; 5876 if (s === 'top10') return __('js_report_title_top10'); 5877 if (s === 'top20') return __('js_report_title_top20'); 5878 return __('js_report_title_top30'); 5879 } 5880 5881 /** 5882 * Texto para WhatsApp, e-mail e partilha nativa: lista conforme o relatório aberto (10/20/30), 5883 * site oficial e link da página (uma vez). 5884 */ 5885 function detectorReportShareBodyText() { 5886 var s = detReportModalScope || 'top30'; 5887 var lim = s === 'top10' ? 10 : s === 'top20' ? 20 : 30; 5888 var lines = []; 5889 var i; 5890 if (s === 'top30') { 5891 var items = mergeTop30ItemsForReport(); 5892 for (i = 0; i < items.length && i < lim; i++) { 5893 lines.push(String(i + 1) + '. ' + String(items[i].name || items[i].service_id || '')); 5894 } 5895 } else { 5896 var rows = communityRowsWithVotesForReport(); 5897 var slice = rows.slice(0, lim); 5898 for (i = 0; i < slice.length; i++) { 5899 lines.push(String(i + 1) + '. ' + String(slice[i].name || slice[i].service_id || '')); 5900 } 5901 } 5902 var head = detectorReportShareTitle() + ' · ' + new Date().toISOString().slice(0, 10); 5903 var siteLine = __('js_report_share_site_line'); 5904 var page = 5905 typeof window !== 'undefined' && window.location && window.location.href 5906 ? window.location.href 5907 : ''; 5908 var body = 5909 head + 5910 '\n\n' + 5911 (lines.length ? lines.join('\n') : __('js_report_share_no_rank_lines')) + 5912 '\n\n' + 5913 siteLine + 5914 '\n' + 5915 page; 5916 return body; 5917 } 5918 5919 function buildTop10SocialCaption(rows) { 5920 var siteLine = __('js_report_share_site_line'); 5921 var page = 5922 typeof window !== 'undefined' && window.location && window.location.href 5923 ? window.location.href 5924 : ''; 5925 var intro = 5926 detTop10SocialSession && detTop10SocialSession.kind === 'games' 5927 ? __('js_t10_social_caption_intro_games') 5928 : __('js_t10_social_caption_intro'); 5929 var lines = []; 5930 for (var i = 0; i < rows.length; i++) { 5931 var base = String(i + 1) + '. ' + String(rows[i].name || rows[i].service_id || ''); 5932 if (detTop10SocialSession && detTop10SocialSession.kind === 'games') { 5933 var svcG = communityRankingRowToSvc(rows[i]); 5934 var msG = svcG ? gameRankingLatencyMs(svcG) : null; 5935 if (msG != null && !isNaN(msG)) base += ' â ' + Math.round(msG) + ' ms'; 5936 } 5937 lines.push(base); 5938 } 5939 return intro + '\n\n' + lines.join('\n') + '\n\n' + siteLine + '\n' + page; 5940 } 5941 5942 function catellixDetectorBrandLogoSrc() { 5943 var src = '/static/assets/logo-catellix%202.png'; 5944 try { 5945 if (typeof window !== 'undefined' && window.location && window.location.origin) { 5946 src = new URL(src, window.location.origin).href; 5947 } 5948 } catch (eSrc) {} 5949 return src; 5950 } 5951 5952 function loadTop10SocialBrandLogo(callback) { 5953 if (detTop10SocialBrandLogoImg && detTop10SocialBrandLogoImg.complete && detTop10SocialBrandLogoImg.naturalHeight > 0) { 5954 callback(detTop10SocialBrandLogoImg); 5955 return; 5956 } 5957 if (!detTop10SocialBrandLogoPromise) { 5958 detTop10SocialBrandLogoPromise = new Promise(function (resolve) { 5959 var settled = false;
5960 var tid = setTimeout(function () { 5961 if (settled) return; 5962 settled = true; 5963 resolve(null); 5964 }, DET_T10_SOCIAL_BRAND_IMG_MS); 5965 var img = new Image(); 5966 img.onload = function () { 5967 if (settled) return; 5968 settled = true; 5969 try { 5970 clearTimeout(tid); 5971 } catch (eT) {} 5972 resolve(img.naturalWidth > 0 ? img : null); 5973 }; 5974 img.onerror = function () { 5975 if (settled) return; 5976 settled = true; 5977 try { 5978 clearTimeout(tid); 5979 } catch (eE) {} 5980 resolve(null); 5981 }; 5982 img.src = catellixDetectorBrandLogoSrc(); 5983 }); 5984 } 5985 detTop10SocialBrandLogoPromise.then(function (img) { 5986 detTop10SocialBrandLogoImg = img; 5987 callback(img); 5988 }); 5989 } 5990 5991 function detSocialCardRoundClip(ctx, width, height, radius) { 5992 var rr = Math.min(radius, width / 2, height / 2); 5993 ctx.beginPath(); 5994 if (typeof ctx.roundRect === 'function') { 5995 ctx.roundRect(0, 0, width, height, rr); 5996 } else { 5997 var x = 0; 5998 var y = 0; 5999 var w0 = width; 6000 var h0 = height; 6001 ctx.moveTo(x + rr, y); 6002 ctx.lineTo(x + w0 - rr, y); 6003 ctx.quadraticCurveTo(x + w0, y, x + w0, y + rr); 6004 ctx.lineTo(x + w0, y + h0 - rr); 6005 ctx.quadraticCurveTo(x + w0, y + h0, x + w0 - rr, y + h0); 6006 ctx.lineTo(x + rr, y + h0); 6007 ctx.quadraticCurveTo(x, y + h0, x, y + h0 - rr); 6008 ctx.lineTo(x, y + rr); 6009 ctx.quadraticCurveTo(x, y, x + rr, y); 6010 ctx.closePath(); 6011 } 6012 } 6013 6014 function detSocialCardRoundRectPath(ctx, x, y, rw, rh, rad) { 6015 var r0 = Math.min(rad, rw / 2, rh / 2); 6016 ctx.beginPath(); 6017 if (typeof ctx.roundRect === 'function') { 6018 ctx.roundRect(x, y, rw, rh, r0); 6019 } else { 6020 ctx.rect(x, y, rw, rh); 6021 } 6022 } 6023 6024 function detAbsDetectorAssetUrl(u) { 6025 if (u == null || u === '') return ''; 6026 var s = String(u).trim(); 6027 if (!s) return ''; 6028 if (/^https?:\/\//i.test(s) || /^data:/i.test(s)) return s; 6029 try { 6030 if (typeof window !== 'undefined' && window.location && window.location.origin) { 6031 return new URL(s, window.location.origin).href; 6032 } 6033 } catch (eAbs) {} 6034 return s; 6035 } 6036 6037 /** URLs para tentar no cartão social (fetch+CORS â blob, sem «taint» no canvas). */ 6038 function detLogoUrlCandidatesForTop10SocialCard(svc) { 6039 if (!svc) return []; 6040 var dom = (svc.logo_domain || '').trim(); 6041 var domLc = dom.toLowerCase(); 6042 var seen = {}; 6043 var out = []; 6044 function addUrl(u) { 6045 var abs = detAbsDetectorAssetUrl(u); 6046 if (!abs) return; 6047 var key = abs.split('#')[0]; 6048 if (seen[key]) return; 6049 seen[key] = 1; 6050 out.push(abs); 6051 } 6052 var man = state.logoManifestByDomain; 6053 if (man && domLc) { 6054 if (man[domLc]) addUrl(man[domLc]); 6055 else if (/\.com\.(br|ar|mx)$/i.test(domLc)) { 6056 var manPar = domLc.replace(/\.com\.(br|ar|mx)$/i, '.com'); 6057 if (man[manPar]) addUrl(man[manPar]); 6058 } 6059 } 6060 var prim = (svc.logo_url || '').trim(); 6061 if (prim) addUrl(prim); 6062 var chain = Array.isArray(svc.logo_icons) && svc.logo_icons.length ? svc.logo_icons : null; 6063 var c; 6064 if (chain) { 6065 for (c = 0; c < chain.length; c++) { 6066 addUrl(chain[c]); 6067 } 6068 } 6069 if (dom) { 6070 addUrl('/static/detector-logos/' + dom.replace(/\./g, '_') + '.png'); 6071 addUrl(faviconForDomain(dom)); 6072 addUrl('https://icons.duckduckgo.com/ip3/' + encodeURIComponent(dom) + '.ico'); 6073 addUrl('https://www.google.com/s2/favicons?domain=' + encodeURIComponent(dom) + '&sz=128'); 6074 } 6075 return prioritizeSameOriginDetectorUrls(out); 6076 } 6077 6078 /** `drawImage` + `toDataURL` só funcionam com mesma origem, data: ou blob: (evita canvas «tainted»). */ 6079 function detImgSafeForCanvasExport(img) {
6080 if (!img || !img.complete || !(img.naturalWidth > 0)) return false; 6081 var src = (img.currentSrc || img.getAttribute('src') || '').trim(); 6082 if (!src) return false; 6083 if (/^data:/i.test(src) || /^blob:/i.test(src)) return true; 6084 try { 6085 var page = window.location.href || 'http://localhost/'; 6086 return new URL(src, page).origin === new URL(page).origin; 6087 } catch (eSafe) { 6088 return false; 6089 } 6090 } 6091 6092 function loadTop10SocialRowLogoImageOne(row) { 6093 var svc = communityRankingRowToSvc(row); 6094 var domImgSrcs = []; 6095 var fetchOne = function () { 6096 if (typeof fetch !== 'function') return Promise.resolve(null); 6097 var metaUrls = detLogoUrlCandidatesForTop10SocialCard(svc); 6098 var merged = domImgSrcs.concat(metaUrls); 6099 var seen = {}; 6100 var unique = []; 6101 for (var k = 0; k < merged.length; k++) { 6102 var u = merged[k]; 6103 if (!u) continue; 6104 var nk = u.split('#')[0]; 6105 if (seen[nk]) continue; 6106 seen[nk] = 1; 6107 unique.push(u); 6108 } 6109 return loadFirstTop10SocialLogoFromUrls(prioritizeSameOriginDetectorUrls(unique)); 6110 }; 6111 var sid = row && row.service_id; 6112 if (sid == null || sid === '') return fetchOne(); 6113 var idSel = 6114 typeof CSS !== 'undefined' && typeof CSS.escape === 'function' 6115 ? CSS.escape(String(sid)) 6116 : String(sid).replace(/\\/g, '\\\\').replace(/"/g, '\\"'); 6117 var domSelectors = [ 6118 '#top10Cards .tile[data-svc-id="' + idSel + '"] .poster-logo-wrap img', 6119 '#top10Cards .top10-cards-shelf--games .tile[data-svc-id="' + idSel + '"] .poster-logo-wrap img', 6120 '#top10Boards .top10-item[data-svc-id="' + idSel + '"] img.top10-logo', 6121 '#detMobileCommunityBoardHost .det-sidebar-line--rank-main[data-svc-id="' + idSel + '"] img.det-sb-logo', 6122 '.det-sidebar-complaints-item .det-sidebar-line--rank-main[data-svc-id="' + idSel + '"] img.det-sb-logo', 6123 '[data-svc-id="' + idSel + '"] img.det-sb-logo', 6124 '[data-svc-id="' + idSel + '"] img[src]', 6125 ]; 6126 6127 function tryDomAt(index) { 6128 if (index >= domSelectors.length) return fetchOne(); 6129 var el = document.querySelector(domSelectors[index]); 6130 if (!el) return tryDomAt(index + 1); 6131 var elSrc = (el.currentSrc || el.getAttribute('src') || '').trim(); 6132 if (elSrc && !/^data:/i.test(elSrc)) domImgSrcs.push(elSrc); 6133 if (detImgSafeForCanvasExport(el)) return Promise.resolve(el); 6134 /* URL externa nunca fica «safe» para canvas após load â não esperar 2,2s por selector. */ 6135 var canBecomeSafe = false; 6136 try { 6137 var pageH = window.location.href || 'http://localhost/'; 6138 if (elSrc && (/^data:/i.test(elSrc) || /^blob:/i.test(elSrc))) canBecomeSafe = true; 6139 else if (elSrc) canBecomeSafe = new URL(elSrc, pageH).origin === new URL(pageH).origin; 6140 } catch (eOrig) {} 6141 if (!canBecomeSafe) return tryDomAt(index + 1); 6142 if (!el.complete) { 6143 return new Promise(function (resolve) { 6144 var settled = false; 6145 function goNext() { 6146 if (settled) return; 6147 settled = true; 6148 try { 6149 clearTimeout(tid); 6150 } catch (eT) {} 6151 tryDomAt(index + 1).then(resolve); 6152 } 6153 var tid = setTimeout(goNext, 2200); 6154 el.addEventListener( 6155 'load', 6156 function () { 6157 if (settled) return; 6158 settled = true; 6159 try { 6160 clearTimeout(tid); 6161 } catch (eL) {} 6162 if (detImgSafeForCanvasExport(el)) resolve(el); 6163 else tryDomAt(index + 1).then(resolve); 6164 }, 6165 { once: true }, 6166 ); 6167 el.addEventListener('error', goNext, { once: true }); 6168 }); 6169 } 6170 return tryDomAt(index + 1); 6171 } 6172 6173 return tryDomAt(0); 6174 } 6175 6176 /** 6177 * URLs de Ãcones externas passam pelo proxy same-origin (/api/detector/fetch-icon), 6178 * para fetchâblobâcanvas no cartão social (CDNs raramente enviam CORS). 6179 */ 6180 function detectorIconProxyWrapUrl(url) { 6181 if (url == null || url === '') return url; 6182 var s = String(url).trim(); 6183 if (!s || /^data:/i.test(s) || /^blob:/i.test(s)) return s; 6184 var pageHref = ''; 6185 try { 6186 pageHref = window.location.href || ''; 6187 } catch (e0) { 6188 return s; 6189 } 6190 var abs = s; 6191 try { 6192 abs = new URL(s, pageHref).href; 6193 } catch (e1) { 6194 return s; 6195 } 6196 if (abs.indexOf('/api/detector/fetch-icon?') !== -1) return abs; 6197 var pageOrigin = ''; 6198 try { 6199 pageOrigin = new URL(pageHref || 'http://localhost').origin; 6200 } catch (e2) { 6201 return s; 6202 } 6203 try { 6204 if (new URL(abs).origin === pageOrigin) return abs; 6205 } catch (e3) { 6206 return s; 6207 } 6208 try { 6209 return pageOrigin + '/api/detector/fetch-icon?u=' + encodeURIComponent(abs); 6210 } catch (e4) { 6211 return s; 6212 } 6213 } 6214 6215 /** 6216 * Ranking comunidade (mobile + sidebar): `src` via `/api/detector/fetch-icon` para o mesmo PNG que o servidor valida 6217 * (evita bloqueio de favicon Google no mobile e 404 antigos no catálogo sem «cara» do unavatar). 6218 */ 6219 function detComplaintsRankLogoSrc(svc, row) { 6220 var dom = 6221 (svc && svc.logo_domain ? String(svc.logo_domain) : '').trim() || 6222 (row && row.logo_domain ? String(row.logo_domain) : '').trim(); 6223 var raw = ''; 6224 if (svc && svc.logo_url != null && String(svc.logo_url).trim() !== '') { 6225 raw = String(svc.logo_url).trim(); 6226 } else if (row && row.logo_url != null && String(row.logo_url).trim() !== '') { 6227 raw = String(row.logo_url).trim(); 6228 } 6229 if (!raw) { 6230 raw = faviconForDomain(dom); 6231 } 6232 var abs = detAbsDetectorAssetUrl(raw); 6233 if (!abs) { 6234 abs = faviconForDomain(dom); 6235 } 6236 return detectorIconProxyWrapUrl(abs); 6237 } 6238 6239 /** Resolve com `fallback` se `promise` não terminar em `ms` (compatÃvel com Promise sem .finally). */ 6240 function detPromiseOrFallback(promise, ms, fallback) { 6241 return new Promise(function (resolve) { 6242 var done = false;
6243 var tid = setTimeout(function () { 6244 if (done) return; 6245 done = true; 6246 resolve(fallback); 6247 }, ms); 6248 promise.then( 6249 function (v) { 6250 if (done) return; 6251 done = true; 6252 try { 6253 clearTimeout(tid); 6254 } catch (eC) {} 6255 resolve(v); 6256 }, 6257 function () { 6258 if (done) return; 6259 done = true; 6260 try { 6261 clearTimeout(tid); 6262 } catch (eC2) {} 6263 resolve(fallback); 6264 }, 6265 ); 6266 }); 6267 } 6268 6269 function fetchImageForCanvasViaBlob(url) { 6270 var cacheKey = url; 6271 var fromCache = logoCacheGet(cacheKey).then(function (cachedBlob) { 6272 if (!cachedBlob) throw new Error('miss'); 6273 return blobToImage(cachedBlob); 6274 }); 6275 var pipeline = fromCache.catch(function () { 6276 var fetchUrl = detectorIconProxyWrapUrl(url); 6277 return fetch(fetchUrl, { mode: 'cors', credentials: 'omit' }) 6278 .then(function (r) { 6279 if (!r.ok) throw new Error('http'); 6280 return r.blob(); 6281 }) 6282 .then(function (blob) { 6283 logoCachePut(cacheKey, blob); 6284 return blobToImage(blob); 6285 }); 6286 }); 6287 var timeoutP = new Promise(function (_, reject) { 6288 setTimeout(function () { 6289 reject(new Error('timeout')); 6290 }, DET_T10_SOCIAL_ICON_FETCH_MS); 6291 }); 6292 return Promise.race([pipeline, timeoutP]); 6293 } 6294 6295 function loadImageCrossOriginDirect(url) { 6296 return new Promise(function (resolve, reject) { 6297 var im = new Image(); 6298 im.crossOrigin = 'anonymous'; 6299 var tid = setTimeout(function () { im.src = ''; reject(); }, 4000); 6300 im.onload = function () { 6301 clearTimeout(tid); 6302 if (im.naturalWidth > 0) resolve(im); 6303 else reject(); 6304 }; 6305 im.onerror = function () { clearTimeout(tid); reject(); }; 6306 var sep = url.indexOf('?') === -1 ? '?' : '&'; 6307 im.src = url + sep + '_c=1'; 6308 }); 6309 } 6310 6311 function loadFirstTop10SocialLogoFromUrls(urls) { 6312 var i = 0; 6313 function nextAttempt() { 6314 if (i >= urls.length) return crossOriginPass(); 6315 var u = urls[i++]; 6316 if (!u) return nextAttempt(); 6317 return fetchImageForCanvasViaBlob(u).catch(nextAttempt); 6318 } 6319 var j = 0; 6320 function crossOriginPass() { 6321 if (j >= urls.length) return Promise.resolve(null); 6322 var u = urls[j++]; 6323 if (!u || /^data:/i.test(u) || /^blob:/i.test(u)) return crossOriginPass(); 6324 return loadImageCrossOriginDirect(u).catch(crossOriginPass); 6325 } 6326 return nextAttempt(); 6327 } 6328 6329 /** 6330 * @param {Array} rows â até 10 linhas do ranking comunidade 6331 * @param {function(Array<HTMLImageElement|null>)} doneCb 6332 */ 6333 function loadTop10SocialRowLogoImages(rows, doneCb) { 6334 var lim = Math.min(rows.length, 10); 6335 if (!lim) { 6336 doneCb([]); 6337 return; 6338 } 6339 var promises = []; 6340 var ri; 6341 for (ri = 0; ri < lim; ri++) { 6342 promises.push( 6343 detPromiseOrFallback(loadTop10SocialRowLogoImageOne(rows[ri]), DET_T10_SOCIAL_ROW_LOGO_MS, null), 6344 ); 6345 } 6346 Promise.all(promises) 6347 .then(function (imgs) { 6348 doneCb(imgs || []); 6349 }) 6350 .catch(function () { 6351 doneCb([]); 6352 }); 6353 } 6354 6355 var _prefetchLogosScheduled = false; 6356 function prefetchTop10Logos() { 6357 if (_prefetchLogosScheduled) return; 6358 _prefetchLogosScheduled = true; 6359 setTimeout(function () { 6360 try { 6361 var rows = communityRowsWithVotesForReport().slice(0, 10); 6362 if (!rows.length) { _prefetchLogosScheduled = false; return; } 6363 fetchDetectorLogoManifest().then(function () { 6364 var idx = 0; 6365 function nextLogo() { 6366 if (idx >= rows.length) return; 6367 var svc = communityRankingRowToSvc(rows[idx]); 6368 idx++; 6369 if (!svc) { nextLogo(); return; } 6370 var urls = detLogoUrlCandidatesForTop10SocialCard(svc); 6371 if (!urls.length) { nextLogo(); return; } 6372 var firstUrl = urls[0]; 6373 logoCacheGet(firstUrl).then(function (cached) { 6374 if (cached) { nextLogo(); return; } 6375 var fetchUrl = detectorIconProxyWrapUrl(firstUrl); 6376 fetch(fetchUrl, { mode: 'cors', credentials: 'omit' }) 6377 .then(function (r) { return r.ok ? r.blob() : null; }) 6378 .then(function (blob) { if (blob) logoCachePut(firstUrl, blob); }) 6379 .catch(function () {}) 6380 .then(nextLogo); 6381 }).catch(nextLogo); 6382 } 6383 nextLogo(); 6384 }).catch(function () { _prefetchLogosScheduled = false; }); 6385 } catch (ex) { _prefetchLogosScheduled = false; } 6386 }, 3000); 6387 } 6388 6389 /** Estrelas Catellix (meias por clip), alinhadas a `rightX`, para o cartão social Top 10. */
6390 function drawTop10SocialCardStarsRight(ctx, rightX, yMid, starValue, starSize) { 6391 var v = Math.max(0, Math.min(5, Number(starValue) || 0)); 6392 ctx.save(); 6393 ctx.font = 'bold ' + starSize + 'px system-ui, "Segoe UI", Roboto, sans-serif'; 6394 ctx.textBaseline = 'middle'; 6395 ctx.textAlign = 'left'; 6396 var starW = ctx.measureText('â ').width || starSize * 0.92; 6397 var gap = Math.max(0.5, Math.round(starSize * 0.06)); 6398 var totalW = 5 * starW + 4 * gap; 6399 var x0 = rightX - totalW; 6400 var i; 6401 for (i = 0; i < 5; i++) { 6402 var sx = x0 + i * (starW + gap); 6403 var frac = Math.max(0, Math.min(1, v - i)); 6404 ctx.shadowBlur = 0; 6405 ctx.fillStyle = 'rgba(100, 116, 139, 0.5)'; 6406 ctx.fillText('â ', sx, yMid); 6407 if (frac > 0.001) { 6408 ctx.save(); 6409 ctx.beginPath(); 6410 ctx.rect(sx, yMid - starSize * 0.55, starW * frac + 0.35, starSize * 1.12); 6411 ctx.clip(); 6412 ctx.fillStyle = '#f59e0b'; 6413 ctx.shadowColor = 'rgba(251, 191, 36, 0.4)'; 6414 ctx.shadowBlur = 3; 6415 ctx.fillText('â ', sx, yMid); 6416 ctx.restore(); 6417 } 6418 } 6419 ctx.restore(); 6420 } 6421 6422 /** 6423 * Cartão vertical para stories/posts: gradiente marca, lista em âpillsâ, logo Catellix + catellix.com. 6424 * @param {HTMLCanvasElement} canvas 6425 * @param {Array} rows 6426 * @param {HTMLImageElement|null} brandImg â de loadTop10SocialBrandLogo 6427 * @param {Array<HTMLImageElement|null>=} rowLogos â Ãcones por linha (opcional) 6428 * @param {{ maxDpr?: number }=} drawOpts â ex.: `{ maxDpr: 1 }` para pré-visualização leve no telemóvel 6429 */ 6430 function drawTop10SocialCard(canvas, rows, brandImg, rowLogos, drawOpts) { 6431 var w = 720; 6432 var h = 940; 6433 var opt = drawOpts && typeof drawOpts === 'object' ? drawOpts : {}; 6434 var shareKind = 6435 (opt && opt.shareKind) || 6436 (detTop10SocialSession && detTop10SocialSession.kind) || 6437 'community'; 6438 var isGamesCard = shareKind === 'games'; 6439 var capDpr = 6440 typeof opt.maxDpr === 'number' && opt.maxDpr > 0 6441 ? opt.maxDpr 6442 : typeof window !== 'undefined' && window.devicePixelRatio 6443 ? Math.min(2, window.devicePixelRatio) 6444 : 1; 6445 var dpr = capDpr; 6446 canvas.width = Math.round(w * dpr); 6447 canvas.height = Math.round(h * dpr); 6448 canvas.style.width = w + 'px'; 6449 canvas.style.height = h + 'px'; 6450 var ctx = canvas.getContext('2d'); 6451 if (!ctx) return; 6452 ctx.scale(dpr, dpr); 6453 ctx.textBaseline = 'alphabetic'; 6454 6455 ctx.save(); 6456 detSocialCardRoundClip(ctx, w, h, 28); 6457 ctx.clip(); 6458 6459 var g = ctx.createLinearGradient(0, 0, w, h * 1.08); 6460 g.addColorStop(0, '#070f1a'); 6461 g.addColorStop(0.28, '#0f2744'); 6462 g.addColorStop(0.55, '#164e63'); 6463 g.addColorStop(0.82, '#0e7490'); 6464 g.addColorStop(1, '#155e75'); 6465 ctx.fillStyle = g; 6466 ctx.fillRect(0, 0, w, h); 6467 6468 var rg = ctx.createRadialGradient(w * 0.12, 0, 0, w * 0.35, h * 0.22, w * 0.95); 6469 rg.addColorStop(0, 'rgba(56, 189, 248, 0.28)'); 6470 rg.addColorStop(0.45, 'rgba(14, 165, 233, 0.1)'); 6471 rg.addColorStop(1, 'rgba(7, 15, 26, 0)'); 6472 ctx.fillStyle = rg; 6473 ctx.fillRect(0, 0, w, h); 6474 6475 ctx.fillStyle = 'rgba(255, 255, 255, 0.045)'; 6476 var dg; 6477 for (dg = 20; dg < w + h; dg += 44) { 6478 ctx.beginPath(); 6479 ctx.moveTo(dg, 0); 6480 ctx.lineTo(dg + h, h); 6481 ctx.strokeStyle = 'rgba(255, 255, 255, 0.03)'; 6482 ctx.lineWidth = 1; 6483 ctx.stroke(); 6484 } 6485 6486 ctx.fillStyle = 'rgba(255, 255, 255, 0.05)'; 6487 var gx; 6488 var gy; 6489 for (gx = 22; gx < w; gx += 40) { 6490 for (gy = 22; gy < h * 0.72; gy += 40) { 6491 ctx.beginPath(); 6492 ctx.arc(gx, gy, 1.25, 0, Math.PI * 2); 6493 ctx.fill(); 6494 } 6495 } 6496 6497 ctx.fillStyle = 'rgba(6, 182, 212, 0.95)';
6498 detSocialCardRoundRectPath(ctx, 28, 40, 56, 6, 3); 6499 ctx.fill(); 6500 6501 ctx.strokeStyle = 'rgba(56, 189, 248, 0.55)'; 6502 ctx.lineWidth = 3; 6503 ctx.beginPath(); 6504 ctx.moveTo(28, 54); 6505 ctx.lineTo(200, 54); 6506 ctx.stroke(); 6507 6508 var tk = isGamesCard ? __('js_t10_games_social_card_kicker') : __('js_t10_social_card_kicker'); 6509 var titleFontPx = 27; 6510 var titleRightReserve = brandImg && brandImg.naturalWidth > 0 ? 200 : 40; 6511 var titleMaxW = w - 28 - titleRightReserve; 6512 ctx.fillStyle = '#f8fafc'; 6513 for (; titleFontPx >= 17; titleFontPx--) { 6514 ctx.font = 'bold ' + titleFontPx + 'px system-ui, "Segoe UI", Roboto, sans-serif'; 6515 if (ctx.measureText(tk).width <= titleMaxW) break; 6516 } 6517 ctx.fillText(tk, 28, 88); 6518 6519 ctx.font = '600 16px system-ui, "Segoe UI", Roboto, sans-serif'; 6520 ctx.fillStyle = 'rgba(226, 232, 240, 0.88)'; 6521 var subY = titleFontPx <= 20 ? 120 : 118; 6522 if (isGamesCard) { 6523 ctx.fillText(__('js_t10_games_social_card_sub') + ' · ' + new Date().toISOString().slice(0, 10), 28, subY); 6524 } else { 6525 var days = VALID_RANKING_DAYS.indexOf(state.complaintsRankingDays) !== -1 ? state.complaintsRankingDays : 1; 6526 var dayLblMap = { 6527 1: __('js_window_1d'), 6528 7: __('js_window_7d'), 6529 15: __('js_window_15d'), 6530 30: __('js_window_30d'), 6531 }; 6532 var winLbl = dayLblMap[days] || __('js_window_1d'); 6533 ctx.fillText(winLbl + ' · ' + new Date().toISOString().slice(0, 10), 28, subY); 6534 } 6535 6536 if (brandImg && brandImg.naturalWidth > 0) { 6537 var headLh = 40; 6538 var headLw = (brandImg.naturalWidth / brandImg.naturalHeight) * headLh; 6539 var headMaxW = 168; 6540 if (headLw > headMaxW) { 6541 headLw = headMaxW; 6542 headLh = (brandImg.naturalHeight / brandImg.naturalWidth) * headLw; 6543 } 6544 ctx.save(); 6545 ctx.globalAlpha = 0.92; 6546 ctx.drawImage(brandImg, w - headLw - 28, 44, headLw, headLh); 6547 ctx.restore(); 6548 } 6549 6550 var rowH = 54; 6551 var pillInnerH = 48; 6552 var rowTop0 = subY + 20; 6553 var pillW = w - 56; 6554 var rankColors = ['#eab308', '#94a3b8', '#d97706', '#0ea5e9']; 6555 var logos = rowLogos || []; 6556 var iconS = 30; 6557 var iconPadL = 72; 6558 var yRankMid = function (y0) { 6559 return y0 + pillInnerH / 2; 6560 }; 6561 if (!isGamesCard) { 6562 ctx.fillStyle = 'rgba(148, 163, 184, 0.62)'; 6563 ctx.font = '600 9px system-ui, "Segoe UI", Roboto, sans-serif'; 6564 ctx.textAlign = 'right'; 6565 ctx.textBaseline = 'alphabetic'; 6566 var entHint = __('js_t10_social_enterprise_hint'); 6567 if (entHint) ctx.fillText(entHint, 28 + pillW - 12, rowTop0 - 6); 6568 } 6569 var r; 6570 for (r = 0; r < rows.length && r < 10; r++) { 6571 var y0 = rowTop0 + r * rowH; 6572 detSocialCardRoundRectPath(ctx, 28, y0, pillW, pillInnerH, 12); 6573 ctx.fillStyle = 'rgba(15, 23, 42, 0.42)'; 6574 ctx.fill(); 6575 var pillGrad = ctx.createLinearGradient(28, y0, 28 + pillW * 0.55, y0); 6576 pillGrad.addColorStop(0, 'rgba(56, 189, 248, 0.07)'); 6577 pillGrad.addColorStop(1, 'rgba(15, 23, 42, 0)'); 6578 ctx.fillStyle = pillGrad; 6579 detSocialCardRoundRectPath(ctx, 28, y0, pillW, pillInnerH, 12); 6580 ctx.fill(); 6581 detSocialCardRoundRectPath(ctx, 28, y0, pillW, pillInnerH, 12); 6582 ctx.strokeStyle = 'rgba(56, 189, 248, 0.28)'; 6583 ctx.lineWidth = 1; 6584 ctx.stroke(); 6585 6586 var ym = yRankMid(y0); 6587 var rankFill = rankColors[r < 3 ? r : 3]; 6588 ctx.beginPath(); 6589 ctx.arc(54, ym, 15, 0, Math.PI * 2); 6590 ctx.fillStyle = rankFill; 6591 ctx.fill(); 6592 ctx.strokeStyle = 'rgba(255,255,255,0.28)'; 6593 ctx.lineWidth = 1; 6594 ctx.stroke(); 6595 6596 ctx.fillStyle = r < 3 ? '#0f172a' : '#f8fafc';
6597 ctx.font = 'bold 14px system-ui, "Segoe UI", Roboto, sans-serif'; 6598 ctx.textAlign = 'center'; 6599 ctx.textBaseline = 'middle'; 6600 ctx.fillText(String(r + 1), 54, ym); 6601 ctx.textAlign = 'left'; 6602 ctx.textBaseline = 'alphabetic'; 6603 6604 var rowLogo = logos[r]; 6605 var hasLogo = rowLogo && rowLogo.naturalWidth > 0; 6606 var iconY = y0 + (pillInnerH - iconS) / 2; 6607 if (hasLogo) { 6608 ctx.save(); 6609 detSocialCardRoundRectPath(ctx, iconPadL, iconY, iconS, iconS, 8); 6610 ctx.clip(); 6611 var iw = rowLogo.naturalWidth; 6612 var ih = rowLogo.naturalHeight; 6613 var sc = Math.min(iconS / iw, iconS / ih); 6614 var dw = iw * sc; 6615 var dh = ih * sc; 6616 var dx = iconPadL + (iconS - dw) / 2; 6617 var dy = iconY + (iconS - dh) / 2; 6618 ctx.drawImage(rowLogo, dx, dy, dw, dh); 6619 ctx.restore(); 6620 } else { 6621 var fullNm0 = String(rows[r].name || rows[r].service_id || ''); 6622 ctx.save(); 6623 detSocialCardRoundRectPath(ctx, iconPadL, iconY, iconS, iconS, 8); 6624 ctx.fillStyle = 'rgba(56, 189, 248, 0.18)'; 6625 ctx.fill(); 6626 ctx.strokeStyle = 'rgba(56, 189, 248, 0.35)'; 6627 ctx.lineWidth = 1; 6628 ctx.stroke(); 6629 ctx.fillStyle = 'rgba(248, 250, 252, 0.92)'; 6630 ctx.font = 'bold 15px system-ui, "Segoe UI", Roboto, sans-serif'; 6631 ctx.textAlign = 'center'; 6632 ctx.textBaseline = 'middle'; 6633 ctx.fillText((fullNm0.charAt(0) || '?').toUpperCase(), iconPadL + iconS / 2, iconY + iconS / 2); 6634 ctx.restore(); 6635 } 6636 6637 var nameX = iconPadL + iconS + 12; 6638 var fullNm = String(rows[r].name || rows[r].service_id || ''); 6639 var svc = communityRankingRowToSvc(rows[r]); 6640 var metricsRight = 28 + pillW - 12; 6641 var maxTextW; 6642 if (isGamesCard) { 6643 var msR = svc ? gameRankingLatencyMs(svc) : null; 6644 var latStr = 6645 msR != null && !isNaN(msR) ? String(Math.round(msR)) + ' ms' : 'â'; 6646 ctx.textAlign = 'right'; 6647 ctx.textBaseline = 'middle'; 6648 ctx.font = 'bold 13px system-ui, "Segoe UI", Roboto, sans-serif'; 6649 var latCol = 6650 msR != null && !isNaN(msR) && msR > 500 6651 ? '#f87171' 6652 : msR != null && !isNaN(msR) && msR > 200 6653 ? '#fbbf24' 6654 : 'rgba(186, 230, 253, 0.95)'; 6655 ctx.fillStyle = latCol; 6656 ctx.fillText(latStr, metricsRight, ym); 6657 ctx.textAlign = 'left'; 6658 var latW = ctx.measureText(latStr).width; 6659 maxTextW = Math.max(48, metricsRight - latW - 14 - nameX); 6660 } else { 6661 var sd = computeModalStarDisplay(svc); 6662 var ts = Number(svc && svc.trust_score); 6663 if (isNaN(ts)) ts = 100; 6664 var scoreStr = ts.toFixed(1).replace('.', ',') + '/100'; 6665 ctx.font = 'bold 11px system-ui, "Segoe UI", Roboto, sans-serif'; 6666 ctx.textBaseline = 'middle'; 6667 var scoreW = ctx.measureText(scoreStr).width; 6668 var starSize = 11; 6669 ctx.font = 'bold ' + starSize + 'px system-ui, "Segoe UI", Roboto, sans-serif'; 6670 var starW = ctx.measureText('â ').width || starSize * 0.92; 6671 var sGap = Math.max(0.5, Math.round(starSize * 0.06)); 6672 var starsBlockW = 5 * starW + 4 * sGap; 6673 var scoreX = metricsRight - starsBlockW - 8 - scoreW; 6674 ctx.textAlign = 'left'; 6675 ctx.fillStyle = 'rgba(186, 230, 253, 0.95)'; 6676 ctx.font = 'bold 11px system-ui, "Segoe UI", Roboto, sans-serif'; 6677 ctx.fillText(scoreStr, scoreX, ym); 6678 drawTop10SocialCardStarsRight(ctx, metricsRight, ym, sd.value, starSize); 6679 maxTextW = Math.max(48, scoreX - 10 - nameX); 6680 } 6681 ctx.fillStyle = '#f1f5f9';
6682 ctx.font = '600 17px system-ui, "Segoe UI", Roboto, sans-serif'; 6683 ctx.textBaseline = 'alphabetic'; 6684 var nm = fullNm; 6685 while (nm.length > 1 && ctx.measureText(nm + 'â¦').width > maxTextW) { 6686 nm = nm.slice(0, -1); 6687 } 6688 if (nm.length < fullNm.length) nm += 'â¦'; 6689 ctx.textBaseline = 'middle'; 6690 ctx.fillText(nm, nameX, ym); 6691 } 6692 6693 var footBand = ctx.createLinearGradient(0, h - 120, 0, h); 6694 footBand.addColorStop(0, 'rgba(7, 15, 26, 0)'); 6695 footBand.addColorStop(0.35, 'rgba(7, 15, 26, 0.55)'); 6696 footBand.addColorStop(1, 'rgba(7, 15, 26, 0.82)'); 6697 ctx.fillStyle = footBand; 6698 ctx.fillRect(0, h - 120, w, 120); 6699 6700 var footTop = h - 96; 6701 var logoH = 44; 6702 if (brandImg && brandImg.naturalWidth > 0) { 6703 var lw = (brandImg.naturalWidth / brandImg.naturalHeight) * logoH; 6704 var logoMaxW = 200; 6705 if (lw > logoMaxW) { 6706 lw = logoMaxW; 6707 logoH = (brandImg.naturalHeight / brandImg.naturalWidth) * lw; 6708 } 6709 ctx.drawImage(brandImg, 28, footTop, lw, logoH); 6710 ctx.fillStyle = '#f0f9ff'; 6711 ctx.font = 'bold 23px system-ui, "Segoe UI", Roboto, sans-serif'; 6712 ctx.textBaseline = 'middle'; 6713 ctx.fillText('catellix.com', 28 + lw + 14, footTop + logoH * 0.52); 6714 } else { 6715 ctx.fillStyle = '#f0f9ff'; 6716 ctx.font = 'bold 23px system-ui, "Segoe UI", Roboto, sans-serif'; 6717 ctx.textBaseline = 'middle'; 6718 ctx.fillText('catellix.com', 28, footTop + 22); 6719 } 6720 6721 ctx.fillStyle = 'rgba(226, 232, 240, 0.78)'; 6722 ctx.font = '13px system-ui, "Segoe UI", Roboto, sans-serif'; 6723 ctx.textBaseline = 'alphabetic'; 6724 ctx.fillText(__('js_t10_social_card_footer'), 28, h - 22); 6725 6726 ctx.restore(); 6727 6728 ctx.save(); 6729 detSocialCardRoundClip(ctx, w, h, 28); 6730 ctx.strokeStyle = 'rgba(56, 189, 248, 0.4)'; 6731 ctx.lineWidth = 2; 6732 ctx.stroke(); 6733 ctx.restore(); 6734 } 6735 6736 function setTop10SocialModalGenerating(isGenerating, errorMsg, opts) { 6737 var o = opts && typeof opts === 'object' ? opts : {}; 6738 var keepPreview = !!o.keepPreviewVisible; 6739 var loadEl = document.getElementById('detTop10SocialLoading'); 6740 var img = document.getElementById('detTop10SocialPreview'); 6741 var act = document.getElementById('detTop10SocialActions'); 6742 if (loadEl) { 6743 if (isGenerating) { 6744 loadEl.hidden = false; 6745 loadEl.textContent = o.loadingText != null ? String(o.loadingText) : __('js_t10_social_preparing'); 6746 loadEl.setAttribute('data-busy', '1'); 6747 } else if (errorMsg) { 6748 loadEl.hidden = false; 6749 loadEl.textContent = errorMsg; 6750 loadEl.removeAttribute('data-busy'); 6751 } else { 6752 loadEl.hidden = true; 6753 loadEl.textContent = ''; 6754 loadEl.removeAttribute('data-busy'); 6755 } 6756 } 6757 if (img) { 6758 if (errorMsg) { 6759 img.hidden = true; 6760 } else if (isGenerating && !keepPreview) { 6761 img.hidden = true; 6762 } 6763 } 6764 if (act) { 6765 act.classList.remove('det-t10-social-actions--disabled'); 6766 } 6767 } 6768 6769 function openTop10GamesSocialShareModal() { 6770 var gRows = rankingRowsForTop10GamesSocialShare(); 6771 if (!gRows.length) { 6772 try { 6773 alert(__('js_t10_games_social_no_data')); 6774 } catch (eA) {} 6775 return; 6776 } 6777 detTop10SocialSession = { kind: 'games', rows: gRows }; 6778 openTop10SocialShareModalBody(); 6779 } 6780 6781 function openTop10SocialShareModal() { 6782 detTop10SocialSession = { kind: 'community', rows: null }; 6783 openTop10SocialShareModalBody(); 6784 } 6785 6786 function openTop10SocialShareModalBody() { 6787 var rows = getTop10SocialShareRowsForModal(); 6788 if (!rows.length) { 6789 try { 6790 alert(__('js_t10_social_no_data')); 6791 } catch (eA) {} 6792 detTop10SocialSession = { kind: 'community', rows: null }; 6793 return; 6794 } 6795 var modal = document.getElementById('detTop10SocialModal'); 6796 var img = document.getElementById('detTop10SocialPreview'); 6797 var cv = document.getElementById('detTop10SocialCanvas'); 6798 if (!modal || !img || !cv) return; 6799 try { 6800 cv.dataset.detExportReady = '0'; 6801 } catch (eDs) {} 6802 try { 6803 img.removeAttribute('src'); 6804 img.alt = ''; 6805 img.hidden = true; 6806 } catch (eClr) {} 6807 var tsm = document.getElementById('detTop10SocialTitle'); 6808 if (tsm) { 6809 tsm.textContent = 6810 detTop10SocialSession && detTop10SocialSession.kind === 'games' 6811 ? __('js_t10_games_social_modal_title') 6812 : __('js_t10_social_modal_title'); 6813 } 6814 var lead = document.getElementById('detTop10SocialLead'); 6815 if (lead) { 6816 lead.textContent = 6817 detTop10SocialSession && detTop10SocialSession.kind === 'games' 6818 ? __('js_t10_games_social_lead') 6819 : __('js_t10_social_lead'); 6820 } 6821 modal.classList.add('open'); 6822 modal.setAttribute('aria-hidden', 'false'); 6823 try { 6824 document.body.style.overflow = 'hidden'; 6825 } catch (eB) {} 6826 6827 function applyPreviewJpegFromCanvas(c) { 6828 try { 6829 img.src = c.toDataURL('image/jpeg', 0.82); 6830 img.alt = __('js_t10_social_preview_alt'); 6831 img.hidden = false;
6832 } catch (eJ) { 6833 try { 6834 img.src = c.toDataURL('image/png'); 6835 img.alt = __('js_t10_social_preview_alt'); 6836 img.hidden = false; 6837 } catch (eP) {} 6838 } 6839 } 6840 6841 var brandCached = 6842 detTop10SocialBrandLogoImg && 6843 detTop10SocialBrandLogoImg.complete && 6844 detTop10SocialBrandLogoImg.naturalHeight > 0 6845 ? detTop10SocialBrandLogoImg 6846 : null; 6847 var preCv = document.createElement('canvas'); 6848 var skShare = 6849 detTop10SocialSession && detTop10SocialSession.kind === 'games' ? 'games' : 'community'; 6850 try { 6851 drawTop10SocialCard(preCv, rows, brandCached, [], { maxDpr: 1, shareKind: skShare }); 6852 applyPreviewJpegFromCanvas(preCv); 6853 } catch (eQuick) {} 6854 6855 setTop10SocialModalGenerating(true, null, { 6856 keepPreviewVisible: true, 6857 loadingText: __('js_t10_social_icons_loading'), 6858 }); 6859 6860 try { 6861 loadTop10SocialBrandLogo(function () {}); 6862 } catch (ePreload) {} 6863 6864 function finishCardOk() { 6865 function applyPngDeferred() { 6866 try { 6867 try { 6868 img.src = cv.toDataURL('image/png'); 6869 } catch (ePng) { 6870 applyPreviewJpegFromCanvas(cv); 6871 } 6872 img.alt = __('js_t10_social_preview_alt'); 6873 img.hidden = false; 6874 try { 6875 cv.dataset.detExportReady = '1'; 6876 } catch (eD2) {} 6877 setTop10SocialModalGenerating(false); 6878 } catch (eImg) { 6879 try { 6880 img.removeAttribute('src'); 6881 img.alt = ''; 6882 img.hidden = true; 6883 } catch (eImg2) {} 6884 setTop10SocialModalGenerating(false, __('js_t10_social_card_failed')); 6885 } 6886 } 6887 try { 6888 if (typeof requestAnimationFrame === 'function') { 6889 requestAnimationFrame(function () { 6890 setTimeout(applyPngDeferred, 0); 6891 }); 6892 } else { 6893 setTimeout(applyPngDeferred, 0); 6894 } 6895 } catch (eRaf) { 6896 applyPngDeferred(); 6897 } 6898 } 6899 function drawSocialCardAfterManifest() { 6900 loadTop10SocialBrandLogo(function (brandImg) { 6901 loadTop10SocialRowLogoImages(rows, function (rowLogos) { 6902 try { 6903 drawTop10SocialCard(cv, rows, brandImg, rowLogos, { shareKind: skShare }); 6904 finishCardOk(); 6905 } catch (eDraw) { 6906 setTop10SocialModalGenerating(false, __('js_t10_social_card_failed')); 6907 } 6908 }); 6909 }); 6910 } 6911 fetchDetectorLogoManifest() 6912 .then(drawSocialCardAfterManifest) 6913 .catch(drawSocialCardAfterManifest); 6914 } 6915 6916 function closeTop10SocialShareModal() { 6917 var modal = document.getElementById('detTop10SocialModal'); 6918 if (!modal) return; 6919 modal.classList.remove('open'); 6920 modal.setAttribute('aria-hidden', 'true'); 6921 try { 6922 document.body.style.overflow = ''; 6923 } catch (e) {} 6924 detTop10SocialSession = { kind: 'community', rows: null }; 6925 } 6926 6927 function fillTop20StudioDom() { 6928 var host = document.getElementById('detTop20ListHost'); 6929 if (!host) return; 6930 var rows = communityRowsWithVotesForReport().slice(0, 20); 6931 if (!rows.length) { 6932 host.innerHTML = 6933 '<p class="det-top20-empty">' + esc(__('js_top20_empty')) + '</p>'; 6934 return; 6935 } 6936 var frag = ''; 6937 for (var i = 0; i < rows.length; i++) { 6938 var rw = rows[i]; 6939 var rnk = i + 1; 6940 var tier = 6941 rnk === 1 ? 'gold' : rnk === 2 ? 'silver' : rnk === 3 ? 'bronze' : 'std'; 6942 var pill = complaintsRankingPillDisplay(rw); 6943 var svc = findSvc(rw.service_id); 6944 var dom = svc && svc.logo_domain ? String(svc.logo_domain) : ''; 6945 var logo = svc && svc.logo_url ? svc.logo_url : faviconForDomain(dom); 6946 frag += 6947 '<article class="det-top20-card det-top20-card--' + 6948 esc(tier) + 6949 '"><div class="det-top20-rank">' + 6950 esc(String(rnk)) + 6951 '</div><img class="det-top20-logo" src="' + 6952 esc(logo) + 6953 '" alt="" width="48" height="48" loading="lazy"' + 6954 imgLogoOnErrorAttr(dom) + 6955 '><div class="det-top20-card-main"><h3 class="det-top20-name">' + 6956 esc(rw.name || rw.service_id) + 6957 '</h3><p class="det-top20-meta">' + 6958 communityRankingMetaLineHtml(rw) + 6959 '</p></div><div class="det-top20-score" title="' + 6960 esc(complaintsRankingPillTitle(rw, pill)) + 6961 '">' + 6962 esc(pill.value) + 6963 '</div></article>'; 6964 } 6965 host.innerHTML = frag; 6966 host.querySelectorAll('.det-top20-card').forEach(function (card, idx) { 6967 card.style.animationDelay = idx * 0.04 + 's'; 6968 }); 6969 } 6970 6971 function openTop20StudioModal() { 6972 var modal = document.getElementById('detTop20Modal'); 6973 if (!modal) return; 6974 fillTop20StudioDom(); 6975 var t = document.getElementById('detTop20Title'); 6976 if (t) t.textContent = __('js_top20_modal_title'); 6977 var lead = document.getElementById('detTop20Lead'); 6978 if (lead) lead.textContent = __('js_top20_modal_lead'); 6979 modal.classList.add('open', 'det-top20-modal--anim'); 6980 modal.setAttribute('aria-hidden', 'false'); 6981 try { 6982 document.body.style.overflow = 'hidden'; 6983 } catch (e) {} 6984 } 6985 6986 function closeTop20StudioModal() { 6987 var modal = document.getElementById('detTop20Modal'); 6988 if (!modal) return; 6989 modal.classList.remove('open', 'det-top20-modal--anim'); 6990 modal.setAttribute('aria-hidden', 'true'); 6991 try { 6992 document.body.style.overflow = ''; 6993 } catch (e2) {} 6994 } 6995 6996 function wireDetectorRankingStudioAndReport() { 6997 if (document.documentElement.dataset.detRankingShareWired === '1') return; 6998 document.documentElement.dataset.detRankingShareWired = '1'; 6999 7000 var btnTop10 = document.getElementById('btnSidebarTop10Share'); 7001 if (btnTop10) { 7002 btnTop10.addEventListener('click', function () { 7003 openDetectorRankingReportModal('top10'); 7004 }); 7005 } 7006 var btnTop10Soc = document.getElementById('btnSidebarTop10Social'); 7007 if (btnTop10Soc) { 7008 btnTop10Soc.addEventListener('click', function () { 7009 openTop10SocialShareModal(); 7010 }); 7011 } 7012 var btnTop20Share = document.getElementById('detTop20Share'); 7013 if (btnTop20Share) { 7014 btnTop20Share.addEventListener('click', function () { 7015 openDetectorRankingReportModal('top20'); 7016 }); 7017 } 7018 var btnTop30Share = document.getElementById('detTop30Share'); 7019 if (btnTop30Share) { 7020 btnTop30Share.addEventListener('click', function () { 7021 openTop30RankingReportAfterPrefetch(); 7022 }); 7023 } 7024 var repClose = document.getElementById('detReportShareClose'); 7025 var repBd = document.getElementById('detReportShareBackdrop'); 7026 if (repClose) repClose.addEventListener('click', closeDetectorRankingReportModal); 7027 if (repBd) repBd.addEventListener('click', closeDetectorRankingReportModal); 7028 7029 var btnPrint = document.getElementById('detReportBtnPrint'); 7030 if (btnPrint) { 7031 btnPrint.addEventListener('click', function () { 7032 window.print(); 7033 }); 7034 } 7035 var btnWa = document.getElementById('detReportBtnWhatsapp'); 7036 if (btnWa) { 7037 btnWa.addEventListener('click', function () { 7038 var text = encodeURIComponent(detectorReportShareBodyText()); 7039 window.open('https://wa.me/?text=' + text, '_blank', 'noopener,noreferrer'); 7040 }); 7041 } 7042 var btnMail = document.getElementById('detReportBtnEmail'); 7043 if (btnMail) { 7044 btnMail.addEventListener('click', function () { 7045 var subj = encodeURIComponent(detectorReportShareTitle() + ' â Catellix'); 7046 var body = encodeURIComponent(detectorReportShareBodyText()); 7047 window.location.href = 'mailto:?subject=' + subj + '&body=' + body; 7048 }); 7049 } 7050 var btnNative = document.getElementById('detReportBtnNative'); 7051 if (btnNative && navigator.share) { 7052 btnNative.addEventListener('click', function () { 7053 navigator 7054 .share({ 7055 title: detectorReportShareTitle(), 7056 text: detectorReportShareBodyText(), 7057 }) 7058 .catch(function () {}); 7059 }); 7060 } else if (btnNative) { 7061 btnNative.hidden = true; 7062 } 7063 7064 var t20Close = document.getElementById('detTop20Close'); 7065 var t20Bd = document.getElementById('detTop20Backdrop'); 7066 if (t20Close) t20Close.addEventListener('click', closeTop20StudioModal); 7067 if (t20Bd) t20Bd.addEventListener('click', closeTop20StudioModal); 7068 7069 document.addEventListener('keydown', function (e) { 7070 if (e.key !== 'Escape') return; 7071 var r = document.getElementById('detReportShareModal'); 7072 if (r && r.classList.contains('open')) closeDetectorRankingReportModal(); 7073 var t = document.getElementById('detTop20Modal'); 7074 if (t && t.classList.contains('open')) closeTop20StudioModal(); 7075 var soc = document.getElementById('detTop10SocialModal'); 7076 if (soc && soc.classList.contains('open')) closeTop10SocialShareModal(); 7077 }); 7078 } 7079 7080 /** Regista partilha do cartão Top 10 no servidor (contador; e-mail interno a cada 1000). */ 7081 function reportDetectorSocialCardShare(channel) { 7082 try { 7083 var origin = ''; 7084 try { 7085 origin = window.location.origin || ''; 7086 } catch (eO) {} 7087 var ch = typeof channel === 'string' ? channel.trim().slice(0, 32) : ''; 7088 fetch(origin + '/api/detector/social-card-share', { 7089 method: 'POST', 7090 headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, 7091 credentials: 'same-origin', 7092 body: JSON.stringify(ch ? { channel: ch } : {}), 7093 }).catch(function () {}); 7094 } catch (eR) {} 7095 } 7096 7097 function waitTop10SocialExportReady(done, timeoutMs) { 7098 var cv = document.getElementById('detTop10SocialCanvas'); 7099 var limit = typeof timeoutMs === 'number' ? timeoutMs : 60000; 7100 var deadline = Date.now() + limit; 7101 function tick() { 7102 try { 7103 if (cv && cv.dataset && cv.dataset.detExportReady === '1') { 7104 done(true); 7105 return; 7106 } 7107 } catch (e0) {} 7108 if (Date.now() > deadline) { 7109 done(false); 7110 return; 7111 } 7112 setTimeout(tick, 120); 7113 } 7114 tick(); 7115 } 7116
7117 function top10SocialCanvasBlob(callback) { 7118 var cv = document.getElementById('detTop10SocialCanvas'); 7119 if (!cv || typeof cv.toBlob !== 'function') { 7120 callback(null); 7121 return; 7122 } 7123 function runBlob() { 7124 cv.toBlob(function (blob) { 7125 callback(blob || null); 7126 }, 'image/png'); 7127 } 7128 try { 7129 if (cv.dataset && cv.dataset.detExportReady === '1') { 7130 runBlob(); 7131 return; 7132 } 7133 } catch (e1) {} 7134 waitTop10SocialExportReady(function (ok) { 7135 if (!ok) { 7136 callback(null); 7137 return; 7138 } 7139 runBlob(); 7140 }, 60000); 7141 } 7142 7143 function top10SocialSharePayloadFromBlob(blob, cap) { 7144 var title = 7145 detTop10SocialSession && detTop10SocialSession.kind === 'games' 7146 ? __('js_t10_games_social_share_title') 7147 : __('js_t10_social_share_title'); 7148 if (!blob) { 7149 return { title: title, text: cap, files: null }; 7150 } 7151 var file = new File([blob], top10SocialCardPngBasename(), { type: 'image/png' }); 7152 return { title: title, text: cap, files: [file] }; 7153 } 7154 7155 function top10SocialTryShareNavigator(payload, onFallback) { 7156 if (!navigator.share) { 7157 onFallback(); 7158 return; 7159 } 7160 if (payload.files && payload.files.length && navigator.canShare) { 7161 if (!navigator.canShare(payload)) { 7162 onFallback(); 7163 return; 7164 } 7165 } 7166 navigator.share(payload).catch(function (err) { 7167 if (err && err.name === 'AbortError') return; 7168 onFallback(); 7169 }); 7170 } 7171 7172 function top10SocialDownloadBlob(blob) { 7173 if (!blob) return; 7174 var u = URL.createObjectURL(blob); 7175 var a = document.createElement('a'); 7176 a.href = u; 7177 a.download = top10SocialCardPngBasename(); 7178 a.click(); 7179 setTimeout(function () { 7180 try { 7181 URL.revokeObjectURL(u); 7182 } catch (eR) {} 7183 }, 2500); 7184 } 7185 7186 function top10SocialOpenFacebookLinkShare(cap) { 7187 var urlPg = encodeURIComponent('https://catellix.com/detector'); 7188 window.open( 7189 'https://www.facebook.com/sharer/sharer.php?u=' + urlPg + '"e=' + encodeURIComponent(cap), 7190 '_blank', 7191 'noopener,noreferrer' 7192 ); 7193 } 7194 7195 function wireTop10SocialShareModal() { 7196 if (document.documentElement.dataset.detTop10SocialWired === '1') return; 7197 document.documentElement.dataset.detTop10SocialWired = '1'; 7198 var closeBtn = document.getElementById('detTop10SocialClose'); 7199 var bd = document.getElementById('detTop10SocialBackdrop'); 7200 if (closeBtn) closeBtn.addEventListener('click', closeTop10SocialShareModal); 7201 if (bd) bd.addEventListener('click', closeTop10SocialShareModal); 7202 var copyBtn = document.getElementById('detTop10SocialCopyCap'); 7203 if (copyBtn) { 7204 copyBtn.addEventListener('click', function () { 7205 var rows = getTop10SocialShareRowsForModal(); 7206 if (!rows.length) return; 7207 reportDetectorSocialCardShare('copy_caption'); 7208 var cap = buildTop10SocialCaption(rows); 7209 if (navigator.clipboard && navigator.clipboard.writeText) { 7210 navigator.clipboard.writeText(cap).catch(function () { 7211 try { 7212 alert(cap); 7213 } catch (e1) {} 7214 }); 7215 } else { 7216 try { 7217 alert(cap); 7218 } catch (e2) {} 7219 } 7220 }); 7221 } 7222 var waBtn = document.getElementById('detTop10SocialWa'); 7223 if (waBtn) { 7224 waBtn.addEventListener('click', function () { 7225 var rows = getTop10SocialShareRowsForModal(); 7226 if (!rows.length) return; 7227 var cap = buildTop10SocialCaption(rows); 7228 try { 7229 window.open('https://wa.me/?text=' + encodeURIComponent(cap), '_blank', 'noopener,noreferrer'); 7230 } catch (eWa) {} 7231 reportDetectorSocialCardShare('whatsapp'); 7232 }); 7233 } 7234 var igBtn = document.getElementById('detTop10SocialIg'); 7235 if (igBtn) { 7236 igBtn.addEventListener('click', function () { 7237 var rows = getTop10SocialShareRowsForModal(); 7238 if (!rows.length) return; 7239 reportDetectorSocialCardShare('instagram'); 7240 try { 7241 window.open('https://www.instagram.com/create/select/', '_blank', 'noopener,noreferrer'); 7242 } catch (eIg) {} 7243 }); 7244 } 7245 var fbBtn = document.getElementById('detTop10SocialFb'); 7246 if (fbBtn) { 7247 fbBtn.addEventListener('click', function () { 7248 var rows = getTop10SocialShareRowsForModal(); 7249 if (!rows.length) return; 7250 var cap = buildTop10SocialCaption(rows); 7251 reportDetectorSocialCardShare('facebook'); 7252 try { 7253 top10SocialOpenFacebookLinkShare(cap); 7254 } catch (eFb) {} 7255 }); 7256 } 7257 var saveBtn = document.getElementById('detTop10SocialSave'); 7258 if (saveBtn) { 7259 saveBtn.addEventListener('click', function () { 7260 var cv = document.getElementById('detTop10SocialCanvas'); 7261 if (!cv) return; 7262 reportDetectorSocialCardShare('save_png'); 7263 function downloadUrl(u) { 7264 var a = document.createElement('a'); 7265 a.href = u; 7266 a.download = top10SocialCardPngBasename(); 7267 a.click(); 7268 } 7269 function doSave() { 7270 if (typeof cv.toBlob === 'function') { 7271 cv.toBlob(function (blob) { 7272 if (!blob) { 7273 try { 7274 downloadUrl(cv.toDataURL('image/png')); 7275 } catch (e0) {} 7276 return; 7277 } 7278 var u = URL.createObjectURL(blob); 7279 downloadUrl(u); 7280 setTimeout(function () { 7281 try { 7282 URL.revokeObjectURL(u); 7283 } catch (eR) {} 7284 }, 2500); 7285 }, 'image/png'); 7286 } else { 7287 try { 7288 downloadUrl(cv.toDataURL('image/png')); 7289 } catch (e3) {} 7290 } 7291 } 7292 try { 7293 if (cv.dataset && cv.dataset.detExportReady === '1') { 7294 doSave(); 7295 return; 7296 } 7297 } catch (eReady) {} 7298 var prevTxt = saveBtn.textContent; 7299 saveBtn.disabled = true; 7300 saveBtn.textContent = __('js_t10_social_preparing'); 7301 waitTop10SocialExportReady(function (ok) { 7302 saveBtn.disabled = false;
7303 saveBtn.textContent = prevTxt; 7304 if (ok) doSave(); 7305 else { 7306 try { 7307 alert(__('js_t10_social_card_failed')); 7308 } catch (eA) {} 7309 } 7310 }, 60000); 7311 }); 7312 } 7313 var nat = document.getElementById('detTop10SocialNative'); 7314 if (nat && navigator.share) { 7315 nat.addEventListener('click', function () { 7316 var rows = getTop10SocialShareRowsForModal(); 7317 if (!rows.length) return; 7318 reportDetectorSocialCardShare('native_share'); 7319 var cap = buildTop10SocialCaption(rows); 7320 var cv0 = document.getElementById('detTop10SocialCanvas'); 7321 var shareTitle = 7322 detTop10SocialSession && detTop10SocialSession.kind === 'games' 7323 ? __('js_t10_games_social_share_title') 7324 : __('js_t10_social_share_title'); 7325 var runText = function () { 7326 navigator.share({ title: shareTitle, text: cap }).catch(function () {}); 7327 }; 7328 function tryShareWithFile() { 7329 top10SocialCanvasBlob(function (blob) { 7330 var built = top10SocialSharePayloadFromBlob(blob, cap); 7331 if (!built.files) { 7332 runText(); 7333 return; 7334 } 7335 var payload = { title: built.title, text: built.text, files: built.files }; 7336 top10SocialTryShareNavigator(payload, runText); 7337 }); 7338 } 7339 try { 7340 if (cv0 && cv0.dataset && cv0.dataset.detExportReady === '1') { 7341 tryShareWithFile(); 7342 return; 7343 } 7344 } catch (eNat0) {} 7345 var prevNat = nat.textContent; 7346 nat.disabled = true; 7347 nat.textContent = __('js_t10_social_preparing'); 7348 waitTop10SocialExportReady(function (ok) { 7349 nat.disabled = false; 7350 nat.textContent = prevNat; 7351 if (ok) tryShareWithFile(); 7352 else runText(); 7353 }, 60000); 7354 }); 7355 } else if (nat) { 7356 nat.hidden = true; 7357 } 7358 } 7359 7360 function wireTop30OutageModal() { 7361 if (document.documentElement.dataset.detTop30Wired === '1') return; 7362 document.documentElement.dataset.detTop30Wired = '1'; 7363 7364 document.addEventListener( 7365 'click', 7366 function (e) { 7367 var t = e.target; 7368 var btn = t && t.closest ? t.closest('.det-js-top30-open') : null; 7369 if (!btn) return; 7370 e.preventDefault(); 7371 if (top30State.debounceT) clearTimeout(top30State.debounceT); 7372 top30State.debounceT = setTimeout(function () { 7373 top30State.debounceT = null; 7374 openTop30RankingReportAfterPrefetch(); 7375 }, TOP30_DEBOUNCE_MS); 7376 }, 7377 false 7378 ); 7379 7380 var closeBtn = document.getElementById('detTop30Close'); 7381 var backdrop = document.getElementById('detTop30Backdrop'); 7382 var modal = document.getElementById('detTop30Modal'); 7383 if (closeBtn) { 7384 closeBtn.addEventListener('click', function () { 7385 closeTop30Modal(); 7386 }); 7387 } 7388 if (backdrop) { 7389 backdrop.addEventListener('click', function () { 7390 closeTop30Modal(); 7391 }); 7392 } 7393 if (modal) { 7394 modal.addEventListener('click', function (e) { 7395 if (e.target === backdrop) closeTop30Modal(); 7396 }); 7397 } 7398 document.addEventListener('keydown', function (e) { 7399 if (e.key !== 'Escape') return; 7400 var m = document.getElementById('detTop30Modal'); 7401 if (m && m.classList.contains('open')) closeTop30Modal(); 7402 }); 7403 } 7404 7405 function refreshSidebarComplaintsTop10() { 7406 var sol = document.getElementById('detComplaintsTop10'); 7407 var sub = document.getElementById('detComplaintsTop10Sub'); 7408 if (!sol) return; 7409 wireSidebarComplaintsRanking(); 7410 syncSidebarComplaintsToolbarLabels(); 7411 var selSide = document.getElementById('selSidebarComplaintsRankingDays'); 7412 if (selSide && selSide.dataset.detWired === '1') {
7413 selSide.value = String(VALID_RANKING_DAYS.indexOf(state.complaintsRankingDays) !== -1 ? state.complaintsRankingDays : 1); 7414 } 7415 var days = VALID_RANKING_DAYS.indexOf(state.complaintsRankingDays) !== -1 ? state.complaintsRankingDays : 1; 7416 var rows = state.communityRankingRows; 7417 var dayLblMap = { 1: __('js_window_1d'), 7: __('js_window_7d'), 15: __('js_window_15d'), 30: __('js_window_30d') }; 7418 var dayLbl = dayLblMap[days] || __('js_window_1d'); 7419 var periodMap = { 1: __('js_comm_period_24h'), 7: __('js_comm_period_7d'), 15: __('js_comm_period_15d'), 30: __('js_comm_period_30d') }; 7420 var periodLbl = periodMap[days] || (days + ' ' + __('js_relato_plural')); 7421 var subTxt; 7422 if (rows === undefined) { 7423 subTxt = __('js_comm_sub_loading_tpl').replace(/\{d\}/g, dayLbl); 7424 } else if (rows === null) { 7425 subTxt = __('js_comm_sub_err_tpl').replace(/\{d\}/g, dayLbl); 7426 } else { 7427 subTxt = __('js_comm_sub_ready_tpl').replace(/\{p\}/g, periodLbl); 7428 } 7429 if (sub) sub.textContent = subTxt; 7430 7431 if (rows === undefined) { 7432 syncDetComplaintsTop10ColumnHead(false); 7433 var sk = ''; 7434 for (var si = 0; si < 5; si++) { 7435 sk += 7436 '<li class="det-sidebar-skel" aria-hidden="true"><span class="det-sb-skel-rank"></span><span class="det-sb-skel-logo"></span><span class="det-sb-skel-text"><span class="det-sb-skel-line"></span><span class="det-sb-skel-line det-sb-skel-line--short"></span></span></li>'; 7437 } 7438 sol.innerHTML = sk; 7439 return; 7440 } 7441 if (rows === null) { 7442 syncDetComplaintsTop10ColumnHead(false); 7443 state.communityRankPrev = {}; 7444 state._complaintsRankTrendPrev = {}; 7445 sol.innerHTML = 7446 '<li class="det-sidebar-empty" role="status">' + esc(__('js_comm_rank_err_li')) + '</li>'; 7447 syncComplaintsTop10BoardsAndCards(); 7448 return; 7449 } 7450 var rowsWithVotes = rows.filter(function (row) { 7451 return row && (Number(row.vote_count) || 0) > 0; 7452 }); 7453 if (!rowsWithVotes.length) { 7454 syncDetComplaintsTop10ColumnHead(false); 7455 state.communityRankPrev = {}; 7456 state._complaintsRankTrendPrev = {}; 7457 sol.innerHTML = 7458 '<li class="det-sidebar-empty" role="status">' + esc(__('js_sidebar_empty_comm')) + '</li>'; 7459 syncComplaintsTop10BoardsAndCards(); 7460 return; 7461 } 7462 var prevMap = state.communityRankPrev || {}; 7463 var top = rowsWithVotes.slice(0, 10); 7464 var listHtml = top 7465 .map(function (row, i) { 7466 var pillDisp = complaintsRankingPillDisplay(row); 7467 var rnk = i + 1; 7468 var rankCls = 7469 i === 0 ? 'det-sb-rank det-sb-rank--gold' : i === 1 ? 'det-sb-rank det-sb-rank--silver' : i === 2 ? 'det-sb-rank det-sb-rank--bronze' : 'det-sb-rank'; 7470 var trendHtml = rankingTrendMarkHtml(row.service_id, rnk, prevMap); 7471 var svc = findSvc(row.service_id); 7472 var dom = svc && svc.logo_domain ? String(svc.logo_domain) : (row.logo_domain ? String(row.logo_domain) : ''); 7473 var logo = detComplaintsRankLogoSrc(svc, row); 7474 var pillTitle = complaintsRankingPillTitle(row, pillDisp); 7475 var rowA11y = 7476 esc(__('js_open_service_aria')) + ' ' + esc(row.name || row.service_id || ''); 7477 return ( 7478 '<li class="det-sidebar-complaints-item"><button type="button" class="det-sidebar-line det-sidebar-line--rank det-sidebar-line--rank-main" data-svc-id="' + 7479 esc(row.service_id) + 7480 '" aria-label="' + 7481 rowA11y + 7482 '"><span class="det-sb-rank-wrap"><span class="' + 7483 esc(rankCls) + 7484 '">' + 7485 esc(String(rnk)) + 7486 '</span>' +
7487 trendHtml + 7488 '</span><img class="det-sb-logo" src="' + 7489 esc(logo) + 7490 '" alt="" width="36" height="36" loading="lazy" decoding="async"' + 7491 imgLogoOnErrorAttr(dom) + 7492 '><span class="det-sb-body"><span class="det-sb-name">' + 7493 esc(row.name) + 7494 '</span><span class="det-sb-meta">' + 7495 communityRankingMetaLineHtml(row) + 7496 '</span></span></button>' + 7497 communityTop10ComplaintsRailHtml(row, pillDisp, pillTitle) + 7498 '</li>' 7499 ); 7500 }) 7501 .join(''); 7502 sol.innerHTML = listHtml; 7503 syncDetComplaintsTop10ColumnHead(true); 7504 state._complaintsRankTrendPrev = prevMap; 7505 state.communityRankPrev = snapshotCommunityRankPositions(rowsWithVotes); 7506 wireComplaintsTop10Clicks(sol); 7507 syncComplaintsTop10BoardsAndCards(); 7508 } 7509 7510 /** Espaços na faixa inferior esquerda (sidebar); eco do catálogo ad_slots. */ 7511 var AD_SLOTS_COUNT = 25; 7512 /** Lugares livres (UI); atualizado com /api/detector/catalog â ad_slots. */ 7513 var AD_SLOTS_AVAILABLE = 22; 7514 /** Faixa fixa no canto superior direito (desktop): só slots 1â15. */ 7515 var AD_SLOTS_RAIL_COUNT = 15; 7516 var AD_SLOTS_RAIL_AVAILABLE = 12; 7517 /** 7518 * Criativos fixos nos slots 1â3 quando o catálogo não traz local_ads_carousel 7519 * (cache SWR antigo, API sem deploy ou ficheiro JSON em falta no servidor). 7520 */ 7521 var DETECTOR_DEFAULT_CAROUSEL_ADS = [ 7522 { 7523 slot: 1, 7524 id: 'catellix_monitoring_sidebar', 7525 image_url: '/static/assets/logo-catellix%202.png', 7526 link_url: 'https://www.catellix.com/welcome', 7527 days_remaining: 30, 7528 }, 7529 { 7530 slot: 2, 7531 id: 'catellix_kids_sidebar', 7532 image_url: '/static/assets/LogoCatellixCKids.png', 7533 link_url: 'https://www.catellix.com/planos-mobile', 7534 days_remaining: 30, 7535 }, 7536 { 7537 slot: 3, 7538 id: 'hojemais_partner', 7539 image_url: '/static/detector-logos/hojemais_com_br.png', 7540 link_url: 'https://www.hojemais.com.br/', 7541 days_remaining: 30, 7542 }, 7543 ]; 7544 7545 /** Modal de contribuição: 6 posições premium (1 Catellix + 5 disponÃveis). */ 7546 var AD_PACK_MODAL_SLOTS_TOTAL = 6; 7547 var AD_PACK_MODAL_SLOTS_AVAILABLE = 5; 7548 7549 /** 7550 * Junta criativos da API com os defaults por número de slot. 7551 * Se a API só trouxer, por exemplo, os dois Catellix, o slot 3 (Hoje+) continua a ser preenchido pelo default â 7552 * o spotlight e o carrossel percorrem todos os preenchidos, não só os que vieram na rede. 7553 */ 7554 function mergeDetectorCarouselSlotDefaults() { 7555 var fixedSlots = {}; 7556 for (var dfi = 0; dfi < DETECTOR_DEFAULT_CAROUSEL_ADS.length; dfi++) { 7557 var d = DETECTOR_DEFAULT_CAROUSEL_ADS[dfi]; 7558 if (!d) continue; 7559 var sl = parseInt(d.slot, 10); 7560 if (isNaN(sl) || sl < 1 || sl > AD_SLOTS_COUNT) continue; 7561 d.days_remaining = 30; 7562 fixedSlots[sl] = d; 7563 } 7564 var bySlot = {}; 7565 var arr = state.detectorCarouselSlotAds || []; 7566 for (var ai = 0; ai < arr.length; ai++) { 7567 var a = arr[ai]; 7568 if (!a) continue; 7569 var sn = parseInt(a.slot, 10); 7570 if (isNaN(sn) || sn < 1 || sn > AD_SLOTS_COUNT) continue; 7571 if (fixedSlots[sn]) continue; 7572 bySlot[sn] = a; 7573 } 7574 for (var fk in fixedSlots) { 7575 if (fixedSlots.hasOwnProperty(fk)) bySlot[fk] = fixedSlots[fk]; 7576 } 7577 var merged = []; 7578 for (var s = 1; s <= AD_SLOTS_COUNT; s++) { 7579 if (bySlot[s]) merged.push(bySlot[s]); 7580 } 7581 state.detectorCarouselSlotAds = merged; 7582 } 7583 7584 var _detLocalAdTakeoverTimer = null; 7585 var _detLocalAdTakeoverWired = false; 7586 var _detLocalAdLoopTimer = null; 7587 var _detLocalAdCountdownTimer = null; 7588 var _detLocalAdAutoCloseAtMs = 0; 7589 var _detTopBarSpinTimer = null; 7590 var _detTopBarSpinWired = false;
7591 var TOP_BAR_LOCAL_SPIN_MS = 6000; 7592 var DET_LOCAL_AD_SHOW_MS = 6000; 7593 var DET_LOCAL_AD_GAP_MS = 6000; 7594 /** Parceiros na barra / faixa lateral só depois da intro do Top 10 (evita propaganda por cima). */ 7595 var _detectorTop10IntroDone = false; 7596 var _detectorPromoScheduleToken = 0; 7597 7598 function shuffleDetectorAdArray(arr) { 7599 var a = arr.slice(); 7600 for (var i = a.length - 1; i > 0; i--) { 7601 var j = Math.floor(Math.random() * (i + 1)); 7602 var t = a[i]; 7603 a[i] = a[j]; 7604 a[j] = t; 7605 } 7606 return a; 7607 } 7608 7609 function teardownDetectorTopBarLocalSpin() { 7610 if (_detTopBarSpinTimer) { 7611 try { 7612 clearInterval(_detTopBarSpinTimer); 7613 } catch (eT) { 7614 /* ignore */ 7615 } 7616 _detTopBarSpinTimer = null; 7617 } 7618 } 7619 7620 function initDetectorTopBarLocalSpin() { 7621 if (state._topBarDismissed) return; 7622 var wrap = document.getElementById('detTopBarLocalSpin'); 7623 var track = document.getElementById('detTopBarCarouselTrack'); 7624 if (!wrap || !track) return; 7625 if (detIsNarrowDetectorViewport()) { 7626 teardownDetectorTopBarLocalSpin(); 7627 wrap.setAttribute('hidden', ''); 7628 wrap.hidden = true; 7629 return; 7630 } 7631 teardownDetectorTopBarLocalSpin(); 7632 var pool = []; 7633 var src = state.detectorCarouselSlotAds && state.detectorCarouselSlotAds.length 7634 ? state.detectorCarouselSlotAds 7635 : DETECTOR_DEFAULT_CAROUSEL_ADS; 7636 for (var pi = 0; pi < src.length; pi++) { 7637 var p = src[pi]; 7638 if (!p) continue; 7639 var sn = parseInt(p.slot, 10); 7640 if (!isNaN(sn) && sn > AD_SLOTS_RAIL_COUNT) continue; 7641 var iu = String(p.image_url || '').trim(); 7642 var lnk = String(p.link_url || '').trim(); 7643 var pid = String(p.id || '').trim(); 7644 if (!iu || !pid) continue; 7645 if (lnk.indexOf('http://') !== 0 && lnk.indexOf('https://') !== 0) continue; 7646 pool.push({ id: pid, image_url: iu, link_url: lnk }); 7647 } 7648 if (!pool.length) { 7649 wrap.setAttribute('hidden', ''); 7650 wrap.hidden = true; 7651 return; 7652 } 7653 7654 var cursor = pool.length > 1 ? Math.floor(Math.random() * pool.length) : 0; 7655 function atIdx(i) { return pool[((i % pool.length) + pool.length) % pool.length]; } 7656 7657 function makeItem(ad, pos) { 7658 var a = document.createElement('a'); 7659 a.href = ad.link_url; 7660 a.target = '_blank'; 7661 a.rel = 'noopener noreferrer sponsored'; 7662 a.className = 'det-topbar-carousel__item' + 7663 (pos === 'center' ? ' det-topbar-carousel__item--center' : ' det-topbar-carousel__item--side'); 7664 a.setAttribute('data-ad-id', ad.id); 7665 var frame = document.createElement('span'); 7666 frame.className = 'det-topbar-carousel__frame'; 7667 var img = document.createElement('img'); 7668 img.className = 'det-topbar-carousel__img'; 7669 img.src = ad.image_url; 7670 img.alt = ''; 7671 img.width = pos === 'center' ? 42 : 32; 7672 img.height = pos === 'center' ? 42 : 32; 7673 img.decoding = 'async'; 7674 frame.appendChild(img); 7675 a.appendChild(frame); 7676 a.addEventListener('click', function () { postLocalAdEvent(ad.id, 'click', 'right'); }); 7677 return a; 7678 } 7679
7680 function renderState() { 7681 var left = atIdx(cursor - 1); 7682 var center = atIdx(cursor); 7683 var right = atIdx(cursor + 1); 7684 track.innerHTML = ''; 7685 track.appendChild(makeItem(left, 'side')); 7686 track.appendChild(makeItem(center, 'center')); 7687 track.appendChild(makeItem(right, 'side')); 7688 setTimeout(function () { postLocalAdEvent(center.id, 'impression', 'right'); }, 120); 7689 } 7690 7691 renderState(); 7692 wrap.removeAttribute('hidden'); 7693 wrap.hidden = false; 7694 7695 if (detIsNarrowDetectorViewport() && !wrap.querySelector('.det-topbar-carousel__dismiss')) { 7696 wrap.style.position = 'relative'; 7697 var dismissAds = document.createElement('button'); 7698 dismissAds.type = 'button'; 7699 dismissAds.className = 'det-topbar-carousel__dismiss det-ad-close-btn--pulse'; 7700 dismissAds.setAttribute('aria-label', __('det_topbar_dismiss_aria')); 7701 dismissAds.innerHTML = 7702 '<span class="det-ad-close-btn__glyph" aria-hidden="true">\u00d7</span>'; 7703 dismissAds.addEventListener('click', function (e) { 7704 e.preventDefault(); 7705 e.stopPropagation(); 7706 state._topBarDismissed = true; 7707 teardownDetectorTopBarLocalSpin(); 7708 wrap.setAttribute('hidden', ''); 7709 wrap.hidden = true; 7710 }); 7711 wrap.appendChild(dismissAds); 7712 } 7713 7714 _detTopBarSpinTimer = setInterval(function () { 7715 cursor++; 7716 renderState(); 7717 }, TOP_BAR_LOCAL_SPIN_MS); 7718 } 7719 7720 /** 7721 * Percorre suavemente os cartões da 1.ª fila do Top 10 (comunidade) antes de libertar publicidade local. 7722 * @param {number} [scheduleToken] â se definido, cancela passos quando `_detectorPromoScheduleToken` muda (SWR + rede). 7723 */ 7724 function runDetectorTop10CommunityIntroThen(cb, scheduleToken) { 7725 var done = typeof cb === 'function' ? cb : function () {}; 7726 function stale() { 7727 return scheduleToken != null && scheduleToken !== _detectorPromoScheduleToken; 7728 } 7729 function later(fn, ms) { 7730 window.setTimeout(function () { 7731 if (stale()) return; 7732 fn(); 7733 }, ms); 7734 } 7735 if ( 7736 typeof window.matchMedia === 'function' && 7737 window.matchMedia('(prefers-reduced-motion: reduce)').matches 7738 ) { 7739 later(done, 120); 7740 return; 7741 } 7742 if (DET_COMPLAINTS_VIEWPORT_MQ.matches) { 7743 later(done, 280); 7744 return; 7745 } 7746 var host = document.getElementById('top10Cards'); 7747 if (!host || host.hasAttribute('hidden')) { 7748 later(done, 320); 7749 return; 7750 } 7751 var row = host.querySelector('.top10-cards-row'); 7752 if (!row) { 7753 later(done, 320); 7754 return; 7755 } 7756 function measureAndRun() { 7757 if (stale()) return; 7758 var max = row.scrollWidth - row.clientWidth; 7759 if (max < 12) { 7760 later(done, 1400); 7761 return; 7762 } 7763 var tiles = row.querySelectorAll('.tile[data-svc-id]'); 7764 if (!tiles.length) { 7765 later(done, 900); 7766 return; 7767 } 7768 var n = Math.min(10, tiles.length); 7769 var i = 0; 7770 function step() { 7771 if (stale()) return; 7772 if (i >= n) { 7773 try { 7774 row.scrollTo({ left: 0, behavior: 'smooth' }); 7775 } catch (eSc) { 7776 row.scrollLeft = 0; 7777 } 7778 later(done, 950); 7779 return; 7780 } 7781 var t = tiles[i]; 7782 try { 7783 t.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' }); 7784 } catch (e2) { 7785 row.scrollLeft = Math.min(max, i * 168); 7786 } 7787 i++; 7788 later(step, 500); 7789 } 7790 try { 7791 row.scrollLeft = 0; 7792 } catch (e0) { 7793 /* ignore */ 7794 } 7795 later(step, 420); 7796 } 7797 requestAnimationFrame(function () { 7798 if (stale()) return; 7799 requestAnimationFrame(measureAndRun); 7800 }); 7801 } 7802 7803 function scheduleDetectorPromotionalUiAfterTop10() { 7804 function enable() { 7805 try { 7806 initDetectorAdSlotsCarousel(); 7807 } catch (eCar) { 7808 /* ignore */ 7809 } 7810 try { 7811 initDetectorAdSlotsCarouselRight(); 7812 } catch (eCarR) { 7813 /* ignore */ 7814 } 7815 try { 7816 initDetectorTopBarLocalSpin(); 7817 } catch (eTb) { 7818 /* ignore */ 7819 } 7820 } 7821 if (_detectorTop10IntroDone) { 7822 enable(); 7823 return; 7824 } 7825 var token = ++_detectorPromoScheduleToken; 7826 runDetectorTop10CommunityIntroThen(function () { 7827 if (token !== _detectorPromoScheduleToken) return; 7828 _detectorTop10IntroDone = true; 7829 enable(); 7830 }, token); 7831 } 7832 7833 function syncDetectorAdSlotsCountUi() { 7834 var row = document.getElementById('detAdSlotsCountRow'); 7835 var capEl = document.getElementById('detAdSlotsCountCaption'); 7836 var availEl = document.getElementById('detAdSlotsCountAvail'); 7837 var totalEl = document.getElementById('detAdSlotsCountTotal'); 7838 var hintEl = document.getElementById('detAdSlotsCountHint'); 7839 var srEl = document.getElementById('detAdSlotsCountSr'); 7840 var micro = document.getElementById('detAdSlotsMicroAdd'); 7841 if (!row || !availEl || !totalEl) return; 7842 if (isMapaAdSlotsPage) { 7843 if (capEl) capEl.textContent = __('sidebar_ad_slots_count_caption'); 7844 availEl.textContent = '01'; 7845 totalEl.textContent = '25'; 7846 if (hintEl) { 7847 hintEl.textContent = __('sidebar_ad_slots_count_hint_tpl') 7848 .replace(/\{avail\}/g, '01') 7849 .replace(/\{total\}/g, '25'); 7850 } 7851 if (srEl) { 7852 srEl.textContent = __('sidebar_ad_slots_count_sr_tpl') 7853 .replace(/\{avail\}/g, '1') 7854 .replace(/\{total\}/g, '25'); 7855 } 7856 if (micro) micro.setAttribute('aria-label', __('sidebar_ad_slots_micro_add_aria')); 7857 return; 7858 } 7859 if (capEl) capEl.textContent = __('sidebar_ad_slots_count_caption'); 7860 var t = AD_SLOTS_COUNT; 7861 var a = AD_SLOTS_AVAILABLE; 7862 if (state.detectorAdSlots && typeof state.detectorAdSlots === 'object') { 7863 if (state.detectorAdSlots.total != null && !isNaN(Number(state.detectorAdSlots.total))) { 7864 t = Number(state.detectorAdSlots.total); 7865 } 7866 if (state.detectorAdSlots.available != null && !isNaN(Number(state.detectorAdSlots.available))) { 7867 a = Number(state.detectorAdSlots.available); 7868 } 7869 } 7870 if (t < 1) t = AD_SLOTS_COUNT; 7871 if (a < 0) a = 0; 7872 if (a > t) a = t; 7873 availEl.textContent = String(a); 7874 totalEl.textContent = String(t); 7875 if (hintEl) { 7876 hintEl.textContent = __('sidebar_ad_slots_count_hint_tpl') 7877 .replace(/\{avail\}/g, String(a)) 7878 .replace(/\{total\}/g, String(t)); 7879 } 7880 if (srEl) { 7881 srEl.textContent = __('sidebar_ad_slots_count_sr_tpl') 7882 .replace(/\{avail\}/g, String(a)) 7883 .replace(/\{total\}/g, String(t)); 7884 } 7885 if (micro) micro.setAttribute('aria-label', __('sidebar_ad_slots_micro_add_aria')); 7886 syncDetectorAdSlotsCountUiRight(); 7887 } 7888 7889 function syncDetectorAdSlotsCountUiRight() { 7890 var row = document.getElementById('detAdSlotsCountRowR'); 7891 var capEl = document.getElementById('detAdSlotsCountCaptionR'); 7892 var availEl = document.getElementById('detAdSlotsCountAvailR'); 7893 var totalEl = document.getElementById('detAdSlotsCountTotalR'); 7894 var hintEl = document.getElementById('detAdSlotsCountHintR'); 7895 var srEl = document.getElementById('detAdSlotsCountSrR'); 7896 var micro = document.getElementById('detAdSlotsMicroAddR'); 7897 if (!row || !availEl || !totalEl) return; 7898 var t = AD_SLOTS_RAIL_COUNT; 7899 var a = AD_SLOTS_RAIL_AVAILABLE; 7900 if (capEl) capEl.textContent = __('sidebar_ad_slots_count_caption'); 7901 availEl.textContent = String(a); 7902 totalEl.textContent = String(t); 7903 if (hintEl) { 7904 hintEl.textContent = __('sidebar_ad_slots_count_hint_tpl') 7905 .replace(/\{avail\}/g, String(a)) 7906 .replace(/\{total\}/g, String(t)); 7907 } 7908 if (srEl) { 7909 srEl.textContent = __('sidebar_ad_slots_count_sr_tpl') 7910 .replace(/\{avail\}/g, String(a)) 7911 .replace(/\{total\}/g, String(t)); 7912 } 7913 if (micro) micro.setAttribute('aria-label', __('sidebar_ad_slots_micro_add_aria')); 7914 } 7915 7916 function applyDetectorAdSlotsFromCatalog(cat) { 7917 var as = cat && cat.ad_slots; 7918 if (as && typeof as === 'object') { 7919 var tt = parseInt(as.total, 10); 7920 var aa = parseInt(as.available, 10); 7921 var tOk = !isNaN(tt) && tt > 0; 7922 var aOk = !isNaN(aa) && aa >= 0; 7923 state.detectorAdSlots = { 7924 total: tOk ? Math.min(100, tt) : AD_SLOTS_COUNT, 7925 available: aOk ? aa : AD_SLOTS_AVAILABLE, 7926 }; 7927 if (state.detectorAdSlots.available > state.detectorAdSlots.total) { 7928 state.detectorAdSlots.available = state.detectorAdSlots.total; 7929 } 7930 AD_SLOTS_AVAILABLE = state.detectorAdSlots.available; 7931 } 7932 var lap = cat && cat.local_ads_pool_size; 7933 var lapN = lap != null ? parseInt(lap, 10) : NaN; 7934 if (!isNaN(lapN) && lapN > 0) { 7935 state.localAdPoolSize = Math.min(100, lapN); 7936 } 7937 state.detectorCarouselSlotAds = []; 7938 var lac = cat && cat.local_ads_carousel; 7939 if (lac && Array.isArray(lac)) { 7940 for (var li = 0; li < lac.length; li++) { 7941 var it = lac[li]; 7942 if (!it || typeof it !== 'object') continue; 7943 var sl = parseInt(it.slot, 10); 7944 var iu = String(it.image_url || '').trim(); 7945 var lnk = String(it.link_url || '').trim(); 7946 var iid = String(it.id || '').trim(); 7947 if (isNaN(sl) || sl < 1 || !iu || !iid) continue; 7948 if (lnk.indexOf('http://') !== 0 && lnk.indexOf('https://') !== 0) continue; 7949 var dr = parseInt(it.days_remaining != null ? it.days_remaining : it.campaign_days_left, 10); 7950 if (isNaN(dr) || dr < 0) dr = 30; 7951 if (dr > 3660) dr = 3660; 7952 state.detectorCarouselSlotAds.push({ 7953 slot: sl, 7954 id: iid, 7955 image_url: iu, 7956 link_url: lnk, 7957 days_remaining: dr, 7958 }); 7959 } 7960 } 7961 mergeDetectorCarouselSlotDefaults(); 7962 syncDetectorAdSlotsCountUi(); 7963 } 7964 7965 function postLocalAdEvent(adId, kind, strip) { 7966 if (!adId || !kind) return; 7967 var origin = window.location.origin || ''; 7968 var body = { ad_id: adId, kind: kind }; 7969 if (strip === 'left' || strip === 'right') body.strip = strip; 7970 /* same-origin: envia cookie CSRF;
7970 csrf.js define X-CSRF-Token (omit quebrava 403 e métricas ficavam a 0) */ 7971 fetch(origin + '/api/detector/local-ad/event', { 7972 method: 'POST', 7973 headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, 7974 credentials: 'same-origin', 7975 body: JSON.stringify(body), 7976 keepalive: true, 7977 }).catch(function () {}); 7978 } 7979 7980 var DET_LOC_AD_SEEN_KEY = 'det_loc_ad_seen_ids_v2'; 7981 7982 function localAdGetSeenIds() { 7983 try { 7984 var j = sessionStorage.getItem(DET_LOC_AD_SEEN_KEY); 7985 var a = j ? JSON.parse(j) : []; 7986 return Array.isArray(a) ? a : []; 7987 } catch (e) { 7988 return []; 7989 } 7990 } 7991 7992 function localAdSetSeenIds(arr) { 7993 try { 7994 sessionStorage.setItem(DET_LOC_AD_SEEN_KEY, JSON.stringify(arr || [])); 7995 } catch (e) {} 7996 } 7997 7998 function localAdPrepareExcludeForFetch() { 7999 var ids = localAdGetSeenIds(); 8000 var ps = state.localAdPoolSize || 0; 8001 if (ps > 0 && ids.length >= ps) { 8002 localAdSetSeenIds([]); 8003 return []; 8004 } 8005 return ids; 8006 } 8007 8008 function stopDetectorLocalAdTakeoverLoop() { 8009 if (_detLocalAdLoopTimer) { 8010 clearTimeout(_detLocalAdLoopTimer); 8011 _detLocalAdLoopTimer = null; 8012 } 8013 } 8014 8015 function scheduleDetectorLocalAdTakeoverLoop(delayMs) { 8016 stopDetectorLocalAdTakeoverLoop(); 8017 var d = Math.max(0, Math.round(Number(delayMs) || 0)); 8018 _detLocalAdLoopTimer = setTimeout(function () { 8019 _detLocalAdLoopTimer = null; 8020 fetchNextLocalAdAndMaybeShow(); 8021 }, d); 8022 } 8023 8024 function startDetectorLocalAdTakeoverLoop() { 8025 scheduleDetectorLocalAdTakeoverLoop(280); 8026 } 8027 8028 function fetchNextLocalAdAndMaybeShow() { 8029 wireDetectorLocalAdTakeoverOnce(); 8030 var ids = localAdPrepareExcludeForFetch(); 8031 var q = ids.length ? '?exclude=' + encodeURIComponent(ids.join(',')) : ''; 8032 var origin = window.location.origin || ''; 8033 fetch(origin + '/api/detector/local-ad/next' + q, { credentials: 'omit' }) 8034 .then(function (r) { 8035 return r.json().catch(function () { 8036 return {}; 8037 }); 8038 }) 8039 .then(function (data) { 8040 if (!data || !data.success) return; 8041 if (data.pool_size != null) state.localAdPoolSize = Number(data.pool_size) || 0; 8042 if (!data.ad || !data.ad.image_url) { 8043 scheduleDetectorLocalAdTakeoverLoop(DET_LOCAL_AD_GAP_MS); 8044 return; 8045 } 8046 var linkU = String(data.ad.link_url || '').trim(); 8047 if (linkU.indexOf('http://') !== 0 && linkU.indexOf('https://') !== 0) { 8048 scheduleDetectorLocalAdTakeoverLoop(DET_LOCAL_AD_GAP_MS); 8049 return; 8050 } 8051 var dur = Number(data.duration_sec); 8052 if (isNaN(dur) || dur < 1) dur = DET_LOCAL_AD_SHOW_MS / 1000; 8053 dur = Math.max(1, Math.min(30, dur)); 8054 dur = Math.max(dur, DET_LOCAL_AD_SHOW_MS / 1000); 8055 openDetectorLocalAdTakeover(data.ad, dur); 8056 }) 8057 .catch(function () { 8058 scheduleDetectorLocalAdTakeoverLoop(DET_LOCAL_AD_GAP_MS); 8059 }); 8060 } 8061 8062 function closeDetectorLocalAdTakeover() { 8063 if (_detLocalAdTakeoverTimer) { 8064 clearTimeout(_detLocalAdTakeoverTimer); 8065 _detLocalAdTakeoverTimer = null; 8066 } 8067 if (_detLocalAdCountdownTimer) { 8068 clearInterval(_detLocalAdCountdownTimer); 8069 _detLocalAdCountdownTimer = null; 8070 } 8071 var root = document.getElementById('detLocalAdTakeover'); 8072 if (root && root._detLocalAdEsc) { 8073 document.removeEventListener('keydown', root._detLocalAdEsc); 8074 root._detLocalAdEsc = null; 8075 } 8076 if (!root) return; 8077 if (root.dataset) root.dataset.adId = ''; 8078 root.classList.remove('det-local-ad-takeover--open'); 8079 root.setAttribute('hidden', ''); 8080 root.hidden = true; 8081 root.setAttribute('aria-hidden', 'true'); 8082 var cd = document.getElementById('detLocalAdTakeoverCountdown'); 8083 if (cd) cd.textContent = ''; 8084 try { 8085 document.body.style.overflow = ''; 8086 } catch (e) {} 8087 scheduleDetectorLocalAdTakeoverLoop(DET_LOCAL_AD_GAP_MS); 8088 } 8089 8090 function wireDetectorLocalAdTakeoverOnce() { 8091 if (_detLocalAdTakeoverWired) return; 8092 _detLocalAdTakeoverWired = true; 8093 var cls = document.getElementById('detLocalAdTakeoverClose'); 8094 if (cls) cls.addEventListener('click', closeDetectorLocalAdTakeover); 8095 var cta = document.getElementById('detLocalAdTakeoverCta'); 8096 if (cta) { 8097 cta.addEventListener('click', function () {
8098 var rid = document.getElementById('detLocalAdTakeover'); 8099 var aid = rid && rid.dataset ? rid.dataset.adId : ''; 8100 if (aid) postLocalAdEvent(aid, 'click'); 8101 setTimeout(closeDetectorLocalAdTakeover, 80); 8102 }); 8103 } 8104 } 8105 8106 function openDetectorLocalAdTakeover(ad, durationSec) { 8107 if (!ad || !ad.id) return; 8108 var imgU = String(ad.image_url || '').trim(); 8109 var linkU = String(ad.link_url || '').trim(); 8110 if (!imgU || (linkU.indexOf('http://') !== 0 && linkU.indexOf('https://') !== 0)) return; 8111 var root = document.getElementById('detLocalAdTakeover'); 8112 var imgEl = document.getElementById('detLocalAdTakeoverImg'); 8113 var cta = document.getElementById('detLocalAdTakeoverCta'); 8114 var cls = document.getElementById('detLocalAdTakeoverClose'); 8115 if (!root || !imgEl || !cta) return; 8116 var seen = localAdGetSeenIds(); 8117 if (seen.indexOf(ad.id) === -1) { 8118 seen.push(ad.id); 8119 localAdSetSeenIds(seen); 8120 } 8121 root.dataset.adId = ad.id; 8122 imgEl.src = imgU; 8123 imgEl.alt = __('det_local_ad_img_alt'); 8124 cta.href = linkU; 8125 cta.textContent = __('det_local_ad_cta'); 8126 if (cls) cls.setAttribute('aria-label', __('det_local_ad_close_aria')); 8127 root.removeAttribute('hidden'); 8128 root.hidden = false; 8129 root.classList.add('det-local-ad-takeover--open'); 8130 root.setAttribute('aria-hidden', 'false'); 8131 try { 8132 document.body.style.overflow = 'hidden'; 8133 } catch (e1) {} 8134 function onEsc(ev) { 8135 if (ev.key === 'Escape') closeDetectorLocalAdTakeover(); 8136 } 8137 root._detLocalAdEsc = onEsc; 8138 document.addEventListener('keydown', onEsc); 8139 setTimeout(function () { 8140 postLocalAdEvent(ad.id, 'impression'); 8141 }, 160); 8142 var cd = document.getElementById('detLocalAdTakeoverCountdown'); 8143 var ms = Math.round(Number(durationSec) * 1000); 8144 if (!ms || ms < DET_LOCAL_AD_SHOW_MS) ms = DET_LOCAL_AD_SHOW_MS; 8145 _detLocalAdAutoCloseAtMs = Date.now() + ms; 8146 function renderCountdown() { 8147 if (!cd) return; 8148 var remain = Math.max(0, _detLocalAdAutoCloseAtMs - Date.now()); 8149 var sec = Math.max(0, Math.ceil(remain / 1000)); 8150 cd.textContent = __('det_local_ad_countdown_tpl').replace(/\{s\}/g, String(sec)); 8151 } 8152 if (_detLocalAdCountdownTimer) { 8153 clearInterval(_detLocalAdCountdownTimer); 8154 _detLocalAdCountdownTimer = null; 8155 } 8156 renderCountdown(); 8157 _detLocalAdCountdownTimer = setInterval(renderCountdown, 250); 8158 _detLocalAdTakeoverTimer = setTimeout(closeDetectorLocalAdTakeover, ms); 8159 } 8160 var AD_PACK_MAX_DAYS = 30; 8161 /** R$ por dia â faixa direita (premium). */ 8162 var AD_PACK_PRICE_RIGHT_PER_DAY = 20; 8163 /** R$ por dia â faixa esquerda. */ 8164 var AD_PACK_PRICE_LEFT_PER_DAY = 10; 8165 /** Preço activo no modal; muda conforme a origem do clique. */ 8166 var _adPackActivePrice = AD_PACK_PRICE_RIGHT_PER_DAY; 8167 var AD_PACK_PROMO_DISCOUNT_PCT = 20.0; 8168 var AD_PACK_TABLE_DAYS = [1, 5, 10, 15, 30]; 8169 var _detAdPixPollTimer = null; 8170 var _detAdPixPaymentId = null; 8171 var _detAdPixEmail = null; 8172 var _detAdPackCheckoutDays = 30; 8173 var _detAdDemoReportClientPid = null; 8174 var _detAdPackPreviewObjUrl = null; 8175 8176 function normalizeDetAdPackTargetUrl(raw) { 8177 var s = String(raw || '').trim(); 8178 if (!s) return ''; 8179 var uStr = s; 8180 if (!/^https?:\/\//i.test(uStr)) uStr = 'https://' + uStr; 8181 try { 8182 var u = new URL(uStr); 8183 if (u.protocol !== 'http:' && u.protocol !== 'https:') return ''; 8184 return u.href; 8185 } catch (eNorm) { 8186 return ''; 8187 } 8188 } 8189 8190 function clearDetAdPackCreativePreview() { 8191 if (_detAdPackPreviewObjUrl) { 8192 try { 8193 URL.revokeObjectURL(_detAdPackPreviewObjUrl); 8194 } catch (eRev) { 8195 /* ignore */ 8196 } 8197 _detAdPackPreviewObjUrl = null; 8198 } 8199 var prevImg = document.getElementById('detAdPackPreviewImg'); 8200 var ph = document.getElementById('detAdPackPreviewPlaceholder'); 8201 var slot = document.getElementById('detAdPackPreviewSlot'); 8202 if (prevImg) { 8203 prevImg.removeAttribute('src'); 8204 prevImg.src = ''; 8205 prevImg.setAttribute('hidden', ''); 8206 prevImg.hidden = true; 8207 } 8208 if (slot) slot.classList.remove('det-ad-pack-preview-slot--has-img'); 8209 if (ph) { 8210 ph.removeAttribute('hidden'); 8211 ph.hidden = false;
8212 } 8213 } 8214 8215 function updateDetAdPackPreviewFromFile(fileInp) { 8216 var prevImg = document.getElementById('detAdPackPreviewImg'); 8217 var ph = document.getElementById('detAdPackPreviewPlaceholder'); 8218 var slot = document.getElementById('detAdPackPreviewSlot'); 8219 if (_detAdPackPreviewObjUrl) { 8220 try { 8221 URL.revokeObjectURL(_detAdPackPreviewObjUrl); 8222 } catch (eR) { 8223 /* ignore */ 8224 } 8225 _detAdPackPreviewObjUrl = null; 8226 } 8227 if (!fileInp || !fileInp.files || !fileInp.files[0]) { 8228 if (prevImg) { 8229 prevImg.removeAttribute('src'); 8230 prevImg.src = ''; 8231 prevImg.setAttribute('hidden', ''); 8232 prevImg.hidden = true; 8233 } 8234 if (slot) slot.classList.remove('det-ad-pack-preview-slot--has-img'); 8235 if (ph) { 8236 ph.removeAttribute('hidden'); 8237 ph.hidden = false; 8238 } 8239 return; 8240 } 8241 _detAdPackPreviewObjUrl = URL.createObjectURL(fileInp.files[0]); 8242 if (prevImg) { 8243 prevImg.src = _detAdPackPreviewObjUrl; 8244 prevImg.removeAttribute('hidden'); 8245 prevImg.hidden = false; 8246 prevImg.setAttribute('alt', __('ad_pack_creative_label')); 8247 } 8248 if (ph) ph.hidden = true; 8249 if (slot) slot.classList.add('det-ad-pack-preview-slot--has-img'); 8250 } 8251 8252 function stopDetAdPixPoll() { 8253 if (_detAdPixPollTimer) { 8254 clearInterval(_detAdPixPollTimer); 8255 _detAdPixPollTimer = null; 8256 } 8257 } 8258 8259 function resetDetAdPixPanel() { 8260 stopDetAdPixPoll(); 8261 _detAdPixPaymentId = null; 8262 _detAdPixEmail = null; 8263 var footCta = document.getElementById('detAdPackFootCta'); 8264 var postPay = document.getElementById('detAdPackPostPay'); 8265 if (footCta && (!postPay || postPay.hidden)) { 8266 footCta.removeAttribute('hidden'); 8267 footCta.hidden = false; 8268 } 8269 var panel = document.getElementById('detAdPackPixPanel'); 8270 if (panel) { 8271 panel.setAttribute('hidden', ''); 8272 panel.hidden = true; 8273 } 8274 var code = document.getElementById('detAdPackPixCode'); 8275 if (code) code.value = ''; 8276 var qr = document.getElementById('detAdPackPixQrImg'); 8277 if (qr) { 8278 qr.src = ''; 8279 qr.removeAttribute('alt'); 8280 } 8281 var wrap = document.getElementById('detAdPackPixQrWrap'); 8282 if (wrap) { 8283 wrap.setAttribute('hidden', ''); 8284 wrap.hidden = true; 8285 } 8286 var tk = document.getElementById('detAdPackPixTicket'); 8287 if (tk) { 8288 tk.setAttribute('hidden', ''); 8289 tk.hidden = true; 8290 tk.href = '#'; 8291 } 8292 var st = document.getElementById('detAdPackPixStatus'); 8293 if (st) st.textContent = ''; 8294 } 8295 8296 function resetAdPackPurchaseUI(fullReset) { 8297 var post = document.getElementById('detAdPackPostPay'); 8298 var payRow = document.getElementById('detAdPackPayRow'); 8299 var footCta = document.getElementById('detAdPackFootCta'); 8300 var est = document.getElementById('detAdPackEstimate'); 8301 var chkFields = document.getElementById('detAdPackCheckoutFields'); 8302 var f = document.getElementById('detAdPackCreativeFile'); 8303 var cmsg = document.getElementById('detAdPackCreativeMsg'); 8304 var csub = document.getElementById('detAdPackCreativeSubmit'); 8305 if (post) { 8306 post.setAttribute('hidden', ''); 8307 post.hidden = true; 8308 } 8309 if (payRow) { 8310 payRow.removeAttribute('hidden'); 8311 payRow.hidden = false; 8312 } 8313 if (footCta) { 8314 footCta.removeAttribute('hidden'); 8315 footCta.hidden = false; 8316 } 8317 if (est) { 8318 est.removeAttribute('hidden'); 8319 est.hidden = false; 8320 } 8321 if (chkFields) { 8322 chkFields.removeAttribute('hidden'); 8323 chkFields.hidden = false; 8324 } 8325 clearDetAdPackCreativePreview(); 8326 if (f) f.value = ''; 8327 var urlF = document.getElementById('detAdPackCreativeUrl'); 8328 if (urlF) { 8329 urlF.value = ''; 8330 urlF.disabled = false;
8331 } 8332 var pickB = document.getElementById('detAdPackPickImageBtn'); 8333 if (pickB) pickB.disabled = false; 8334 if (cmsg) { 8335 cmsg.setAttribute('hidden', ''); 8336 cmsg.textContent = ''; 8337 cmsg.style.color = ''; 8338 } 8339 if (csub) csub.disabled = false; 8340 if (fullReset) { 8341 _detAdDemoReportClientPid = null; 8342 } 8343 } 8344 8345 function enterAdPackPurchasedMode() { 8346 var post = document.getElementById('detAdPackPostPay'); 8347 var payRow = document.getElementById('detAdPackPayRow'); 8348 var footCta = document.getElementById('detAdPackFootCta'); 8349 var est = document.getElementById('detAdPackEstimate'); 8350 var chkFields = document.getElementById('detAdPackCheckoutFields'); 8351 var pix = document.getElementById('detAdPackPixPanel'); 8352 if (payRow) { 8353 payRow.setAttribute('hidden', ''); 8354 payRow.hidden = true; 8355 } 8356 if (footCta) { 8357 footCta.setAttribute('hidden', ''); 8358 footCta.hidden = true; 8359 } 8360 if (est) { 8361 est.setAttribute('hidden', ''); 8362 est.hidden = true; 8363 } 8364 if (chkFields) { 8365 chkFields.setAttribute('hidden', ''); 8366 chkFields.hidden = true; 8367 } 8368 if (pix) { 8369 pix.setAttribute('hidden', ''); 8370 pix.hidden = true; 8371 } 8372 if (post) { 8373 post.removeAttribute('hidden'); 8374 post.hidden = false; 8375 } 8376 var f = document.getElementById('detAdPackCreativeFile'); 8377 var csub = document.getElementById('detAdPackCreativeSubmit'); 8378 var donePid = _detAdPixPaymentId || ''; 8379 var creativeDone = false; 8380 if (donePid) { 8381 try { 8382 creativeDone = !!sessionStorage.getItem('det_ad_creative_done_' + donePid); 8383 } catch (eSt) { 8384 creativeDone = false; 8385 } 8386 } 8387 if (f) f.disabled = creativeDone; 8388 var urlF = document.getElementById('detAdPackCreativeUrl'); 8389 if (urlF) urlF.disabled = creativeDone; 8390 var pickB2 = document.getElementById('detAdPackPickImageBtn'); 8391 if (pickB2) pickB2.disabled = creativeDone; 8392 if (csub) csub.disabled = creativeDone; 8393 } 8394 8395 function persistAdPackUploadPending() { 8396 if (!_detAdPixPaymentId || !_detAdPixEmail) return; 8397 try { 8398 sessionStorage.setItem( 8399 'det_ad_upload_pending', 8400 JSON.stringify({ 8401 pid: _detAdPixPaymentId, 8402 email: _detAdPixEmail, 8403 days: _detAdPackCheckoutDays, 8404 }) 8405 ); 8406 } catch (e1) { 8407 /* ignore */ 8408 } 8409 } 8410 8411 function tryRestorePendingAdPackUpload() { 8412 try { 8413 var raw = sessionStorage.getItem('det_ad_upload_pending'); 8414 if (!raw) return; 8415 var o = JSON.parse(raw); 8416 if (!o || !o.pid || !o.email) return; 8417 if (sessionStorage.getItem('det_ad_creative_done_' + o.pid)) return; 8418 _detAdPixPaymentId = o.pid; 8419 _detAdPixEmail = o.email; 8420 _detAdPackCheckoutDays = Math.max(1, Math.min(AD_PACK_MAX_DAYS, Math.round(Number(o.days)) || 30)); 8421 enterAdPackPurchasedMode(); 8422 } catch (e2) { 8423 /* ignore */ 8424 } 8425 } 8426 8427 function sendDetectorAdDemoReportIfNeeded(pid, email, days) { 8428 if (!pid || !email) return; 8429 if (_detAdDemoReportClientPid === pid) return; 8430 _detAdDemoReportClientPid = pid; 8431 var d = Math.max(1, Math.min(AD_PACK_MAX_DAYS, Math.round(Number(days)) || 30)); 8432 adPackPostJson('/api/detector/ad-send-demo-report', { 8433 payment_id: pid, 8434 email: email, 8435 days: d, 8436 }).then(function (res) { 8437 var d0 = res.data || {}; 8438 if (!res.okHttp || !d0.success) { 8439 var m = document.getElementById('detAdPackFormMsg'); 8440 if (m) { 8441 m.removeAttribute('hidden'); 8442 m.textContent = __('ad_pack_demo_report_err'); 8443 m.style.color = '#b45309'; 8444 } 8445 } 8446 }); 8447 } 8448 8449 function adPackPostJson(path, body) { 8450 var origin = window.location.origin || ''; 8451 return fetch(origin + path, { 8452 method: 'POST', 8453 headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, 8454 credentials: 'same-origin', 8455 body: JSON.stringify(body), 8456 }).then(function (r) { 8457 return r.text().then(function (t) { 8458 var data = {}; 8459 if (t && String(t).trim()) { 8460 try { 8461 data = JSON.parse(t); 8462 } catch (eJson) { 8463 data = {}; 8464 } 8465 } 8466 return { status: r.status, okHttp: r.ok, data: data }; 8467 }); 8468 }); 8469 } 8470 8471 function pollDetAdPixOnce() { 8472 if (!_detAdPixPaymentId || !_detAdPixEmail) return; 8473 adPackPostJson('/api/detector/ad-pix-status', { 8474 payment_id: _detAdPixPaymentId, 8475 email: _detAdPixEmail, 8476 }).then(function (res) { 8477 var d = res.data || {}; 8478 var stEl = document.getElementById('detAdPackPixStatus'); 8479 if (res.okHttp && d.success && d.approved) { 8480 stopDetAdPixPoll(); 8481 if (stEl) stEl.textContent = __('ad_pack_pix_approved'); 8482 var msg2 = document.getElementById('detAdPackFormMsg'); 8483 if (msg2) { 8484 msg2.removeAttribute('hidden'); 8485 msg2.textContent = __('ad_pack_pix_approved'); 8486 msg2.style.color = ''; 8487 } 8488 var daysInp2 = document.getElementById('detAdPackInputDays'); 8489 var dCh = 8490 Math.max( 8491 1, 8492 Math.min( 8493 AD_PACK_MAX_DAYS, 8494 Math.round(Number(daysInp2 && daysInp2.value)) || _detAdPackCheckoutDays 8495 ) 8496 ); 8497 sendDetectorAdDemoReportIfNeeded(_detAdPixPaymentId, _detAdPixEmail, dCh); 8498 persistAdPackUploadPending(); 8499 enterAdPackPurchasedMode(); 8500 } 8501 }); 8502 } 8503 8504 function detectorAdPackPriceForDays(days) { 8505 var d = Math.max(1, Math.min(AD_PACK_MAX_DAYS, Math.round(Number(days)) || 0)); 8506 if (!d) d = 1; 8507 var ppd = _adPackActivePrice || AD_PACK_PRICE_RIGHT_PER_DAY; 8508 var base = Math.round(ppd * d * 100) / 100; 8509 var pct = d <= 1 ? 0 : Math.round((d - 1) / (AD_PACK_MAX_DAYS - 1) * AD_PACK_PROMO_DISCOUNT_PCT * 10) / 10; 8510 pct = Math.min(pct, AD_PACK_PROMO_DISCOUNT_PCT); 8511 var promo = pct > 0; 8512 var finalVal = promo ? Math.round(base * (1 - pct / 100) * 100) / 100 : base; 8513 return { 8514 days: d, 8515 base: base, 8516 final: finalVal, 8517 promo: promo, 8518 discountPct: pct, 8519 }; 8520 } 8521 8522 function detectorFormatBrl(amount) { 8523 var loc = 8524 typeof i18nSortLocale === 'function' && i18nSortLocale() === 'en' ? 'en-US' : 'pt-BR';
8525 return Number(amount).toLocaleString(loc, { style: 'currency', currency: 'BRL' }); 8526 } 8527 8528 function syncDetectorAdPackEstimate() { 8529 var daysInp = document.getElementById('detAdPackInputDays'); 8530 var est = document.getElementById('detAdPackEstimate'); 8531 if (!daysInp || !est) return; 8532 var raw = Number(daysInp.value); 8533 if (daysInp.value === '' || daysInp.value === '-' || isNaN(raw)) { 8534 est.innerHTML = ''; 8535 return; 8536 } 8537 var d = Math.max(1, Math.min(AD_PACK_MAX_DAYS, Math.round(raw))); 8538 var priceObj = detectorAdPackPriceForDays(d); 8539 var priceTxt = esc(detectorFormatBrl(priceObj.final)); 8540 if (priceObj.promo) { 8541 var pN = priceObj.discountPct; 8542 var pctStr = '-' + (pN % 1 === 0 ? pN.toFixed(0) : pN.toFixed(1)) + '%'; 8543 var tierCls = pN >= 15 ? ' det-ad-pack-price-badge--hot' : pN >= 8 ? ' det-ad-pack-price-badge--warm' : ''; 8544 var fire = pN >= 15 ? ' ð¥' : ''; 8545 priceTxt += 8546 ' <span class="det-ad-pack-price--old" style="font-size:0.85em;">' + 8547 esc(detectorFormatBrl(priceObj.base)) + 8548 '</span> ' + 8549 '<span class="det-ad-pack-price-badge' + tierCls + '" style="vertical-align:middle;">' + 8550 esc(pctStr) + fire + 8551 '</span>'; 8552 } 8553 est.innerHTML = esc(__('ad_pack_estimate_tpl')) 8554 .replace(/\{days\}/g, esc(String(d))) 8555 .replace(/\{price\}/g, priceTxt); 8556 } 8557 8558 function refreshDetectorAdPackModalContent() { 8559 var baseEl = document.getElementById('detAdPackBaseNote'); 8560 var tb = document.getElementById('detAdPackTableBody'); 8561 var daysInp = document.getElementById('detAdPackInputDays'); 8562 if (daysInp) { 8563 daysInp.setAttribute('max', String(AD_PACK_MAX_DAYS)); 8564 daysInp.setAttribute('min', '1'); 8565 } 8566 if (baseEl) { 8567 var ppd = _adPackActivePrice || AD_PACK_PRICE_RIGHT_PER_DAY; 8568 var p30 = detectorAdPackPriceForDays(AD_PACK_MAX_DAYS); 8569 baseEl.textContent = __('ad_pack_base_note_tpl') 8570 .replace(/\{dayrate\}/g, detectorFormatBrl(ppd)) 8571 .replace(/\{days\}/g, String(AD_PACK_MAX_DAYS)) 8572 .replace(/\{full30\}/g, detectorFormatBrl(p30.base)) 8573 .replace(/\{final30\}/g, detectorFormatBrl(p30.final)); 8574 } 8575 var stripN = document.getElementById('detAdPackLeftStripNotice'); 8576 if (stripN) { 8577 var isLeft = _adPackActivePrice === AD_PACK_PRICE_LEFT_PER_DAY; 8578 if (isLeft) { 8579 stripN.textContent = __('ad_pack_right_strip_notice'); 8580 } else { 8581 stripN.textContent = __('ad_pack_left_strip_notice'); 8582 } 8583 } 8584 var isLeftCtx = _adPackActivePrice === AD_PACK_PRICE_LEFT_PER_DAY; 8585 var slotsPill = document.getElementById('detAdPackSlotsPill'); 8586 if (slotsPill) { 8587 if (isMapaAdSlotsPage && !isLeftCtx) { 8588 slotsPill.textContent = __('ad_pack_slots_tpl') 8589 .replace(/\{avail\}/g, '01') 8590 .replace(/\{total\}/g, '25'); 8591 } else if (isLeftCtx) { 8592 slotsPill.textContent = __('ad_pack_slots_tpl') 8593 .replace(/\{avail\}/g, String(AD_SLOTS_AVAILABLE)) 8594 .replace(/\{total\}/g, String(AD_SLOTS_COUNT)); 8595 } else { 8596 slotsPill.textContent = __('ad_pack_slots_tpl') 8597 .replace(/\{avail\}/g, String(AD_SLOTS_RAIL_AVAILABLE)) 8598 .replace(/\{total\}/g, String(AD_SLOTS_RAIL_COUNT)); 8599 } 8600 } 8601 if (tb) { 8602 tb.innerHTML = AD_PACK_TABLE_DAYS.map(function (d) { 8603 var p = detectorAdPackPriceForDays(d); 8604 var priceHtml = ''; 8605 if (p.promo) { 8606 var pctNum = p.discountPct; 8607 var badgeTxt = '-' + (pctNum % 1 === 0 ? pctNum.toFixed(0) : pctNum.toFixed(1)) + '%'; 8608 var tierCls = pctNum >= 15 ? ' det-ad-pack-price-badge--hot' 8609 : pctNum >= 8 ? ' det-ad-pack-price-badge--warm' : ''; 8610 var fireIcon = pctNum >= 15 ? ' ð¥' : ''; 8611 priceHtml = 8612 '<span class="det-ad-pack-price">' + 8613 '<span class="det-ad-pack-price--old">' + 8614 esc(detectorFormatBrl(p.base)) + 8615 '</span>' + 8616 '<span class="det-ad-pack-price--new">' + 8617 esc(detectorFormatBrl(p.final)) + 8618 '</span>' + 8619 '<span class="det-ad-pack-price-badge' + tierCls + '">' + 8620 esc(badgeTxt) + fireIcon + 8621 '</span>' + 8622 '</span>'; 8623 } else { 8624 priceHtml = 8625 '<span class="det-ad-pack-price det-ad-pack-price--new">' + 8626 esc(detectorFormatBrl(p.final)) + 8627 '</span>'; 8628 } 8629 return ( 8630 '<tr><td>' + esc(String(d)) + '</td><td>' + priceHtml + '</td></tr>' 8631 ); 8632 }).join(''); 8633 } 8634 var mobileBadge = document.getElementById('detAdRightMobileBadge'); 8635 var mobileSeal = document.getElementById('detAdPackMobilePremiumSeal'); 8636 var isRightCtx = _adPackActivePrice !== AD_PACK_PRICE_LEFT_PER_DAY; 8637 if (mobileBadge) { 8638 mobileBadge.hidden = !isRightCtx; 8639 } 8640 if (mobileSeal) { 8641 mobileSeal.hidden = !isRightCtx; 8642 var sealText = document.getElementById('detAdPackMobileSealText'); 8643 if (sealText) sealText.textContent = __('ad_pack_mobile_premium_seal'); 8644 } 8645 var rpTitle = document.getElementById('detAdPackReportTitle'); 8646 if (rpTitle) rpTitle.textContent = __('ad_pack_report_title'); 8647 var rpThM = document.getElementById('detAdPackReportThMetric'); 8648 if (rpThM) rpThM.textContent = __('ad_pack_report_th_metric'); 8649 var rpThV = document.getElementById('detAdPackReportThValue'); 8650 if (rpThV) rpThV.textContent = __('ad_pack_report_th_value'); 8651 var rpFoot = document.getElementById('detAdPackReportFooter'); 8652 if (rpFoot) rpFoot.textContent = __('ad_pack_report_footer'); 8653 var rpTbody = document.getElementById('detAdPackReportTbody'); 8654 if (rpTbody) { 8655 var isLeftRp = _adPackActivePrice === AD_PACK_PRICE_LEFT_PER_DAY; 8656 var exImpr = isLeftRp ? '4.230' : '8.710'; 8657 var exClk = isLeftRp ? '187' : '412'; 8658 var exCtr = isLeftRp ? '4,4%' : '4,7%'; 8659 var exDay = '12 / 30'; 8660 var exRemain = '18'; 8661 var exStrip = isLeftRp ? __('ad_pack_report_strip_left') : __('ad_pack_report_strip_right'); 8662 rpTbody.innerHTML = 8663 '<tr><td>' + esc(__('ad_pack_report_row_impressions')) + '</td><td class="det-rp-val">' + exImpr + '</td></tr>' + 8664 '<tr><td>' + esc(__('ad_pack_report_row_clicks')) + '</td><td class="det-rp-val">' + exClk + '</td></tr>' + 8665 '<tr><td>' + esc(__('ad_pack_report_row_ctr')) + '</td><td class="det-rp-val">' + exCtr + '</td></tr>' + 8666 '<tr><td>' + esc(__('ad_pack_report_row_day')) + '</td><td class="det-rp-val">' + exDay + '</td></tr>' + 8667 '<tr><td>' + esc(__('ad_pack_report_row_remaining')) + '</td><td class="det-rp-val">' + exRemain + '</td></tr>' + 8668 '<tr><td>' + esc(__('ad_pack_report_row_strip')) + '</td><td class="det-rp-val">' + esc(exStrip) + '</td></tr>'; 8669 } 8670 syncDetectorAdPackEstimate(); 8671 var pickAria = document.getElementById('detAdPackPickImageBtn'); 8672 if (pickAria) pickAria.setAttribute('aria-label', __('ad_pack_pick_image')); 8673 } 8674 8675 function detectorAdPackSourceForCarouselSfx(sfx) { 8676 if (isMapaAdSlotsPage) return 'right'; 8677 return sfx === 'R' ? 'right' : 'left'; 8678 } 8679 8680 function openDetectorAdPackModal(source) { 8681 if (source === 'left') { 8682 _adPackActivePrice = AD_PACK_PRICE_LEFT_PER_DAY; 8683 } else if (source === 'right') { 8684 _adPackActivePrice = AD_PACK_PRICE_RIGHT_PER_DAY; 8685 } 8686 var modal = document.getElementById('detAdPackModal'); 8687 if (!modal) return; 8688 resetDetAdPixPanel(); 8689 tryRestorePendingAdPackUpload(); 8690 var post = document.getElementById('detAdPackPostPay'); 8691 if (!post || post.hidden) { 8692 resetAdPackPurchaseUI(true); 8693 } 8694 refreshDetectorAdPackModalContent(); 8695 try { 8696 resetDetectorAdPackEnterprise3dTilt(); 8697 } catch (eEnt3d) { 8698 /* ignore */ 8699 } 8700 var msg = document.getElementById('detAdPackFormMsg'); 8701 if (msg) { 8702 msg.setAttribute('hidden', ''); 8703 msg.textContent = ''; 8704 msg.style.color = ''; 8705 } 8706 modal.classList.add('open'); 8707 modal.removeAttribute('hidden'); 8708 modal.setAttribute('aria-hidden', 'false'); 8709 } 8710 8711 function closeDetectorAdPackModal() { 8712 stopDetAdPixPoll(); 8713 var modal = document.getElementById('detAdPackModal'); 8714 if (!modal) return; 8715 modal.classList.remove('open'); 8716 modal.setAttribute('hidden', ''); 8717 modal.setAttribute('aria-hidden', 'true'); 8718 } 8719 8720 function wireDetectorAdPackEnterprise3dOnce() { 8721 var root = document.getElementById('detAdPackEnterpriseCard'); 8722 var rotate = document.getElementById('detAdPackEnterpriseRotate'); 8723 if (!root || !rotate || root.dataset.detEnterprise3dWired === '1') return; 8724 root.dataset.detEnterprise3dWired = '1'; 8725 8726 var reduceMotion = 8727 typeof window.matchMedia === 'function' && 8728 window.matchMedia('(prefers-reduced-motion: reduce)').matches; 8729 if (reduceMotion) { 8730 root.classList.add('det-ad-pack-enterprise--no-3d'); 8731 return; 8732 } 8733 8734 root.classList.add('det-ad-pack-enterprise--3d'); 8735 8736 var tilt = { rx: 0, ry: 0 }; 8737 rotate._detEnterpriseTilt = tilt; 8738 var dragging = false;
8739 var startX = 0; 8740 var startY = 0; 8741 var startRx = 0; 8742 var startRy = 0; 8743 var maxRx = 26; 8744 var maxRy = 34; 8745 8746 function applyTransform() { 8747 rotate.style.transform = 8748 'rotateX(' + tilt.rx.toFixed(2) + 'deg) rotateY(' + tilt.ry.toFixed(2) + 'deg)'; 8749 } 8750 8751 function isInteractiveTarget(t) { 8752 if (!t || !t.closest) return false; 8753 return !!t.closest('button, a, input, label, textarea, select'); 8754 } 8755 8756 root.addEventListener('pointerdown', function (e) { 8757 if (e.pointerType === 'mouse' && e.button !== 0) return; 8758 if (isInteractiveTarget(e.target)) return; 8759 dragging = true; 8760 root.classList.add('det-ad-pack-enterprise--dragging'); 8761 rotate.style.transition = 'none'; 8762 startX = e.clientX; 8763 startY = e.clientY; 8764 startRx = tilt.rx; 8765 startRy = tilt.ry; 8766 try { 8767 root.setPointerCapture(e.pointerId); 8768 } catch (eCap) { 8769 /* ignore */ 8770 } 8771 e.preventDefault(); 8772 }); 8773 8774 root.addEventListener('pointermove', function (e) { 8775 if (!dragging) return; 8776 var dx = e.clientX - startX; 8777 var dy = e.clientY - startY; 8778 tilt.ry = startRy + dx * 0.32; 8779 tilt.rx = startRx - dy * 0.28; 8780 tilt.ry = Math.max(-maxRy, Math.min(maxRy, tilt.ry)); 8781 tilt.rx = Math.max(-maxRx, Math.min(maxRx, tilt.rx)); 8782 applyTransform(); 8783 }); 8784 8785 function endDrag(e) { 8786 if (!dragging) return; 8787 dragging = false; 8788 root.classList.remove('det-ad-pack-enterprise--dragging'); 8789 try { 8790 if (e && e.pointerId != null) root.releasePointerCapture(e.pointerId); 8791 } catch (eRel) { 8792 /* ignore */ 8793 } 8794 rotate.style.transition = 'transform 0.65s cubic-bezier(0.22, 1, 0.36, 1)'; 8795 tilt.rx = 0; 8796 tilt.ry = 0; 8797 applyTransform(); 8798 window.setTimeout(function () { 8799 rotate.style.transition = ''; 8800 }, 700); 8801 } 8802 8803 root.addEventListener('pointerup', endDrag); 8804 root.addEventListener('pointercancel', endDrag); 8805 root.addEventListener('lostpointercapture', function () { 8806 if (dragging) { 8807 dragging = false; 8808 root.classList.remove('det-ad-pack-enterprise--dragging'); 8809 } 8810 }); 8811 8812 root.addEventListener('mouseenter', function () { 8813 if (dragging) return; 8814 if (tilt.rx === 0 && tilt.ry === 0) return; 8815 rotate.style.transition = 'transform 0.45s cubic-bezier(0.22, 1, 0.36, 1)'; 8816 tilt.rx = 0; 8817 tilt.ry = 0; 8818 applyTransform(); 8819 window.setTimeout(function () { 8820 if (!dragging) rotate.style.transition = ''; 8821 }, 460); 8822 }); 8823 8824 applyTransform(); 8825 } 8826 8827 function resetDetectorAdPackEnterprise3dTilt() { 8828 var rotate = document.getElementById('detAdPackEnterpriseRotate'); 8829 if (!rotate) return; 8830 var tilt = rotate._detEnterpriseTilt; 8831 if (tilt) { 8832 tilt.rx = 0; 8833 tilt.ry = 0; 8834 } 8835 rotate.style.transition = 'transform 0.45s cubic-bezier(0.22, 1, 0.36, 1)'; 8836 rotate.style.transform = 'rotateX(0deg) rotateY(0deg)'; 8837 window.setTimeout(function () { 8838 rotate.style.transition = ''; 8839 }, 480); 8840 } 8841 8842 function wireDetectorAdZoneDismissButtons() { 8843 var pairs = [ 8844 { btnId: 'detAdZoneDismissL', sfx: '', strip: 'left' }, 8845 { btnId: 'detAdZoneDismissR', sfx: 'R', strip: 'right' } 8846 ]; 8847 for (var pi = 0; pi < pairs.length; pi++) { 8848 (function (pair) { 8849 var btn = document.getElementById(pair.btnId); 8850 if (!btn || btn._detDismissWired) return; 8851 btn._detDismissWired = true; 8852 btn.addEventListener('click', function (e) { 8853 e.preventDefault(); 8854 e.stopPropagation(); 8855 var zone = btn.closest('.det-ad-slots-zone'); 8856 var dismissKey = pair.sfx === 'R' ? '_adZoneDismissedR' : '_adZoneDismissedL'; 8857 var supportKey = pair.sfx === 'R' ? '_adZoneSupportUiR' : '_adZoneSupportUiL'; 8858 if (detIsNarrowDetectorViewport()) { 8859 state[dismissKey] = true; 8860 teardownDetectorAdSlotsCarousel(pair.sfx); 8861 if (zone) zone.classList.add('det-ad-slots-zone--dismissed'); 8862 return; 8863 } 8864 state[supportKey] = true; 8865 teardownDetectorAdSlotsCarousel(pair.sfx); 8866 var vp = document.getElementById('detAdSlotsViewport' + pair.sfx); 8867 var trk = document.getElementById('detAdSlotsTrack' + pair.sfx); 8868 if (trk) { 8869 trk.style.visibility = 'hidden'; 8870 trk.style.pointerEvents = 'none'; 8871 } 8872 var carousel = document.getElementById('detAdSlotsCarousel' + pair.sfx); 8873 if (carousel) carousel._detAdSupportUiActive = true; 8874 mountDetectorSupport3dInViewport(vp, pair.sfx); 8875 btn.style.display = 'none'; 8876 var hintEl = document.getElementById('detAdSlotsHoverHint' + pair.sfx); 8877 if (hintEl) hintEl.textContent = ''; 8878 }); 8879 })(pairs[pi]); 8880 } 8881 } 8882 8883 function wireDetectorAdPackModalOnce() { 8884 var modal = document.getElementById('detAdPackModal'); 8885 if (!window._detAdZoneDismissWiredOnce) { 8886 window._detAdZoneDismissWiredOnce = true; 8887 try { 8888 wireDetectorAdZoneDismissButtons(); 8889 } catch (eZ) { 8890 /* ignore */ 8891 } 8892 } 8893 if (!modal) { 8894 var microL0 = document.getElementById('detAdSlotsMicroAdd'); 8895 if (microL0 && !microL0._detAdMicroWired) { 8896 microL0._detAdMicroWired = true; 8897 microL0.addEventListener('click', function () { 8898 openDetectorAdPackModal(isMapaAdSlotsPage ? 'right' : 'left'); 8899 }); 8900 } 8901 return; 8902 } 8903 if (modal.dataset.detPackWired === '1') return; 8904 modal.dataset.detPackWired = '1'; 8905 8906 var addBtn = document.getElementById('detAdSlotsCtaAdd'); 8907 if (addBtn && !addBtn._detAdCtaWired) { 8908 addBtn._detAdCtaWired = true; 8909 addBtn.addEventListener('click', function () { 8910 openDetectorAdPackModal('left'); 8911 }); 8912 } 8913 var microAdd = document.getElementById('detAdSlotsMicroAdd'); 8914 if (microAdd && !microAdd._detAdMicroWired) { 8915 microAdd._detAdMicroWired = true; 8916 microAdd.addEventListener('click', function () { 8917 openDetectorAdPackModal(isMapaAdSlotsPage ? 'right' : 'left'); 8918 }); 8919 } 8920 modal.setAttribute('aria-hidden', 'true'); 8921 8922 var bc = document.getElementById('detAdPackModalClose'); 8923 var bx = document.getElementById('detAdPackBtnCancel'); 8924 if (bc) bc.addEventListener('click', closeDetectorAdPackModal); 8925 if (bx) bx.addEventListener('click', closeDetectorAdPackModal); 8926 modal.addEventListener('click', function (e) { 8927 if (e.target === modal) closeDetectorAdPackModal(); 8928 }); 8929 document.addEventListener('keydown', function (e) { 8930 if (e.key !== 'Escape') return; 8931 if (!modal.classList.contains('open')) return; 8932 closeDetectorAdPackModal(); 8933 }); 8934 8935 var form = document.getElementById('detAdPackForm'); 8936 var daysInp = document.getElementById('detAdPackInputDays'); 8937 var emailInp = document.getElementById('detAdPackInputEmail'); 8938 if (daysInp) { 8939 daysInp.addEventListener('input', syncDetectorAdPackEstimate); 8940 daysInp.addEventListener('change', syncDetectorAdPackEstimate); 8941 } 8942 if (form) { 8943 form.addEventListener('submit', function (e) { 8944 e.preventDefault(); 8945 }); 8946 } 8947 8948 function validateAdPackForm(msgEl) { 8949 if (!daysInp || !emailInp) return null; 8950 var d = Math.round(Number(daysInp.value));
8951 var em = String(emailInp.value || '').trim(); 8952 if (isNaN(d) || d < 1 || d > AD_PACK_MAX_DAYS) { 8953 if (msgEl) { 8954 msgEl.removeAttribute('hidden'); 8955 msgEl.textContent = __('ad_pack_err_days_tpl').replace(/\{max\}/g, String(AD_PACK_MAX_DAYS)); 8956 msgEl.style.color = '#b91c1c'; 8957 } 8958 return null; 8959 } 8960 if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(em)) { 8961 if (msgEl) { 8962 msgEl.removeAttribute('hidden'); 8963 msgEl.textContent = __('ad_pack_err_email'); 8964 msgEl.style.color = '#b91c1c'; 8965 } 8966 return null; 8967 } 8968 var fileInp0 = document.getElementById('detAdPackCreativeFile'); 8969 var urlInp0 = document.getElementById('detAdPackCreativeUrl'); 8970 if (!fileInp0 || !fileInp0.files || !fileInp0.files[0] || !normalizeDetAdPackTargetUrl(urlInp0 && urlInp0.value)) { 8971 if (msgEl) { 8972 msgEl.removeAttribute('hidden'); 8973 msgEl.textContent = __('ad_pack_err_creative_before_pay'); 8974 msgEl.style.color = '#b91c1c'; 8975 } 8976 return null; 8977 } 8978 if (msgEl) { 8979 msgEl.setAttribute('hidden', ''); 8980 msgEl.textContent = ''; 8981 msgEl.style.color = ''; 8982 } 8983 return { days: d, email: em }; 8984 } 8985 8986 var btnMp = document.getElementById('detAdPackBtnMp'); 8987 var btnPix = document.getElementById('detAdPackBtnPix'); 8988 if (btnMp) { 8989 btnMp.addEventListener('click', function () { 8990 var msg = document.getElementById('detAdPackFormMsg'); 8991 var v = validateAdPackForm(msg); 8992 if (!v) return; 8993 btnMp.disabled = true; 8994 if (btnPix) btnPix.disabled = true; 8995 if (msg) { 8996 msg.removeAttribute('hidden'); 8997 msg.textContent = __('ad_pack_redirecting'); 8998 msg.style.color = ''; 8999 } 9000 adPackPostJson('/api/detector/ad-checkout', { 9001 days: v.days, 9002 email: v.email.toLowerCase(), 9003 method: 'checkout_pro', 9004 strip: _adPackActivePrice === AD_PACK_PRICE_LEFT_PER_DAY ? 'left' : 'right', 9005 }) 9006 .then(function (res) { 9007 btnMp.disabled = false; 9008 if (btnPix) btnPix.disabled = false; 9009 var d = res.data || {}; 9010 if (res.okHttp && d.success && d.checkout_url) { 9011 window.location.href = d.checkout_url; 9012 return; 9013 } 9014 if (msg) { 9015 msg.removeAttribute('hidden'); 9016 msg.textContent = (d && d.detail) || __('ad_pack_err_checkout'); 9017 msg.style.color = '#b91c1c'; 9018 } 9019 }) 9020 .catch(function () { 9021 btnMp.disabled = false; 9022 if (btnPix) btnPix.disabled = false; 9023 if (msg) { 9024 msg.removeAttribute('hidden'); 9025 msg.textContent = __('ad_pack_err_network'); 9026 msg.style.color = '#b91c1c'; 9027 } 9028 }); 9029 }); 9030 } 9031 if (btnPix) { 9032 btnPix.addEventListener('click', function () { 9033 var msg = document.getElementById('detAdPackFormMsg'); 9034 var v = validateAdPackForm(msg); 9035 if (!v) return; 9036 resetDetAdPixPanel(); 9037 btnPix.disabled = true; 9038 if (btnMp) btnMp.disabled = true; 9039 adPackPostJson('/api/detector/ad-checkout', { 9040 days: v.days, 9041 email: v.email.toLowerCase(), 9042 method: 'pix',
9043 strip: _adPackActivePrice === AD_PACK_PRICE_LEFT_PER_DAY ? 'left' : 'right', 9044 }) 9045 .then(function (res) { 9046 btnPix.disabled = false; 9047 if (btnMp) btnMp.disabled = false; 9048 var d = res.data || {}; 9049 if (!res.okHttp || !d.success || !d.payment_id) { 9050 if (msg) { 9051 msg.removeAttribute('hidden'); 9052 msg.textContent = (d && d.detail) || __('ad_pack_err_checkout'); 9053 msg.style.color = '#b91c1c'; 9054 } 9055 return; 9056 } 9057 var panel = document.getElementById('detAdPackPixPanel'); 9058 var codeIn = document.getElementById('detAdPackPixCode'); 9059 var qrImg = document.getElementById('detAdPackPixQrImg'); 9060 var qrWrap = document.getElementById('detAdPackPixQrWrap'); 9061 var ticket = document.getElementById('detAdPackPixTicket'); 9062 var stEl = document.getElementById('detAdPackPixStatus'); 9063 var footCta = document.getElementById('detAdPackFootCta'); 9064 if (footCta) { 9065 footCta.setAttribute('hidden', ''); 9066 footCta.hidden = true; 9067 } 9068 if (panel) { 9069 panel.removeAttribute('hidden'); 9070 panel.hidden = false; 9071 } 9072 if (codeIn) codeIn.value = d.qr_code || ''; 9073 if (d.qr_code_base64 && qrImg && qrWrap) { 9074 qrImg.src = 'data:image/png;base64,' + d.qr_code_base64; 9075 qrImg.setAttribute('alt', 'QR Code PIX'); 9076 qrWrap.removeAttribute('hidden'); 9077 qrWrap.hidden = false; 9078 } 9079 if (ticket && d.ticket_url) { 9080 ticket.href = d.ticket_url; 9081 ticket.removeAttribute('hidden'); 9082 ticket.hidden = false; 9083 } 9084 if (stEl) stEl.textContent = __('ad_pack_pix_pending'); 9085 if (msg) msg.setAttribute('hidden', ''); 9086 _detAdPackCheckoutDays = v.days; 9087 _detAdPixPaymentId = d.payment_id; 9088 _detAdPixEmail = v.email.toLowerCase(); 9089 stopDetAdPixPoll(); 9090 _detAdPixPollTimer = setInterval(pollDetAdPixOnce, 9000); 9091 }) 9092 .catch(function () { 9093 btnPix.disabled = false; 9094 if (btnMp) btnMp.disabled = false; 9095 if (msg) { 9096 msg.removeAttribute('hidden'); 9097 msg.textContent = __('ad_pack_err_network'); 9098 msg.style.color = '#b91c1c'; 9099 } 9100 }); 9101 }); 9102 } 9103 var paidBtn = document.getElementById('detAdPackPixPaidBtn'); 9104 if (paidBtn) paidBtn.addEventListener('click', pollDetAdPixOnce); 9105 var copyBtn = document.getElementById('detAdPackPixCopyBtn'); 9106 var pixCode = document.getElementById('detAdPackPixCode'); 9107 if (copyBtn && pixCode) { 9108 copyBtn.addEventListener('click', function () { 9109 pixCode.select(); 9110 try { 9111 navigator.clipboard.writeText(pixCode.value); 9112 } catch (eCopy) { 9113 /* ignore */ 9114 } 9115 }); 9116 } 9117 9118 var supBtn = document.getElementById('detAdPackOpenSupport'); 9119 if (supBtn) { 9120 supBtn.addEventListener('click', function () { 9121 closeDetectorAdPackModal(); 9122 var o = document.getElementById('det-btn-open-contact'); 9123 if (o) o.click(); 9124 }); 9125 } 9126 9127 var ticketTop = document.getElementById('detAdPackTicketTop'); 9128 if (ticketTop) { 9129 ticketTop.addEventListener('click', function () { 9130 closeDetectorAdPackModal(); 9131 var o2 = document.getElementById('det-btn-open-contact'); 9132 if (o2) o2.click(); 9133 }); 9134 } 9135 9136 var pickImgBtn = document.getElementById('detAdPackPickImageBtn'); 9137 var creativeFileInp = document.getElementById('detAdPackCreativeFile'); 9138 if (pickImgBtn && creativeFileInp) { 9139 pickImgBtn.addEventListener('click', function () { 9140 if (pickImgBtn.disabled) return; 9141 creativeFileInp.click(); 9142 }); 9143 } 9144 if (creativeFileInp) { 9145 creativeFileInp.addEventListener('change', function () { 9146 updateDetAdPackPreviewFromFile(creativeFileInp); 9147 }); 9148 } 9149 9150 var creativeBtn = document.getElementById('detAdPackCreativeSubmit'); 9151 if (creativeBtn) { 9152 creativeBtn.addEventListener('click', function () { 9153 var fileInp = document.getElementById('detAdPackCreativeFile'); 9154 var cmsg = document.getElementById('detAdPackCreativeMsg'); 9155 var pid = _detAdPixPaymentId; 9156 var em = _detAdPixEmail; 9157 if (!pid || !em) { 9158 if (cmsg) { 9159 cmsg.removeAttribute('hidden'); 9160 cmsg.textContent = __('ad_pack_err_checkout'); 9161 cmsg.style.color = '#b91c1c'; 9162 } 9163 return; 9164 } 9165 if (!fileInp || !fileInp.files || !fileInp.files[0]) { 9166 if (cmsg) { 9167 cmsg.removeAttribute('hidden'); 9168 cmsg.textContent = __('ad_pack_creative_need_file'); 9169 cmsg.style.color = '#b91c1c'; 9170 } 9171 return; 9172 } 9173 var urlInp = document.getElementById('detAdPackCreativeUrl'); 9174 var urlNorm = normalizeDetAdPackTargetUrl(urlInp && urlInp.value); 9175 if (!urlNorm) { 9176 if (cmsg) { 9177 cmsg.removeAttribute('hidden'); 9178 cmsg.textContent = __('ad_pack_creative_err_url'); 9179 cmsg.style.color = '#b91c1c'; 9180 } 9181 return; 9182 } 9183 creativeBtn.disabled = true; 9184 if (cmsg) cmsg.setAttribute('hidden', ''); 9185 var origin0 = window.location.origin || ''; 9186 var fd = new FormData(); 9187 fd.append('payment_id', pid); 9188 fd.append('email', em); 9189 fd.append('target_url', urlNorm); 9190 fd.append('creative', fileInp.files[0]); 9191 fetch(origin0 + '/api/detector/ad-creative', { 9192 method: 'POST', 9193 body: fd, 9194 credentials: 'same-origin', 9195 }) 9196 .then(function (r) { 9197 return r.text().then(function (t) { 9198 var j = {}; 9199 if (t && String(t).trim()) { 9200 try { 9201 j = JSON.parse(t); 9202 } catch (eJ) { 9203 j = {}; 9204 } 9205 } 9206 return { okHttp: r.ok, j: j }; 9207 }); 9208 }) 9209 .then(function (pack) { 9210 if (cmsg) cmsg.removeAttribute('hidden'); 9211 if (pack.okHttp && pack.j && pack.j.success) { 9212 try { 9213 sessionStorage.setItem('det_ad_creative_done_' + pid, '1'); 9214 sessionStorage.removeItem('det_ad_upload_pending'); 9215 } catch (eSs) { 9216 /* ignore */ 9217 } 9218 if (cmsg) { 9219 cmsg.textContent = __('ad_pack_creative_ok'); 9220 cmsg.style.color = '#047857'; 9221 } 9222 if (fileInp) fileInp.disabled = true; 9223 if (urlInp) urlInp.disabled = true; 9224 var pickDone = document.getElementById('detAdPackPickImageBtn'); 9225 if (pickDone) pickDone.disabled = true; 9226 } else { 9227 creativeBtn.disabled = false;
9228 if (cmsg) { 9229 cmsg.textContent = (pack.j && pack.j.detail) || __('ad_pack_creative_err'); 9230 cmsg.style.color = '#b91c1c'; 9231 } 9232 } 9233 }) 9234 .catch(function () { 9235 creativeBtn.disabled = false; 9236 if (cmsg) { 9237 cmsg.removeAttribute('hidden'); 9238 cmsg.textContent = __('ad_pack_err_network'); 9239 cmsg.style.color = '#b91c1c'; 9240 } 9241 }); 9242 }); 9243 } 9244 9245 try { 9246 wireDetectorAdPackEnterprise3dOnce(); 9247 } catch (eWire3d) { 9248 /* ignore */ 9249 } 9250 } 9251 9252 /** 9253 * Confetes discretos só na zona «Espaços publicitários»; dispara só com o destaque (evita spam). 9254 */ 9255 function teardownDetectorAdSlotsConfetti(sfx) { 9256 var confLayer = document.getElementById('detAdSlotsConfetti' + (sfx || '')); 9257 if (confLayer) confLayer.innerHTML = ''; 9258 } 9259 9260 function fireDetectorAdSlotsConfettiBurst(sfx) { 9261 var layer = document.getElementById('detAdSlotsConfetti' + (sfx || '')); 9262 if (!layer) return; 9263 if ( 9264 typeof window.matchMedia === 'function' && 9265 window.matchMedia('(prefers-reduced-motion: reduce)').matches 9266 ) { 9267 return; 9268 } 9269 if (document.hidden) return; 9270 var carnival = 9271 isMapaAdSlotsPage === true || 9272 (typeof window.CatellixCarnivalConfetti === 'boolean' && window.CatellixCarnivalConfetti); 9273 var colors = carnival 9274 ? ['#fbbf24', '#f59e0b', '#fde047', '#f472b6', '#ec4899', '#a855f7', '#22c55e', '#34d399', '#38bdf8', '#ef4444'] 9275 : ['#38bdf8', '#22d3ee', '#fbbf24', '#fcd34d', '#a78bfa', '#34d399', '#f472b6']; 9276 var n = carnival ? 16 : 4; 9277 for (var i = 0; i < n; i++) { 9278 var p = document.createElement('span'); 9279 p.className = 'det-ad-slots-confetti-piece'; 9280 p.style.left = 8 + Math.random() * 84 + '%'; 9281 var dur = 4.8 + Math.random() * 2.4; 9282 p.style.animationDuration = dur + 's'; 9283 p.style.animationDelay = Math.random() * 0.5 + 's'; 9284 p.style.setProperty('--det-cf-drift', (Math.random() * 32 - 16).toFixed(1) + 'px'); 9285 p.style.background = colors[Math.floor(Math.random() * colors.length)]; 9286 var w = 3.2 + Math.random() * 2.8; 9287 p.style.width = w.toFixed(1) + 'px'; 9288 p.style.height = (w * 1.2 + 1.2).toFixed(1) + 'px'; 9289 layer.appendChild(p); 9290 (function (el, ms) { 9291 window.setTimeout(function () { 9292 try { 9293 if (el && el.parentNode) el.parentNode.removeChild(el); 9294 } catch (eRm) { 9295 /* ignore */ 9296 } 9297 }, ms); 9298 })(p, Math.ceil((dur + 0.85) * 1000)); 9299 } 9300 } 9301 9302 function detIsNarrowDetectorViewport() { 9303 return ( 9304 typeof window.matchMedia === 'function' && 9305 window.matchMedia('(max-width: 1023px)').matches 9306 ); 9307 } 9308 9309 function mountDetectorSupport3dInViewport(viewport, sfx) { 9310 sfx = sfx || ''; 9311 if (!viewport) return; 9312 var old = viewport.querySelector('.det-ad-support-3d-wrap'); 9313 if (old && old.parentNode) { 9314 try { 9315 old.parentNode.removeChild(old); 9316 } catch (eO) { 9317 /* ignore */ 9318 } 9319 } 9320 var strip = detectorAdPackSourceForCarouselSfx(sfx); 9321 var wrap = document.createElement('div'); 9322 wrap.className = 'det-ad-support-3d-wrap'; 9323 wrap.setAttribute('role', 'region'); 9324 wrap.setAttribute('aria-label', __('det_support_3d_title')); 9325 9326 var scene = document.createElement('div'); 9327 scene.className = 'det-ad-support-3d-scene'; 9328 9329 var rot = document.createElement('div'); 9330 rot.className = 'det-ad-support-3d-rot'; 9331 var plate = document.createElement('div'); 9332 plate.className = 'det-ad-support-3d-plate'; 9333 plate.setAttribute('aria-hidden', 'true'); 9334 rot.appendChild(plate); 9335 scene.appendChild(rot); 9336 9337 var card = document.createElement('div'); 9338 card.className = 'det-ad-support-3d-card'; 9339 9340 var title = document.createElement('p'); 9341 title.className = 'det-ad-support-3d-title';
9342 title.textContent = __('det_support_3d_title'); 9343 9344 var sub = document.createElement('p'); 9345 sub.className = 'det-ad-support-3d-sub'; 9346 sub.textContent = __('det_support_3d_sub'); 9347 9348 card.appendChild(title); 9349 card.appendChild(sub); 9350 scene.appendChild(card); 9351 wrap.appendChild(scene); 9352 9353 var cta = document.createElement('button'); 9354 cta.type = 'button'; 9355 cta.className = 'det-ad-support-3d-cta'; 9356 cta.textContent = __('det_support_3d_cta'); 9357 cta.addEventListener('click', function () { 9358 openDetectorAdPackModal(strip); 9359 }); 9360 wrap.appendChild(cta); 9361 9362 viewport.appendChild(wrap); 9363 } 9364 9365 function dismissDetectorAdSlotZoneBySfx(sfx) { 9366 sfx = sfx || ''; 9367 teardownDetectorAdSlotsCarousel(sfx); 9368 var carEl = document.getElementById('detAdSlotsCarousel' + sfx); 9369 var zone = carEl && carEl.closest ? carEl.closest('.det-ad-slots-zone') : null; 9370 if (zone) zone.classList.add('det-ad-slots-zone--dismissed'); 9371 } 9372 9373 /** 9374 * Carrossel de slots (faixa fixa): ciclo 6 s a rolar + 6 s em destaque (X + contagem) dentro do viewport. 9375 */ 9376 function teardownDetectorAdSlotsCarousel(sfx) { 9377 sfx = sfx || ''; 9378 var carouselPre = document.getElementById('detAdSlotsCarousel' + sfx); 9379 if (carouselPre) { 9380 if (carouselPre._detSpotlightAutoHideTimer != null) { 9381 try { window.clearTimeout(carouselPre._detSpotlightAutoHideTimer); } catch (eSa) {} 9382 carouselPre._detSpotlightAutoHideTimer = null; 9383 } 9384 if (carouselPre._detSpotlightCountdownStartTimer != null) { 9385 try { window.clearTimeout(carouselPre._detSpotlightCountdownStartTimer); } catch (eScs) {} 9386 carouselPre._detSpotlightCountdownStartTimer = null; 9387 } 9388 if (carouselPre._detSpotlightCountdownInterval != null) { 9389 try { window.clearInterval(carouselPre._detSpotlightCountdownInterval); } catch (eSci) {} 9390 carouselPre._detSpotlightCountdownInterval = null; 9391 } 9392 if (carouselPre._detAdSlotsSpotlightTimer != null) { 9393 try { window.clearTimeout(carouselPre._detAdSlotsSpotlightTimer); } catch (eSt) {} 9394 carouselPre._detAdSlotsSpotlightTimer = null; 9395 } 9396 if (carouselPre._detAdSlotPhaseTimer != null) { 9397 try { window.clearTimeout(carouselPre._detAdSlotPhaseTimer); } catch (ePh) {} 9398 carouselPre._detAdSlotPhaseTimer = null; 9399 } 9400 if (carouselPre._detSpotlightFadeOutTimer != null) { 9401 try { window.clearTimeout(carouselPre._detSpotlightFadeOutTimer); } catch (eFo) {} 9402 carouselPre._detSpotlightFadeOutTimer = null; 9403 } 9404 } 9405 teardownDetectorAdSlotsConfetti(sfx); 9406 var carousel = document.getElementById('detAdSlotsCarousel' + sfx); 9407 if (!carousel || !carousel._detAdSlotsInit) return; 9408 if (carousel._detAdSlotsRaf) { 9409 try { window.cancelAnimationFrame(carousel._detAdSlotsRaf); } catch (eRf) {} 9410 carousel._detAdSlotsRaf = 0; 9411 } 9412 carousel._detAdSlotsInit = false; 9413 if (carousel._detAdSlotsVisAbo) { 9414 try { carousel._detAdSlotsVisAbo.abort(); } catch (eAb0) {} 9415 carousel._detAdSlotsVisAbo = null; 9416 } 9417 if (carousel._detAdSlotsAbo) { 9418 try { carousel._detAdSlotsAbo.abort(); } catch (eAb) {} 9419 carousel._detAdSlotsAbo = null; 9420 } 9421 var track = document.getElementById('detAdSlotsTrack' + sfx); 9422 if (track) { 9423 track.innerHTML = ''; 9424 track.classList.remove('det-ad-slots-track--static'); 9425 track.style.transform = ''; 9426 track.style.visibility = ''; 9427 track.style.pointerEvents = ''; 9428 } 9429 var viewportTe = document.getElementById('detAdSlotsViewport' + sfx); 9430 if (viewportTe) { 9431 var supRm = viewportTe.querySelector('.det-ad-support-3d-wrap'); 9432 if (supRm && supRm.parentNode) { 9433 try { 9434 supRm.parentNode.removeChild(supRm); 9435 } catch (eSp) { 9436 /* ignore */ 9437 } 9438 } 9439 } 9440 var carouselEl = document.getElementById('detAdSlotsCarousel' + sfx); 9441 if (carouselEl) carouselEl._detAdSupportUiActive = false;
9442 try { 9443 var zone = document.getElementById('detAdSlotsCarousel' + sfx); 9444 var parent = zone || document; 9445 var mounts = parent.querySelectorAll('.det-ad-slots-spotlight-mount'); 9446 for (var mi = 0; mi < mounts.length; mi++) { 9447 var m = mounts[mi]; 9448 if (m.parentNode) m.parentNode.removeChild(m); 9449 } 9450 } catch (eMt) {} 9451 } 9452 9453 function computeCarouselSlotHash() { 9454 var arr = state.detectorCarouselSlotAds || []; 9455 var parts = []; 9456 for (var hi = 0; hi < arr.length; hi++) { 9457 var a = arr[hi]; 9458 if (!a) continue; 9459 parts.push(String(a.slot || '') + ':' + String(a.id || '') + ':' + String(a.image_url || '') + ':' + String(a.link_url || '')); 9460 } 9461 return parts.join('|'); 9462 } 9463 9464 function invalidateCarouselSlotHash(sfx) { 9465 sfx = sfx || ''; 9466 var c = document.getElementById('detAdSlotsCarousel' + sfx); 9467 if (c) c._detSlotHash = null; 9468 } 9469 9470 function initDetectorAdSlotsCarousel(sfx, slotMax) { 9471 sfx = sfx || ''; 9472 var dismissKey = sfx === 'R' ? '_adZoneDismissedR' : '_adZoneDismissedL'; 9473 var supportKey = sfx === 'R' ? '_adZoneSupportUiR' : '_adZoneSupportUiL'; 9474 if (state[dismissKey] || state[supportKey]) return; 9475 var smax = slotMax != null && !isNaN(Number(slotMax)) ? Number(slotMax) : AD_SLOTS_COUNT; 9476 if (smax < 1) smax = AD_SLOTS_COUNT; 9477 wireDetectorAdZoneDismissButtons(); 9478 wireDetectorAdPackModalOnce(); 9479 var carousel = document.getElementById('detAdSlotsCarousel' + sfx); 9480 if (!carousel) return; 9481 9482 var newHash = computeCarouselSlotHash(); 9483 if ( 9484 carousel._detAdSlotsInit && 9485 carousel._detSlotHash === newHash && 9486 carousel._detSlotMax === smax 9487 ) { 9488 return; 9489 } 9490 9491 var isVirginCarousel = !carousel._detSpotlightCycleIdx && !carousel._detScrollQueueHead && !carousel._detCarouselGen; 9492 var prevCycleIdx = carousel._detSpotlightCycleIdx || 0; 9493 var prevQueueHead = carousel._detScrollQueueHead || 0; 9494 var initGen = (carousel._detCarouselGen || 0) + 1; 9495 9496 teardownDetectorAdSlotsCarousel(sfx); 9497 var viewport = document.getElementById('detAdSlotsViewport' + sfx); 9498 var track = document.getElementById('detAdSlotsTrack' + sfx); 9499 var hintEl = document.getElementById('detAdSlotsHoverHint' + sfx); 9500 if (!viewport || !track) return; 9501 carousel._detAdSlotsInit = true; 9502 carousel._detCarouselGen = initGen; 9503 carousel._detSlotHash = newHash; 9504 carousel._detSlotMax = smax; 9505 9506 function buildSlotsHtml() { 9507 var bySlot = {}; 9508 var fad = state.detectorCarouselSlotAds || []; 9509 for (var fj = 0; fj < fad.length; fj++) { 9510 var fa = fad[fj]; 9511 if (!fa) continue; 9512 var sn = parseInt(fa.slot, 10); 9513 if (!isNaN(sn) && sn >= 1 && sn <= smax) { 9514 bySlot[sn] = fa; 9515 } 9516 } 9517 var html = ''; 9518 for (var i = 1; i <= smax; i++) { 9519 var aria = __('ad_pack_slot_aria_tpl').replace(/\{n\}/g, String(i)); 9520 var ad = bySlot[i]; 9521 if (ad && ad.image_url && ad.link_url) { 9522 var imgU = String(ad.image_url).trim(); 9523 var linkU = String(ad.link_url).trim(); 9524 var adId = esc(String(ad.id || '')); 9525 var dRem = 30; 9526 if (ad.days_remaining != null) { 9527 var dp = parseInt(ad.days_remaining, 10); 9528 if (!isNaN(dp)) dRem = Math.max(0, Math.min(3660, dp)); 9529 } 9530 html += 9531 '<a href="' + 9532 esc(linkU) + 9533 '" class="det-ad-slot det-ad-slot--filled" data-slot="' + 9534 i + 9535 '" data-ad-id="' + 9536 adId + 9537 '" data-campaign-days="' + 9538 esc(String(dRem)) + 9539 '" target="_blank" rel="noopener noreferrer" aria-label="' + 9540 esc(aria) + 9541 '"><img class="det-ad-slot-img" src="' + 9542 esc(imgU) + 9543 '" alt="" width="80" height="80" loading="lazy" decoding="async"></a>'; 9544 } else { 9545 html += 9546 '<button type="button" class="det-ad-slot" data-slot="' + 9547 i + 9548 '" aria-label="' + 9549 esc(aria) + 9550 '"><span class="det-ad-slot-plus" aria-hidden="true">+</span></button>'; 9551 } 9552 } 9553 return html; 9554 } 9555 9556 var reduceMotion = 9557 typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches; 9558 if (reduceMotion) { 9559 track.innerHTML = buildSlotsHtml(); 9560 track.classList.add('det-ad-slots-track--static'); 9561 if (hintEl) hintEl.textContent = __('sidebar_ad_slots_reduced_hint'); 9562 var rmFilled = track.querySelectorAll('.det-ad-slot--filled'); 9563 for (var rmi = 0; rmi < rmFilled.length; rmi++) { 9564 var rmAid = rmFilled[rmi].getAttribute('data-ad-id'); 9565 if (rmAid) postLocalAdEvent(rmAid, 'impression', sfx === 'R' ? 'right' : 'left'); 9566 } 9567 track.addEventListener('click', function (e) { 9568 var btn = e.target && e.target.closest ? e.target.closest('.det-ad-slot') : null; 9569 if (!btn || !track.contains(btn)) return; 9570 if (btn.classList && btn.classList.contains('det-ad-slot--filled')) { 9571 var aid = btn.getAttribute('data-ad-id'); 9572 if (aid) postLocalAdEvent(aid, 'click', sfx === 'R' ? 'right' : 'left'); 9573 return; 9574 } 9575 e.preventDefault(); 9576 openDetectorAdPackModal(detectorAdPackSourceForCarouselSfx(sfx)); 9577 }); 9578 return; 9579 } 9580 9581 track.innerHTML = buildSlotsHtml() + buildSlotsHtml(); 9582 9583 var paused = false;
9584 var tabHidden = false; 9585 var pos = 0; 9586 var speed = 0.42; 9587 var scrollQueueHead = prevQueueHead; 9588 9589 if (isVirginCarousel) { 9590 var filledCount = track.querySelectorAll('.det-ad-slot--filled').length / 2; 9591 if (filledCount > 1) { 9592 var rndStart = Math.floor(Math.random() * filledCount); 9593 scrollQueueHead = rndStart; 9594 prevCycleIdx = rndStart; 9595 } 9596 } 9597 9598 var _sidebarImprSent = {}; 9599 (function fireInitialSidebarImpressions() { 9600 var uniqSlots = {}; 9601 var nodes = track.querySelectorAll('.det-ad-slot--filled'); 9602 for (var si = 0; si < nodes.length; si++) { 9603 var sAid = nodes[si].getAttribute('data-ad-id'); 9604 if (sAid && !uniqSlots[sAid]) { 9605 uniqSlots[sAid] = true; 9606 _sidebarImprSent[sAid] = true; 9607 postLocalAdEvent(sAid, 'impression', sfx === 'R' ? 'right' : 'left'); 9608 } 9609 } 9610 })(); 9611 9612 var spotlightEl = null; 9613 var spotlightCarouselFrozen = false; 9614 var spotlightFrozenBest = null; 9615 var SPOTLIGHT_HOLD_SEC = 6; 9616 var SPOTLIGHT_FADE_IN_MS = 420; 9617 var SPOTLIGHT_FADE_OUT_MS = 520; 9618 var CAROUSEL_SCROLL_PHASE_MS = 6000; 9619 var spotlightCycleIndex = prevCycleIdx; 9620 /** scroll | spotlight â prazos em performance.now(); avançam com o relógio só quando não está em pausa (hover ou separador). */ 9621 var carouselPhase = 'scroll'; 9622 var scrollPhaseUntil = 0; 9623 var spotlightPhaseUntil = 0; 9624 var spotlightCountdownStartAt = 0; 9625 var lastCarouselTickT = performance.now(); 9626 9627 function pickUniqueFilledAdSlotEls() { 9628 var nodes = track.querySelectorAll('.det-ad-slot--filled'); 9629 var seen = {}; 9630 var uniq = []; 9631 for (var pi = 0; pi < nodes.length; pi++) { 9632 var n = nodes[pi]; 9633 var ds = n.getAttribute('data-slot'); 9634 if (!ds || seen[ds]) continue; 9635 seen[ds] = true; 9636 uniq.push(n); 9637 } 9638 return uniq; 9639 } 9640 9641 function getFilledSlotsSorted() { 9642 var uniq = pickUniqueFilledAdSlotEls(); 9643 uniq.sort(function (a, b) { 9644 return ( 9645 parseInt(a.getAttribute('data-slot'), 10) - parseInt(b.getAttribute('data-slot'), 10) 9646 ); 9647 }); 9648 return uniq; 9649 } 9650 9651 function centerScrollQueueHeadEntry() { 9652 var filled = getFilledSlotsSorted(); 9653 if (filled.length) { 9654 var idx = scrollQueueHead % filled.length; 9655 snapSlotToViewportCenter(filled[idx]); 9656 } else { 9657 var halfW = track.scrollWidth / 2; 9658 var vr = viewport.getBoundingClientRect(); 9659 if (halfW > 0 && vr.width > 0) { 9660 var slot1CenterInTrack = 64; 9661 var initPos = halfW + slot1CenterInTrack - vr.width / 2; 9662 pos = Math.max(0, Math.min(halfW - 1, initPos)); 9663 track.style.transform = 'translate3d(' + (-pos).toFixed(2) + 'px,0,0)'; 9664 } 9665 } 9666 try { 9667 updateScales(); 9668 } catch (eSc) { 9669 /* ignore */ 9670 } 9671 } 9672 9673 function clearAdSlotPhaseTimer() { 9674 if (carousel._detAdSlotPhaseTimer != null) { 9675 try { 9676 window.clearTimeout(carousel._detAdSlotPhaseTimer); 9677 } catch (eCt) { 9678 /* ignore */ 9679 } 9680 carousel._detAdSlotPhaseTimer = null; 9681 } 9682 } 9683 9684 /** 9685 * Após fechar destaque: 6 s de carrossel a rolar, depois próximo destaque. 9686 * @param {boolean} [isFirstBoot] â se true, não avança a fila indiana (primeira entrada). 9687 */ 9688 function runScrollPhaseThenSpotlight(isFirstBoot) { 9689 clearAdSlotPhaseTimer(); 9690 if (!carousel._detAdSlotsInit) return; 9691 finishSpotlightHide(); 9692 spotlightCarouselFrozen = false;
9693 spotlightFrozenBest = null; 9694 carouselPhase = 'scroll'; 9695 var filledRot = getFilledSlotsSorted(); 9696 if (isFirstBoot === true) { 9697 if (filledRot.length) { 9698 scrollQueueHead = spotlightCycleIndex % filledRot.length; 9699 } 9700 } else { 9701 if (filledRot.length) { 9702 scrollQueueHead = (scrollQueueHead + 1) % filledRot.length; 9703 } 9704 } 9705 syncCycleStateToCarousel(); 9706 centerScrollQueueHeadEntry(); 9707 scrollPhaseUntil = performance.now() + CAROUSEL_SCROLL_PHASE_MS; 9708 } 9709 9710 function syncCycleStateToCarousel() { 9711 if (carousel._detCarouselGen !== initGen) return; 9712 carousel._detSpotlightCycleIdx = spotlightCycleIndex; 9713 carousel._detScrollQueueHead = scrollQueueHead; 9714 } 9715 9716 function openNextSpotlight() { 9717 if (!carousel._detAdSlotsInit) return; 9718 var uniq = pickUniqueFilledAdSlotEls(); 9719 uniq.sort(function (a, b) { 9720 return ( 9721 parseInt(a.getAttribute('data-slot'), 10) - parseInt(b.getAttribute('data-slot'), 10) 9722 ); 9723 }); 9724 if (!uniq.length) { 9725 scrollPhaseUntil = performance.now() + CAROUSEL_SCROLL_PHASE_MS; 9726 return; 9727 } 9728 var pick = uniq[spotlightCycleIndex % uniq.length]; 9729 spotlightCycleIndex++; 9730 syncCycleStateToCarousel(); 9731 snapSlotToViewportCenter(pick); 9732 updateScales(); 9733 if (!showAdSlotSpotlight(pick)) { 9734 /* Não bloquear o ciclo 6s+6s: tenta o próximo slot quase de seguida. */ 9735 scrollPhaseUntil = performance.now() + 120; 9736 return; 9737 } 9738 spotlightCarouselFrozen = true; 9739 spotlightFrozenBest = pick; 9740 carouselPhase = 'spotlight'; 9741 var spAdId = pick.getAttribute('data-ad-id'); 9742 if (spAdId) postLocalAdEvent(spAdId, 'impression', sfx === 'R' ? 'right' : 'left'); 9743 var nowSpot = performance.now(); 9744 spotlightCountdownStartAt = nowSpot + SPOTLIGHT_FADE_IN_MS; 9745 spotlightPhaseUntil = spotlightCountdownStartAt + SPOTLIGHT_HOLD_SEC * 1000; 9746 clearSpotlightHoldTimers(); 9747 try { 9748 fireCenterBurst(); 9749 } catch (eCf0) { 9750 /* ignore */ 9751 } 9752 try { 9753 fireDetectorAdSlotsConfettiBurst(sfx); 9754 } catch (eCf) { 9755 /* ignore */ 9756 } 9757 } 9758 9759 function snapSlotToViewportCenter(slotEl) { 9760 var half = loopHalfWidth(); 9761 if (half <= 0 || !slotEl) return; 9762 var vr = viewport.getBoundingClientRect(); 9763 var er = slotEl.getBoundingClientRect(); 9764 var vMid = vr.left + vr.width / 2; 9765 var eMid = er.left + er.width / 2; 9766 var delta = eMid - vMid; 9767 pos += delta; 9768 while (pos >= half) pos -= half; 9769 while (pos < 0) pos += half; 9770 track.style.transform = 'translate3d(' + (-pos).toFixed(2) + 'px,0,0)'; 9771 } 9772 9773 function clearSpotlightHoldTimers() { 9774 if (carousel._detSpotlightAutoHideTimer != null) { 9775 try { 9776 window.clearTimeout(carousel._detSpotlightAutoHideTimer); 9777 } catch (eCl) { 9778 /* ignore */ 9779 } 9780 carousel._detSpotlightAutoHideTimer = null; 9781 } 9782 if (carousel._detSpotlightCountdownStartTimer != null) { 9783 try { 9784 window.clearTimeout(carousel._detSpotlightCountdownStartTimer); 9785 } catch (eCs) { 9786 /* ignore */ 9787 } 9788 carousel._detSpotlightCountdownStartTimer = null; 9789 } 9790 if (carousel._detSpotlightCountdownInterval != null) { 9791 try { 9792 window.clearInterval(carousel._detSpotlightCountdownInterval); 9793 } catch (eCi) { 9794 /* ignore */ 9795 } 9796 carousel._detSpotlightCountdownInterval = null; 9797 } 9798 } 9799 9800 function finishSpotlightHide() { 9801 clearSpotlightHoldTimers(); 9802 if (spotlightEl && spotlightEl.parentNode) { 9803 try { 9804 spotlightEl.parentNode.removeChild(spotlightEl); 9805 } catch (eRm) { 9806 /* ignore */ 9807 } 9808 } 9809 spotlightEl = null; 9810 spotlightCarouselFrozen = false;
9811 spotlightFrozenBest = null; 9812 } 9813 9814 function beginSpotlightHide() { 9815 if (!spotlightEl || !spotlightEl.parentNode) { 9816 finishSpotlightHide(); 9817 if (carousel._detAdSlotsInit) runScrollPhaseThenSpotlight(); 9818 return; 9819 } 9820 if (spotlightEl.classList.contains('det-ad-slots-spotlight-mount--closing')) return; 9821 clearSpotlightHoldTimers(); 9822 spotlightEl.classList.remove('det-ad-slots-spotlight-mount--open'); 9823 spotlightEl.classList.add('det-ad-slots-spotlight-mount--closing'); 9824 var animEl = 9825 spotlightEl.querySelector('.det-ad-slots-spotlight-frame') || spotlightEl; 9826 var done = false; 9827 function onEnd(e) { 9828 if (done) return; 9829 if (e && e.propertyName && e.propertyName !== 'opacity' && e.propertyName !== 'transform') { 9830 return; 9831 } 9832 done = true; 9833 try { 9834 animEl.removeEventListener('transitionend', onEnd); 9835 } catch (eL) { 9836 /* ignore */ 9837 } 9838 finishSpotlightHide(); 9839 if (carousel._detAdSlotsInit) runScrollPhaseThenSpotlight(); 9840 } 9841 animEl.addEventListener('transitionend', onEnd); 9842 carousel._detSpotlightFadeOutTimer = window.setTimeout(function () { 9843 carousel._detSpotlightFadeOutTimer = null; 9844 onEnd({}); 9845 }, SPOTLIGHT_FADE_OUT_MS + 140); 9846 } 9847 9848 function hideAdSlotSpotlight() { 9849 beginSpotlightHide(); 9850 } 9851 9852 function showAdSlotSpotlight(slotEl) { 9853 finishSpotlightHide(); 9854 var img = slotEl.querySelector('img'); 9855 if (!img) return false; 9856 var srcRaw = ( 9857 img.getAttribute('src') || 9858 img.getAttribute('data-src') || 9859 (img.currentSrc ? img.currentSrc : '') || 9860 (img.src && String(img.src).indexOf('data:') !== 0 ? img.src : '') 9861 ); 9862 srcRaw = String(srcRaw || '').trim(); 9863 if (!srcRaw) return false; 9864 var href = slotEl.getAttribute('href') || '#'; 9865 var adId = slotEl.getAttribute('data-ad-id') || ''; 9866 var vr = viewport.getBoundingClientRect(); 9867 var maxSide = Math.min(vr.width, vr.height) * 0.82; 9868 var side = Math.max(88, Math.min(240, maxSide)); 9869 9870 var wrap = document.createElement('div'); 9871 wrap.className = 'det-ad-slots-spotlight-mount det-ad-slots-spotlight-mount--inline'; 9872 wrap.setAttribute('role', 'presentation'); 9873 9874 var frame = document.createElement('div'); 9875 frame.className = 'det-ad-slots-spotlight-frame'; 9876 9877 var glowRing = document.createElement('div'); 9878 glowRing.className = 'det-ad-slots-spotlight-timer-glow'; 9879 if ( 9880 typeof window.matchMedia === 'function' && 9881 window.matchMedia('(prefers-reduced-motion: reduce)').matches 9882 ) { 9883 glowRing.classList.add('det-ad-slots-spotlight-timer-glow--static'); 9884 } 9885 frame.appendChild(glowRing); 9886 9887 var a = document.createElement('a'); 9888 a.className = 'det-ad-slots-spotlight-card'; 9889 a.href = href; 9890 a.target = '_blank'; 9891 a.rel = 'noopener noreferrer'; 9892 if (adId) a.setAttribute('data-ad-id', adId); 9893 a.addEventListener('click', function () { 9894 if (adId) postLocalAdEvent(adId, 'click', sfx === 'R' ? 'right' : 'left'); 9895 }); 9896 9897 var im = document.createElement('img'); 9898 im.className = 'det-ad-slots-spotlight-img'; 9899 im.alt = ''; 9900 im.decoding = 'async'; 9901 im.src = img.currentSrc || srcRaw; 9902 9903 a.appendChild(im); 9904 frame.appendChild(a); 9905 9906 var campDays = parseInt(slotEl.getAttribute('data-campaign-days') || '', 10); 9907 if (isNaN(campDays) || campDays < 0) campDays = 30; 9908 if (campDays > 3660) campDays = 3660; 9909 var campBadge = document.createElement('div'); 9910 campBadge.className = 'det-ad-slots-spotlight-camp-badge'; 9911 campBadge.setAttribute('aria-hidden', 'true'); 9912 var bolt = document.createElement('span'); 9913 bolt.className = 'det-ad-slots-spotlight-camp-badge__bolt'; 9914 bolt.textContent = '\u26A1'; 9915 var daysTxt = document.createElement('span'); 9916 daysTxt.className = 'det-ad-slots-spotlight-camp-badge__days'; 9917 daysTxt.textContent = __('sidebar_spotlight_campaign_badge_tpl').replace(/\{n\}/g, String(campDays)); 9918 campBadge.appendChild(bolt); 9919 campBadge.appendChild(daysTxt); 9920 frame.appendChild(campBadge); 9921 9922 var cdSpan = document.createElement('span'); 9923 cdSpan.className = 'det-ad-slots-spotlight-countdown'; 9924 cdSpan.setAttribute('role', 'status'); 9925 cdSpan.setAttribute('aria-live', 'polite'); 9926 cdSpan.setAttribute('aria-atomic', 'true'); 9927 frame.appendChild(cdSpan); 9928 var closeBtn = document.createElement('button'); 9929 closeBtn.type = 'button'; 9930 closeBtn.className = 'det-ad-slots-spotlight-close det-ad-close-btn--pulse'; 9931 closeBtn.setAttribute('aria-label', __('ad_pack_close_aria')); 9932 closeBtn.innerHTML = 9933 '<span class="det-ad-close-btn__glyph" aria-hidden="true">\u00d7</span>'; 9934 closeBtn.addEventListener('click', function (e) { 9935 e.preventDefault(); 9936 e.stopPropagation(); 9937 beginSpotlightHide(); 9938 }); 9939 frame.appendChild(closeBtn); 9940 frame.style.width = side + 'px'; 9941 frame.style.height = side + 'px'; 9942 wrap.appendChild(frame); 9943 viewport.appendChild(wrap); 9944 spotlightEl = wrap; 9945 try { 9946 void wrap.offsetHeight; 9947 wrap.classList.add('det-ad-slots-spotlight-mount--open'); 9948 } catch (eOp) { 9949 /* ignore */ 9950 } 9951 return true; 9952 } 9953 var visAbo = typeof AbortController !== 'undefined' ? new AbortController() : null; 9954 var visSignal = visAbo ? visAbo.signal : undefined; 9955 if (visSignal) { 9956 carousel._detAdSlotsVisAbo = visAbo; 9957 document.addEventListener( 9958 'visibilitychange', 9959 function onDetAdVis() { 9960 tabHidden = document.hidden; 9961 }, 9962 { signal: visSignal } 9963 ); 9964 } else { 9965 document.addEventListener('visibilitychange', function onDetAdVis() { 9966 tabHidden = document.hidden; 9967 }); 9968 } 9969 9970 function loopHalfWidth() { 9971 var w = track.scrollWidth; 9972 return w > 0 ? w / 2 : 0; 9973 } 9974 9975 function fireCenterBurst() { 9976 var layer = document.getElementById('detAdSlotsConfetti' + sfx); 9977 if (!layer) return; 9978 if (typeof window.matchMedia === 'function' && 9979 window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; 9980 if (document.hidden) return; 9981 var colors = ['#fbbf24', '#fcd34d', '#f59e0b', '#38bdf8', '#a78bfa', '#34d399', '#f472b6', '#ff6b6b', '#4ade80']; 9982 var n = 18; 9983 for (var bi = 0; bi < n; bi++) { 9984 var p = document.createElement('span'); 9985 p.className = 'det-ad-slots-confetti-piece';
9986 /* Espalha ao redor do centro */ 9987 p.style.left = 15 + Math.random() * 70 + '%'; 9988 var dur = 2.6 + Math.random() * 1.8; 9989 p.style.animationDuration = dur + 's'; 9990 p.style.animationDelay = (Math.random() * 0.22) + 's'; 9991 p.style.setProperty('--det-cf-drift', (Math.random() * 52 - 26).toFixed(1) + 'px'); 9992 p.style.background = colors[Math.floor(Math.random() * colors.length)]; 9993 var w = 4 + Math.random() * 4; 9994 p.style.width = w.toFixed(1) + 'px'; 9995 p.style.height = (w * 1.3 + 1).toFixed(1) + 'px'; 9996 p.style.borderRadius = Math.random() > 0.5 ? '50%' : '2px'; 9997 layer.appendChild(p); 9998 (function (el, ms) { 9999 window.setTimeout(function () { 10000 try { if (el && el.parentNode) el.parentNode.removeChild(el); } catch (e) {} 10001 }, ms); 10002 })(p, Math.ceil((dur + 0.5) * 1000)); 10003 } 10004 } 10005 10006 function updateScales() { 10007 var vr = viewport.getBoundingClientRect(); 10008 var mid = vr.left + vr.width / 2; 10009 var slots = track.querySelectorAll('.det-ad-slot'); 10010 var best = null; 10011 var bestD = 1e9; 10012 for (var i = 0; i < slots.length; i++) { 10013 var slot = slots[i]; 10014 var r = slot.getBoundingClientRect(); 10015 var c = r.left + r.width / 2; 10016 var d = Math.abs(c - mid); 10017 if (d < bestD) { 10018 bestD = d; 10019 best = slot; 10020 } 10021 var norm = Math.min(1, d / (vr.width * 0.42)); 10022 /* Centro da faixa: escala extra (efeito «expandir» como no destaque). */ 10023 var sc = 0.74 + (1 - norm) * 0.48; 10024 var op = 0.5 + (1 - norm) * 0.5; 10025 slot.style.transform = 'scale(' + sc.toFixed(3) + ')'; 10026 slot.style.opacity = op.toFixed(3); 10027 slot.classList.remove('det-ad-slot--hot'); 10028 } 10029 if (best) { 10030 best.classList.add('det-ad-slot--hot'); 10031 } 10032 return best; 10033 } 10034 10035 function tick() { 10036 if (carousel._detAdSupportUiActive) return; 10037 var now = performance.now(); 10038 /* Com hover ou separador em segundo plano, o tempo lógico do ciclo 6s+6s para â não salta para o próximo destaque. */ 10039 if (paused || tabHidden) { 10040 scrollPhaseUntil += now - lastCarouselTickT; 10041 spotlightPhaseUntil += now - lastCarouselTickT; 10042 spotlightCountdownStartAt += now - lastCarouselTickT; 10043 } 10044 lastCarouselTickT = now; 10045 10046 var frozen = spotlightCarouselFrozen; 10047 var half = loopHalfWidth(); 10048 10049 if (carouselPhase === 'spotlight' && spotlightEl && !paused && !tabHidden) { 10050 if (now >= spotlightPhaseUntil) { 10051 beginSpotlightHide(); 10052 } else if (now >= spotlightCountdownStartAt) { 10053 var cdEl0 = spotlightEl.querySelector('.det-ad-slots-spotlight-countdown'); 10054 if (cdEl0) { 10055 var secLeft = Math.max(0, Math.ceil((spotlightPhaseUntil - now) / 1000)); 10056 if (cdEl0.textContent !== String(secLeft)) cdEl0.textContent = String(secLeft); 10057 } 10058 var glowEl = spotlightEl.querySelector('.det-ad-slots-spotlight-timer-glow'); 10059 if ( 10060 glowEl && 10061 !glowEl.classList.contains('det-ad-slots-spotlight-timer-glow--static') && 10062 typeof window.matchMedia === 'function' && 10063 !window.matchMedia('(prefers-reduced-motion: reduce)').matches 10064 ) { 10065 var totalSpotMs = SPOTLIGHT_HOLD_SEC * 1000; 10066 var leftMs = Math.max(0, spotlightPhaseUntil - now); 10067 var rem = totalSpotMs > 0 ? leftMs / totalSpotMs : 0; 10068 var pulseDur = 0.45 + 1.85 * rem; 10069 if (glowEl._detPulseDur !== pulseDur) { 10070 glowEl._detPulseDur = pulseDur; 10071 glowEl.style.animationDuration = pulseDur.toFixed(2) + 's'; 10072 } 10073 var br = 0.88 + 0.28 * (1 - rem); 10074 if (glowEl._detGlowBr !== br) { 10075 glowEl._detGlowBr = br; 10076 glowEl.style.filter = 'brightness(' + br.toFixed(3) + ')'; 10077 } 10078 } 10079 } 10080 } 10081 10082 if (carouselPhase === 'scroll' && !frozen && !paused && !tabHidden && now >= scrollPhaseUntil) { 10083 if (half > 0) { 10084 openNextSpotlight(); 10085 } else { 10086 scrollPhaseUntil = now + 400; 10087 } 10088 } 10089 10090 if (!paused && !tabHidden && half > 0 && !frozen) { 10091 pos += speed; 10092 if (pos >= half) pos -= half; 10093 track.style.transform = 'translate3d(' + (-pos).toFixed(2) + 'px,0,0)'; 10094 } 10095 10096 if (frozen && spotlightFrozenBest) { 10097 /* Durante o destaque 6s a escala da faixa fica estável (como antes). */ 10098 } else { 10099 updateScales(); 10100 } 10101 10102 if (hintEl && !paused && !tabHidden && !frozen) { 10103 hintEl.textContent = __('sidebar_ad_slots_scroll_hint'); 10104 } 10105 carousel._detAdSlotsRaf = window.requestAnimationFrame(tick); 10106 } 10107 10108 var evAbo = typeof AbortController !== 'undefined' ? new AbortController() : null; 10109 var evSig = evAbo ? evAbo.signal : undefined; 10110 carousel._detAdSlotsAbo = evAbo; 10111 var evOpts = evSig ? { signal: evSig } : false;
10112 10113 function addVpEv(name, fn, opts) { 10114 if (evOpts) viewport.addEventListener(name, fn, Object.assign({}, opts || {}, evOpts)); 10115 else viewport.addEventListener(name, fn, opts || false); 10116 } 10117 10118 addVpEv( 10119 'click', 10120 function (e) { 10121 var btn = e.target && e.target.closest ? e.target.closest('.det-ad-slot') : null; 10122 if (!btn || !viewport.contains(btn)) return; 10123 if (btn.classList && btn.classList.contains('det-ad-slot--filled')) { 10124 var aid2 = btn.getAttribute('data-ad-id'); 10125 if (aid2) postLocalAdEvent(aid2, 'click', sfx === 'R' ? 'right' : 'left'); 10126 return; 10127 } 10128 e.preventDefault(); 10129 openDetectorAdPackModal(detectorAdPackSourceForCarouselSfx(sfx)); 10130 }, 10131 false 10132 ); 10133 10134 addVpEv( 10135 'mouseenter', 10136 function () { 10137 paused = true; 10138 }, 10139 false 10140 ); 10141 addVpEv('mouseleave', function () { 10142 paused = false; 10143 if (hintEl) hintEl.textContent = __('sidebar_ad_slots_scroll_hint'); 10144 }); 10145 addVpEv( 10146 'touchstart', 10147 function () { 10148 paused = true; 10149 }, 10150 { passive: true } 10151 ); 10152 addVpEv( 10153 'touchend', 10154 function () { 10155 paused = false; 10156 if (hintEl) hintEl.textContent = __('sidebar_ad_slots_scroll_hint'); 10157 }, 10158 { passive: true } 10159 ); 10160 10161 carousel._detAdSlotsRaf = window.requestAnimationFrame(tick); 10162 runScrollPhaseThenSpotlight(true); 10163 } 10164 10165 function initDetectorAdSlotsCarouselRight() { 10166 if (isMapaAdSlotsPage) return; 10167 initDetectorAdSlotsCarousel('R', AD_SLOTS_RAIL_COUNT); 10168 var microR = document.getElementById('detAdSlotsMicroAddR'); 10169 if (microR && !microR._detWired) { 10170 microR._detWired = true; 10171 microR.addEventListener('click', function () { openDetectorAdPackModal('right'); }); 10172 } 10173 } 10174 10175 function renderCommunityRankingOl() { 10176 var ol = document.getElementById('rankCommunity'); 10177 if (!ol) return; 10178 var rows = state.communityRankingRows; 10179 var rowsVotes = (rows || []).filter(function (row) { 10180 return row && (Number(row.vote_count) || 0) > 0; 10181 }); 10182 if (!rowsVotes.length) { 10183 ol.innerHTML = 10184 '<li class="det-sidebar-empty" role="status">' + esc(__('js_sidebar_empty_comm')) + '</li>'; 10185 updateRankMoreBtn('btnMoreCommunity', 'community', 0); 10186 refreshSidebarComplaintsTop10(); 10187 try { 10188 renderTop30List(); 10189 } catch (eT30e) {} 10190 return; 10191 } 10192 var lim = rankLimit('community'); 10193 var items = rowsVotes.slice(0, lim); 10194 var prevMapPanel = state.communityRankPrev || {}; 10195 ol.innerHTML = items 10196 .map(function (row, i) { 10197 var rnk = i + 1; 10198 var tr = rankingTrendMarkHtml(row.service_id, rnk, prevMapPanel); 10199 var pillDisp = complaintsRankingPillDisplay(row); 10200 var rvalCls = 'rval' + (pillDisp.isDown ? ' rval--down' : pillDisp.isSpike ? ' rval--spike' : ''); 10201 return ( 10202 '<li><button type="button" class="rank-line" data-svc-id="' + 10203 esc(row.service_id) + 10204 '"><span class="rn-wrap"><span class="rn">' + 10205 esc(String(rnk)) + 10206 '</span>' + 10207 tr + 10208 '</span><span class="rbody"><span class="rname">' + 10209 esc(row.name) + 10210 '</span><span class="rmeta">' + 10211 communityRankingMetaLineHtml(row) + 10212 '</span></span><span class="' + 10213 esc(rvalCls) + 10214 '" title="' + 10215 esc(complaintsRankingPillTitle(row, pillDisp)) + 10216 '"><span class="rval-label">' + esc(__('js_rank_pill_label') || 'Pulso') + '</span>' + 10217 esc(pillDisp.value) + 10218 '</span></button></li>' 10219 ); 10220 }) 10221 .join(''); 10222 ol.querySelectorAll('.rank-line[data-svc-id]').forEach(function (btn) { 10223 btn.addEventListener('click', function () { 10224 var id = btn.getAttribute('data-svc-id'); 10225 if (id) openModal(id); 10226 }); 10227 }); 10228 updateRankMoreBtn('btnMoreCommunity', 'community', rowsVotes.length); 10229 refreshSidebarComplaintsTop10(); 10230 try { 10231 renderTop30List(); 10232 } catch (eT30r) {} 10233 } 10234 10235 function mountCommunityRanking(forceRefresh) { 10236 var ol = document.getElementById('rankCommunity'); 10237 var disc = document.getElementById('rankCommunityDisclaimer'); 10238 if (disc && state.communityDisclaimerPt) disc.textContent = state.communityDisclaimerPt; 10239 if (!ol) return; 10240 var d = VALID_RANKING_DAYS.indexOf(state.complaintsRankingDays) !== -1 ? state.complaintsRankingDays : 1; 10241 var origin = window.location.origin || ''; 10242 10243 if (forceRefresh) { 10244 state.communityRankingRows = undefined; 10245 state._fetchedCommunityRankingDays = null; 10246 state._fetchedCommunityRankingLimit = null; 10247 refreshSidebarComplaintsTop10(); 10248 fetch(origin + '/api/detector/community-ranking?' + communityRankingApiQuery(d), { credentials: 'omit' }) 10249 .then(function (r) { 10250 return r.json().then(function (data) { 10251 return { ok: r.ok, data: data }; 10252 }); 10253 }) 10254 .then(function (pack) { 10255 var data = pack.data || {}; 10256 if (!pack.ok || !data.success) { 10257 state.communityRankingRows = null; 10258 state._fetchedCommunityRankingDays = null; 10259 state._fetchedCommunityRankingLimit = null; 10260 ol.innerHTML = 10261 '<li class="rank-empty" style="color:var(--muted);padding:8px 0;">' + 10262 esc(__('js_comm_ol_unavail')) + 10263 '</li>'; 10264 updateRankMoreBtn('btnMoreCommunity', 'community', 0); 10265 refreshSidebarComplaintsTop10(); 10266 try { 10267 renderTop30List(); 10268 } catch (eT30f) {} 10269 return; 10270 } 10271 writeCommunityRankingSwrCache(data); 10272 scheduleCommunityRankingMidnightRefresh(data.next_reset_utc); 10273 if (data.disclaimer_pt && disc) disc.textContent = data.disclaimer_pt; 10274 state.communityRankingRows = data.ranking || []; 10275 state._fetchedCommunityRankingDays = d; 10276 state._fetchedCommunityRankingLimit = COMMUNITY_RANKING_API_LIMIT; 10277 if (data.disclaimer_pt) state.communityDisclaimerPt = data.disclaimer_pt; 10278 if (!state.communityRankingRows.length) { 10279 ol.innerHTML = 10280 '<li class="rank-empty" style="color:var(--muted);padding:8px 0;">' + 10281 esc(__('js_comm_ol_empty')) + 10282 '</li>'; 10283 updateRankMoreBtn('btnMoreCommunity', 'community', 0); 10284 refreshSidebarComplaintsTop10(); 10285 try { 10286 renderTop30List(); 10287 } catch (eT30g) {} 10288 return; 10289 } 10290 applyCommunityRankingToCatalog(state.communityRankingRows); 10291 renderCommunityRankingOl(); 10292 }) 10293 .catch(function () { 10294 state.communityRankingRows = null; 10295 state._fetchedCommunityRankingDays = null; 10296 state._fetchedCommunityRankingLimit = null; 10297 ol.innerHTML = 10298 '<li class="rank-empty" style="color:var(--muted);padding:8px 0;">' + 10299 esc(__('js_comm_ol_unavail')) + 10300 '</li>'; 10301 updateRankMoreBtn('btnMoreCommunity', 'community', 0); 10302 refreshSidebarComplaintsTop10(); 10303 try { 10304 renderTop30List(); 10305 } catch (eT30h) {} 10306 }); 10307 return; 10308 } 10309 10310 if ( 10311 state._fetchedCommunityRankingDays === d && 10312 state._fetchedCommunityRankingLimit === COMMUNITY_RANKING_API_LIMIT && 10313 Array.isArray(state.communityRankingRows) 10314 ) { 10315 applyCommunityRankingToCatalog(state.communityRankingRows); 10316 renderCommunityRankingOl(); 10317 refreshSidebarComplaintsTop10(); 10318 syncComplaintsTop10BoardsAndCards(); 10319 if (state.detectorLoadInProgress) return; 10320 } else { 10321 var disk = readCommunityRankingSwrCache(d, COMMUNITY_RANKING_API_LIMIT); 10322 if (disk) { 10323 state.communityRankingRows = disk.ranking; 10324 state._fetchedCommunityRankingDays = d; 10325 state._fetchedCommunityRankingLimit = COMMUNITY_RANKING_API_LIMIT; 10326 if (disk.disclaimer_pt) { 10327 state.communityDisclaimerPt = disk.disclaimer_pt; 10328 if (disc) disc.textContent = disk.disclaimer_pt; 10329 } 10330 if (disk.next_reset_utc) scheduleCommunityRankingMidnightRefresh(disk.next_reset_utc); 10331 applyCommunityRankingToCatalog(state.communityRankingRows); 10332 renderCommunityRankingOl(); 10333 refreshSidebarComplaintsTop10(); 10334 syncComplaintsTop10BoardsAndCards(); 10335 if (state.detectorLoadInProgress) return; 10336 } else { 10337 state.communityRankingRows = undefined; 10338 refreshSidebarComplaintsTop10(); 10339 if (state.detectorLoadInProgress) return; 10340 } 10341 } 10342 10343 fetch(origin + '/api/detector/community-ranking?' + communityRankingApiQuery(d), { credentials: 'omit' }) 10344 .then(function (r) { 10345 return r.json().then(function (data) { 10346 return { ok: r.ok, data: data }; 10347 }); 10348 }) 10349 .then(function (pack) { 10350 var data = pack.data || {}; 10351 if (!pack.ok || !data.success) { 10352 if (state._fetchedCommunityRankingDays === d && Array.isArray(state.communityRankingRows)) { 10353 try { 10354 renderTop30List(); 10355 } catch (eT30f2) {} 10356 return; 10357 } 10358 state.communityRankingRows = null; 10359 state._fetchedCommunityRankingDays = null; 10360 state._fetchedCommunityRankingLimit = null; 10361 ol.innerHTML = 10362 '<li class="rank-empty" style="color:var(--muted);padding:8px 0;">' + 10363 esc(__('js_comm_ol_unavail')) + 10364 '</li>'; 10365 updateRankMoreBtn('btnMoreCommunity', 'community', 0); 10366 refreshSidebarComplaintsTop10(); 10367 try { 10368 renderTop30List(); 10369 } catch (eT30f) {} 10370 return; 10371 } 10372 writeCommunityRankingSwrCache(data); 10373 scheduleCommunityRankingMidnightRefresh(data.next_reset_utc); 10374 if (data.disclaimer_pt && disc) disc.textContent = data.disclaimer_pt; 10375 state.communityRankingRows = data.ranking || []; 10376 state._fetchedCommunityRankingDays = d; 10377 state._fetchedCommunityRankingLimit = COMMUNITY_RANKING_API_LIMIT; 10378 if (data.disclaimer_pt) state.communityDisclaimerPt = data.disclaimer_pt; 10379 if (!state.communityRankingRows.length) { 10380 ol.innerHTML = 10381 '<li class="rank-empty" style="color:var(--muted);padding:8px 0;">' + 10382 esc(__('js_comm_ol_empty')) + 10383 '</li>'; 10384 updateRankMoreBtn('btnMoreCommunity', 'community', 0); 10385 refreshSidebarComplaintsTop10(); 10386 try { 10387 renderTop30List(); 10388 } catch (eT30g) {} 10389 return; 10390 } 10391 applyCommunityRankingToCatalog(state.communityRankingRows); 10392 renderCommunityRankingOl(); 10393 }) 10394 .catch(function () { 10395 if (state._fetchedCommunityRankingDays === d && Array.isArray(state.communityRankingRows)) { 10396 try { 10397 renderTop30List(); 10398 } catch (eT30h2) {} 10399 return; 10400 } 10401 state.communityRankingRows = null; 10402 state._fetchedCommunityRankingDays = null; 10403 state._fetchedCommunityRankingLimit = null; 10404 ol.innerHTML = 10405 '<li class="rank-empty" style="color:var(--muted);padding:8px 0;">' + 10406 esc(__('js_comm_ol_unavail')) + 10407 '</li>'; 10408 updateRankMoreBtn('btnMoreCommunity', 'community', 0); 10409 refreshSidebarComplaintsTop10(); 10410 try { 10411 renderTop30List(); 10412 } catch (eT30h) {} 10413 }); 10414 } 10415 10416 function bumpCommunityVoteLocal(svcId) { 10417 if (!state.catalog || !svcId) return; 10418 for (var i = 0; i < state.catalog.length; i++) { 10419 if (state.catalog[i].id === svcId) { 10420 var c = Number(state.catalog[i].community_vote_count) || 0; 10421 state.catalog[i].community_vote_count = c + 1; 10422 break; 10423 } 10424 } 10425 } 10426 10427 function passesIpStackFilter(svc) { 10428 var f = state.ipStackFilter || 'all'; 10429 if (f === 'all') return true; 10430 var st = svc.ip_stack || 'unknown'; 10431 if (f === 'v4') return st === 'v4'; 10432 if (f === 'dual') return st === 'dual'; 10433 if (f === 'v6') return st === 'v6'; 10434 return true; 10435 } 10436 10437 /** Minúsculas + remove diacrÃticos (pt), para bater «comercio» com «comércio». */ 10438 function normalizeCatalogSearchString(s) { 10439 var t = String(s || '').toLowerCase(); 10440 try { 10441 t = t.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); 10442 } catch (e) { 10443 /* IE / ambientes sem normalize */ 10444 } 10445 return t; 10446 } 10447 10448 /** Só letras e dÃgitos â «Chat GPT» â «chatgpt», útil para «gpt» bater no nome composto. */ 10449 function catalogSearchCompact(s) { 10450 return normalizeCatalogSearchString(s).replace(/[^a-z0-9]+/g, ''); 10451 } 10452 10453 /** 10454 * Texto agregado para busca: nome, id, descrição, domÃnio, categoria, URLs e alvos de sonda. 10455 * Inclui campos que antes não entravam (ex.: official_url), para termos como marcas/paths. 10456 */ 10457 function catalogSearchHaystackParts(svc) { 10458 var parts = []; 10459 function push(x) { 10460 if (x != null && x !== '') parts.push(String(x)); 10461 } 10462 push(svc.name); 10463 push(svc.id); 10464 push(svc.description); 10465 push(svc.logo_domain); 10466 push(svc.category); 10467 push(catLabel(svc.category)); 10468 push(svc.official_url); 10469 push(svc.status_url);
10470 push(svc.validation_url); 10471 push(svc.probe_url); 10472 push(svc.card_probe_url); 10473 var tl = svc.targets_live; 10474 if (Array.isArray(tl)) { 10475 for (var i = 0; i < tl.length; i++) { 10476 var x = tl[i]; 10477 if (!x) continue; 10478 push(x.url); 10479 push(x.label); 10480 push(x.key); 10481 } 10482 } 10483 var targets = svc.targets; 10484 if (Array.isArray(targets)) { 10485 for (var j = 0; j < targets.length; j++) { 10486 var y = targets[j]; 10487 if (!y) continue; 10488 push(y.url); 10489 push(y.label); 10490 push(y.key); 10491 } 10492 } 10493 return parts; 10494 } 10495 10496 /** Mesma lógica do filtro do catálogo, com texto de pesquisa explÃcito (ex.: modal Enter). */ 10497 function passesCatalogSearchQuery(svc, raw) { 10498 raw = String(raw || '').trim(); 10499 if (!raw) return true; 10500 var normQ = normalizeCatalogSearchString(raw); 10501 var tokens = normQ.split(/\s+/).filter(function (w) { 10502 return w.length > 0; 10503 }); 10504 if (!tokens.length) return true; 10505 var parts = catalogSearchHaystackParts(svc); 10506 var hayNormal = normalizeCatalogSearchString(parts.join(' \n ')); 10507 var hayCompact = catalogSearchCompact(parts.join(' ')); 10508 for (var t = 0; t < tokens.length; t++) { 10509 var tok = tokens[t]; 10510 if (hayNormal.indexOf(tok) >= 0) continue; 10511 var tokC = catalogSearchCompact(tok); 10512 if (tokC.length >= 2 && hayCompact.indexOf(tokC) >= 0) continue; 10513 return false; 10514 } 10515 return true; 10516 } 10517 10518 function passesCatalogSearch(svc) { 10519 return passesCatalogSearchQuery(svc, state.catalogSearch || ''); 10520 } 10521 10522 var CATALOG_SEARCH_MODAL_MAX = 120; 10523 10524 function catalogSearchModalSortKey(svc, raw) { 10525 var normQ = normalizeCatalogSearchString(String(raw || '').trim()); 10526 var cq = catalogSearchCompact(raw); 10527 var name = normalizeCatalogSearchString(svc.name || ''); 10528 var id = normalizeCatalogSearchString(String(svc.id || '')); 10529 var cname = catalogSearchCompact(svc.name || ''); 10530 var cid = catalogSearchCompact(String(svc.id || '')); 10531 var tier = 40; 10532 if (normQ && (id === normQ || cid === cq)) tier = 0; 10533 else if (normQ && name.indexOf(normQ) === 0) tier = 1; 10534 else if (cq.length >= 2 && cname.indexOf(cq) === 0) tier = 2; 10535 else if (normQ && name.indexOf(normQ) >= 0) tier = 3; 10536 else tier = 4; 10537 return { tier: tier, name: String(svc.name || '').toLowerCase() }; 10538 } 10539 10540 function catalogSearchModalMatches(raw) { 10541 var q = String(raw || '').trim(); 10542 var cat = state.catalog; 10543 if (!cat || !cat.length) return []; 10544 var out = []; 10545 for (var i = 0; i < cat.length; i++) { 10546 var s = cat[i]; 10547 if (!passesFilterSansSearch(s)) continue; 10548 if (!passesCatalogSearchQuery(s, q)) continue; 10549 out.push(s); 10550 } 10551 out.sort(function (a, b) { 10552 var ka = catalogSearchModalSortKey(a, q); 10553 var kb = catalogSearchModalSortKey(b, q); 10554 if (ka.tier !== kb.tier) return ka.tier - kb.tier; 10555 return ka.name.localeCompare(kb.name, i18nSortLocale()); 10556 }); 10557 return out; 10558 } 10559 10560 function catalogSearchModalCardHtml(svc) { 10561 var cat = catLabel(svc.category) || svc.category || ''; 10562 var dom = svc.logo_domain ? String(svc.logo_domain) : ''; 10563 var lat = rankingLatencyMs(svc); 10564 var latTxt = 10565 svc.last_ok === true && lat != null && !isNaN(lat) 10566 ? String(Math.round(lat)) + ' ms' 10567 : svc.last_ok === false 10568 ? 'erro' 10569 : 'â'; 10570 var st = svc.last_ok === true ? 'ok' : svc.last_ok === false ? 'bad' : 'unk'; 10571 return ( 10572 '<button type="button" class="csm-card" data-svc-id="' + 10573 esc(svc.id) + 10574 '" role="listitem">' + 10575 detLogoImgHtml(svc, 'csm-card__logo', '') + 10576 '<span class="csm-card__body">' + 10577 '<span class="csm-card__name">' + 10578 esc(svc.name || svc.id) + 10579 '</span>' + 10580 '<span class="csm-card__meta">' + 10581 esc(cat) + 10582 (dom ? '<span class="csm-card__dot"> · </span><span class="csm-card__dom">' + esc(dom) + '</span>' : '') + 10583 '</span>' + 10584 '</span>' + 10585 '<span class="csm-card__lat csm-card__lat--' + 10586 st + 10587 '" aria-label="Latência">' + 10588 esc(latTxt) + 10589 '</span>' + 10590 '</button>' 10591 ); 10592 } 10593 10594 function renderCatalogSearchModal(raw) { 10595 var modal = document.getElementById('catalogSearchModal'); 10596 var qEl = document.getElementById('catalogSearchModalQuery'); 10597 var hintEl = document.getElementById('catalogSearchModalHint'); 10598 var grid = document.getElementById('catalogSearchModalGrid'); 10599 var foot = document.getElementById('catalogSearchModalFoot'); 10600 if (!modal || !grid) return; 10601 var q = String(raw || '').trim(); 10602 if (qEl) { 10603 qEl.textContent = q ? '«' + q + '»' : ''; 10604 qEl.style.display = q ? '' : 'none'; 10605 } 10606 if (!q) { 10607 if (hintEl) { 10608 hintEl.hidden = false;
10609 hintEl.textContent = 10610 'Escreva um termo na caixa de pesquisa (nome, domÃnio ou ID) e prima Enter para ver correspondências em todo o catálogo.'; 10611 } 10612 grid.innerHTML = 10613 '<div class="csm-empty csm-empty--soft" role="status">Nenhuma pesquisa ativa.</div>'; 10614 if (foot) { 10615 foot.hidden = true; 10616 foot.textContent = ''; 10617 } 10618 return; 10619 } 10620 if (hintEl) hintEl.hidden = true; 10621 var all = catalogSearchModalMatches(q); 10622 var total = all.length; 10623 var slice = all.slice(0, CATALOG_SEARCH_MODAL_MAX); 10624 if (!total) { 10625 grid.innerHTML = 10626 '<div class="csm-empty" role="status">Nenhum serviço corresponde a esta pesquisa no catálogo completo.</div>'; 10627 } else { 10628 grid.innerHTML = slice.map(catalogSearchModalCardHtml).join(''); 10629 bindDetLogoFallbacks(grid); 10630 } 10631 if (foot) { 10632 if (total === 0) { 10633 foot.hidden = true; 10634 foot.textContent = ''; 10635 } else if (total > CATALOG_SEARCH_MODAL_MAX) { 10636 foot.hidden = false; 10637 foot.textContent = 10638 'A mostrar ' + 10639 String(CATALOG_SEARCH_MODAL_MAX) + 10640 ' de ' + 10641 String(total) + 10642 ' resultados. Refine a pesquisa para listar menos entradas.'; 10643 } else { 10644 foot.hidden = false; 10645 foot.textContent = 10646 total === 1 ? '1 resultado no catálogo completo.' : String(total) + ' resultados no catálogo completo.'; 10647 } 10648 } 10649 } 10650 10651 function openCatalogSearchResultsModal() { 10652 var modal = document.getElementById('catalogSearchModal'); 10653 if (!modal) return; 10654 var inp = document.getElementById('catalogSearchInput'); 10655 if (inp) { 10656 if (_catalogSearchDebounce) { 10657 clearTimeout(_catalogSearchDebounce); 10658 _catalogSearchDebounce = null; 10659 } 10660 state.catalogSearch = inp.value || ''; 10661 } 10662 renderCatalogSearchModal(state.catalogSearch || ''); 10663 modal.classList.add('open'); 10664 modal.setAttribute('aria-hidden', 'false'); 10665 document.body.style.overflow = 'hidden'; 10666 var closeBtn = document.getElementById('catalogSearchModalClose'); 10667 if (closeBtn) closeBtn.focus(); 10668 } 10669 10670 function closeCatalogSearchResultsModal() { 10671 var modal = document.getElementById('catalogSearchModal'); 10672 if (!modal) return; 10673 modal.classList.remove('open'); 10674 modal.setAttribute('aria-hidden', 'true'); 10675 document.body.style.overflow = ''; 10676 var inp = document.getElementById('catalogSearchInput'); 10677 if (inp) inp.focus(); 10678 } 10679 10680 function wireCatalogSearchModal() { 10681 var modal = document.getElementById('catalogSearchModal'); 10682 if (!modal || modal.dataset.detectorCsmWired === '1') return; 10683 modal.dataset.detectorCsmWired = '1'; 10684 var closeBtn = document.getElementById('catalogSearchModalClose'); 10685 if (closeBtn) { 10686 closeBtn.addEventListener('click', function () { 10687 closeCatalogSearchResultsModal(); 10688 }); 10689 } 10690 modal.addEventListener('click', function (e) { 10691 if (e.target === modal) closeCatalogSearchResultsModal(); 10692 }); 10693 document.addEventListener('keydown', function (e) { 10694 if (e.key === 'Escape' && modal.classList.contains('open')) { 10695 closeCatalogSearchResultsModal(); 10696 } 10697 }); 10698 var grid = document.getElementById('catalogSearchModalGrid'); 10699 if (grid) { 10700 grid.addEventListener('click', function (e) { 10701 var btn = e.target && e.target.closest ? e.target.closest('.csm-card[data-svc-id]') : null; 10702 if (!btn) return; 10703 var id = btn.getAttribute('data-svc-id'); 10704 if (!id) return; 10705 closeCatalogSearchResultsModal(); 10706 openModal(id); 10707 }); 10708 } 10709 } 10710 10711 function passesFilter(svc) { 10712 if (!passesIpStackFilter(svc)) return false;
10713 if (state.filter !== 'all' && (svc.category || '') !== state.filter) return false; 10714 if (!passesCatalogSearch(svc)) return false; 10715 return true; 10716 } 10717 10718 /** Categoria + stack IP (sem texto da caixa de busca) â rankings Top 5 e problemas globais. */ 10719 function passesFilterSansSearch(svc) { 10720 if (!passesIpStackFilter(svc)) return false; 10721 if (state.filter !== 'all' && (svc.category || '') !== state.filter) return false; 10722 return true; 10723 } 10724 10725 function filteredCatalog() { 10726 if (!state.catalog) return []; 10727 return state.catalog.filter(passesFilter); 10728 } 10729 10730 function filteredCatalogSansSearch() { 10731 if (!state.catalog) return []; 10732 return state.catalog.filter(passesFilterSansSearch); 10733 } 10734 10735 function nonSearchFiltersActive() { 10736 if (state.filter && state.filter !== 'all') return true; 10737 var ipf = state.ipStackFilter || 'all'; 10738 if (ipf !== 'all') return true; 10739 return false; 10740 } 10741 10742 /** Pesquisa, categoria ou stack IP â «todos» â tops e banner seguem o subconjunto filtrado. */ 10743 function catalogFiltersActive() { 10744 var q = (state.catalogSearch || '').trim(); 10745 if (q) return true; 10746 if (state.filter && state.filter !== 'all') return true; 10747 var ipf = state.ipStackFilter || 'all'; 10748 if (ipf !== 'all') return true; 10749 return false; 10750 } 10751 10752 function updateCatalogFilterBanner() { 10753 var sec = document.getElementById('secShelves'); 10754 var ban = document.getElementById('catalogFilterBanner'); 10755 if (!sec || !ban) return; 10756 if (!catalogFiltersActive()) { 10757 sec.classList.remove('is-filtered'); 10758 ban.innerHTML = ''; 10759 ban.setAttribute('hidden', ''); 10760 return; 10761 } 10762 var n = filteredCatalog().length; 10763 var chips = []; 10764 var q = (state.catalogSearch || '').trim(); 10765 if (q) { 10766 chips.push( 10767 '<span class="cfb-chip cfb-chip--search" title="Pesquisa no catálogo">Busca: <strong>' + 10768 esc(q) + 10769 '</strong></span>', 10770 ); 10771 } 10772 if (state.filter && state.filter !== 'all') { 10773 var cl = catLabel(state.filter) || state.filter; 10774 chips.push('<span class="cfb-chip cfb-chip--cat" title="Categoria">Cat.: ' + esc(cl) + '</span>'); 10775 } 10776 var ipf = state.ipStackFilter || 'all'; 10777 if (ipf !== 'all') { 10778 var ipLab = 10779 ipf === 'v4' ? 'Só IPv4' : ipf === 'dual' ? 'IPv4 + IPv6' : ipf === 'v6' ? 'Só IPv6' : ipf; 10780 chips.push('<span class="cfb-chip cfb-chip--ip" title="Stack IP">' + esc(ipLab) + '</span>'); 10781 } 10782 ban.innerHTML = 10783 '<div class="cfb-inner">' + 10784 '<span class="cfb-label">' + 10785 esc(__('js_filter_banner_label')) + 10786 '</span>' + 10787 '<div class="cfb-chips">' + 10788 chips.join('') + 10789 '</div>' + 10790 '<span class="cfb-count">' + 10791 esc(String(n)) + 10792 ' serviço' + 10793 (n !== 1 ? 's' : '') + 10794 '</span>' + 10795 '<button type="button" class="cfb-clear" id="btnCatalogFilterClear">Limpar filtro</button>' + 10796 '</div>'; 10797 ban.removeAttribute('hidden'); 10798 sec.classList.add('is-filtered'); 10799 var clr = document.getElementById('btnCatalogFilterClear'); 10800 if (clr) { 10801 clr.onclick = function () { 10802 resetCatalogHomeView(); 10803 }; 10804 } 10805 } 10806 10807 function problemsIndex() { 10808 var d = state.summary; 10809 var ix = {}; 10810 if (!d || !d.success) return ix; 10811 var raw = d.saas_problems || []; 10812 for (var i = 0; i < raw.length; i++) { 10813 var p = raw[i]; 10814 var sl = String(p.slug || '') 10815 .trim() 10816 .toLowerCase(); 10817 if (sl) ix[sl] = p; 10818 } 10819 return ix; 10820 } 10821 10822 function svcStatuspageSlug(svc) { 10823 var id = svc.id || ''; 10824 if (id.indexOf('sp_') === 0) return id.slice(3).toLowerCase(); 10825 return ''; 10826 } 10827 10828 function latencyMs(svc) { 10829 var v = rankingLatencyMs(svc); 10830 return v != null && !isNaN(v) ? v : null; 10831 } 10832 10833 /** 10834 * Ordenação estável entre estados da sonda principal: OK â sem amostra â erro. 10835 * Mantém cartões com falha na sondagem HTTPS/TLS no fim de ca
10835da prateleira (melhor UX). 10836 */ 10837 function svcProbeOkSortGroup(svc) { 10838 if (svc.last_ok === false) return 2; 10839 if (svc.last_ok === true) return 0; 10840 return 1; 10841 } 10842 10843 function sortItems(items) { 10844 var mode = state.rankSort || 'live_problems'; 10845 var prob = problemsIndex(); 10846 var arr = items.slice(); 10847 var saasBySlug = null; 10848 var gamesBySlug = null; 10849 if (mode === 'live_problems') { 10850 saasBySlug = buildSaasSlugMap(); 10851 gamesBySlug = buildGamesSlugMap(); 10852 } 10853 function nm(s) { 10854 return String(s.name || '').toLowerCase(); 10855 } 10856 function liveScore(svc) { 10857 return problemEntryForService(svc, saasBySlug, gamesBySlug).score; 10858 } 10859 arr.sort(function (a, b) { 10860 var ga = svcProbeOkSortGroup(a); 10861 var gb = svcProbeOkSortGroup(b); 10862 if (ga !== gb) return ga - gb; 10863 if (mode === 'name') return nm(a).localeCompare(nm(b), i18nSortLocale()); 10864 if (mode === 'name_desc') return nm(b).localeCompare(nm(a), i18nSortLocale()); 10865 if (mode === 'live_problems') { 10866 var sb = liveScore(b); 10867 var sa = liveScore(a); 10868 if (sb !== sa) return sb - sa; 10869 return cmpRankingLatencyWorst(a, b); 10870 } 10871 if (mode === 'rank_global') { 10872 var ra = a.latency_rank != null ? a.latency_rank : 999999; 10873 var rb = b.latency_rank != null ? b.latency_rank : 999999; 10874 if (ra !== rb) return ra - rb; 10875 return nm(a).localeCompare(nm(b), i18nSortLocale()); 10876 } 10877 if (mode === 'fail_first') { 10878 var fa = a.last_ok === false ? 0 : a.last_ok === true ? 1 : 2; 10879 var fb = b.last_ok === false ? 0 : b.last_ok === true ? 1 : 2; 10880 if (fa !== fb) return fa - fb; 10881 var la = latencyMs(a); 10882 var lb = latencyMs(b); 10883 if (la != null && lb != null && la !== lb) return la - lb; 10884 return nm(a).localeCompare(nm(b), i18nSortLocale()); 10885 } 10886 if (mode === 'incident_first') { 10887 var sa = svcStatuspageSlug(a); 10888 var sb = svcStatuspageSlug(b); 10889 var pa = sa ? prob[sa] : null; 10890 var pb = sb ? prob[sb] : null; 10891 var ia = pa ? (Number(pa.severity) || 0) * 1000 + (Number(pa.incidents_count) || 0) : 0; 10892 var ib = pb ? (Number(pb.severity) || 0) * 1000 + (Number(pb.incidents_count) || 0) : 0; 10893 if (ib !== ia) return ib - ia; 10894 var l1 = latencyMs(a); 10895 var l2 = latencyMs(b); 10896 if (l1 != null && l2 != null && l1 !== l2) return l1 - l2; 10897 return nm(a).localeCompare(nm(b), i18nSortLocale()); 10898 } 10899 if (mode === 'latency_desc') { 10900 return cmpRankingLatencyWorst(a, b); 10901 } 10902 var x = latencyMs(a); 10903 var y = latencyMs(b); 10904 if (x == null && y == null) return nm(a).localeCompare(nm(b), i18nSortLocale()); 10905 if (x == null) return 1; 10906 if (y == null) return -1; 10907 var rx = Math.round(x); 10908 var ry = Math.round(y); 10909 if (rx !== ry) return rx - ry; 10910 var pxa = marketProminenceForLatencyRank(a); 10911 var pxb = marketProminenceForLatencyRank(b); 10912 if (pxb !== pxa) return pxb - pxa; 10913 if (x !== y) return x - y; 10914 return nm(a).localeCompare(nm(b), i18nSortLocale()); 10915 }); 10916 return arr; 10917 } 10918 10919 function rebuildCategorySelect() { 10920 var sel = document.getElementById('selCategory'); 10921 if (!sel || !state.catalog) return; 10922 var cur = state.filter; 10923 sel.innerHTML = ''; 10924 var o0 = document.createElement('option'); 10925 o0.value = 'all'; 10926 o0.textContent = __('cat_filter_all'); 10927 sel.appendChild(o0); 10928 var seen = {}; 10929 for (var i = 0; i < state.catalog.length; i++) { 10930 var c = state.catalog[i].category || 'core'; 10931 seen[c] = true; 10932 } 10933 var keys = Object.keys(seen).sort(function (a, b) { 10934 return catLabel(a).localeCompare(catLabel(b), i18nSortLocale()); 10935 }); 10936 for (var k = 0; k < keys.length; k++) { 10937 var key = keys[k]; 10938 var o = document.createElement('option'); 10939 o.value = key;
10940 o.textContent = catLabel(key); 10941 sel.appendChild(o); 10942 } 10943 var oTodos = document.createElement('option'); 10944 oTodos.value = '__all__'; 10945 oTodos.textContent = __('cat_filter_todos'); 10946 sel.appendChild(oTodos); 10947 if (cur === 'all') sel.value = '__all__'; 10948 else if (seen[cur]) sel.value = cur; 10949 else { 10950 sel.value = '__all__'; 10951 state.filter = 'all'; 10952 } 10953 } 10954 10955 function fillRankSelectOptions() { 10956 var sel = document.getElementById('selRank'); 10957 if (!sel) return; 10958 var cur = sel.value || state.rankSort; 10959 sel.innerHTML = ''; 10960 for (var i = 0; i < RANK_OPTION_KEYS.length; i++) { 10961 var o = document.createElement('option'); 10962 o.value = RANK_OPTION_KEYS[i][0]; 10963 o.textContent = __(RANK_OPTION_KEYS[i][1]); 10964 sel.appendChild(o); 10965 } 10966 var okVal = false; 10967 for (var j = 0; j < RANK_OPTION_KEYS.length; j++) { 10968 if (RANK_OPTION_KEYS[j][0] === cur) { 10969 okVal = true; 10970 break; 10971 } 10972 } 10973 sel.value = okVal ? cur : state.rankSort; 10974 if (sel.value !== cur && okVal === false) state.rankSort = sel.value || 'live_problems'; 10975 } 10976 10977 function initRankSelect() { 10978 var sel = document.getElementById('selRank'); 10979 if (!sel || sel.dataset.detectorInited === '1') return; 10980 sel.dataset.detectorInited = '1'; 10981 fillRankSelectOptions(); 10982 sel.addEventListener('change', function () { 10983 state.rankSort = sel.value || 'live_problems'; 10984 mountShelves(); 10985 mountRankPanels(); 10986 }); 10987 } 10988 10989 function wireCategorySelect() { 10990 var sel = document.getElementById('selCategory'); 10991 if (!sel || sel.dataset.detectorWired === '1') return; 10992 sel.dataset.detectorWired = '1'; 10993 sel.addEventListener('change', function () { 10994 var v = sel.value || 'all'; 10995 state.filter = v === '__all__' || v === 'all' ? 'all' : v; 10996 state.rankExpanded.latFast = false; 10997 state.rankExpanded.latSlow = false; 10998 mountShelves(); 10999 mountRankPanels(); 11000 mountProblemsCatalog(); 11001 updateMetricsBar(); 11002 applyFilterSections(); 11003 refreshShelfHints(); 11004 }); 11005 } 11006 11007 function wireIpStackSelect() { 11008 var sel = document.getElementById('selIpStack'); 11009 if (!sel || sel.dataset.detectorIpWired === '1') return; 11010 sel.dataset.detectorIpWired = '1'; 11011 sel.addEventListener('change', function () { 11012 state.ipStackFilter = sel.value || 'all'; 11013 state.rankExpanded.latFast = false; 11014 state.rankExpanded.latSlow = false; 11015 mountShelves(); 11016 mountRankPanels(); 11017 mountProblemsCatalog(); 11018 updateMetricsBar(); 11019 }); 11020 } 11021 11022 var _catalogSearchDebounce = null; 11023 function wireCatalogSearchInput() { 11024 var inp = document.getElementById('catalogSearchInput'); 11025 if (!inp || inp.dataset.detectorSearchWired === '1') return; 11026 inp.dataset.detectorSearchWired = '1'; 11027 inp.addEventListener('input', function () { 11028 state.catalogSearch = inp.value || ''; 11029 mountTop10Cards(); 11030 updateCatalogFilterBanner(); 11031 if (_catalogSearchDebounce) clearTimeout(_catalogSearchDebounce); 11032 _catalogSearchDebounce = setTimeout(function () { 11033 _catalogSearchDebounce = null; 11034 var refocusSearch = document.activeElement === inp; 11035 var selA = inp.selectionStart; 11036 var selB = inp.selectionEnd; 11037 state.rankExpanded.latFast = false; 11038 state.rankExpanded.latSlow = false; 11039 mountCatalogGridAndIcmpSection(); 11040 updateMetricsBar(); 11041 applyFilterSections(); 11042 if (refocusSearch) { 11043 var inp2 = document.getElementById('catalogSearchInput'); 11044 if (inp2) { 11045 inp2.focus(); 11046 if (typeof inp2.setSelectionRange === 'function') { 11047 var len = String(inp2.value || '').length; 11048 var a = typeof selA === 'number' && !isNaN(selA) ? selA : len; 11049 var b = typeof selB === 'number' && !isNaN(selB) ? selB : len; 11050 if (a > len) a = len; 11051 if (b > len) b = len; 11052 try { 11053 inp2.setSelectionRange(a, b); 11054 } catch (eSel) { 11055 /* IME / type=search edge cases */ 11056 } 11057 } 11058 } 11059 } 11060 }, 0); 11061 }); 11062 inp.addEventListener('keydown', function (e) { 11063 if (e.key !== 'Enter') return; 11064 e.preventDefault(); 11065 if (_catalogSearchDebounce) { 11066 clearTimeout(_catalogSearchDebounce); 11067 _catalogSearchDebounce = null; 11068 } 11069 state.catalogSearch = inp.value || ''; 11070 state.rankExpanded.latFast = false;
11071 state.rankExpanded.latSlow = false; 11072 mountShelves(); 11073 updateMetricsBar(); 11074 applyFilterSections(); 11075 openCatalogSearchResultsModal(); 11076 }); 11077 } 11078 11079 function updateProbePopBadge(pp) { 11080 var el = document.getElementById('probePopBadge'); 11081 var lbl = document.getElementById('probePopBadgeLabel'); 11082 if (!el) return; 11083 var pub = pp && pp.public_hint_pt ? String(pp.public_hint_pt).trim() : ''; 11084 if (!pub) { 11085 el.style.display = 'none'; 11086 el.title = ''; 11087 el.classList.remove('probe-pop-badge--ix'); 11088 return; 11089 } 11090 el.style.display = 'inline-flex'; 11091 el.title = pub; 11092 var ix = !!(pp && pp.delegate_active); 11093 el.classList.toggle('probe-pop-badge--ix', ix); 11094 if (lbl) lbl.textContent = ix ? 'IX·BR' : 'Rede Catellix'; 11095 } 11096 11097 function renderObservationHub(profile) { 11098 var wrap = document.getElementById('detObservationHub'); 11099 var list = document.getElementById('detObservationHubList'); 11100 var note = document.getElementById('detObservationHubNote'); 11101 var dashObs = document.getElementById('detDashObs'); 11102 if (!wrap || !list || !note) return; 11103 profile = profile || {}; 11104 var pts = profile.observation_points || []; 11105 if (!pts.length) { 11106 wrap.style.display = 'none'; 11107 if (dashObs) dashObs.hidden = true; 11108 return; 11109 } 11110 if (dashObs) dashObs.hidden = false; 11111 wrap.style.display = 'block'; 11112 list.innerHTML = pts 11113 .map(function (p) { 11114 var active = p.active !== false; 11115 var lab = esc(p.label_pt || p.label || ''); 11116 return ( 11117 '<li class="' + 11118 (active ? '' : 'det-obs-inactive') + 11119 '">' + 11120 lab + 11121 (active ? '' : ' · inativo') + 11122 '</li>' 11123 ); 11124 }) 11125 .join(''); 11126 note.textContent = ''; 11127 note.style.display = 'none'; 11128 } 11129 11130 function buildSaasSlugMap() { 11131 var map = {}; 11132 var probs = (state.summary && state.summary.saas_problems) || []; 11133 for (var i = 0; i < probs.length; i++) { 11134 var p = probs[i]; 11135 var sl = String(p.slug || '') 11136 .trim() 11137 .toLowerCase(); 11138 if (!sl) continue; 11139 if (!map[sl] || (p.severity || 0) > (map[sl].severity || 0)) map[sl] = p; 11140 } 11141 return map; 11142 } 11143 11144 function buildGamesSlugMap() { 11145 var map = {}; 11146 var g = (state.summary && state.summary.games) || []; 11147 for (var j = 0; j < g.length; j++) { 11148 var x = g[j]; 11149 var sl = String(x.slug || '') 11150 .trim() 11151 .toLowerCase(); 11152 if (sl) map[sl] = x; 11153 } 11154 return map; 11155 } 11156 11157 function catalogServiceForGameSlug(slug) { 11158 if (!state.catalog) return null; 11159 var s0 = String(slug || '') 11160 .trim() 11161 .toLowerCase(); 11162 if (!s0) return null; 11163 var dom = GAME_SLUG_LOGO[s0]; 11164 for (var i = 0; i < state.catalog.length; i++) { 11165 var s = state.catalog[i]; 11166 var id = String(s.id || '').toLowerCase(); 11167 if (id === 'sp_' + s0) return s; 11168 if (dom && String(s.logo_domain || '').toLowerCase() === dom) return s; 11169 } 11170 return null; 11171 } 11172 11173 function gameIssuesForService(svc, gamesBySlug) { 11174 var extra = 0; 11175 var rs = []; 11176 for (var gslug in gamesBySlug) { 11177 if (!Object.prototype.hasOwnProperty.call(gamesBySlug, gslug)) continue; 11178 var hit = catalogServiceForGameSlug(gslug); 11179 if (!hit || hit.id !== svc.id) continue; 11180 var gg = gamesBySlug[gslug]; 11181 if (!gg) continue; 11182 if (gg.state === 'bad') { 11183 extra += 600; 11184 rs.push('Plataforma (jogos): incidente / alerta'); 11185 } else if (gg.state === 'warn') { 11186 extra += 320; 11187 rs.push('Plataforma (jogos): atenção'); 11188 } 11189 } 11190 return { extra: extra, reasons: rs }; 11191 } 11192 11193 function problemEntryForService(svc, saasBySlug, gamesBySlug) { 11194 var score = 0; 11195 var reasons = []; 11196 if (svc.last_ok === false) { 11197 score += 10000; 11198 reasons.push('Sondagem HTTPS falhou'); 11199 } 11200 var slugFromId = ''; 11201 if ((svc.id || '').indexOf('sp_') === 0) { 11202 slugFromId = svc.id.slice(3).toLowerCase(); 11203 } 11204 var sp = slugFromId ? saasBySlug[slugFromId] : null; 11205 if (sp && (sp.severity || 0) >= 1) { 11206 score += 800 + (sp.severity || 0) * 200 + (sp.incidents_count || 0) * 30; 11207 var ind = String(sp.indicator || '').trim(); 11208 reasons.push( 11209 'Statuspage' + 11210 (ind ? ' (' + ind + ')' : '') + 11211 (sp.incidents_count ? ' · ' + sp.incidents_count + ' ativos' : ''), 11212 ); 11213 } 11214 var gi = gameIssuesForService(svc, gamesBySlug); 11215 score += gi.extra; 11216 reasons = reasons.concat(gi.reasons); 11217 var votes = Number(svc.community_vote_count) || 0; 11218 if (votes >= 1) { 11219 score += Math.min(votes, 25) * 40; 11220 reasons.push('Comunidade: ' + votes + ' relato(s) (' + state.communityWindowDays + 'd)'); 11221 } 11222 return { svc: svc, score: score, reasons: reasons }; 11223 } 11224 11225 function sortProblemEntries(a, b) { 11226 if (b.score !== a.score) return b.score - a.score; 11227 return cmpRankingLatencyWorst(a.svc, b.svc); 11228 } 11229
11230 function topWorstLatencyEntries(svcs, n) { 11231 return svcs 11232 .filter(function (s) { 11233 return hasRankingLatencySample(s); 11234 }) 11235 .sort(function (a, b) { 11236 return cmpRankingLatencyWorst(a, b); 11237 }) 11238 .slice(0, n) 11239 .map(function (svc) { 11240 return { svc: svc, score: 0, reasons: [], latencyFallback: true }; 11241 }); 11242 } 11243 11244 function problemRowHtml(ent, idx, modeLatency) { 11245 var svc = ent.svc; 11246 var meta = 11247 modeLatency || ent.latencyFallback 11248 ? 'Pior latência entre sondagens OK neste grupo.' 11249 : esc(ent.reasons.join(' · ')); 11250 var rlm = rankingLatencyMs(svc); 11251 var ms = 11252 rlm != null && !isNaN(rlm) ? esc(String(Math.round(rlm))) + ' ms' : 'â'; 11253 return ( 11254 '<li><button type="button" class="rank-line" data-svc-id="' + 11255 esc(svc.id) + 11256 '"><span class="rn">' + 11257 esc(String(idx + 1)) + 11258 '</span>' + 11259 detLogoImgHtml(svc, 'rfav', '') + 11260 '<span class="rbody"><span class="rname">' + 11261 esc(svc.name) + 11262 '</span><span class="rmeta">' + 11263 esc(catLabel(svc.category) || svc.category || '') + 11264 ipStackTagsHtmlCompact(svc) + 11265 ' · ' + 11266 meta + 11267 '</span></span><span class="rval">' + 11268 ms + 11269 '</span></button></li>' 11270 ); 11271 } 11272 11273 function mountProblemsCatalog() { 11274 var gOl = document.getElementById('probCatalogGlobal'); 11275 var byHost = document.getElementById('probCatalogByCat'); 11276 var gTitle = document.getElementById('probCatalogGlobalTitle'); 11277 var gSub = document.getElementById('probCatalogGlobalSub'); 11278 if (!gOl || !byHost) return; 11279 if (!state.catalog || !state.catalog.length) { 11280 gOl.innerHTML = ''; 11281 byHost.innerHTML = ''; 11282 return; 11283 } 11284 var cat = filteredCatalogSansSearch(); 11285 if (!cat.length) { 11286 gOl.innerHTML = 11287 '<li class="rank-empty" style="color:var(--muted);padding:8px 0;">Nenhum serviço neste filtro.</li>'; 11288 byHost.innerHTML = ''; 11289 return; 11290 } 11291 var saasBySlug = buildSaasSlugMap(); 11292 var gamesBySlug = buildGamesSlugMap(); 11293 var allScored = cat.map(function (s) { 11294 return problemEntryForService(s, saasBySlug, gamesBySlug); 11295 }); 11296 var probGlobal = allScored 11297 .filter(function (e) { 11298 return e.score > 0; 11299 }) 11300 .sort(sortProblemEntries) 11301 .slice(0, PROB_TOP); 11302 var useLatGlobal = probGlobal.length === 0; 11303 var globalList = useLatGlobal ? topWorstLatencyEntries(cat, PROB_TOP) : probGlobal; 11304 var probScopeLab = nonSearchFiltersActive() ? 'filtro atual' : 'geral'; 11305 if (gTitle) { 11306 gTitle.textContent = useLatGlobal 11307 ? 'Top 10 · pior latência (' + probScopeLab + ')' 11308 : 'Top 10 · problemas (' + probScopeLab + ')'; 11309 } 11310 if (gSub) { 11311 var gSubBase = useLatGlobal 11312 ? __('js_prob_sort_latency') 11313 : 'Falhas de sondagem, Statuspage, jogos e comunidade.'; 11314 if (state.filter !== 'all') { 11315 gSubBase = 11316 'Categoria: ' + 11317 (catLabel(state.filter) || state.filter) + 11318 '. ' + 11319 gSubBase; 11320 } else if (nonSearchFiltersActive()) { 11321 gSubBase = 'Filtro categoria/IP. ' + gSubBase; 11322 } 11323 gSub.textContent = gSubBase; 11324 } 11325 gOl.innerHTML = globalList.length 11326 ? globalList.map(function (e, i) { 11327 return problemRowHtml(e, i, useLatGlobal); 11328 }).join('') 11329 : '<li class="rank-empty" style="color:var(--muted);padding:8px 0;">Sem dados para listar.</li>'; 11330 11331 var catKeys = {}; 11332 for (var c = 0; c < cat.length; c++) { 11333 var ck = cat[c].category || 'core'; 11334 catKeys[ck] = true; 11335 } 11336 var order = Object.keys(catKeys).sort(function (a, b) { 11337 return catLabel(a).localeCompare(catLabel(b), i18nSortLocale()); 11338 }); 11339 var blocks = ''; 11340 for (var o = 0; o < order.length; o++) { 11341 var key = order[o]; 11342 var inCat = cat.filter(function (s) { 11343 return (s.category || 'core') === key; 11344 }); 11345 var scored = inCat.map(function (s) { 11346 return problemEntryForService(s, saasBySlug, gamesBySlug); 11347 }); 11348 var probCat = scored 11349 .filter(function (e) { 11350 return e.score > 0; 11351 }) 11352 .sort(sortProblemEntries) 11353 .slice(0, PROB_TOP); 11354 var useLatCat = probCat.length === 0; 11355 var list = useLatCat ? topWorstLatencyEntries(inCat, PROB_TOP) : probCat;
11356 var label = catLabel(key); 11357 var modeHint = useLatCat ? 'latência' : 'alertas'; 11358 blocks += 11359 '<details class="det-prob-cat-details" data-prob-cat="' + 11360 esc(key) + 11361 '">' + 11362 '<summary class="det-prob-cat-sum">' + 11363 '<span class="det-dash-chev det-dash-chev--sm" aria-hidden="true">â¼</span>' + 11364 '<span class="det-prob-cat-name">' + 11365 esc(label) + 11366 '</span>' + 11367 '<span class="det-prob-cat-pill">' + 11368 esc(String(inCat.length)) + 11369 ' · ' + 11370 esc(modeHint) + 11371 '</span>' + 11372 '</summary>' + 11373 '<div class="det-prob-cat-body"><ol class="prob-catalog-ol">' + 11374 (list.length 11375 ? list 11376 .map(function (e, i) { 11377 return problemRowHtml(e, i, useLatCat); 11378 }) 11379 .join('') 11380 : '<li class="rank-empty" style="color:var(--muted);padding:8px 0;">Sem dados.</li>') + 11381 '</ol></div></details>'; 11382 } 11383 byHost.innerHTML = blocks; 11384 bindDetLogoFallbacks(gOl); 11385 bindDetLogoFallbacks(byHost); 11386 } 11387 11388 function updateMetricsBar() { 11389 var cat = state.catalog; 11390 var sum = state.summary; 11391 if (!cat || !cat.length) return; 11392 var ok = 0; 11393 var fail = 0; 11394 var unk = 0; 11395 var targets = 0; 11396 var sp = 0; 11397 var latSum = 0; 11398 var latN = 0; 11399 for (var i = 0; i < cat.length; i++) { 11400 var s = cat[i]; 11401 if (s.last_ok === true) ok++; 11402 else if (s.last_ok === false) fail++; 11403 else unk++; 11404 var tc = parseInt(String(s.target_count != null ? s.target_count : 1), 10); 11405 if (isNaN(tc) || tc < 1) tc = 1; 11406 targets += tc; 11407 if ((s.id || '').indexOf('sp_') === 0) sp++; 11408 if (hasRankingLatencySample(s)) { 11409 latSum += Number(rankingLatencyMs(s)); 11410 latN++; 11411 } 11412 } 11413 var saasN = 0; 11414 if (sum && sum.success && sum.saas_problems) { 11415 for (var j = 0; j < sum.saas_problems.length; j++) { 11416 if ((sum.saas_problems[j].severity || 0) >= 1) saasN++; 11417 } 11418 } 11419 var gamesAlert = sum && sum.success && sum.games_with_issue != null ? sum.games_with_issue : 0; 11420 var ddLen = sum && sum.trending_issues ? sum.trending_issues.length : 0; 11421 var set = function (id, v) { 11422 var el = document.getElementById(id); 11423 if (el) el.textContent = String(v); 11424 }; 11425 set('stTotal', cat.length); 11426 set('stReach', ok); 11427 set('stFail', fail); 11428 set('stUnknown', unk); 11429 set('stTargets', targets); 11430 set('stSp', sp); 11431 set('stSaas', saasN); 11432 set('stGames', gamesAlert); 11433 set('stDd', ddLen); 11434 set('stProbeSec', state.probeIntervalSec != null ? state.probeIntervalSec : 'â'); 11435 if (latN > 0) set('stLatAvg', Math.round(latSum / latN)); 11436 else set('stLatAvg', 'â'); 11437 11438 var vis = filteredCatalog().length; 11439 var mt = document.getElementById('stTotal'); 11440 if (mt && state.filter !== 'all') { 11441 mt.title = __('js_metrics_catalog_full_tpl') 11442 .replace(/\{n\}/g, String(cat.length)) 11443 .replace(/\{v\}/g, String(vis)); 11444 } else if (mt) {
11445 mt.title = __('js_metrics_catalog_entries'); 11446 } 11447 } 11448 11449 function mountRankPanels() { 11450 var list = filteredCatalogSansSearch(); 11451 var fastOl = document.getElementById('rankLatFast'); 11452 var slowOl = document.getElementById('rankLatSlow'); 11453 var incOl = document.getElementById('rankIncidents'); 11454 var trOl = document.getElementById('rankTrends'); 11455 var measurable = list.filter(function (s) { 11456 return hasRankingLatencySample(s); 11457 }); 11458 var byLat = measurable.slice().sort(cmpRankingLatencyBest); 11459 var byLatFastPool = byLat.filter(passesIcmpForFastestRank); 11460 var fast = byLatFastPool.slice(0, rankLimit('latFast')); 11461 var slow = measurable.slice().sort(cmpRankingLatencyWorst); 11462 slow = slow.slice(0, rankLimit('latSlow')); 11463 11464 function rowLatency(svc, idx, ms) { 11465 return ( 11466 '<li><button type="button" class="rank-line" data-svc-id="' + 11467 esc(svc.id) + 11468 '"><span class="rn">' + 11469 esc(String(idx + 1)) + 11470 '</span>' + 11471 detLogoImgHtml(svc, 'rfav', '') + 11472 '<span class="rbody"><span class="rname">' + 11473 esc(svc.name) + 11474 '</span><span class="rmeta">' + 11475 esc(catLabel(svc.category) || svc.category || '') + 11476 ipStackTagsHtmlCompact(svc) + 11477 '</span></span><span class="rval">' + 11478 (ms != null && !isNaN(ms) ? esc(String(Math.round(ms))) + ' ms' : 'â') + 11479 '</span></button></li>' 11480 ); 11481 } 11482 if (fastOl) { 11483 fastOl.innerHTML = fast.length 11484 ? fast.map(function (s, i) { 11485 return rowLatency(s, i, rankingLatencyMs(s)); 11486 }).join('') 11487 : '<li class="rank-empty" style="color:var(--muted);padding:8px 0;">' + 11488 esc(__('js_rank_no_ok')) + 11489 '</li>'; 11490 } 11491 if (slowOl) { 11492 slowOl.innerHTML = slow.length 11493 ? slow.map(function (s, i) { 11494 return rowLatency(s, i, rankingLatencyMs(s)); 11495 }).join('') 11496 : '<li class="rank-empty" style="color:var(--muted);padding:8px 0;">' + 11497 esc(__('js_rank_no_data')) + 11498 '</li>'; 11499 } 11500 11501 var probs = (state.summary && state.summary.saas_problems) || []; 11502 var rankedAll = probs 11503 .filter(function (p) { 11504 return (p.severity || 0) >= 1; 11505 }) 11506 .slice() 11507 .sort(function (a, b) { 11508 var sb = (b.severity || 0) - (a.severity || 0); 11509 if (sb !== 0) return sb; 11510 return (b.incidents_count || 0) - (a.incidents_count || 0); 11511 }); 11512 var ranked = rankedAll.slice(0, rankLimit('incidents')); 11513 if (incOl) { 11514 incOl.innerHTML = ranked.length 11515 ? ranked 11516 .map(function (p, i) { 11517 var url = esc(p.status_url || '#'); 11518 return ( 11519 '<li><a class="rank-line rank-line--ext" href="' + 11520 url + 11521 '" target="_blank" rel="noopener noreferrer"><span class="rn">' + 11522 esc(String(i + 1)) + 11523 '</span><span class="rbody"><span class="rname">' + 11524 esc(p.service || p.slug) + 11525 '</span><span class="rmeta">' + 11526 esc(String(p.indicator || '')) + 11527 ' · ' + 11528 esc(String(p.incidents_count || 0)) + 11529 ' ' + 11530 esc(__('js_rank_active_count')) + 11531 '</span></span><span class="rval">â</span></a></li>' 11532 ); 11533 }) 11534 .join('') 11535 : '<li class="rank-empty" style="color:var(--muted);padding:8px 0;">' + 11536 esc(__('js_rank_incidents_none')) + 11537 '</li>'; 11538 } 11539 updateRankMoreBtn('btnMoreIncidents', 'incidents', rankedAll.length); 11540 11541 var ddFull = (state.summary && state.summary.trending_issues) || []; 11542 var dd = ddFull.slice(0, rankLimit('trends')); 11543 if (trOl) { 11544 trOl.innerHTML = dd.length 11545 ? dd.map(function (x, i) { 11546 var raw = trendHref(x); 11547 var url = esc(raw); 11548 var tgt = trendLinkTargetAttrs(raw); 11549 return ( 11550 '<li><a class="rank-line rank-line--ext" href="' + 11551 url + 11552 '"' + 11553 tgt + 11554 '><span class="rn">' + 11555 esc(String(i + 1)) + 11556 '</span><span class="rbody"><span class="rname">' + 11557 esc(x.name || x.slug) + 11558 '</span><span class="rmeta">' + 11559 esc(__('js_rank_trend_badge')) + 11560 '</span></span><span class="rval">â</span></a></li>' 11561 ); 11562 }).join('') 11563 : '<li class="rank-empty" style="color:var(--muted);padding:8px 0;">' + 11564 esc(__('js_unavailable')) + 11565 '</li>'; 11566 } 11567 updateRankMoreBtn('btnMoreTrends', 'trends', ddFull.length); 11568 11569 var gOl = document.getElementById('rankGames'); 11570 var games = (state.summary && state.summary.games) || []; 11571 var gSortedFull = games.slice().sort(function (a, b) { 11572 var orderBad = { bad: 0, warn: 1, unknown: 2, ok: 3 }; 11573 var orderGood = { ok: 0, unknown: 1, warn: 2, bad: 3 }; 11574 var order = state.rankMood === 'good' ? orderGood : orderBad; 11575 var oa = order[a.state] != null ? order[a.state] : 2;
11576 var ob = order[b.state] != null ? order[b.state] : 2; 11577 if (oa !== ob) return oa - ob; 11578 return String(a.name || '').localeCompare(String(b.name || ''), i18nSortLocale()); 11579 }); 11580 var gSorted = gSortedFull.slice(0, rankLimit('games')); 11581 if (gOl) { 11582 gOl.innerHTML = gSorted.length 11583 ? gSorted.map(function (g, i) { 11584 var url = esc(g.status_url || '#'); 11585 var st = g.state || 'unknown'; 11586 var lab = g.status_label 11587 ? String(g.status_label) 11588 : st === 'ok' 11589 ? __('js_game_state_ok') 11590 : st === 'bad' 11591 ? __('js_game_state_bad') 11592 : st === 'warn' 11593 ? __('js_game_state_warn') 11594 : 'â'; 11595 var cls = st === 'ok' ? 'var(--ok)' : st === 'bad' ? 'var(--bad)' : 'var(--warn)'; 11596 return ( 11597 '<li><a class="rank-line rank-line--ext" href="' + 11598 url + 11599 '" target="_blank" rel="noopener noreferrer"><span class="rn">' + 11600 esc(String(i + 1)) + 11601 '</span><span class="rbody"><span class="rname">' + 11602 esc(g.name || g.slug) + 11603 '</span><span class="rmeta">' + 11604 esc(lab) + 11605 '</span></span><span class="rval" style="color:' + 11606 cls + 11607 ';">â</span></a></li>' 11608 ); 11609 }).join('') 11610 : '<li class="rank-empty" style="color:var(--muted);padding:8px 0;">' + 11611 esc(__('js_rank_no_data')) + 11612 '</li>'; 11613 } 11614 updateRankMoreBtn('btnMoreGames', 'games', gSortedFull.length); 11615 11616 updateRankMoreBtn('btnMoreLatFast', 'latFast', byLatFastPool.length); 11617 updateRankMoreBtn('btnMoreLatSlow', 'latSlow', byLat.length); 11618 11619 if (fastOl) { 11620 fastOl.querySelectorAll('.rank-line[data-svc-id]').forEach(function (btn) { 11621 btn.addEventListener('click', function () { 11622 var id = btn.getAttribute('data-svc-id'); 11623 if (id) openModal(id); 11624 }); 11625 }); 11626 } 11627 if (slowOl) { 11628 slowOl.querySelectorAll('.rank-line[data-svc-id]').forEach(function (btn) { 11629 btn.addEventListener('click', function () { 11630 var id = btn.getAttribute('data-svc-id'); 11631 if (id) openModal(id); 11632 }); 11633 }); 11634 } 11635 } 11636 11637 var RANK_MOOD_STORAGE_KEY = 'detector_rank_mood'; 11638 11639 function loadRankMoodFromStorage() { 11640 try { 11641 var v = localStorage.getItem(RANK_MOOD_STORAGE_KEY); 11642 if (v === 'good' || v === 'bad') state.rankMood = v; 11643 } catch (e) { 11644 /* ignore */ 11645 } 11646 } 11647 11648 function applyRankMoodUI() { 11649 var outer = document.getElementById('rankPanelsOuter'); 11650 var btn = document.getElementById('btnRankMoodToggle'); 11651 var hint = document.getElementById('rankMoodHint'); 11652 if (outer) { 11653 outer.classList.remove('rank-mood--good', 'rank-mood--bad'); 11654 outer.classList.add(state.rankMood === 'good' ? 'rank-mood--good' : 'rank-mood--bad'); 11655 } 11656 if (btn) { 11657 btn.setAttribute('aria-pressed', state.rankMood === 'good' ? 'true' : 'false'); 11658 } 11659 if (hint) { 11660 hint.textContent = 11661 state.rankMood === 'good' ? __('js_rank_mood_hint_good') : __('js_rank_mood_hint_bad'); 11662 } 11663 var gameSub = document.querySelector('#secRankPanels .rank-col--both .rank-sub'); 11664 if (gameSub) { 11665 gameSub.textContent = 11666 state.rankMood === 'good' 11667 ? __('js_rank_mood_game_sub_good') 11668 : __('js_rank_mood_game_sub_bad'); 11669 } 11670 } 11671 11672 function wireRankMoodToggle() { 11673 loadRankMoodFromStorage(); 11674 var btn = document.getElementById('btnRankMoodToggle'); 11675 if (!btn || btn.dataset.detectorMoodWired === '1') return; 11676 btn.dataset.detectorMoodWired = '1'; 11677 btn.addEventListener('click', function () { 11678 state.rankMood = state.rankMood === 'good' ? 'bad' : 'good'; 11679 try { 11680 localStorage.setItem(RANK_MOOD_STORAGE_KEY, state.rankMood); 11681 } catch (e) { 11682 /* ignore */ 11683 } 11684 var outer = document.getElementById('rankPanelsOuter'); 11685 if (outer) { 11686 outer.classList.remove('rank-mood-flash-good', 'rank-mood-flash-bad', 'is-rank-mood-anim'); 11687 void outer.offsetWidth; 11688 outer.classList.add(state.rankMood === 'good' ? 'rank-mood-flash-good' : 'rank-mood-flash-bad'); 11689 outer.classList.add('is-rank-mood-anim'); 11690 setTimeout(function () { 11691 outer.classList.remove('is-rank-mood-anim'); 11692 }, 320); 11693 setTimeout(function () { 11694 outer.classList.remove('rank-mood-flash-good', 'rank-mood-flash-bad'); 11695 }, 680); 11696 } 11697 applyRankMoodUI(); 11698 mountRankPanels(); 11699 }); 11700 applyRankMoodUI(); 11701 } 11702 11703 /** /detector#incidentes ou #incidents â vista «ruins», painel de rankings aberto, scroll até Top 5 · incidentes. */ 11704 function applyDeepLinkFromHash() { 11705 var raw = (window.location.hash || '').replace(/^#/, '').toLowerCase(); 11706 if (raw !== 'incidentes' && raw !== 'incidents') return; 11707 state.rankMood = 'bad'; 11708 applyRankMoodUI(); 11709 mountRankPanels(); 11710 var det = document.getElementById('detDashRank'); 11711 if (det) { 11712 try { 11713 det.open = true; 11714 } catch (eOpen) { 11715 var sum = det.querySelector('summary'); 11716 if (sum) sum.click(); 11717 } 11718 } 11719 var target = document.getElementById('rankColIncidents'); 11720 if (target && target.scrollIntoView) { 11721 setTimeout(function () { 11722 target.scrollIntoView({ behavior: 'smooth', block: 'center' }); 11723 }, 120); 11724 } 11725 } 11726 11727 /* ââ Top 5 Ranking Boards ââ */ 11728 var TOP_N = 5; 11729 11730 function mountTop10Boards() { 11731 var host = document.getElementById('top10Boards'); 11732 if (!host || !state.catalog || !state.catalog.length) return; 11733 11734 /* Latência: catálogo completo. Pior/melhor avaliação: nota + estrelas + latência + perda + jitter + uptime + relatos · telecom. */ 11735 var allGlobal = state.catalog; 11736 11737 function poolTelecomForRatings(all) { 11738 var tel = all.filter(function (s) {
11739 return String(s.category || '') === 'telecom'; 11740 }); 11741 return tel.length ? tel : all; 11742 } 11743 11744 function passesRatingBoard(s) { 11745 var t = Number(s.trust_score); 11746 if (!isNaN(t) && t > 0) return true; 11747 return (Number(s.star_rating_count) || 0) >= 1; 11748 } 11749 11750 function valRatingBoardCell(s, worst) { 11751 var t = Number(s.trust_score); 11752 if (isNaN(t)) t = 0; 11753 var cls; 11754 if (worst) { 11755 cls = t < 70 ? 'top10-val--bad' : t < 90 ? 'top10-val--warn' : 'top10-val--blue'; 11756 } else { 11757 cls = t >= 95 ? 'top10-val--good' : t >= 90 ? 'top10-val--blue' : 'top10-val--warn'; 11758 } 11759 var votes = Number(s.star_rating_count) || 0; 11760 var avg = votes > 0 ? Number(s.avg_star_rating) || 5 : 5; 11761 var voteLbl = 11762 votes === 1 ? __('js_star_badge_votes') : __('js_star_badge_votes_plural'); 11763 var starTitle = 11764 avg.toFixed(1) + 11765 ' ' + 11766 __('js_star_badge_title') + 11767 ' (' + 11768 votes + 11769 ' ' + 11770 voteLbl + 11771 ')'; 11772 var starBadge = 11773 '<span class="t10-star-badge" title="' + 11774 esc(starTitle) + 11775 '">â ' + 11776 avg.toFixed(1) + 11777 '</span>'; 11778 return { text: t.toFixed(1) + ' ' + starBadge, cls: cls, html: true }; 11779 } 11780 11781 var ratingPool = poolTelecomForRatings(allGlobal); 11782 11783 var byWorstLat = allGlobal 11784 .filter(function (s) { return hasRankingLatencySample(s); }) 11785 .sort(cmpRankingLatencyWorst) 11786 .slice(0, TOP_N); 11787 11788 var ratingEligible = ratingPool.filter(passesRatingBoard); 11789 var sortedWorst = sortCatalogForRatingBoard(ratingEligible, true); 11790 var sortedBest = sortCatalogForRatingBoard(ratingEligible, false); 11791 11792 var byWorstScore = sortedWorst.slice(0, TOP_N); 11793 var worstIds = {}; 11794 byWorstScore.forEach(function (s) { worstIds[s.id] = true; }); 11795 11796 var byBestScore = []; 11797 for (var bi = 0; bi < sortedBest.length && byBestScore.length < TOP_N; bi++) { 11798 if (!worstIds[sortedBest[bi].id]) byBestScore.push(sortedBest[bi]); 11799 } 11800 11801 var byBestLat = allGlobal 11802 .filter(function (s) { 11803 var rl = rankingLatencyMs(s); 11804 return ( 11805 rl != null && 11806 !isNaN(rl) && 11807 rl > 0 && 11808 passesIcmpForFastestRank(s) 11809 ); 11810 }) 11811 .sort(cmpRankingLatencyBest) 11812 .slice(0, TOP_N); 11813 11814 function boardHtml(cfg) { 11815 var rows = ''; 11816 if (!cfg.items.length) { 11817 rows = 11818 '<div class="top10-empty">' + 11819 esc(cfg.emptyMessage != null ? cfg.emptyMessage : __('js_top10_empty_shelf')) + 11820 '</div>'; 11821 } else { 11822 rows = '<ol class="top10-list">'; 11823 for (var i = 0; i < cfg.items.length; i++) { 11824 var s = cfg.items[i]; 11825 var val; 11826 try { 11827 val = cfg.valFn(s); 11828 } catch (eRow) { 11829 val = { text: 'â', cls: 'top10-val--warn' }; 11830 } 11831 if (!val || val.text == null) val = { text: 'â', cls: 'top10-val--warn' }; 11832 var valContent = val.html ? String(val.text) : esc(String(val.text)); 11833 var valCls = val.cls != null ? String(val.cls) : ''; 11834 rows += 11835 '<li class="top10-item" data-svc-id="' + esc(s.id) + '" tabindex="0" title="' + esc(s.name) + '">' + 11836 '<span class="top10-pos">' + (i + 1) + '</span>' + 11837 detLogoImgHtml(s, 'top10-logo', '') + 11838 '<span class="top10-name">' + esc(s.name) + '</span>' + 11839 '<span class="top10-val ' + valCls + '">' + valContent + '</span>' + 11840 '</li>'; 11841 } 11842 rows += '</ol>'; 11843 } 11844 var rawSub = cfg.sub != null ? String(cfg.sub).trim() : ''; 11845 var subEl = rawSub ? '<small>' + esc(rawSub) + '</small>' : ''; 11846 var infoKind = cfg.showInfo ? String(cfg.infoKind || 'rating') : ''; 11847 var infoBtn = cfg.showInfo 11848 ? '<button type="button" class="t10-info-btn" aria-label="' + 11849 esc(__('js_t10_info_btn_aria')) + 11850 '" data-t10-info-kind="' + 11851 esc(infoKind) + 11852 '">i</button>' 11853 : ''; 11854 return ( 11855 '<div class="top10-board ' + cfg.boardCls + '">' + 11856 '<div class="top10-board-head">' + 11857 '<span class="t10-icon">' + cfg.icon + '</span>' + 11858 '<div class="t10-title-col"><h4>' + esc(cfg.title) + '</h4>' + subEl + '</div>' + 11859 (cfg.headExtra || '') + 11860 infoBtn + 11861 '</div>' + 11862 rows + '</div>' 11863 ); 11864 } 11865 11866 host.innerHTML = 11867 boardHtml({ 11868 title: __('js_more_latency'), 11869 icon: 'ð¢', 11870 boardCls: 'top10-board--bad', 11871 items: byWorstLat, 11872 showInfo: true, 11873 infoKind: 'latency_worst', 11874 valFn: function (s) { 11875 var ms = Math.round(rankingLatencyMs(s)); 11876 var cls = ms > 500 ? 'top10-val--bad' : ms > 200 ? 'top10-val--warn' : 'top10-val--blue'; 11877 return { text: ms + ' ms', cls: cls }; 11878 } 11879 }) + 11880 boardHtml({ 11881 title: __('js_worst_rating'), 11882 icon: 'ð', 11883 boardCls: 'top10-board--bad', 11884 items: byWorstScore, 11885 showInfo: true, 11886 infoKind: 'rating', 11887 valFn: function (s) { 11888 return valRatingBoardCell(s, true); 11889 } 11890 }) + 11891 boardHtml({ 11892 title: __('js_less_latency'), 11893 icon: 'â¡', 11894 boardCls: 'top10-board--good', 11895 items: byBestLat, 11896 showInfo: true, 11897 infoKind: 'latency_best', 11898 valFn: function (s) { 11899 var ms = Math.round(rankingLatencyMs(s)); 11900 var cls = ms < 50 ? 'top10-val--good' : ms < 150 ? 'top10-val--blue' : 'top10-val--warn'; 11901 return { text: ms + ' ms', cls: cls }; 11902 } 11903 }) + 11904 boardHtml({ 11905 title: __('js_best_rating'), 11906 icon: 'â', 11907 boardCls: 'top10-board--good', 11908 items: byBestScore, 11909 showInfo: true, 11910 infoKind: 'rating', 11911 valFn: function (s) { 11912 return valRatingBoardCell(s, false); 11913 } 11914 }); 11915 11916 bindDetLogoFallbacks(host); 11917 host.querySelectorAll('.top10-item[data-svc-id]').forEach(function (el) { 11918 function doOpen() { 11919 var id = el.getAttribute('data-svc-id'); 11920 if (id) openModal(id); 11921 } 11922 el.addEventListener('click', doOpen); 11923 el.addEventListener('keydown', function (e) { 11924 if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); doOpen(); } 11925 }); 11926 }); 11927 11928 host.querySelectorAll('.t10-info-btn').forEach(function (btn) { 11929 btn.addEventListener('click', function (e) { 11930 e.stopPropagation(); 11931 var kind = btn.getAttribute('data-t10-info-kind') || 'rating'; 11932 var infoTitle; 11933 var infoBody; 11934 if (kind === 'latency_worst') { 11935 infoTitle = __('js_latency_rank_info_title'); 11936 infoBody = __('js_latency_rank_worst_body'); 11937 } else if (kind === 'latency_best') { 11938 infoTitle = __('js_latency_rank_info_title'); 11939 infoBody = __('js_latency_rank_best_body'); 11940 } else { 11941 infoTitle = __('js_rating_board_info_title'); 11942 infoBody = __('js_rating_board_info_body'); 11943 } 11944 var ov = document.createElement('div'); 11945 ov.className = 't10-info-overlay is-open'; 11946 ov.innerHTML = 11947 '<div class="t10-info-card">' + 11948 '<button type="button" class="t10-info-close" aria-label="' + 11949 esc(__('modal_close')) + 11950 '">×</button>' + 11951 '<h5>' + esc(infoTitle) + '</h5>' + 11952 '<p>' + esc(infoBody) + '</p>' + 11953 '</div>'; 11954 document.body.appendChild(ov); 11955 function closeOv() { if (ov.parentNode) ov.parentNode.removeChild(ov); } 11956 ov.querySelector('.t10-info-close').addEventListener('click', closeOv); 11957 ov.addEventListener('click', function (ev) { if (ev.target === ov) closeOv(); }); 11958 document.addEventListener('keydown', function onEsc(ev) { 11959 if (ev.key === 'Escape') { closeOv(); document.removeEventListener('keydown', onEsc); } 11960 }); 11961 }); 11962 }); 11963 } 11964 11965 function mountTop10Cards() { 11966 var host = document.getElementById('top10Cards'); 11967 if (!host || !state.catalog || !state.catalog.length) return; 11968 if (DET_COMPLAINTS_VIEWPORT_MQ.matches) { 11969 host.innerHTML = ''; 11970 host.setAttribute('hidden', ''); 11971 return; 11972 } 11973 host.removeAttribute('hidden'); 11974 11975 var allGlobal = state.catalog; 11976 var CARD_N = 10; 11977 var topLbl = __('js_label_top'); 11978 11979 function shelfHtml(cfg) { 11980 var n = cfg.items.length; 11981 var sub = 11982 cfg.shelfSub != null 11983 ? cfg.shelfSub 11984 : 'Top ' + n; 11985 var head = 11986 '<div class="top10-cards-shelf-head">' + 11987 '<span class="t10c-icon">' + 11988 cfg.icon + 11989 '</span>' + 11990 '<h4>' + 11991 cfg.title + 11992 '</h4><small>' + 11993 esc(sub) + 11994 '</small></div>'; 11995 if (!n) { 11996 if (cfg.placeholder) { 11997 return ( 11998 '<div class="top10-cards-shelf ' + 11999 cfg.shelfCls + 12000 '">' + 12001 head + 12002 '<div class="top10-cards-placeholder" role="status">' + 12003 cfg.placeholder + 12004 '</div></div>' 12005 ); 12006 } 12007 return ''; 12008 } 12009 var cards = cfg.items.map(function (s) { return tileHtml(s); }).join(''); 12010 return ( 12011 '<div class="top10-cards-shelf ' + cfg.shelfCls + '">' + 12012 head + 12013 '<div class="top10-cards-row shelf-row-wrap">' + 12014 cards + 12015 '</div></div>' 12016 ); 12017 } 12018 12019 function shelfSubFullTop(n) { 12020 return __('js_t10_shelf_ctx_top_tpl')
12021 .replace(/\{ctx\}/g, __('js_catalog_full_short')) 12022 .replace(/\{top\}/g, topLbl) 12023 .replace(/\{n\}/g, String(n)); 12024 } 12025 12026 var qNav = String(state.catalogSearch || '').trim(); 12027 var blockFirst; 12028 var rankRows = state.communityRankingRows; 12029 if (qNav) { 12030 var found = catalogSearchModalMatches(qNav).slice(0, CARD_N); 12031 var subSrch = __('js_t10_search_results_sub_tpl') 12032 .replace(/\{n\}/g, String(found.length)) 12033 .replace(/\{q\}/g, qNav); 12034 blockFirst = shelfHtml({ 12035 title: __('js_t10_search_results_title'), 12036 icon: 'ð', 12037 shelfCls: 'top10-cards-shelf--neutral', 12038 shelfSub: subSrch, 12039 items: found, 12040 placeholder: found.length ? undefined : esc(__('js_t10_search_no_results')), 12041 }); 12042 } else { 12043 var complaintsItems = []; 12044 var phFirst; 12045 if (rankRows === undefined) { 12046 phFirst = esc(__('js_cards_loading_rank')) + 'â¦'; 12047 } else if (rankRows === null) { 12048 phFirst = esc(__('js_comm_rank_err_li')); 12049 } else if (!rankRows.length) { 12050 phFirst = esc( 12051 __('js_complaints_empty_generic').replace(/\{d\}/g, String(state.complaintsRankingDays)), 12052 ); 12053 } else if ( 12054 !rankRows.some(function (r) { 12055 return r && (Number(r.vote_count) || 0) > 0; 12056 }) 12057 ) { 12058 phFirst = esc( 12059 __('js_complaints_empty_generic').replace(/\{d\}/g, String(state.complaintsRankingDays)), 12060 ); 12061 } else { 12062 for (var ri = 0; ri < rankRows.length && complaintsItems.length < CARD_N; ri++) { 12063 if ((Number(rankRows[ri].vote_count) || 0) < 1) continue; 12064 var s0 = communityRankingRowToSvc(rankRows[ri]); 12065 if (s0 && passesFilterSansSearch(s0)) complaintsItems.push(s0); 12066 } 12067 if (!complaintsItems.length) { 12068 phFirst = esc(__('js_cards_empty_filter')); 12069 } 12070 } 12071 var days = VALID_RANKING_DAYS.indexOf(state.complaintsRankingDays) !== -1 ? state.complaintsRankingDays : 1; 12072 var dayLblMapC = { 1: __('js_window_1d'), 7: __('js_window_7d'), 15: __('js_window_15d'), 30: __('js_window_30d') }; 12073 var dayLbl = dayLblMapC[days] || __('js_window_1d'); 12074 var winTopSub = 12075 rankRows !== undefined && rankRows !== null && rankRows.length && complaintsItems.length 12076 ? __('js_t10_complaints_window_top_tpl') 12077 .replace(/\{w\}/g, dayLbl) 12078 .replace(/\{top\}/g, topLbl) 12079 .replace(/\{n\}/g, String(complaintsItems.length)) 12080 : null; 12081 blockFirst = shelfHtml({ 12082 title: __('js_t10_complaints_comm_title'), 12083 icon: 'ð£', 12084 shelfCls: 'top10-cards-shelf--neutral top10-cards-shelf--dup-mobile-hide', 12085 shelfSub: winTopSub, 12086 items: complaintsItems, 12087 placeholder: phFirst, 12088 }); 12089 } 12090 12091 var byWorstLat = allGlobal 12092 .filter(function (s) { return hasRankingLatencySample(s); }) 12093 .sort(cmpRankingLatencyWorst) 12094 .slice(0, CARD_N); 12095 12096 var byBestScore = sortCatalogForRatingBoard( 12097 allGlobal.filter(function (s) { 12098 var t = Number(s.trust_score); 12099 return !isNaN(t) && t > 0; 12100 }), 12101 false, 12102 ).slice(0, CARD_N); 12103 12104 var byBestLat = allGlobal 12105 .filter(function (s) { 12106 var rl = rankingLatencyMs(s); 12107 return ( 12108 rl != null && 12109 !isNaN(rl) && 12110 rl > 0 && 12111 passesIcmpForFastestRank(s) 12112 ); 12113 }) 12114 .sort(cmpRankingLatencyBest) 12115 .slice(0, CARD_N); 12116 12117 var blockWorst = shelfHtml({ 12118 title: __('js_more_latency'), 12119 icon: 'ð¢', 12120 shelfCls: 'top10-cards-shelf--bad', 12121 shelfSub: shelfSubFullTop(byWorstLat.length), 12122 items: byWorstLat, 12123 }); 12124 var blockScore = shelfHtml({ 12125 title: __('js_best_rating'), 12126 icon: 'â', 12127 shelfCls: 'top10-cards-shelf--good', 12128 shelfSub: shelfSubFullTop(byBestScore.length), 12129 items: byBestScore, 12130 }); 12131 var blockBest = shelfHtml({ 12132 title: __('js_less_latency'), 12133 icon: 'â¡', 12134 shelfCls: 'top10-cards-shelf--good', 12135 shelfSub: shelfSubFullTop(byBestLat.length), 12136 items: byBestLat, 12137 }); 12138 12139 var blockGames = ''; 12140 var gamesSvcs = top10GamesCatalogServices(); 12141 if (gamesSvcs.length) { 12142 var gamesShareSvg = 12143 '<svg class="det-sd-action__ico" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + 12144 '<circle cx="18" cy="5" r="3" /><circle cx="6" cy="12" r="3" /><circle cx="18" cy="19" r="3" />' + 12145 '<line x1="8.59" y1="13.51" x2="15.42" y2="17.49" /><line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />' + 12146 '</svg>'; 12147 var gamesHead = 12148 '<div class="top10-cards-shelf-head top10-cards-shelf-head--games-share">' + 12149 '<span class="t10c-icon">ð®</span>' + 12150 '<div class="t10c-title-stack"><h4>' + 12151 esc(__('js_t10_games_title')) + 12152 '</h4><small>' + 12153 esc(__('js_t10_games_sub')) + 12154 '</small></div>' + 12155 '<button type="button" class="det-sd-action det-sd-action--social" id="bt
12155nTop10GamesSocialShare" title="' + 12156 esc(__('js_t10_social_aria')) + 12157 '" aria-label="' + 12158 esc(__('js_t10_social_aria')) + 12159 '">' + 12160 gamesShareSvg + 12161 '<span class="det-sd-action__lbl">' + 12162 esc(__('js_t10_social_btn_short')) + 12163 '</span></button></div>'; 12164 var gamesCards = gamesSvcs.map(function (s) { 12165 return tileHtml(s); 12166 }).join(''); 12167 blockGames = 12168 '<div class="top10-cards-shelf top10-cards-shelf--bad top10-cards-shelf--games top10-cards-shelf--dup-mobile-hide">' + 12169 gamesHead + 12170 '<div class="top10-cards-row shelf-row-wrap">' + 12171 gamesCards + 12172 '</div></div>'; 12173 } 12174 12175 var assembled = (blockFirst || '') + blockGames + blockWorst + blockScore + blockBest; 12176 if (!assembled.trim()) { 12177 host.innerHTML = 12178 '<div class="top10-cards-filter-empty" role="status">' + 12179 '<strong>' + 12180 esc(__('js_t10_filter_empty_title')) + 12181 '</strong> ' + 12182 esc(__('js_t10_filter_empty_p')) + 12183 '</div>'; 12184 return; 12185 } 12186 host.innerHTML = assembled; 12187 12188 bindDetLogoFallbacks(host); 12189 var btnGamesSoc = document.getElementById('btnTop10GamesSocialShare'); 12190 if (btnGamesSoc) { 12191 btnGamesSoc.addEventListener('click', function (e) { 12192 e.preventDefault(); 12193 e.stopPropagation(); 12194 openTop10GamesSocialShareModal(); 12195 }); 12196 } 12197 attachTileInteractions(host, function (el) { 12198 var id = el.getAttribute('data-svc-id'); 12199 if (id) openModal(id); 12200 }); 12201 bindCommunityReportButtons(host); 12202 bindCommunityStatsButtons(host); 12203 bindStarRatingButtons(host); 12204 bindTileMoreButtons(host); 12205 host.querySelectorAll('.shelf-row-wrap').forEach(bindShelfScrollHint); 12206 refreshComplaintsTrendSparklines(document); 12207 } 12208 12209 function mountIcmpNoReplySection() { 12210 var host = document.getElementById('icmpNoReplyHost'); 12211 if (!host || !state.catalog || !state.catalog.length) return; 12212 var pp = state.probeProfile || {}; 12213 var icmpOn = pp.icmp_status_enabled !== false; 12214 if (!icmpOn) { 12215 host.innerHTML = ''; 12216 host.setAttribute('hidden', ''); 12217 return; 12218 } 12219 host.removeAttribute('hidden'); 12220 var all = state.catalog.filter(function (s) { return passesFilter(s); }); 12221 var hasAnyIcmp = all.some(function (s) { 12222 return s.icmp_responds === true || s.icmp_responds === false; 12223 }); 12224 if (!hasAnyIcmp) { 12225 host.innerHTML = 12226 '<div class="icmp-section icmp-section--empty" role="status">' + 12227 '<p>Ainda não há resultado de <strong>ping</strong> neste painel. Volte daqui a cerca de um minuto.</p>' + 12228 '</div>'; 12229 return; 12230 } 12231 var noIcmp = all 12232 .filter(function (s) { 12233 return s.icmp_responds === false && s.last_ok === true; 12234 }) 12235 .sort(function (a, b) { return (a.name || '').localeCompare(b.name || '', i18nSortLocale()); }); 12236 if (!noIcmp.length) { 12237 host.innerHTML = 12238 '<div class="icmp-section icmp-section--empty" role="status">' + 12239 '<p>Com o filtro atual não há aqui serviços em que o <strong>site responde bem</strong> mas o <strong>ping não</strong>. ' + 12240 'Se um serviço estiver <strong>fora do ar</strong>, aparece nos cartões normais como erro â não nesta lista.</p>' + 12241 '</div>'; 12242 return; 12243 } 12244 var cards = noIcmp.map(function (s) { return tileHtml(s); }).join(''); 12245 host.innerHTML = 12246 '<div class="icmp-section" role="region" aria-label="Site ok, sem ping ICMP">' + 12247 '<div class="icmp-section-head">' + 12248 '<h3>Site ok · sem ping (ICMP)</h3>' + 12249 '<p>Estes serviços <strong>não respondem a ping</strong> (ICMP). Isso é <strong>normal</strong> em muitos sites e apps na internet. ' + 12250 'Mesmo assim <strong>respondem a outros testes</strong>: em geral <strong>HTTPS</strong> (ligação web segura, porta 443) e, em alguns cartões, <strong>
12250HTTP ou outras portas TCP</strong> (por exemplo 80 ou 443).</p>' + 12251 '<p>O <strong>tempo em ms</strong> em destaque em cada cartão vem sobretudo da verificação <strong>HTTPS/TLS</strong>. ' + 12252 'Quando vê números ao lado das portas no cartão, é a <strong>latência medida por TCP</strong> nessa porta. ' + 12253 'Serviços <strong>indisponÃveis</strong> aparecem no catálogo geral em erro, não aqui.</p>' + 12254 '</div>' + 12255 '<div class="icmp-cards-row shelf-row-wrap">' + cards + '</div>' + 12256 '</div>'; 12257 bindDetLogoFallbacks(host); 12258 attachTileInteractions(host, function (el) { 12259 var id = el.getAttribute('data-svc-id'); 12260 if (id) openModal(id); 12261 }); 12262 bindCommunityReportButtons(host); 12263 bindCommunityStatsButtons(host); 12264 bindStarRatingButtons(host); 12265 bindTileMoreButtons(host); 12266 host.querySelectorAll('.shelf-row-wrap').forEach(bindShelfScrollHint); 12267 refreshComplaintsTrendSparklines(document); 12268 } 12269 12270 /** Grelha por categoria + secção ICMP (responde a `passesFilter`, inclui texto de busca). */ 12271 function mountCatalogGridAndIcmpSection() { 12272 var host = document.getElementById('shelvesHost'); 12273 if (!host || !state.catalog || !state.catalog.length) return; 12274 var searchBar = document.getElementById('catalogSearchBar'); 12275 var catalogHead = document.querySelector('.block-head--catalog'); 12276 if (searchBar && catalogHead) { 12277 try { 12278 catalogHead.appendChild(searchBar); 12279 } catch (ePreserveSearch) { 12280 /* ignore */ 12281 } 12282 } 12283 var byCat = {}; 12284 state.catalog.forEach(function (svc) { 12285 if (!passesFilter(svc)) return; 12286 var c = svc.category || 'core'; 12287 if (!byCat[c]) byCat[c] = []; 12288 byCat[c].push(svc); 12289 }); 12290 var multiKeys = []; 12291 var orphanItems = []; 12292 Object.keys(byCat).forEach(function (c) { 12293 var bucket = byCat[c]; 12294 if (bucket.length >= 2) { 12295 multiKeys.push(c); 12296 } else if (bucket.length === 1) { 12297 orphanItems.push(bucket[0]); 12298 } 12299 }); 12300 multiKeys.sort(function (a, b) { 12301 return catLabel(a).localeCompare(catLabel(b), i18nSortLocale()); 12302 }); 12303 orphanItems = sortItems(orphanItems); 12304 12305 var html = ''; 12306 function shelfBlockHtml(cat, items, opts) { 12307 opts = opts || {}; 12308 var noTitle = !!opts.noTitle; 12309 var titleLabel = opts.titleLabel != null ? opts.titleLabel : catLabel(cat); 12310 var rowId = opts.rowId || 'row_' + String(cat).replace(/[^a-z0-9]+/gi, '_'); 12311 var shelfSearchSlot = !!opts.catalogSearchSlot; 12312 var h = ''; 12313 h += '<div class="shelf-block' + (noTitle ? ' shelf-block--no-title' : '') + '" data-cat="' + esc(cat) + '">'; 12314 if (!noTitle) { 12315 h += '<div class="shelf-head-row">'; 12316 h += 12317 '<div class="shelf-title">⸠<span>' + 12318 esc(titleLabel) + 12319 '</span> <small style="color:var(--muted);font-weight:500;">(' + 12320 items.length + 12321 ')</small></div>'; 12322 if (shelfSearchSlot) { 12323 h += '<div id="catalogSearchShelfSlot" class="shelf-catalog-search-slot"></div>'; 12324 } 12325 h += '</div>'; 12326 } 12327 h += '<div class="shelf-row-wrap">'; 12328 h += '<div id="' + rowId + '" class="row-scroll"></div>'; 12329 h += 12330 '<div class="shelf-scroll-hint" aria-hidden="true" title="Deslize para o lado â há mais serviços">âº</div>'; 12331 h += '</div>'; 12332 h += '<div id="' + rowId + '_grid" class="grid-view" style="display:none;"></div>'; 12333 h += '<div id="' + rowId + '_list" class="list-view" style="display:none;"></div>'; 12334 h += '</div>'; 12335 return h; 12336 } 12337 for (var mk = 0; mk < multiKeys.length; mk++) { 12338 html += shelfBlockHtml(multiKeys[mk], byCat[multiKeys[mk]], { catalogSearchSlot: mk === 0 }); 12339 } 12340 if (orphanItems.length === 1) { 12341 html += shelfBlockHtml(orphanItems[0].category || 'core', orphanItems, { 12342 noTitle: true, 12343 rowId: 'row__orphan_single', 12344 }); 12345 } else if (orphanItems.length > 1) { 12346 html += shelfBlockHtml('__mixed__', orphanItems, { 12347 titleLabel: __('shelf_orphans_title'), 12348 rowId: 'row__orphans_mixed', 12349 catalogSearchSlot: multiKeys.length === 0, 12350 }); 12351 } 12352 host.innerHTML = html || '<div class="empty">Nenhum serviço neste filtro.</div>'; 12353 12354 function fillShelfRow(rowId, items) { 12355 var row = document.getElementById(rowId); 12356 var grid = document.getElementById(rowId + '_grid'); 12357 var list = document.getElementById(rowId + '_list'); 12358 if (!row) return; 12359 var hr = ''; 12360 var hg = ''; 12361 var hl = ''; 12362 for (var i = 0; i < items.length; i++) { 12363 var t = tileHtml(items[i]); 12364 hr += t; 12365 hg += t; 12366 var it = items[i]; 12367 var ltList = latencyTierMeta(it); 12368 var lr = it.latency_rank ? '<span class="list-rank">#' + esc(it.latency_rank) + '</span>' : ''; 12369 var ltxt; 12370 var listLatMs = rankingLatencyMs(it); 12371 if (it.last_ok === true && listLatMs != null && !isNaN(listLatMs)) { 12372 ltxt = '<span class="list-lat">' + esc(Math.round(listLatMs)) + ' ms</span>'; 12373 } else if (it.last_ok === false) { 12374 ltxt = '<span class="list-lat bad">erro</span>'; 12375 } else { 12376 ltxt = '<span class="list-lat" style="color:var(--muted)">â</span>'; 12377 } 12378 var listReportLbl = esc(__('js_list_report') || 'Reportar'); 12379 var listReportAria = esc(__('js_list_report_aria') || 'Reportar problema'); 12380 var listMoreInner = 12381 '<span class="tile-info-row">' + 12382 INFO_ICON_HTML + 12383 '<span class="tile-info-txt">+ info</span></span>'; 12384 hl += 12385 '<div class="list-row ' + 12386 (it.last_ok === true ? 'state-ok' : it.last_ok === false ? 'state-bad' : 'state-unknown') + 12387 ' ' + 12388 ltList.cls + 12389 '" role="button" tabindex="0" data-svc-id="' + 12390 esc(it.id) + 12391 '">' + 12392 lr + 12393 '<div class="limg">' + 12394 detLogoImgHtml(it, '', '') + 12395 '</div>' + 12396 '<div class="lbody">' + 12397 '<div class="lbody-top">' + 12398 '<div class="t">' + 12399 esc(it.name) + 12400 ipStackTagsHtmlCompact(it) + 12401 '</div>' + 12402 '<button type="button" class="list-more-btn list-more-btn--mob-only" data-more-sid="' + 12403 esc(it.id) + 12404 '" aria-label="+ info: mais detalhes, hub e gráfico">' + 12405 listMoreInner + 12406 '</button>' + 12407 '</div>' + 12408 '<div class="lbody-sub">' + 12409 '<div class="d">' + 12410 esc((it.description || '').slice(0, 160)) + 12411 '</div>' + 12412 '<span class="list-lat--mob-only">' + 12413 ltxt + 12414 '</span>' + 12415 '</div></div>' + 12416 '<div class="list-row-extras">' + 12417 '<div class="list-row-metrics">' + 12418 '<div class="list-lat-slot">' + 12419 ltxt + 12420 '</div>' + 12421 '<span class="list-lat-pill ' + 12422 ltList.cls + 12423 '" title="' + 12424 esc(ltList.hint) + 12425 '">' + 12426 esc(ltList.pill) + 12427 '</span>' + 12428 '<div class="list-sparkline-wrap" title="Tendência recente da latência">' + 12429 sparklineSvg(it.latency_sparkline_ms, 88) + 12430 '</div>' + 12431 '</div>' + 12432 '<div class="list-row-cta">' + 12433 '<button type="button" class="list-more-btn list-more-btn--desk-only" data-more-sid="' + 12434 esc(it.id) + 12435 '" aria-label="+ info: mais detalhes, hub e gráfico">' + 12436 listMoreInner + 12437 '</button>' + 12438 '<button type="button" class="list-report-btn" data-report-sid="' + 12439 esc(it.id) + 12440 '" aria-label="' + 12441 listReportAria + 12442 '"><span class="list-report-shine" aria-hidden="true"></span><span class="list-report-label">' + 12443 listReportLbl + 12444 '</span></button>' + 12445 '</div></div>' + 12446 '<span class="chev">â</span></div>'; 12447 } 12448 row.innerHTML = hr; 12449 if (grid) grid.innerHTML = hg; 12450 if (list) list.innerHTML = hl; 12451 } 12452 12453 for (var ok = 0; ok < multiKeys.length; ok++) { 12454 var catK = multiKeys[ok]; 12455 fillShelfRow('row_' + catK.replace(/[^a-z0-9]+/gi, '_'), sortItems(byCat[catK])); 12456 } 12457 if (orphanItems.length === 1) { 12458 fillShelfRow('row__orphan_single', orphanItems); 12459 } else if (orphanItems.length > 1) { 12460 fillShelfRow('row__orphans_mixed', orphanItems); 12461 } 12462 12463 bindDetLogoFallbacks(host); 12464 host.querySelectorAll('.tile[data-svc-id], .list-row[data-svc-id]').forEach(function (el) { 12465 function doOpen() { 12466 var id = el.getAttribute('data-svc-id'); 12467 if (id) openModal(id); 12468 } 12469 bindTouchAwareOpen(el, doOpen); 12470 el.addEventListener('keydown', function (e) { 12471 if (e.key === 'Enter' || e.key === ' ') { 12472 e.preventDefault(); 12473 doOpen(); 12474 } 12475 }); 12476 }); 12477 host.querySelectorAll('.shelf-row-wrap').forEach(bindShelfScrollHint);
12478 bindCommunityReportButtons(host); 12479 bindCommunityStatsButtons(host); 12480 bindStarRatingButtons(host); 12481 bindTileMoreButtons(host); 12482 applyViewToShelves(); 12483 relocateCatalogSearchForViewport(); 12484 mountIcmpNoReplySection(); 12485 refreshComplaintsTrendSparklines(document); 12486 } 12487 12488 function mountShelves() { 12489 if (!markLazySectionDirty('top10Boards')) mountTop10Boards(); 12490 mountTop10Cards(); 12491 updateCatalogFilterBanner(); 12492 if (!markLazySectionDirty('shelvesHost')) mountCatalogGridAndIcmpSection(); 12493 } 12494 12495 /** Repõe filtros do catálogo e volta a mostrar os Top 10 em cartões (vista global). */ 12496 function resetCatalogHomeView() { 12497 state.filter = 'all'; 12498 state.ipStackFilter = 'all'; 12499 state.rankSort = 'live_problems'; 12500 state.catalogSearch = ''; 12501 var inp = document.getElementById('catalogSearchInput'); 12502 if (inp) inp.value = ''; 12503 rebuildCategorySelect(); 12504 var ip = document.getElementById('selIpStack'); 12505 if (ip) ip.value = 'all'; 12506 var sr = document.getElementById('selRank'); 12507 if (sr) sr.value = 'live_problems'; 12508 state.view = 'list'; 12509 syncSegViewButtonsFromState(); 12510 state.rankExpanded = { 12511 latFast: false, 12512 latSlow: false, 12513 incidents: false, 12514 trends: false, 12515 games: false, 12516 community: false, 12517 }; 12518 mountShelves(); 12519 updateCatalogViewToggleBtn(); 12520 mountRankPanels(); 12521 mountProblemsCatalog(); 12522 updateMetricsBar(); 12523 applyFilterSections(); 12524 refreshShelfHints(); 12525 var scrollEl = document.getElementById('top10Cards'); 12526 if (DET_COMPLAINTS_VIEWPORT_MQ.matches) { 12527 scrollEl = document.getElementById('secShelves') || document.getElementById('catalogSearchBar') || scrollEl; 12528 } 12529 if (scrollEl && scrollEl.scrollIntoView) { 12530 try { 12531 scrollEl.scrollIntoView({ behavior: 'smooth', block: 'start' }); 12532 } catch (e) { 12533 scrollEl.scrollIntoView(true); 12534 } 12535 } 12536 } 12537 12538 function effectiveDetectorView() { 12539 return state.view || 'list'; 12540 } 12541 12542 function syncSegViewButtonsFromState() { 12543 document.querySelectorAll('#toolbarCompact .seg-view button[data-view]').forEach(function (x) { 12544 x.classList.toggle('active', (x.getAttribute('data-view') || '') === state.view); 12545 }); 12546 } 12547 12548 function updateCatalogViewToggleBtn() { 12549 var btn = document.getElementById('btnCatalogViewToggle'); 12550 if (!btn) return; 12551 var txt = btn.querySelector('.catalog-view-toggle-txt'); 12552 var ic = btn.querySelector('.catalog-view-toggle-ic'); 12553 if (state.view === 'grid') { 12554 if (txt) txt.textContent = __('catalog_view_toggle_list_txt'); 12555 if (ic) ic.textContent = 'â°'; 12556 btn.setAttribute('title', __('catalog_view_toggle_list_title')); 12557 btn.setAttribute('aria-label', __('catalog_view_toggle_list_aria')); 12558 btn.setAttribute('data-mode-to', 'list'); 12559 } else { 12560 if (txt) txt.textContent = __('catalog_view_toggle_cards_txt'); 12561 if (ic) ic.textContent = 'â¦'; 12562 btn.setAttribute('title', __('catalog_view_toggle_cards_title')); 12563 btn.setAttribute('aria-label', __('catalog_view_toggle_cards_aria')); 12564 btn.setAttribute('data-mode-to', 'grid'); 12565 } 12566 } 12567 12568 window.detectorUpdateCatalogViewToggleI18n = updateCatalogViewToggleBtn; 12569 12570 function applyViewToShelves() { 12571 var v = effectiveDetectorView(); 12572 document.querySelectorAll('.shelf-block').forEach(function (block) { 12573 var row = block.querySelector('.row-scroll'); 12574 var grid = block.querySelector('.grid-view'); 12575 var list = block.querySelector('.list-view'); 12576 if (row) row.style.display = v === 'row' ? 'flex' : 'none'; 12577 if (grid) grid.style.display = v === 'grid' ? 'grid' : 'none'; 12578 if (list) list.style.display = v === 'list' ? 'flex' : 'none'; 12579 }); 12580 refreshShelfHints(); 12581 } 12582 12583 function mountDd() { 12584 var dd = (state.summary && state.summary.trending_issues) || []; 12585 var ddRow = document.getElementById('ddRow'); 12586 if (!ddRow) return; 12587 var h = ''; 12588 for (var j = 0; j < dd.length; j++) { 12589 var x = dd[j]; 12590 var rawH = trendHref(x); 12591 var logo = faviconForDomain(trendFaviconDomain(x)); 12592 h += 12593 '<a class="tile state-warn" href="' + 12594 esc(rawH) + 12595 '"' + 12596 trendLinkTargetAttrs(rawH) + 12597 '>' + 12598 '<div class="poster"><img src="' + 12599 esc(logo) + 12600 '" alt="" loading="lazy"' + 12601 imgLogoOnErrorAttr(trendFaviconDomain(x)) + 12602 '></div>' + 12603 '<div class="meta"><div class="name">' + 12604 esc(x.name || x.slug) + 12605 '</div><span class="badge warn">' + 12606 esc(__('js_rank_trend_badge')) + 12607 '</span></div></a>'; 12608 } 12609 ddRow.innerHTML = h || '<div class="empty">' + esc(__('js_dd_empty')) + '</div>'; 12610 } 12611 12612 function findSvc(id) { 12613 if (!state.catalog || id == null || id === '') return null; 12614 var sid = String(id); 12615 for (var i = 0; i < state.catalog.length; i++) { 12616 if (String(state.catalog[i].id) === sid) return state.catalog[i]; 12617 } 12618 return null; 12619 } 12620 12621 function destroyChart() { 12622 if (state.chart) { 12623 state.chart.destroy(); 12624 state.chart = null; 12625 } 12626 var rzBtn = document.getElementById('svcChartResetZoom'); 12627 if (rzBtn) rzBtn.hidden = true; 12628 } 12629 12630 function getChartRangeHours() { 12631 var rangeSel = document.getElementById('svcChartRange'); 12632 if (rangeSel && rangeSel.value) { 12633 var n = parseInt(rangeSel.value, 10); 12634 if (!isNaN(n) && n > 0) return n; 12635 } 12636 return state.chartRangeHours || 168; 12637 } 12638 12639 /** Chart.js: carregamento preguiçoso; após o catálogo, pré-carrega em idle para o modal abrir mais rápido. */ 12640 var chartJsLoadPromise = null; 12641 function ensureChartJsLoaded() { 12642 if (typeof Chart !== 'undefined') { 12643 return Promise.resolve(); 12644 } 12645 if (chartJsLoadPromise) { 12646 return chartJsLoadPromise; 12647 } 12648 function loadScript(src) { 12649 return new Promise(function (resolve, reject) { 12650 var s = document.createElement('script'); 12651 s.src = src; 12652 s.async = true; 12653 s.onload = resolve; 12654 s.onerror = function () { 12655 reject(new Error('script')); 12656 }; 12657 document.head.appendChild(s); 12658 }); 12659 } 12660 chartJsLoadPromise = loadScript( 12661 'https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js', 12662 ) 12663 .then(function () { 12664 return loadScript( 12665 'https://cdn.jsdelivr.net/npm/[email protected]/dist/chartjs-adapter-date-fns.bundle.min.js', 12666 ); 12667 }) 12668 .then(function () { 12669 return loadScript( 12670 'https://cdn.jsdelivr.net/npm/[email protected]/hammer.min.js', 12671 ); 12672 }) 12673 .then(function () { 12674 return loadScript( 12675 'https://cdn.jsdelivr.net/npm/[email protected]/dist/chartjs-plugin-zoom.min.js', 12676 ); 12677 }) 12678 .catch(function () { 12679 chartJsLoadPromise = null; 12680 throw new Error('chart_js'); 12681 }); 12682 return chartJsLoadPromise; 12683 } 12684 12685 function renderCatellixProductsPing(payload) { 12686 var monE = document.getElementById('catProdMonitoringStatus'); 12687 var kidE = document.getElementById('catProdKidsStatus'); 12688 var monC = document.getElementById('catProdCardMonitoring'); 12689 var kidC = document.getElementById('catProdCardKids'); 12690 function line(block) { 12691 if (!block) return 'â'; 12692 var ok = !!block.ok; 12693 var ms = block.ms != null ? String(Math.round(Number(block.ms))) : 'â'; 12694 var st = 12695 '<span class="' + 12696 (ok ? 'cat-ok' : 'cat-bad') + 12697 '">' + 12698 (ok ? 'OK' : 'Falha') + 12699 '</span> · ' + 12700 esc(ms) + 12701 ' ms'; 12702 return st; 12703 } 12704 if (payload && payload.success && payload.monitoring && payload.kids) { 12705 if (monE) monE.innerHTML = line(payload.monitoring); 12706 if (kidE) kidE.innerHTML = line(payload.kids); 12707 var tip = 12708 (payload.monitoring && payload.monitoring.measure_kind_pt) || 12709 (payload.measure_note_pt || '').slice(0, 220); 12710 if (monC) { 12711 monC.classList.toggle('cat-prod--ok', !!payload.monitoring.ok); 12712 monC.classList.toggle('cat-prod--bad', !payload.monitoring.ok); 12713 if (tip) monC.title = tip; 12714 } 12715 if (kidC) { 12716 kidC.classList.toggle('cat-prod--ok', !!payload.kids.ok); 12717 kidC.classList.toggle('cat-prod--bad', !payload.kids.ok); 12718 if (tip) kidC.title = tip; 12719 } 12720 } else { 12721 if (monE) monE.textContent = 'IndisponÃvel'; 12722 if (kidE) kidE.textContent = 'IndisponÃvel'; 12723 if (monC) { 12724 monC.classList.remove('cat-prod--ok'); 12725 monC.classList.add('cat-prod--bad'); 12726 monC.removeAttribute('title'); 12727 } 12728 if (kidC) { 12729 kidC.classList.remove('cat-prod--ok'); 12730 kidC.classList.add('cat-prod--bad'); 12731 kidC.removeAttribute('title'); 12732 } 12733 } 12734 } 12735 12736 function chartRangeLabelPt(hrs) { 12737 if (hrs < 24) return 'últ. ' + hrs + ' h'; 12738 if (hrs === 24) return 'últ. 1 dia'; 12739 var d = hrs / 24; 12740 if (d === Math.floor(d)) return 'últ. ' + Math.floor(d) + ' dias'; 12741 return 'últ. ' + hrs + ' h'; 12742 } 12743 12744 function z2(n) { 12745 return (n < 10 ? '0' : '') + n; 12746 } 12747 12748 function localDayKey(d) { 12749 return d.getFullYear() + '-' + z2(d.getMonth() + 1) + '-' + z2(d.getDate()); 12750 } 12751 12752 function getLast7LocalDays() { 12753 var out = []; 12754 for (var i = 6; i >= 0; i--) { 12755 var d = new Date(); 12756 d.setHours(12, 0, 0, 0); 12757 d.setDate(d.getDate() - i); 12758 out.push({ key: localDayKey(d), date: d }); 12759 } 12760 return out; 12761 } 12762 12763 /** Estrelas do modal: com votos â média 1â5 (meias estrelas); sem votos â mapa justo a partir do trust_score (100â5, piso 58â1), alinhado ao algoritmo de confiança. */ 12764 function computeModalStarDisplay(svc) { 12765 var trust = Number(svc && svc.trust_score); 12766 if (isNaN(trust)) trust = 100; 12767 var cnt = parseInt(String((svc && svc.star_rating_count) || 0), 10) || 0; 12768 var avg = Number(svc && svc.avg_star_rating); 12769 if (cnt >= 1 && !isNaN(avg) && avg > 0) { 12770 var v = Math.max(0, Math.min(5, avg)); 12771 var av = v.toFixed(1).replace('.', ','); 12772 return { 12773 value: v, 12774 source: 'votes', 12775 hint: 12776 cnt === 1 12777 ? __('js_star_vote_one_tpl').replace(/\{avg\}/g, av) 12778 : __('js_star_votes_hint_tpl') 12779 .replace(/\{n\}/g, String(cnt)) 12780 .replace(/\{avg\}/g, av), 12781 }; 12782 } 12783 var lo = 58; 12784 var hi = 100; 12785 var t = Math.max(lo, Math.min(hi, trust)); 12786 var mapped = 1 + ((t - lo) / (hi - lo)) * 4; 12787 mapped = Math.max(0.5, Math.min(5, mapped)); 12788 return { 12789 value: mapped, 12790 source: 'trust', 12791 hint: __('js_star_vote_hint'), 12792 }; 12793 } 12794 12795 function modalTrustPillClass(score) { 12796 if (score >= 85) return 'svc-modal-trust-pill svc-modal-trust-pill--good'; 12797 if (score >= 72) return 'svc-modal-trust-pill svc-modal-trust-pill--warn'; 12798 return 'svc-modal-trust-pill svc-modal-trust-pill--bad'; 12799 } 12800
12801 function renderStarsDisplayHtml(starValue, ariaLabel) { 12802 var v = Number(starValue); 12803 if (isNaN(v)) v = 0; 12804 v = Math.max(0, Math.min(5, v)); 12805 var html = 12806 '<span class="svc-stars-display" role="img" aria-label="' + 12807 esc(ariaLabel) + 12808 '">'; 12809 for (var i = 1; i <= 5; i++) { 12810 var fill = Math.max(0, Math.min(1, v - (i - 1))); 12811 var pct = Math.round(fill * 100); 12812 html += 12813 '<span class="svc-star-cell" aria-hidden="true">' + 12814 '<span class="svc-star-bg">â </span>' + 12815 '<span class="svc-star-fill" style="width:' + 12816 pct + 12817 '%">â </span>' + 12818 '</span>'; 12819 } 12820 html += '</span>'; 12821 return html; 12822 } 12823 12824 /** Estrelas no cabeçalho do modal de detalhe: cada uma abre o fluxo de voto com N estrelas. */ 12825 function renderModalStarsInteractiveHtml(svc, sd) { 12826 var v = Number(sd.value); 12827 if (isNaN(v)) v = 0; 12828 v = Math.max(0, Math.min(5, v)); 12829 var sid = svc && svc.id != null ? String(svc.id) : ''; 12830 var sname = svc && svc.name != null ? String(svc.name) : sid; 12831 var ariaLabel = 12832 sd.value.toFixed(1).replace('.', ',') + 12833 ' de 5 estrelas. ' + 12834 (sd.source === 'votes' ? __('js_modal_stars_aria_votes') : __('js_modal_stars_aria_trust')); 12835 var html = 12836 '<span class="svc-stars-display svc-stars-display--interactive" role="img" aria-label="' + 12837 esc(ariaLabel) + 12838 '">'; 12839 var i; 12840 for (i = 1; i <= 5; i++) { 12841 var fill = Math.max(0, Math.min(1, v - (i - 1))); 12842 var pct = Math.round(fill * 100); 12843 var voteAria = __('js_modal_star_vote_n').replace(/\{n\}/g, String(i)); 12844 html += 12845 '<button type="button" class="svc-star-vote" data-svc-id="' + 12846 esc(sid) + 12847 '" data-svc-name="' + 12848 esc(sname) + 12849 '" data-star-vote="' + 12850 i + 12851 '" aria-label="' + 12852 esc(voteAria) + 12853 '">' + 12854 '<span class="svc-star-cell" aria-hidden="true">' + 12855 '<span class="svc-star-bg">â </span>' + 12856 '<span class="svc-star-fill" style="width:' + 12857 pct + 12858 '%">â </span>' + 12859 '</span></button>'; 12860 } 12861 html += '</span>'; 12862 return html; 12863 } 12864 12865 function updateSvcModalHeaderScores(svc) { 12866 var pill = document.getElementById('svcModalScorePill'); 12867 var host = document.getElementById('svcModalStarsHost'); 12868 if (!pill || !host) return; 12869 var ts = Number(svc && svc.trust_score); 12870 if (isNaN(ts)) ts = 100; 12871 pill.hidden = false; 12872 pill.className = modalTrustPillClass(ts) + ' svc-modal-trust-pill--clickable'; 12873 pill.textContent = Math.round(ts) + '/100'; 12874 pill.setAttribute('role', 'button'); 12875 pill.setAttribute('tabindex', '0'); 12876 pill.setAttribute( 12877 'title', 12878 __('trust_score_help_title'), 12879 ); 12880 var sd = computeModalStarDisplay(svc); 12881 var hintSpan = 12882 sd.hint && String(sd.hint).trim() 12883 ? '<span class="svc-modal-star-hint">' + esc(sd.hint) + '</span>' 12884 : ''; 12885 hintSpan += 12886 '<span class="svc-modal-star-hint svc-modal-star-hint--tap">' + 12887 esc(__('js_modal_stars_group_hint')) + 12888 '</span>'; 12889 host.innerHTML = renderModalStarsInteractiveHtml(svc, sd) + hintSpan; 12890 } 12891 12892 function clearSvcModalHeaderScores() { 12893 var pill = document.getElementById('svcModalScorePill'); 12894 var host = document.getElementById('svcModalStarsHost'); 12895 if (pill) { 12896 pill.hidden = true; 12897 pill.textContent = ''; 12898 pill.className = 'svc-modal-trust-pill svc-modal-trust-pill--good'; 12899 pill.removeAttribute('role'); 12900 pill.removeAttribute('tabindex'); 12901 pill.removeAttribute('title'); 12902 } 12903 if (host) host.innerHTML = ''; 12904 } 12905 12906 function openTrustScoreExplainModal() { 12907 var modal = document.getElementById('trustScoreExplainModal'); 12908 var body = document.getElementById('trustScoreExplainBody'); 12909 var titleEl = document.getElementById('trustScoreExplainTitle'); 12910 if (!modal || !body) return; 12911 if (titleEl) titleEl.textContent = __('trust_score_help_title'); 12912 body.innerHTML = 12913 '<p class="trust-score-help-lead">' + 12914 esc(__('trust_score_help_p1')) + 12915 '</p>' + 12916 '<ul class="trust-score-help-ul">' + 12917 '<li>' + 12918 esc(__('trust_score_help_p2')) + 12919 '</li>' + 12920 '<li>' + 12921 esc(__('trust_score_help_p3')) + 12922 '</li>' + 12923 '<li>' + 12924 esc(__('trust_score_help_p4')) + 12925 '</li>' + 12926 '</ul>' + 12927 '<p class="trust-score-help-foot">' + 12928 esc(__('trust_score_help_p5')) + 12929 '</p>'; 12930 modal.classList.add('open'); 12931 modal.setAttribute('aria-hidden', 'false'); 12932 document.body.style.overflow = 'hidden'; 12933 } 12934 12935 function closeTrustScoreExplainModal() { 12936 var modal = document.getElementById('trustScoreExplainModal'); 12937 if (modal) { 12938 modal.classList.remove('open'); 12939 modal.setAttribute('aria-hidden', 'true'); 12940 } 12941 var svc = document.getElementById('svcModal'); 12942 var star = document.getElementById('starRatingModal'); 12943 if ((svc && svc.classList.contains('open')) || (star && star.classList.contains('open'))) { 12944 document.body.style.overflow = 'hidden'; 12945 } else { 12946 document.body.style.overflow = ''; 12947 } 12948 } 12949
12950 function renderSvcAvailabilityPlan(pts, targetKey) { 12951 var el = document.getElementById('svcAvailabilityPlan'); 12952 if (!el) return; 12953 var days = getLast7LocalDays(); 12954 var byDay = {}; 12955 for (var i = 0; i < (pts || []).length; i++) { 12956 var p = pts[i]; 12957 if (!p || !p.t) continue; 12958 var dt = new Date(p.t); 12959 if (isNaN(dt.getTime())) continue; 12960 var k = localDayKey(dt); 12961 if (!byDay[k]) byDay[k] = []; 12962 byDay[k].push(p); 12963 } 12964 var cleanCount = 0; 12965 var rows = ''; 12966 for (var d = 0; d < days.length; d++) { 12967 var dk = days[d].key; 12968 var dd = days[d].date; 12969 var arr = byDay[dk] || []; 12970 var status = 'unk'; 12971 if (arr.length > 0) { 12972 var anyFail = false; 12973 for (var j = 0; j < arr.length; j++) { 12974 if (arr[j].ok === false) { 12975 anyFail = true; 12976 break; 12977 } 12978 } 12979 status = anyFail ? 'bad' : 'ok'; 12980 } 12981 if (status === 'ok') cleanCount++; 12982 var lab = dd.toLocaleDateString('pt-BR', { weekday: 'short', day: '2-digit', month: '2-digit' }); 12983 var fillCls = 12984 status === 'ok' ? 'svc-ap-bar-fill--ok' : status === 'bad' ? 'svc-ap-bar-fill--bad' : 'svc-ap-bar-fill--unk'; 12985 var tag = 12986 status === 'ok' ? 'OK' : status === 'bad' ? 'Falha' : 'Sem dados'; 12987 var tagCls = 12988 status === 'ok' ? 'svc-ap-tag svc-ap-tag--ok' : status === 'bad' ? 'svc-ap-tag svc-ap-tag--bad' : 'svc-ap-tag'; 12989 rows += 12990 '<div class="svc-ap-row">' + 12991 '<span class="svc-ap-label">' + 12992 esc(lab) + 12993 '</span>' + 12994 '<div class="svc-ap-bar-track"><div class="svc-ap-bar-fill ' + 12995 fillCls + 12996 '"></div></div>' + 12997 '<span class="' + 12998 tagCls + 12999 '">' + 13000 esc(tag) + 13001 '</span>' + 13002 '</div>'; 13003 } 13004 el.innerHTML = 13005 '<div class="svc-ap-title">' + 13006 esc(__('js_svc_ap_title')) + 13007 '</div>' + 13008 '<p class="svc-ap-sub">' + 13009 esc(__('js_svc_ap_sub')) + 13010 '</p>' + 13011 '<div class="svc-ap-scoreline">' + 13012 esc(__('js_svc_ap_score')) + 13013 ': <strong>+' + 13014 esc(String(cleanCount)) + 13015 '</strong> <span style="color:var(--muted);font-weight:600;">/ 7</span></div>' + 13016 rows; 13017 el.hidden = false; 13018 } 13019 13020 function clearSvcAvailabilityPlan() { 13021 var el = document.getElementById('svcAvailabilityPlan'); 13022 if (!el) return; 13023 el.innerHTML = ''; 13024 el.hidden = true; 13025 } 13026 13027 /** Cores de grelha, eixos e tooltip do Chart.js conforme tema claro / escuro. */ 13028 function getDetectorChartPalette() { 13029 var dark = getDetTheme() === 'dark'; 13030 if (dark) { 13031 return { 13032 xGrid: 'rgba(255, 255, 255, 0.05)', 13033 yGrid: 'rgba(130, 138, 152, 0.2)', 13034 axisLine: 'rgba(255, 255, 255, 0.11)', 13035 tick: 'rgba(212, 216, 222, 0.9)', 13036 title: 'rgba(232, 234, 237, 0.92)', 13037 legend: 'rgba(220, 224, 230, 0.9)', 13038 tooltipBg: 'rgba(48, 54, 64, 0.96)', 13039 tooltipTitle: '#9ca8b8', 13040 tooltipBody: '#e8eaed', 13041 tooltipBorder: 'rgba(255, 255, 255, 0.12)', 13042 }; 13043 } 13044 return { 13045 xGrid: 'rgba(2, 132, 199, 0.07)', 13046 yGrid: 'rgba(100, 116, 139, 0.13)', 13047 axisLine: 'rgba(2, 132, 199, 0.24)',
13048 tick: 'rgba(30, 41, 59, 0.86)', 13049 title: 'rgba(30, 41, 59, 0.88)', 13050 legend: 'rgba(30, 41, 59, 0.9)', 13051 tooltipBg: 'rgba(255, 255, 255, 0.98)', 13052 tooltipTitle: '#0369a1', 13053 tooltipBody: '#0f172a', 13054 tooltipBorder: 'rgba(2, 132, 199, 0.38)', 13055 }; 13056 } 13057 13058 /** Gradiente vertical sob a linha do gráfico de latência (adaptado ao tema). */ 13059 function detectorLatencyAreaGradient(ctx, chartArea) { 13060 if (!chartArea || chartArea.bottom <= chartArea.top) { 13061 return getDetTheme() === 'dark' ? 'rgba(148, 163, 184, 0.12)' : 'rgba(14, 165, 233, 0.16)'; 13062 } 13063 var dark = getDetTheme() === 'dark'; 13064 var g = ctx.createLinearGradient(0, chartArea.top, 0, chartArea.bottom); 13065 if (dark) { 13066 g.addColorStop(0, 'rgba(148, 163, 184, 0.28)'); 13067 g.addColorStop(0.38, 'rgba(113, 113, 122, 0.1)'); 13068 g.addColorStop(0.78, 'rgba(72, 78, 88, 0.06)'); 13069 g.addColorStop(1, 'rgba(38, 42, 50, 0.02)'); 13070 } else { 13071 g.addColorStop(0, 'rgba(0, 163, 255, 0.34)'); 13072 g.addColorStop(0.32, 'rgba(2, 132, 199, 0.18)'); 13073 g.addColorStop(0.68, 'rgba(56, 189, 248, 0.09)'); 13074 g.addColorStop(1, 'rgba(248, 250, 252, 0.02)'); 13075 } 13076 return g; 13077 } 13078 13079 /** Cores semi-transparentes por série (modo comparativo). */ 13080 var CHART_MULTI_LINE_RGB = [ 13081 '56, 189, 248', 13082 '167, 139, 250', 13083 '52, 211, 153', 13084 '251, 191, 36', 13085 '248, 113, 113', 13086 '244, 114, 182', 13087 '94, 234, 212', 13088 '147, 197, 253', 13089 '249, 168, 212', 13090 '192, 132, 252', 13091 ]; 13092 13093 function setSvcChartMultiUi(active) { 13094 var wrap = document.getElementById('svcLatencyChartWrap'); 13095 var legHint = document.getElementById('svcChartMultiHint'); 13096 var sel = document.getElementById('svcChartTarget'); 13097 if (wrap) wrap.classList.toggle('chart-wrap--multi', !!active); 13098 if (legHint) { 13099 legHint.textContent = ''; 13100 legHint.hidden = true; 13101 } 13102 if (sel) sel.disabled = !!active; 13103 } 13104 13105 /** Overlay dentro da caixa do gráfico (evita caixa âvaziaâ enquanto a API responde). */ 13106 function setSvcLatencyChartLoading(show, mainText) { 13107 var el = document.getElementById('svcLatencyChartLoading'); 13108 var lab = document.getElementById('svcLatencyChartLoadingLabel'); 13109 var sub = document.getElementById('svcLatencyChartLoadingSub'); 13110 var wrap = document.getElementById('svcLatencyChartWrap'); 13111 if (!el) return; 13112 if (show) { 13113 if (lab && mainText) lab.textContent = mainText; 13114 if (sub) sub.textContent = __('js_chart_loading_overlay_sub'); 13115 el.hidden = false; 13116 if (wrap) wrap.setAttribute('aria-busy', 'true'); 13117 } else { 13118 el.hidden = true; 13119 if (wrap) wrap.removeAttribute('aria-busy'); 13120 } 13121 } 13122 13123 function isSvcChartMultiMode() { 13124 var c = document.getElementById('svcChartMulti'); 13125 return !!(c && c.checked); 13126 } 13127 13128 function prettyChartTargetLabel(key) { 13129 var k = (key || 'primary').toString(); 13130 if (k === 'primary') return 'Principal (HTTPS)'; 13131 if (k === 'icmp_echo') return 'ICMP (ping)'; 13132 if (k === 'status') return 'Página de status'; 13133 if (k === 'community') return 'Comunidade'; 13134 if (/^tcp_\d+$/.test(k)) return 'TCP :' + k.slice(4); 13135 return k; 13136 } 13137 13138 function chartTargetKeySortOrder(k) { 13139 var order = { primary: 0, status: 1, community: 2, icmp_echo: 10 }; 13140 if (order[k] !== undefined) return order[k]; 13141 var m = /^tcp_(\d+)$/.exec(k); 13142 if (m) return 20 + parseInt(m[1], 10) / 65535; 13143 return 5; 13144 } 13145 13146 function detectorLatencyFillGradientTinted(ctx, chartArea, rgbStr) { 13147 if (!chartArea || chartArea.bottom <= chartArea.top) { 13148 return 'rgba(' + rgbStr + ', ' + (getDetTheme() === 'dark' ? '0.16' : '0.12') + ')'; 13149 } 13150 var dark = getDetTheme() === 'dark'; 13151 var g = ctx.createLinearGradient(0, chartArea.top, 0, chartArea.bottom); 13152 g.addColorStop(0, 'rgba(' + rgbStr + ', ' + (dark ? '0.4' : '0.34') + ')'); 13153 g.addColorStop(0.42, 'rgba(' + rgbStr + ', ' + (dark ? '0.16' : '0.14') + ')'); 13154 g.addColorStop(1, 'rgba(' + rgbStr + ', ' + (dark ? '0.03' : '0.02') + ')'); 13155 return g; 13156 } 13157 13158 function chartTooltipLabelLine(ctx) { 13159 var raw = ctx.raw; 13160 var y = ctx.parsed && ctx.parsed.y; 13161 var name = (ctx.dataset && ctx.dataset.label) || ''; 13162 if (raw && typeof raw === 'object' && raw._ok === false) { 13163 return ' ' + name + ': falha (sem latência)'; 13164 } 13165 if (y == null || isNaN(Number(y))) { 13166 return ' ' + name + ': sem medição neste instante'; 13167 } 13168 var tag = 13169 raw && raw._ok === true ? 'OK' : raw && raw._ok === false ? 'falhou' : 'â'; 13170 return ' ' + name + ': ' + Math.round(Number(y)) + ' ms · ' + tag; 13171 } 13172 13173 function reloadModalChartFromControls() { 13174 var sid = state.modalSvcId; 13175 if (!sid) return; 13176 if (isSvcChartMultiMode()) {
13177 loadModalChartAllTargets(sid); 13178 } else { 13179 var tsel = document.getElementById('svcChartTarget'); 13180 loadModalChart(sid, tsel && tsel.value ? tsel.value : 'primary'); 13181 } 13182 } 13183 13184 /** 13185 * Um gráfico, N linhas: todos os target_key (HTTPS, ICMP, tcp_*) no mesmo eixo temporal. 13186 * Legenda Chart.js: clique para ocultar/mostrar série. 13187 */ 13188 function loadModalChartAllTargets(svcId) { 13189 setSvcChartMultiUi(true); 13190 destroyChart(); 13191 setSvcLatencyChartLoading(true, __('js_chart_comp_loading')); 13192 var hint = document.getElementById('svcChartProbeHint'); 13193 if (hint) hint.textContent = ''; 13194 clearSvcAvailabilityPlan(); 13195 var hrs = getChartRangeHours(); 13196 state.chartRangeHours = hrs; 13197 var origin = window.location.origin || ''; 13198 var api = 13199 origin + 13200 '/api/detector/service/' + 13201 encodeURIComponent(svcId) + 13202 '/latency?hours=' + 13203 encodeURIComponent(String(hrs)) + 13204 '&target_key=all'; 13205 var fetchP = fetch(api, { credentials: 'omit' }).then(function (r) { 13206 return r 13207 .json() 13208 .catch(function () { 13209 return {}; 13210 }) 13211 .then(function (data) { 13212 if (!r.ok) { 13213 var det = (data && (data.detail || data.message)) || ''; 13214 var err = new Error(det || 'HTTP ' + r.status); 13215 err._httpStatus = r.status; 13216 throw err; 13217 } 13218 return data; 13219 }); 13220 }); 13221 Promise.all([ensureChartJsLoaded(), fetchP]) 13222 .then(function (pair) { 13223 var data = pair[1]; 13224 var pts = (data && data.points) || []; 13225 if (hint) hint.textContent = ''; 13226 var byKey = {}; 13227 for (var i = 0; i < pts.length; i++) { 13228 var p = pts[i]; 13229 var kk = (p.target_key || 'primary').toString(); 13230 if (!byKey[kk]) byKey[kk] = []; 13231 byKey[kk].push(p); 13232 } 13233 var keys = Object.keys(byKey).sort(function (a, b) { 13234 var d = chartTargetKeySortOrder(a) - chartTargetKeySortOrder(b); 13235 return d !== 0 ? d : a.localeCompare(b); 13236 }); 13237 var canvas = document.getElementById('svcLatencyChart'); 13238 if (!canvas) { 13239 setSvcLatencyChartLoading(false); 13240 if (hint) hint.textContent = 'Ãrea do gráfico indisponÃvel na página.'; 13241 return; 13242 } 13243 if (typeof Chart === 'undefined') { 13244 setSvcLatencyChartLoading(false); 13245 if (hint) { 13246 hint.textContent = 13247 'Chart.js não ficou carregado. Verifique rede ou bloqueios a cdn.jsdelivr.net.'; 13248 } 13249 return; 13250 } 13251 if (keys.length === 0 || pts.length === 0) { 13252 setSvcLatencyChartLoading(false); 13253 if (hint) { 13254 hint.textContent = 13255 'Sem pontos na base para este serviço neste perÃodo. Aguarde o worker de sondas.'; 13256 } 13257 return; 13258 } 13259 var datasets = []; 13260 var totalXY = 0; 13261 for (var di = 0; di < keys.length; di++) { 13262 var kk = keys[di]; 13263 var raw = byKey[kk]; 13264 var rgb = CHART_MULTI_LINE_RGB[di % CHART_MULTI_LINE_RGB.length]; 13265 var xy = raw 13266 .map(function (p) { 13267 var xd = p.t ? new Date(p.t) : null; 13268 if (xd && isNaN(xd.getTime())) xd = null; 13269 return { 13270 x: xd, 13271 y: p.ok && p.ms != null ? Number(p.ms) : null, 13272 _ok: p.ok, 13273 }; 13274 }) 13275 .filter(function (pt) { 13276 return pt.x != null; 13277 }) 13278 .sort(function (a, b) { 13279 return a.x.getTime() - b.x.getTime(); 13280 }); 13281 totalXY += xy.length; 13282 datasets.push({ 13283 label: prettyChartTargetLabel(kk), 13284 data: xy, 13285 parsing: false, 13286 borderColor: 'rgba(' + rgb + ', 0.94)', 13287 backgroundColor: function (ctx) { 13288 return detectorLatencyFillGradientTinted( 13289 ctx.chart.ctx, 13290 ctx.chart.chartArea, 13291 rgb, 13292 ); 13293 }, 13294 spanGaps: true, 13295 tension: 0.36, 13296 fill: true, 13297 pointRadius: 0, 13298 pointHoverRadius: 6, 13299 pointHoverBorderWidth: 2,
13300 pointHoverBackgroundColor: 'rgba(255,255,255,0.9)', 13301 pointHoverBorderColor: 'rgba(' + rgb + ', 1)', 13302 hitRadius: 16, 13303 borderWidth: 2.45, 13304 borderJoinStyle: 'round', 13305 borderCapStyle: 'round', 13306 }); 13307 } 13308 var useDecimate = totalXY > 280; 13309 var cp = getDetectorChartPalette(); 13310 try { 13311 state.chart = new Chart(canvas, { 13312 type: 'line', 13313 data: { datasets: datasets }, 13314 options: { 13315 responsive: true, 13316 maintainAspectRatio: false, 13317 resizeDelay: 16, 13318 interaction: { mode: 'index', intersect: false, axis: 'x' }, 13319 hover: { mode: 'index', intersect: false }, 13320 animation: { 13321 duration: 13322 (typeof window.matchMedia === 'function' && 13323 window.matchMedia('(max-width: 768px)').matches) || 13324 totalXY > 500 13325 ? 0 13326 : 720, 13327 easing: 'easeOutQuart', 13328 }, 13329 elements: { 13330 line: { borderJoinStyle: 'round', borderCapStyle: 'round' }, 13331 point: { hoverRadius: 6 }, 13332 }, 13333 scales: { 13334 x: { 13335 type: 'time', 13336 min: new Date(Date.now() - hrs * 3600000), 13337 max: new Date(), 13338 time: { 13339 tooltipFormat: 'dd/MM/yyyy HH:mm', 13340 displayFormats: { 13341 millisecond: 'HH:mm:ss.SSS', 13342 second: 'HH:mm:ss', 13343 minute: 'HH:mm', 13344 hour: 'HH:mm', 13345 day: 'dd/MM', 13346 week: 'dd/MM', 13347 month: 'MMM yyyy', 13348 quarter: 'MMM yyyy', 13349 year: 'yyyy', 13350 }, 13351 }, 13352 grid: { 13353 color: cp.xGrid, 13354 lineWidth: 1, 13355 borderDash: [4, 6], 13356 }, 13357 border: { 13358 display: true, 13359 color: cp.axisLine, 13360 dash: [2, 2], 13361 }, 13362 ticks: { 13363 color: cp.tick, 13364 maxRotation: 0, 13365 maxTicksLimit: hrs <= 24 ? 12 : 8, 13366 font: { size: 10, family: "system-ui, 'Segoe UI', sans-serif" }, 13367 }, 13368 }, 13369 y: { 13370 grace: '12%', 13371 title: { 13372 display: true, 13373 text: 'Latência (ms)', 13374 color: cp.title, 13375 font: { size: 11, weight: '600' }, 13376 }, 13377 grid: { 13378 color: cp.yGrid, 13379 lineWidth: 1, 13380 borderDash: [3, 5], 13381 }, 13382 border: { 13383 display: true, 13384 color: cp.axisLine, 13385 dash: [2, 2], 13386 }, 13387 ticks: { 13388 color: cp.tick, 13389 font: { size: 10, family: "system-ui, 'Segoe UI', sans-serif" }, 13390 callback: function (v) { 13391 var n = Number(v); 13392 return isNaN(n) ? v : Math.round(n); 13393 }, 13394 }, 13395 }, 13396 }, 13397 plugins: { 13398 decimation: useDecimate 13399 ? { 13400 enabled: true, 13401 algorithm: 'lttb', 13402 samples: Math.min(200, Math.max(60, Math.floor(totalXY / keys.length))), 13403 } 13404 : { enabled: false }, 13405 legend: { 13406 display: true, 13407 position: 'bottom', 13408 align: 'start', 13409 labels: { 13410 color: cp.legend, 13411 boxWidth: 20, 13412 boxHeight: 3, 13413 padding: 10, 13414 font: { size: 10, weight: '600' }, 13415 usePointStyle: true, 13416 pointStyle: 'line', 13417 }, 13418 }, 13419 tooltip: { 13420 enabled: true, 13421 backgroundColor: cp.tooltipBg, 13422 titleColor: cp.tooltipTitle, 13423 bodyColor: cp.tooltipBody, 13424 borderColor: cp.tooltipBorder, 13425 borderWidth: 1, 13426 padding: 12, 13427 cornerRadius: 12, 13428 displayColors: true, 13429 boxPadding: 4, 13430 titleFont: { size: 12, weight: '600' }, 13431 bodyFont: { size: 12 }, 13432 callbacks: { 13433 label: chartTooltipLabelLine, 13434 }, 13435 }, 13436 zoom: { 13437 pan: { 13438 enabled: true, 13439 mode: 'x', 13440 modifierKey: 'ctrl', 13441 onPanComplete: function () { 13442 var btn = document.getElementById('svcChartResetZoom'); 13443 if (btn) btn.hidden = false;
13444 }, 13445 }, 13446 limits: { x: { min: 'original', max: 'original' } }, 13447 zoom: { 13448 pinch: { enabled: true }, 13449 drag: { 13450 enabled: true, 13451 borderColor: 'rgba(2,132,199,0.35)', 13452 borderWidth: 1, 13453 backgroundColor: 'rgba(2,132,199,0.08)', 13454 }, 13455 mode: 'x', 13456 onZoomComplete: function () { 13457 var btn = document.getElementById('svcChartResetZoom'); 13458 if (btn) btn.hidden = false; 13459 }, 13460 }, 13461 }, 13462 }, 13463 }, 13464 }); 13465 setSvcLatencyChartLoading(false); 13466 } catch (e2) { 13467 setSvcLatencyChartLoading(false); 13468 if (typeof console !== 'undefined' && console.warn) console.warn('[detector] Chart multi', e2); 13469 if (hint) { 13470 hint.textContent = 13471 'Não foi possÃvel desenhar o gráfico comparativo. ' + 13472 (e2 && e2.message ? e2.message : ''); 13473 } 13474 } 13475 }) 13476 .catch(function (err) { 13477 setSvcLatencyChartLoading(false); 13478 if (typeof console !== 'undefined' && console.warn) { 13479 console.warn('[detector] loadModalChartAllTargets', err); 13480 } 13481 if (hint) { 13482 if (err && err.message === 'chart_js') { 13483 hint.textContent = 13484 'Não foi possÃvel carregar Chart.js (rede ou bloqueador a jsDelivr).'; 13485 } else if (err && err._httpStatus === 429) { 13486 hint.textContent = 'Servidor limitou pedidos (429). Aguarde e tente de novo.'; 13487 } else if (err && err.message) { 13488 hint.textContent = 'Gráfico comparativo indisponÃvel: ' + err.message; 13489 } else { 13490 hint.textContent = 'Gráfico comparativo indisponÃvel.'; 13491 } 13492 } 13493 }); 13494 } 13495 13496 function loadModalChart(svcId, targetKey) { 13497 if (isSvcChartMultiMode()) { 13498 loadModalChartAllTargets(svcId); 13499 return; 13500 } 13501 setSvcChartMultiUi(false); 13502 destroyChart(); 13503 setSvcLatencyChartLoading(true, __('js_chart_loading')); 13504 var hint = document.getElementById('svcChartProbeHint'); 13505 if (hint) hint.textContent = ''; 13506 clearSvcAvailabilityPlan(); 13507 var planEl = document.getElementById('svcAvailabilityPlan'); 13508 if (planEl) { 13509 planEl.hidden = false; 13510 planEl.innerHTML = 13511 '<p class="chart-probe-hint" style="margin-top:12px;">A carregar plano de 7 diasâ¦</p>'; 13512 } 13513 var hrs = getChartRangeHours(); 13514 state.chartRangeHours = hrs; 13515 var origin = window.location.origin || ''; 13516 var tk = targetKey || 'primary'; 13517 var api = 13518 origin + 13519 '/api/detector/service/' + 13520 encodeURIComponent(svcId) + 13521 '/latency?hours=' + 13522 encodeURIComponent(String(hrs)) + 13523 '&target_key=' + 13524 encodeURIComponent(tk) + 13525 '&include_availability_plan=true'; 13526 var fetchP = fetch(api, { credentials: 'omit' }).then(function (r) { 13527 return r 13528 .json() 13529 .catch(function () { 13530 return {}; 13531 }) 13532 .then(function (data) { 13533 if (!r.ok) { 13534 var det = (data && (data.detail || data.message)) || ''; 13535 var err = new Error(det || 'HTTP ' + r.status); 13536 err._httpStatus = r.status; 13537 throw err; 13538 } 13539 return data; 13540 }); 13541 }); 13542 Promise.all([ensureChartJsLoaded(), fetchP]) 13543 .then(function (pair) { 13544 var data = pair[1]; 13545 var pts = (data && data.points) || []; 13546 var probe = ''; 13547 for (var i = pts.length - 1; i >= 0; i--) { 13548 if (pts[i].probe_endpoint) { 13549 probe = pts[i].probe_endpoint; 13550 break; 13551 } 13552 } 13553 if (hint) { 13554 if (probe) { 13555 var pu = String(probe); 13556 hint.textContent = pu.length > 64 ? pu.slice(0, 61) + '\u2026' : pu; 13557 } else { 13558 hint.textContent = ''; 13559 } 13560 } 13561 var xy = pts 13562 .map(function (p) { 13563 var xd = p.t ? new Date(p.t) : null; 13564 if (xd && isNaN(xd.getTime())) xd = null; 13565 return { 13566 x: xd, 13567 y: p.ok && p.ms != null ? Number(p.ms) : null, 13568 _ok: p.ok, 13569 }; 13570 }) 13571 .filter(function (pt) { 13572 return pt.x != null; 13573 }) 13574 .sort(function (a, b) { 13575 return a.x.getTime() - b.x.getTime(); 13576 }); 13577 if (data.plan_points && data.plan_points.length) { 13578 renderSvcAvailabilityPlan(data.plan_points, tk); 13579 } else { 13580 renderSvcAvailabilityPlan([], tk); 13581 } 13582 var canvas = document.getElementById('svcLatencyChart'); 13583 if (!canvas) { 13584 setSvcLatencyChartLoading(false); 13585 if (hint) hint.textContent = 'Ãrea do gráfico indisponÃvel na página.'; 13586 return; 13587 } 13588 if (typeof Chart === 'undefined') { 13589 setSvcLatencyChartLoading(false); 13590 if (hint) { 13591 hint.textContent = 13592 'Chart.js não ficou carregado. Verifique rede ou bloqueios a cdn.jsdelivr.net.'; 13593 } 13594 return; 13595 } 13596 if (xy.length === 0) { 13597 setSvcLatencyChartLoading(false); 13598 if (hint) { 13599 hint.textContent = 13600 'Sem pontos de latência na base para este destino neste perÃodo. ' + 13601 'O worker do Catellix regista o histórico ao longo do tempo; tente outro intervalo ou volte mais tarde. ' + 13602 '(Statuspage: o gráfico reflecte sondagens HTTPS ao URL de estado.)'; 13603 } 13604 return; 13605 } 13606 var nPts = xy.length; 13607 var useDecimate = nPts > 160; 13608 var chartLabel = 'Latência (ms) · ' + tk + ' · ' + chartRangeLabelPt(hrs); 13609 var cpSingle = getDetectorChartPalette(); 13610 try { 13611 state.chart = new Chart(canvas, { 13612 type: 'line', 13613 data: { 13614 datasets: [ 13615 {
13616 label: chartLabel, 13617 data: xy, 13618 parsing: false, 13619 borderColor: 13620 getDetTheme() === 'dark' 13621 ? 'rgba(148, 163, 184, 0.95)' 13622 : 'rgba(14, 165, 233, 0.98)', 13623 backgroundColor: function (ctx) { 13624 return detectorLatencyAreaGradient( 13625 ctx.chart.ctx, 13626 ctx.chart.chartArea, 13627 ); 13628 }, 13629 spanGaps: true, 13630 tension: 0.38, 13631 fill: true, 13632 pointRadius: 0, 13633 pointHoverRadius: 7, 13634 pointHoverBorderWidth: 2, 13635 pointHoverBackgroundColor: 13636 getDetTheme() === 'dark' 13637 ? 'rgba(48, 54, 64, 0.96)' 13638 : 'rgba(255, 255, 255, 0.98)', 13639 pointHoverBorderColor: 13640 getDetTheme() === 'dark' 13641 ? 'rgba(168, 180, 200, 1)' 13642 : 'rgba(14, 165, 233, 1)', 13643 hitRadius: 22, 13644 borderWidth: 2.65, 13645 borderJoinStyle: 'round', 13646 borderCapStyle: 'round', 13647 }, 13648 ], 13649 }, 13650 options: { 13651 responsive: true, 13652 maintainAspectRatio: false, 13653 resizeDelay: 16, 13654 interaction: { mode: 'index', intersect: false, axis: 'x' }, 13655 hover: { mode: 'index', intersect: false }, 13656 animation: { 13657 duration: 13658 (typeof window.matchMedia === 'function' && 13659 window.matchMedia('(max-width: 768px)').matches) || 13660 nPts > 400 13661 ? 0 13662 : 780, 13663 easing: 'easeOutQuart', 13664 }, 13665 elements: { 13666 line: { 13667 borderJoinStyle: 'round', 13668 borderCapStyle: 'round', 13669 }, 13670 point: { 13671 hoverRadius: 7, 13672 }, 13673 }, 13674 scales: { 13675 x: { 13676 type: 'time', 13677 min: new Date(Date.now() - hrs * 3600000), 13678 max: new Date(), 13679 time: { 13680 tooltipFormat: 'dd/MM/yyyy HH:mm', 13681 displayFormats: { 13682 millisecond: 'HH:mm:ss.SSS', 13683 second: 'HH:mm:ss', 13684 minute: 'HH:mm', 13685 hour: 'HH:mm', 13686 day: 'dd/MM', 13687 week: 'dd/MM', 13688 month: 'MMM yyyy', 13689 quarter: 'MMM yyyy', 13690 year: 'yyyy', 13691 }, 13692 }, 13693 grid: { 13694 color: cpSingle.xGrid, 13695 lineWidth: 1, 13696 borderDash: [4, 6], 13697 drawTicks: true, 13698 }, 13699 border: { 13700 display: true, 13701 color: cpSingle.axisLine, 13702 dash: [2, 2], 13703 }, 13704 ticks: { 13705 color: cpSingle.tick, 13706 maxRotation: 0, 13707 autoSkipPadding: 10, 13708 maxTicksLimit: hrs <= 24 ? 12 : 9, 13709 font: { size: 11, family: "system-ui, 'Segoe UI', sans-serif" }, 13710 }, 13711 }, 13712 y: { 13713 grace: '14%', 13714 title: { 13715 display: true, 13716 text: 'Latência (ms)', 13717 color: cpSingle.title, 13718 font: { size: 11, weight: '600' }, 13719 }, 13720 grid: { 13721 color: cpSingle.yGrid, 13722 lineWidth: 1, 13723 borderDash: [3, 5], 13724 }, 13725 border: { 13726 display: true, 13727 color: cpSingle.axisLine, 13728 dash: [2, 2], 13729 }, 13730 ticks: { 13731 color: cpSingle.tick, 13732 font: { size: 11, family: "system-ui, 'Segoe UI', sans-serif" }, 13733 callback: function (v) { 13734 var n = Number(v); 13735 return isNaN(n) ? v : Math.round(n); 13736 }, 13737 }, 13738 }, 13739 }, 13740 plugins: { 13741 decimation: useDecimate 13742 ? { 13743 enabled: true, 13744 algorithm: 'lttb', 13745 samples: Math.min(220, Math.max(80, Math.floor(nPts / 3))), 13746 } 13747 : { enabled: false }, 13748 legend: { 13749 display: true, 13750 position: 'top', 13751 align: 'start', 13752 labels: { 13753 color: cpSingle.legend, 13754 boxWidth: 16, 13755 boxHeight: 6, 13756 padding: 14, 13757 font: { size: 11, weight: '600' }, 13758 }, 13759 }, 13760 tooltip: { 13761 enabled: true, 13762 backgroundColor: cpSingle.tooltipBg, 13763 titleColor: cpSingle.tooltipTitle, 13764 bodyColor: cpSingle.tooltipBody, 13765 borderColor: cpSingle.tooltipBorder, 13766 borderWidth: 1, 13767 padding: 12, 13768 cornerRadius: 12, 13769 displayColors: true, 13770 boxPadding: 6, 13771 titleFont: { size: 12, weight: '600' }, 13772 bodyFont: { size: 13 }, 13773 callbacks: { 13774 label: chartTooltipLabelLine, 13775 }, 13776 }, 13777 zoom: { 13778 pan: { 13779 enabled: true, 13780 mode: 'x', 13781 modifierKey: 'ctrl', 13782 onPanComplete: function () { 13783 var btn = document.getElementById('svcChartResetZoom'); 13784 if (btn) btn.hidden = false;
13785 }, 13786 }, 13787 limits: { x: { min: 'original', max: 'original' } }, 13788 zoom: { 13789 pinch: { enabled: true }, 13790 drag: { 13791 enabled: true, 13792 borderColor: 'rgba(2,132,199,0.35)', 13793 borderWidth: 1, 13794 backgroundColor: 'rgba(2,132,199,0.08)', 13795 }, 13796 mode: 'x', 13797 onZoomComplete: function () { 13798 var btn = document.getElementById('svcChartResetZoom'); 13799 if (btn) btn.hidden = false; 13800 }, 13801 }, 13802 }, 13803 }, 13804 }, 13805 }); 13806 setSvcLatencyChartLoading(false); 13807 } catch (chartErr) { 13808 setSvcLatencyChartLoading(false); 13809 if (typeof console !== 'undefined' && console.warn) { 13810 console.warn('[detector] Chart', chartErr); 13811 } 13812 if (hint) { 13813 hint.textContent = 13814 'Não foi possÃvel desenhar o gráfico. ' + 13815 (chartErr && chartErr.message ? chartErr.message : 'Dados ou escala inválidos.'); 13816 } 13817 } 13818 }) 13819 .catch(function (err) { 13820 setSvcLatencyChartLoading(false); 13821 if (typeof console !== 'undefined' && console.warn) { 13822 console.warn('[detector] loadModalChart', err); 13823 } 13824 if (hint) { 13825 if (err && err.message === 'chart_js') { 13826 hint.textContent = 13827 'Não foi possÃvel carregar Chart.js (rede, firewall ou bloqueador a jsDelivr). Desative bloqueios ou tente outra rede.'; 13828 } else if (err && err._httpStatus === 429) { 13829 hint.textContent = 13830 'Servidor limitou pedidos (429). Aguarde um minuto e abra o gráfico de novo.'; 13831 } else if (err && err._httpStatus === 400) { 13832 hint.textContent = 13833 (err.message || 'Pedido inválido.') + 13834 ' Verifique o perÃodo do gráfico (valores permitidos na lista).'; 13835 } else if (err && err._httpStatus === 404) { 13836 hint.textContent = 'Serviço não encontrado no catálogo â atualize a página.'; 13837 } else if (err && err.message) { 13838 hint.textContent = 'Gráfico indisponÃvel: ' + err.message; 13839 } else { 13840 hint.textContent = 'Gráfico indisponÃvel para este destino (rede ou servidor).'; 13841 } 13842 } 13843 clearSvcAvailabilityPlan(); 13844 }); 13845 } 13846 13847 function statuspageIndicatorClass(ind) { 13848 var k = String(ind || '') 13849 .trim() 13850 .toLowerCase() 13851 .replace(/[^a-z0-9]+/g, ''); 13852 if (k === 'none' || k === '') return 'svc-sp-ind--none'; 13853 if (k === 'minor') return 'svc-sp-ind--minor'; 13854 if (k === 'major') return 'svc-sp-ind--major'; 13855 if (k === 'critical') return 'svc-sp-ind--critical'; 13856 if (k.indexOf('maintenance') >= 0) return 'svc-sp-ind--maintenance'; 13857 return 'svc-sp-ind--unk'; 13858 } 13859 13860 function renderDetailEnrichment(detailData) { 13861 var strip = document.getElementById('svcDetailEnrich'); 13862 if (!strip) return; 13863 if (!detailData || !detailData.success) { 13864 strip.innerHTML = ''; 13865 strip.style.display = 'none'; 13866 return; 13867 } 13868 function clipNote(t, max) { 13869 var s = (t || '').trim(); 13870 if (s.length <= max) return s; 13871 return s.slice(0, max - 1) + '\u2026'; 13872 } 13873 var parts = []; 13874 var pitch = clipNote(detailData.monitoring_positioning_pt, 140); 13875 if (pitch) { 13876 parts.push('<p class="svc-enrich-pitch">' + esc(pitch) + '</p>'); 13877 } 13878 var sp = detailData.statuspage_public; 13879 if (sp && sp.ok) { 13880 var indCls = statuspageIndicatorClass(sp.indicator); 13881 var desc = clipNote(sp.status_description || 'â', 12
138810); 13882 var pu = (sp.page_url || '').trim(); 13883 var link = 13884 pu.indexOf('http') === 0 13885 ? '<a href="' + 13886 esc(pu) + 13887 '" target="_blank" rel="noopener noreferrer">' + 13888 esc(__('js_enrich_open_status')) + 13889 '</a>' 13890 : ''; 13891 var comp = ''; 13892 if (sp.components_total > 0) { 13893 comp = 13894 '<div class="svc-sp-comp">' + 13895 esc( 13896 __('js_enrich_components_tpl') 13897 .replace(/\{n\}/g, String(sp.components_non_operational)) 13898 .replace(/\{t\}/g, String(sp.components_total)), 13899 ) + 13900 '</div>'; 13901 } 13902 var upd = (sp.updated_at || '').trim(); 13903 var meta = (link ? link : '') + (upd ? (link ? ' · ' : '') + esc(upd) : ''); 13904 parts.push( 13905 '<div class="svc-sp-card ' + 13906 indCls + 13907 '"><h5>' + 13908 esc(__('js_enrich_statuspage_title')) + 13909 '</h5><div class="svc-sp-status">' + 13910 esc(desc) + 13911 '</div>' + 13912 (meta ? '<div class="svc-sp-meta">' + meta + '</div>' : '') + 13913 comp + 13914 '</div>', 13915 ); 13916 } 13917 var foot = clipNote(detailData.detector_enrichment_note_pt, 110); 13918 if (foot) { 13919 parts.push('<p class="svc-enrich-foot">' + esc(foot) + '</p>'); 13920 } 13921 strip.innerHTML = parts.join(''); 13922 strip.style.display = parts.length ? 'block' : 'none'; 13923 } 13924 13925 function renderLatencyHub(detailData) { 13926 var el = document.getElementById('svcLatencyHub'); 13927 if (!el) return; 13928 if (!detailData || !detailData.success) { 13929 el.innerHTML = ''; 13930 el.style.display = 'none'; 13931 return; 13932 } 13933 var sources = detailData.latency_sources || []; 13934 var best = detailData.best_latency_ms; 13935 var note = detailData.hub_note_pt || ''; 13936 var apiNote = (detailData.note || '').trim(); 13937 var lq = detailData.latency_quality || {}; 13938 var lc = detailData.latency_class || {}; 13939 var tierKey = lc.key ? String(lc.key) : lq.key ? String(lq.key) : 'unknown'; 13940 if (tierKey === 'great') tierKey = 'excellent'; 13941 if (tierKey === 'fair') tierKey = 'medium'; 13942 if (tierKey === 'slow') tierKey = 'critical'; 13943 var mergedBest = null; 13944 for (var bi = 0; bi < sources.length; bi++) { 13945 var bm = sources[bi].ms; 13946 if (bm != null && !isNaN(Number(bm))) { 13947 var bn = Number(bm); 13948 mergedBest = mergedBest == null ? bn : Math.min(mergedBest, bn); 13949 } 13950 } 13951 if (mergedBest == null && best != null && !isNaN(Number(best))) mergedBest = Number(best); 13952 var msRounded = mergedBest != null ? Math.round(mergedBest) : null; 13953 var tierLabel = lc.short_pt ? String(lc.short_pt) : lq.short_pt ? String(lq.short_pt) : 'â'; 13954 function hubClip(t, max) { 13955 var s = (t || '').trim(); 13956 if (s.length <= max) return s; 13957 return s.slice(0, max - 1) + '\u2026'; 13958 } 13959 var html = 13960 '<div class="latency-hub-card latency-hub-card--compact"><h4 class="latency-hub-title">' + 13961 esc(__('js_hub_card_title')) + 13962 '</h4>'; 13963 html += '<div class="latency-hub-hero lh-tier-' + esc(tierKey) + '">'; 13964 html += '<div class="lh-hero-icon" aria-hidden="true">ð¡</div><div class="lh-hero-body">'; 13965 html += '<div class="lh-hero-kicker">' + esc(__('js_hub_hero_kicker')) + '</div>'; 13966 if (msRounded != null) { 13967 html += 13968 '<div class="lh-ms-pill"><span class="lh-ms-num">' + 13969 esc(String(msRounded)) + 13970 '</span> <span class="lh-ms-unit">ms</span></div>'; 13971 html += 13972 '<div class="lh-hero-tier">' + 13973 esc(__('js_hub_tier_lbl')) + 13974 ': <span>' + 13975 esc(tierLabel) + 13976 '</span></div>'; 13977 if (lc.color_hex) { 13978 html += 13979 '<span class="lh-class-badge" style="border-color:' + 13980 esc(String(lc.color_hex)) + 13981 ';color:' + 13982 esc(String(lc.color_hex)) + 13983 '">' + 13984 esc(tierLabel) + 13985 '</span>'; 13986 } 13987 } else { 13988 html += '<div class="lh-ms-pill">' + esc(__('js_no_recent')) + '</div>'; 13989 html += '<div class="lh-hero-tier">' + esc(__('js_hub_empty_hint')) + '</div>'; 13990 } 13991 html += '</div></div>'; 13992 var met = detailData.latency_metrics || {}; 13993 var hasMet = 13994 met.current_ms != null || 13995 met.avg_ms != null || 13996 met.max_ms != null || 13997 met.jitter_ms != null || 13998 met.packet_loss_pct != null ||
13999 msRounded != null; 14000 if (hasMet) { 14001 function metricCell(label, val, unit) { 14002 if (val == null || val === '') return ''; 14003 var u = unit != null ? unit : ''; 14004 return ( 14005 '<div class="lh-metric-cell"><b>' + 14006 esc(label) + 14007 '</b><span class="lh-metric-val">' + 14008 esc(String(val)) + 14009 esc(u) + 14010 '</span></div>' 14011 ); 14012 } 14013 html += 14014 '<div class="lh-metrics-grid" aria-label="' + esc(__('js_hub_metrics_aria')) + '">'; 14015 html += metricCell( 14016 __('js_hub_metric_current'), 14017 met.current_ms != null ? Math.round(Number(met.current_ms)) : msRounded, 14018 ' ms', 14019 ); 14020 html += metricCell( 14021 __('js_hub_metric_avg'), 14022 met.avg_ms != null ? Number(met.avg_ms).toFixed(1) : null, 14023 ' ms', 14024 ); 14025 html += metricCell( 14026 __('js_hub_metric_max'), 14027 met.max_ms != null ? Number(met.max_ms).toFixed(1) : null, 14028 ' ms', 14029 ); 14030 html += metricCell( 14031 __('js_hub_metric_jitter'), 14032 met.jitter_ms != null ? Number(met.jitter_ms).toFixed(1) : null, 14033 ' ms', 14034 ); 14035 html += metricCell( 14036 __('js_hub_metric_loss'), 14037 met.packet_loss_pct != null ? Number(met.packet_loss_pct).toFixed(1) : null, 14038 ' %', 14039 ); 14040 html += '</div>'; 14041 } 14042 html += '<ul class="latency-hub-list">'; 14043 for (var i = 0; i < sources.length; i++) { 14044 var s = sources[i]; 14045 var ic = s.ms != null ? 'ð°' : 'ð'; 14046 if (s.ms != null) { 14047 html += 14048 '<li class="lh-src"><span class="lh-src-ic" aria-hidden="true">' + 14049 ic + 14050 '</span><span><span class="lh-label">' + 14051 esc(s.label_pt) + 14052 '</span> <strong class="lh-ms">' + 14053 esc(String(s.ms)) + 14054 ' ms</strong></span></li>'; 14055 } else if (s.note_pt) { 14056 var npt = hubClip(s.note_pt, 96); 14057 html += 14058 '<li class="lh-src lh-note"><span class="lh-src-ic" aria-hidden="true">' + 14059 ic + 14060 '</span><span>' + 14061 (s.label_pt 14062 ? '<span class="lh-label" style="display:block;margin-bottom:4px;">' + 14063 esc(s.label_pt) + 14064 '</span>' 14065 : '') + 14066 esc(npt) + 14067 '</span></li>'; 14068 } 14069 } 14070 html += '</ul>'; 14071 if (mergedBest != null) { 14072 html += 14073 '<p class="lh-best">' + 14074 esc(__('js_hub_best_tpl').replace(/\{ms\}/g, String(Math.round(mergedBest)))) + 14075 '</p>'; 14076 } 14077 var noteC = note ? hubClip(note, 100) : ''; 14078 var apiC = apiNote ? hubClip(apiNote, 100) : ''; 14079 if (noteC) html += '<p class="lh-foot">' + esc(noteC) + '</p>'; 14080 if (apiC) html += '<p class="lh-foot">' + esc(apiC) + '</p>'; 14081 html += '</div>'; 14082 el.innerHTML = html; 14083 el.style.display = 'block'; 14084 } 14085 14086 function openModal(svcId) { 14087 var svc = findSvc(svcId); 14088 if (!svc) return; 14089 state.modalSvcId = svcId; 14090 state.communityFeedReplyParentId = null; 14091 state.communityFeedVerifyToken = null; 14092 var modal = document.getElementById('svcModal'); 14093 var title = document.getElementById('svcModalName'); 14094 var desc = document.getElementById('svcModalDesc'); 14095 var links = document.getElementById('svcModalLinks'); 14096 var logoEl = document.getElementById('svcModalLogo'); 14097 var sel = document.getElementById('svcChartTarget'); 14098 var wrap = document.getElementById('svcTargetsTableWrap'); 14099 var tb = document.getElementById('svcTargetsTbody'); 14100 var hubEl = document.getElementById('svcLatencyHub'); 14101 if (!modal || !title || !desc || !links) return; 14102 renderSvcTcpPortsBlock(svc.tcp_ports, state.probeProfile); 14103 renderProbeLayersBlock(svc, null); 14104 title.textContent = svc.name || svcId; 14105 updateSvcModalHeaderScores(svc); 14106 desc.textContent = enrichDescription(svc); 14107 var enrichEl = document.getElementById('svcDetailEnrich'); 14108 if (enrichEl) { 14109 enrichEl.innerHTML = ''; 14110 enrichEl.style.display = 'none'; 14111 } 14112 if (hubEl) { 14113 hubEl.style.display = 'block'; 14114 hubEl.innerHTML = 14115 '<p class="latency-hub-loading" style="color:var(--muted);font-size:0.82rem;">' + 14116 esc(__('js_hub_loading')) + 14117 '</p>'; 14118 } 14119 if (logoEl) { 14120 logoEl.onerror = null; 14121 logoEl.removeAttribute('data-fb'); 14122 logoEl.removeAttribute('data-lx'); 14123 logoEl.removeAttribute('data-lxf'); 14124 logoEl.removeAttribute('data-logo-chain'); 14125 logoEl.removeAttribute('data-logo-i');
14126 var domM = (svc.logo_domain || '').trim(); 14127 logoEl.setAttribute('data-logo-domain', domM); 14128 var chainM = Array.isArray(svc.logo_icons) && svc.logo_icons.length ? svc.logo_icons : null; 14129 if (chainM) { 14130 logoEl.classList.add('det-logo-fallback'); 14131 logoEl.setAttribute('data-logo-chain', encodeURIComponent(JSON.stringify(chainM))); 14132 logoEl.setAttribute('data-logo-i', '0'); 14133 logoEl.src = chainM[0]; 14134 attachLogoChainOnError(logoEl); 14135 } else { 14136 logoEl.classList.remove('det-logo-fallback'); 14137 logoEl.src = svc.logo_url || faviconForDomain(domM); 14138 logoEl.onerror = function () { 14139 var d = (svc.logo_domain || '').trim(); 14140 var step = parseInt(logoEl.getAttribute('data-lxf'), 10) || 0; 14141 if (step === 0 && d) { 14142 logoEl.setAttribute('data-lxf', '1'); 14143 logoEl.src = 'https://unavatar.io/' + encodeURIComponent(d); 14144 return; 14145 } 14146 if (step === 1 && d) { 14147 logoEl.setAttribute('data-lxf', '2'); 14148 logoEl.src = faviconDdg(d); 14149 return; 14150 } 14151 logoEl.onerror = null; 14152 }; 14153 } 14154 } 14155 function modalLinkChip(icon, labelKey, url) { 14156 var u = (url || '').trim(); 14157 if (!u || u === '#') return ''; 14158 var hosty = u.replace(/^https?:\/\//i, ''); 14159 if (hosty.length > 52) hosty = hosty.slice(0, 49) + '\u2026'; 14160 return ( 14161 '<a class="modal-link-chip" href="' + 14162 esc(u) + 14163 '" target="_blank" rel="noopener noreferrer">' + 14164 '<span class="mlc-ico" aria-hidden="true">' + 14165 icon + 14166 '</span><span><span class="mlc-lbl">' + 14167 esc(__(labelKey)) + 14168 '</span><span class="mlc-url">' + 14169 esc(hosty) + 14170 '</span></span></a>' 14171 ); 14172 } 14173 links.innerHTML = 14174 '<div class="modal-links-compact">' + 14175 modalLinkChip('ð', 'js_modal_link_site', svc.official_url) + 14176 modalLinkChip('ð', 'js_modal_link_status', svc.status_url) + 14177 modalLinkChip('â ', 'js_modal_link_validate', svc.validation_url || svc.probe_url) + 14178 '</div>'; 14179 var reportBtn = document.getElementById('svcModalReportBtn'); 14180 if (reportBtn) { 14181 reportBtn.onclick = function (e) { 14182 e.preventDefault(); 14183 openCommunityReportModal(svcId); 14184 }; 14185 } 14186 14187 modal.classList.add('open'); 14188 destroyChart(); 14189 var hint0 = document.getElementById('svcChartProbeHint'); 14190 if (hint0) hint0.textContent = ''; 14191 var rangeSel = document.getElementById('svcChartRange'); 14192 if (rangeSel) { 14193 rangeSel.value = String(state.chartRangeHours || 168); 14194 rangeSel.onchange = function () { 14195 var h = parseInt(rangeSel.value, 10); 14196 state.chartRangeHours = !isNaN(h) && h > 0 ? h : 168; 14197 reloadModalChartFromControls(); 14198 }; 14199 } 14200 var multiCb = document.getElementById('svcChartMulti'); 14201 if (multiCb) { 14202 multiCb.checked = true; 14203 multiCb.onchange = function () { 14204 reloadModalChartFromControls(); 14205 }; 14206 } 14207 var rzBtn = document.getElementById('svcChartResetZoom'); 14208 if (rzBtn) { 14209 rzBtn.hidden = true; 14210 rzBtn.onclick = function () { 14211 if (state.chart && typeof state.chart.resetZoom === 'function') { 14212 state.chart.resetZoom(); 14213 } 14214 rzBtn.hidden = true; 14215 }; 14216 } 14217 setSvcChartMultiUi(true); 14218 if (sel) { 14219 sel.innerHTML = ''; 14220 sel.onchange = null; 14221 } 14222 if (tb) tb.innerHTML = ''; 14223 if (wrap) wrap.style.display = 'none'; 14224
14225 loadModalChartAllTargets(svcId); 14226 14227 var origin = window.location.origin || ''; 14228 var detailUrl = 14229 origin + 14230 '/api/detector/service/' + 14231 encodeURIComponent(svcId) + 14232 '/detail?anon_token=' + 14233 encodeURIComponent(detectorCommunityAnonToken()); 14234 fetch(detailUrl, { credentials: 'omit' }) 14235 .then(function (r) { 14236 return r.json().then(function (data) { 14237 if (!r.ok) throw new Error(data.detail || 'HTTP ' + r.status); 14238 return data; 14239 }); 14240 }) 14241 .then(function (data) { 14242 if (data && data.service) { 14243 var apiDesc = (data.service.description || '').trim(); 14244 var cur = (svc.description || '').trim(); 14245 if (apiDesc.length >= cur.length && apiDesc.length > 0) { 14246 desc.textContent = enrichDescription( 14247 Object.assign({}, svc, { description: data.service.description }), 14248 ); 14249 } 14250 } 14251 renderDetailEnrichment(data); 14252 renderLatencyHub(data); 14253 renderPublicFeedbackBlock(data, svcId); 14254 renderSvcTcpPortsBlock(data.tcp_ports, data.probe_profile || state.probeProfile); 14255 renderProbeLayersBlock(svc, data); 14256 var targets = (data && data.targets) || []; 14257 if (wrap) wrap.style.display = targets.length ? 'block' : 'none'; 14258 if (tb && targets.length) { 14259 var rows = ''; 14260 for (var j = 0; j < targets.length; j++) { 14261 var t = targets[j]; 14262 var lat = 14263 t.last_latency_ms != null ? esc(String(Math.round(t.last_latency_ms))) + ' ms' : 'â'; 14264 var st = t.last_ok === false ? ' (falhou)' : ''; 14265 var ipsHtml = 'â'; 14266 var det = t.resolved_ips_detail; 14267 if (det && det.length) { 14268 ipsHtml = det 14269 .map(function (d) { 14270 var ip = d.ip || ''; 14271 var fam = d.family; 14272 var cls = 14273 fam === 6 14274 ? 'ip-tag ip-tag--v6 ip-tag--dns' 14275 : 'ip-tag ip-tag--v4 ip-tag--dns'; 14276 return ( 14277 '<span class="' + 14278 cls + 14279 '" title="' + 14280 esc(fam === 6 ? 'IPv6 (AAAA)' : 'IPv4 (A)') + 14281 '">' + 14282 esc(ip) + 14283 '</span>' 14284 ); 14285 }) 14286 .join(''); 14287 } else if ((t.resolved_ips || []).length) { 14288 ipsHtml = (t.resolved_ips || []) 14289 .map(function (ip) { 14290 var is6 = String(ip).indexOf(':') >= 0; 14291 var cls = is6 14292 ? 'ip-tag ip-tag--v6 ip-tag--dns' 14293 : 'ip-tag ip-tag--v4 ip-tag--dns'; 14294 return ( 14295 '<span class="' + 14296 cls + 14297 '">' + 14298 esc(String(ip)) + 14299 '</span>' 14300 ); 14301 }) 14302 .join(''); 14303 } 14304 rows += 14305 '<tr><td><strong>' + 14306 esc(t.label || t.key) + 14307 '</strong><br><small style="color:var(--muted);word-break:break-all;">' + 14308 esc(t.url || '') + 14309 '</small></td><td>' + 14310 lat + 14311 esc(st) + 14312 '</td><td class="ip-dns-cell"><small>' + 14313 ipsHtml + 14314 '</small></td></tr>'; 14315 } 14316 tb.innerHTML = rows; 14317 } 14318 if (sel && targets.length) { 14319 for (var k = 0; k < targets.length; k++) { 14320 var t2 = targets[k]; 14321 var opt = document.createElement('option'); 14322 opt.value = t2.key || 'primary'; 14323 opt.textContent = t2.label || t2.key; 14324 sel.appendChild(opt); 14325 } 14326 sel.onchange = function () { 14327 reloadModalChartFromControls(); 14328 }; 14329 if (!isSvcChartMultiMode()) { 14330 reloadModalChartFromControls(); 14331 } 14332 } else if (!isSvcChartMultiMode()) { 14333 reloadModalChartFromControls(); 14334 } 14335 }) 14336 .catch(function () { 14337 renderDetailEnrichment(null); 14338 renderLatencyHub(null); 14339 renderPublicFeedbackBlock(null, svcId); 14340 renderSvcTcpPortsBlock(svc.tcp_ports, state.probeProfile); 14341 renderProbeLayersBlock(svc, null); 14342 if (wrap) wrap.style.display = 'none'; 14343 if (!isSvcChartMultiMode()) { 14344 reloadModalChartFromControls(); 14345 } 14346 }); 14347 } 14348 14349 function closeModal() { 14350 var pfb = document.getElementById('svcPublicFeedback'); 14351 if (pfb) { 14352 pfb.style.display = 'none'; 14353 pfb.innerHTML = ''; 14354 } 14355 state.modalDetailData = null; 14356 state.communityFeedReplyParentId = null; 14357 state.communityFeedVerifyToken = null; 14358 clearSvcModalHeaderScores(); 14359 clearSvcAvailabilityPlan(); 14360 var modal = document.getElementById('svcModal'); 14361 if (modal) modal.classList.remove('open'); 14362 destroyChart(); 14363 setSvcLatencyChartLoading(false); 14364 state.modalSvcId = null; 14365 var sel = document.getElementById('svcChartTarget'); 14366 if (sel) sel.onchange = null; 14367 var rangeSel = document.getElementById('svcChartRange'); 14368 if (rangeSel) rangeSel.onchange = null; 14369 var multiCb = document.getElementById('svcChartMulti'); 14370 if (multiCb) { 14371 multiCb.onchange = null; 14372 multiCb.checked = false;
14373 } 14374 setSvcChartMultiUi(false); 14375 var tb = document.getElementById('svcTargetsTbody'); 14376 if (tb) tb.innerHTML = ''; 14377 var wrap = document.getElementById('svcTargetsTableWrap'); 14378 if (wrap) wrap.style.display = 'none'; 14379 var hint = document.getElementById('svcChartProbeHint'); 14380 if (hint) hint.textContent = ''; 14381 var hub = document.getElementById('svcLatencyHub'); 14382 if (hub) { 14383 hub.innerHTML = ''; 14384 hub.style.display = 'none'; 14385 } 14386 var enrich = document.getElementById('svcDetailEnrich'); 14387 if (enrich) { 14388 enrich.innerHTML = ''; 14389 enrich.style.display = 'none'; 14390 } 14391 var tcpEl = document.getElementById('svcTcpPortsHost'); 14392 if (tcpEl) { 14393 tcpEl.innerHTML = ''; 14394 tcpEl.style.display = 'none'; 14395 } 14396 var plEl = document.getElementById('svcProbeLayersHost'); 14397 if (plEl) { 14398 plEl.innerHTML = ''; 14399 plEl.style.display = 'none'; 14400 } 14401 } 14402 14403 function readDetectorCatalogSwrCache() { 14404 try { 14405 var raw = localStorage.getItem(DETECTOR_CATALOG_SWR_KEY); 14406 if (!raw) { 14407 raw = sessionStorage.getItem(DETECTOR_CATALOG_SWR_KEY); 14408 } 14409 if (!raw) return null; 14410 var o = JSON.parse(raw); 14411 if (!o || typeof o.t !== 'number' || !o.payload || o.payload.success !== true) return null; 14412 if (Date.now() - o.t > CATALOG_SWR_MAX_DISPLAY_AGE_MS) return null; 14413 return o; 14414 } catch (e) { 14415 return null; 14416 } 14417 } 14418 14419 function writeDetectorCatalogSwrCache(payload) { 14420 if (!payload || payload.success !== true) return; 14421 var blob = JSON.stringify({ t: Date.now(), payload: payload }); 14422 try { 14423 localStorage.setItem(DETECTOR_CATALOG_SWR_KEY, blob); 14424 } catch (e) { 14425 try { 14426 sessionStorage.setItem(DETECTOR_CATALOG_SWR_KEY, blob); 14427 } catch (e2) { /* quota */ } 14428 } 14429 } 14430 14431 function setCatalogSyncing(on) { 14432 var el = document.getElementById('detCatalogSyncHint'); 14433 if (!el) return; 14434 if (on) { 14435 el.hidden = false; 14436 el.textContent = __('catalog_sync_hint'); 14437 } else { 14438 el.hidden = true; 14439 el.textContent = ''; 14440 } 14441 } 14442 14443 function setLoading(on) { 14444 var el = document.getElementById('detectorLoading'); 14445 if (!el) return; 14446 if (on) el.classList.add('is-visible'); 14447 else { 14448 el.classList.remove('is-visible'); 14449 var sk = document.getElementById('detSkeletonPlaceholder'); 14450 if (sk) sk.remove(); 14451 } 14452 } 14453 14454 /** 14455 * Aplica resposta de /api/detector/catalog ao estado e à UI. 14456 * @param {Object} ctx - { err, finishLoad, btn } 14457 */ 14458 function applyDetectorCatalogPayload(cat, ctx) { 14459 var err = ctx.err; 14460 var finishLoad = ctx.finishLoad; 14461 var btn = ctx.btn; 14462 if (!cat.success) throw new Error(cat.error || __('js_err_detector_load')); 14463 applyDetectorAdSlotsFromCatalog(cat); 14464 state.catalog = cat.services || []; 14465 state.communityWindowDays = 14466 cat.community_vote_window_days != null ? Number(cat.community_vote_window_days) : 7; 14467 if (isNaN(state.communityWindowDays)) state.communityWindowDays = 7; 14468 state.complaintsRankingDays = readComplaintsRankingDays(); 14469 var dCat = VALID_RANKING_DAYS.indexOf(state.complaintsRankingDays) !== -1 ? state.complaintsRankingDays : 1; 14470 state.complaintsRankingDays = dCat; 14471 updateHeroTagline(); 14472 state.communityDisclaimerPt = cat.community_disclaimer_pt || ''; 14473 state.probeIntervalSec = 14474 cat.probe_interval_sec != null ? Number(cat.probe_interval_sec) : 60; 14475 if (isNaN(state.probeIntervalSec)) state.probeIntervalSec = 60; 14476 state.probeProfile = cat.probe_profile && typeof cat.probe_profile === 'object' ? cat.probe_profile : {}; 14477 state.latencyClassConfig = 14478 cat.latency_class_config && typeof cat.latency_class_config === 'object' 14479 ? cat.latency_class_config 14480 : {}; 14481 if ( 14482 typeof DetectorLatencyClassify !== 'undefined' && 14483 DetectorLatencyClassify.setThresholdsFromApi 14484 ) { 14485 DetectorLatencyClassify.setThresholdsFromApi(state.latencyClassConfig); 14486 } 14487 var _ric = typeof requestIdleCallback === 'function' 14488 ? requestIdleCallback 14489 : function (cb) { setTimeout(cb, 40); }; 14490 try { 14491 rebuildCategorySelect(); 14492 initRankSelect(); 14493 wireCategorySelect(); 14494 wireIpStackSelect(); 14495 var ipstackSel = document.getElementById('selIpStack'); 14496 if (ipstackSel && ['all', 'v4', 'dual', 'v6'].indexOf(state.ipStackFilter || '') >= 0) { 14497 ipstackSel.value = state.ipStackFilter; 14498 } 14499 var sr = document.getElementById('selRank'); 14500 if (sr) sr.value = state.rankSort; 14501 updateProbePopBadge(cat.probe_profile || {}); 14502 mountCommunityRanking(); 14503 if (!markLazySectionDirty('top10Boards')) mountTop10Boards(); 14504 mountTop10Cards(); 14505 try { 14506 scheduleDetectorPromotionalUiAfterTop10(); 14507 } catch (ePromo) { 14508 /* ignore */ 14509 } 14510 updateCatalogFilterBanner(); 14511 applyFilterSections(); 14512 14513 /* Fase 1 â leve: UI que não envolve a grid do catálogo */ 14514 _ric(function () { 14515 try { 14516 renderObservationHub(cat.probe_profile || {}); 14517 refreshComplaintsTrendSparklines(document); 14518 refreshDetectorSummaryDependentUi(); 14519 syncDetectorMobileFlowLayout(); 14520 updateCatalogViewToggleBtn(); 14521 renderTop30List(); 14522 } catch (eP1) { 14523 if (typeof console !== 'undefined' && console.error) console.error('[detector] deferred-p1', eP1); 14524 } 14525 }); 14526 /* Fase 2 â pesada: grid do catálogo (shelves) â só quando perto do viewport */ 14527 setTimeout(function () { 14528 try { 14529 markLazySectionDirty('shelvesHost'); 14530 applyViewToShelves(); 14531 mountIcmpNoReplySection(); 14532 if (!ddRowHintBound) { 14533 ddRowHintBound = true; 14534 bindShelfScrollHint(document.getElementById('ddRowWrap')); 14535 } 14536 refreshShelfHints(); 14537 var rpm = document.getElementById('detReportPickerModal'); 14538 if (rpm && rpm.classList.contains('open')) { 14539 var si = document.getElementById('detReportPickerSearch'); 14540 renderReportPickerList(si ? si.value : ''); 14541 } 14542 if (finishLoad) { 14543 setTimeout(function () { 14544 try { applyDeepLinkFromHash(); } catch (eH) {} 14545 }, 200); 14546 } 14547 } catch (eP2) { 14548 if (typeof console !== 'undefined' && console.error) console.error('[detector] deferred-p2', eP2); 14549 } 14550 }, 120); 14551 } catch (uiErr) { 14552 if (typeof console !== 'undefined' && console.error) { 14553 console.error('[detector] erro ao montar UI após catálogo', uiErr); 14554 } 14555 try { 14556 mountShelves(); 14557 syncDetectorMobileFlowLayout(); 14558 updateCatalogViewToggleBtn(); 14559 renderTop30List(); 14560 } catch (e2) { /* ignore */ } 14561 try { 14562 mountDd(); 14563 } catch (e3) { /* ignore */ } 14564 try { 14565 scheduleDetectorPromotionalUiAfterTop10(); 14566 } catch (ePromo2) { 14567 /* ignore */ 14568 } 14569 if (err) { 14570 err.style.display = 'block'; 14571 err.textContent = 14572 __('js_ui_partial_err') + 14573 (uiErr && uiErr.message ? ' (' + uiErr.message + ')' : ''); 14574 } 14575 } 14576 if (finishLoad) { 14577 state.detectorLoadInProgress = false;
14578 scheduleNextRefreshDeadline(); 14579 updateRefreshCountdownUi(); 14580 if (btn) btn.disabled = false; 14581 setLoading(false); 14582 setCatalogSyncing(false); 14583 _ric(function () { 14584 if (typeof Chart === 'undefined') { 14585 ensureChartJsLoaded().catch(function () {}); 14586 } 14587 }); 14588 } 14589 } 14590 14591 /** Atualiza blocos que dependem de /api/detector/summary (SaaS, jogos, tendências DD). */ 14592 function refreshDetectorSummaryDependentUi() { 14593 if (!state.catalog || !state.catalog.length) return; 14594 mountProblemsCatalog(); 14595 mountRankPanels(); 14596 updateMetricsBar(); 14597 mountDd(); 14598 } 14599 14600 function normalizeDetectorSummaryPayload(data) { 14601 if (data && data.success !== false) return data; 14602 return { 14603 success: false, 14604 saas_problems: [], 14605 saas_total_issues: 0, 14606 games: [], 14607 games_with_issue: 0, 14608 games_total: 0, 14609 trending_issues: [], 14610 }; 14611 } 14612 14613 function applyDetectorAuxiliaryClientData(pingPack, reasons, gcfg) { 14614 state.reportReasons = (reasons && reasons.success && reasons.reasons) || []; 14615 state.googleClientId = (gcfg && gcfg.client_id) || ''; 14616 renderCatellixProductsPing(pingPack && pingPack.ok ? pingPack.data : null); 14617 var rpm = document.getElementById('detReportPickerModal'); 14618 if (rpm && rpm.classList.contains('open')) { 14619 var si = document.getElementById('detReportPickerSearch'); 14620 renderReportPickerList(si ? si.value : ''); 14621 } 14622 } 14623 14624 function loadAll() { 14625 var btn = document.getElementById('btnRefresh'); 14626 var err = document.getElementById('errBox'); 14627 state.detectorLoadInProgress = true; 14628 detectorLoadAllGen++; 14629 var loadGen = detectorLoadAllGen; 14630 try { 14631 fetchDetectorLogoManifest(); 14632 } catch (eLm) { 14633 /* ignore */ 14634 } 14635 updateRefreshCountdownUi(); 14636 if (btn) btn.disabled = true; 14637 state.complaintsRankingDays = readComplaintsRankingDays(); 14638 var dParallel = VALID_RANKING_DAYS.indexOf(state.complaintsRankingDays) !== -1 ? state.complaintsRankingDays : 1; 14639 state.complaintsRankingDays = dParallel; 14640 updateHeroTagline(); 14641 if (hydrateCommunityRankingFromSwrCache(dParallel)) { 14642 wireSidebarComplaintsRanking(); 14643 refreshSidebarComplaintsTop10(); 14644 try { 14645 syncComplaintsTop10BoardsAndCards(); 14646 } catch (eHy) {} 14647 } 14648 var hadSwrPrime = false; 14649 var swrCached = readDetectorCatalogSwrCache(); 14650 if (swrCached && swrCached.payload) { 14651 try { 14652 applyDetectorCatalogPayload(swrCached.payload, { 14653 err: err, 14654 finishLoad: false, 14655 btn: btn, 14656 }); 14657 if (state.communityRankingRows && state.communityRankingRows.length) { 14658 applyCommunityRankingToCatalog(state.communityRankingRows); 14659 renderCommunityRankingOl(); 14660 syncComplaintsTop10BoardsAndCards(); 14661 } 14662 setLoading(false); 14663 setCatalogSyncing(true); 14664 hadSwrPrime = true; 14665 setTimeout(function () { 14666 try { refreshComplaintsTrendSparklines(document); } catch (e) {} 14667 try { renderTop30List(); } catch (e) {} 14668 }, 200); 14669 } catch (ePrime) { 14670 hadSwrPrime = false; 14671 } 14672 } 14673 if (!hadSwrPrime) { 14674 setLoading(true); 14675 } 14676 if (err) { 14677 err.style.display = 'none'; 14678 err.textContent = ''; 14679 } 14680 var origin = window.location.origin || ''; 14681 fetch(origin + '/api/detector/community-ranking?' + communityRankingApiQuery(dParallel), { 14682 credentials: 'omit', 14683 }) 14684 .then(function (r) { 14685 return r 14686 .json() 14687 .then(function (data) { 14688 return { ok: r.ok, data: data }; 14689 }) 14690 .catch(function () { 14691 return { ok: false, data: {} }; 14692 }); 14693 }) 14694 .then(function (pack) { 14695 if (loadGen !== detectorLoadAllGen) return; 14696 var data = pack.data || {}; 14697 if (!pack.ok || !data.success) return; 14698 var dNow = VALID_RANKING_DAYS.indexOf(state.complaintsRankingDays) !== -1 ? state.complaintsRankingDays : 1; 14699 if (dNow !== dParallel) return; 14700 writeCommunityRankingSwrCache(data); 14701 state.communityRankingRows = data.ranking || []; 14702 state._fetchedCommunityRankingDays = dParallel; 14703 state._fetchedCommunityRankingLimit = COMMUNITY_RANKING_API_LIMIT; 14704 scheduleCommunityRankingMidnightRefresh(data.next_reset_utc); 14705 if (data.disclaimer_pt) { 14706 state.communityDisclaimerPt = data.disclaimer_pt; 14707 var discP = document.getElementById('rankCommunityDisclaimer'); 14708 if (discP) discP.textContent = data.disclaimer_pt; 14709 } 14710 refreshSidebarComplaintsTop10(); 14711 if (state.catalog && state.catalog.length) { 14712 applyCommunityRankingToCatalog(state.communityRankingRows); 14713 renderCommunityRankingOl(); 14714 syncComplaintsTop10BoardsAndCards(); 14715 try { 14716 refreshComplaintsTrendSparklines(document); 14717 } catch (eSk) { 14718 /* ignore */ 14719 } 14720 } 14721 try { 14722 renderTop30List(); 14723 } catch (eT30) {} 14724 }) 14725 .catch(function () {}); 14726 14727 var p1 = fetch(origin + '/api/detector/catalog', { credentials: 'omit' }).then(function (r) { 14728 return r.json().then(function (data) { 14729 if (!r.ok) throw new Error((data && data.detail) || 'HTTP ' + r.status); 14730 return data; 14731 }); 14732 }); 14733 14734 var p2 = fetch(origin + '/api/detector/summary', { credentials: 'omit' }) 14735 .then(function (r) { 14736 return r.json().then(function (data) { 14737 if (!r.ok) { 14738 return normalizeDetectorSummaryPayload({ 14739 success: false, 14740 detail: (data && data.detail) || 'HTTP ' + r.status, 14741 }); 14742 } 14743 return normalizeDetectorSummaryPayload(data); 14744 }); 14745 }) 14746 .catch(function () { 14747 return normalizeDetectorSummaryPayload(null); 14748 }); 14749 14750 p2.then(function (sum) { 14751 state.summary = sum; 14752 refreshDetectorSummaryDependentUi(); 14753 }); 14754 14755 function loadDeferredApis() { 14756 var p3 = fetch(origin + '/api/public/catellix-products-ping', { credentials: 'omit' }) 14757 .then(function (r) { 14758 return r.json().then(function (data) { 14759 return { ok: r.ok, data: data }; 14760 }); 14761 }) 14762 .catch(function () { 14763 return { ok: false, data: null }; 14764 }); 14765 var p4 = fetch(origin + '/api/detector/report-reasons', { credentials: 'omit' }) 14766 .then(function (r) { 14767 return r.json().catch(function () { 14768 return { success: false }; 14769 }); 14770 }) 14771 .catch(function () { 14772 return { success: false }; 14773 }); 14774 var p5 = fetch(origin + '/api/config/google-client-id', { credentials: 'omit' }) 14775 .then(function (r) { 14776 return r.json().catch(function () { 14777 return {}; 14778 }); 14779 }) 14780 .catch(function () { 14781 return {}; 14782 }); 14783 Promise.all([p3, p4, p5]) 14784 .then(function (triple) { 14785 applyDetectorAuxiliaryClientData(triple[0], triple[1], triple[2] || {}); 14786 }) 14787 .catch(function () { 14788 applyDetectorAuxiliaryClientData({ ok: false, data: null }, { success: false }, {}); 14789 }); 14790 14791 try { 14792 mergeDetectorCarouselSlotDefaults(); 14793 } catch (eFill) { 14794 /* ignore */ 14795 } 14796 try { 14797 syncDetectorAdSlotsCountUi(); 14798 wireDetectorLocalAdTakeoverOnce(); 14799 } catch (eSlotsUi) { 14800 /* ignore */ 14801 } 14802 } 14803 setTimeout(loadDeferredApis, 80); 14804 14805 p1.then(function (cat) { 14806 if (loadGen !== detectorLoadAllGen) return; 14807 try { 14808 applyDetectorCatalogPayload(cat, { 14809 err: err, 14810 finishLoad: true, 14811 btn: btn, 14812 }); 14813 writeDetectorCatalogSwrCache(cat); 14814 } catch (eApply) { 14815 if (err) { 14816 err.style.display = 'block'; 14817 err.textContent = 14818 __('js_err_detector_load') + ' ' + (eApply.message || __('js_try_again')); 14819 } 14820 state.detectorLoadInProgress = false;
14821 scheduleNextRefreshDeadline(); 14822 updateRefreshCountdownUi(); 14823 if (btn) btn.disabled = false; 14824 setLoading(false); 14825 setCatalogSyncing(false); 14826 } 14827 }).catch(function (e) { 14828 if (err) { 14829 err.style.display = 'block'; 14830 err.textContent = 14831 __('js_err_detector_load') + ' ' + (e.message || __('js_try_again')); 14832 } 14833 state.detectorLoadInProgress = false; 14834 scheduleNextRefreshDeadline(); 14835 updateRefreshCountdownUi(); 14836 if (btn) btn.disabled = false; 14837 setLoading(false); 14838 setCatalogSyncing(false); 14839 }); 14840 } 14841 14842 function applyFilterSections() { 14843 var d = document.getElementById('secDd'); 14844 if (d) d.classList.toggle('filter-hide', state.filter !== 'all'); 14845 } 14846 14847 document.querySelectorAll('#toolbarCompact .seg-view button').forEach(function (b) { 14848 b.addEventListener('click', function () { 14849 document.querySelectorAll('#toolbarCompact .seg-view button').forEach(function (x) { 14850 x.classList.remove('active'); 14851 }); 14852 b.classList.add('active'); 14853 state.view = b.getAttribute('data-view') || 'list'; 14854 applyViewToShelves(); 14855 updateCatalogViewToggleBtn(); 14856 }); 14857 }); 14858 14859 var btnCatalogViewToggle = document.getElementById('btnCatalogViewToggle'); 14860 if (btnCatalogViewToggle && btnCatalogViewToggle.dataset.detWired !== '1') { 14861 btnCatalogViewToggle.dataset.detWired = '1'; 14862 btnCatalogViewToggle.addEventListener('click', function () { 14863 var to = btnCatalogViewToggle.getAttribute('data-mode-to') || 'grid'; 14864 state.view = to === 'grid' ? 'grid' : 'list'; 14865 syncSegViewButtonsFromState(); 14866 applyViewToShelves(); 14867 updateCatalogViewToggleBtn(); 14868 }); 14869 } 14870 14871 var btnRefresh = document.getElementById('btnRefresh'); 14872 if (btnRefresh) btnRefresh.addEventListener('click', loadAll); 14873 14874 var secRankPanels = document.getElementById('secRankPanels'); 14875 if (secRankPanels && secRankPanels.dataset.detectorRankMoreWired !== '1') { 14876 secRankPanels.dataset.detectorRankMoreWired = '1'; 14877 secRankPanels.addEventListener('click', function (e) { 14878 var t = e.target && e.target.closest ? e.target.closest('.rank-more-btn') : null; 14879 if (!t || t.disabled) return; 14880 var panel = t.getAttribute('data-panel'); 14881 if (!panel || state.rankExpanded[panel] === undefined) return; 14882 e.preventDefault(); 14883 state.rankExpanded[panel] = !state.rankExpanded[panel]; 14884 mountRankPanels(); 14885 if (panel === 'community') renderCommunityRankingOl(); 14886 }); 14887 } 14888 14889 var mc = document.getElementById('svcModalClose'); 14890 if (mc) mc.addEventListener('click', closeModal); 14891 var mb = document.getElementById('svcModal'); 14892 if (mb) { 14893 mb.addEventListener('click', function (e) { 14894 if (e.target === mb) closeModal(); 14895 }); 14896 if (mb.dataset.detSvcHeaderWired !== '1') { 14897 mb.dataset.detSvcHeaderWired = '1'; 14898 mb.addEventListener('click', function (e) { 14899 var pill = e.target && e.target.closest ? e.target.closest('#svcModalScorePill') : null; 14900 if (!pill || pill.hidden) return; 14901 e.preventDefault(); 14902 e.stopPropagation(); 14903 openTrustScoreExplainModal(); 14904 }); 14905 mb.addEventListener('keydown', function (e) { 14906 if (e.key !== 'Enter' && e.key !== ' ') return; 14907 var pill = e.target && e.target.id === 'svcModalScorePill' ? e.target : null; 14908 if (!pill || pill.hidden) return; 14909 e.preventDefault(); 14910 openTrustScoreExplainModal(); 14911 }); 14912 mb.addEventListener('click', function (e) { 14913 var btn = e.target && e.target.closest ? e.target.closest('.svc-star-vote') : null; 14914 if (!btn || !mb.contains(btn)) return; 14915 e.preventDefault(); 14916 e.stopPropagation(); 14917 var sid = btn.getAttribute('data-svc-id'); 14918 var sname = btn.getAttribute('data-svc-name') || sid; 14919 var n = Number(btn.getAttribute('data-star-vote')); 14920 if (!sid || isNaN(n)) return; 14921 openStarRatingModal(sid, sname, n); 14922 }); 14923 } 14924 } 14925 14926 (function wireTrustScoreExplainModal() { 14927 var modal = document.getElementById('trustScoreExplainModal'); 14928 var closeBtn = document.getElementById('trustScoreExplainClose'); 14929 if (!modal || modal.dataset.detWired === '1') return; 14930 modal.dataset.detWired = '1'; 14931 modal.setAttribute('aria-hidden', 'true'); 14932 if (closeBtn) closeBtn.addEventListener('click', closeTrustScoreExplainModal); 14933 modal.addEventListener('click', function (e) { 14934 if (e.target === modal) closeTrustScoreExplainModal(); 14935 }); 14936 document.addEventListener('keydown', function (e) { 14937 if (e.key !== 'Escape') return; 14938 if (!modal.classList.contains('open')) return; 14939 closeTrustScoreExplainModal(); 14940 }); 14941 })(); 14942 14943 var crm = document.getElementById('communityReportModal'); 14944 if (crm) 14945 crm.addEventListener('click', function (e) { 14946 if (e.target === crm) closeCommunityReportModal(); 14947 }); 14948 var crClose = document.getElementById('crClose'); 14949 if (crClose) crClose.addEventListener('click', closeCommunityReportModal); 14950 var crNo = document.getElementById('crNo'); 14951 if (crNo) crNo.addEventListener('click', closeCommunityReportModal); 14952 var crYes = document.getElementById('crYes'); 14953 if (crYes) 14954 crYes.addEventListener('click', function () { 14955 var s0 = document.getElementById('crStepConfirm'); 14956 var sm = document.getElementById('crStepMeta'); 14957 if (s0) s0.style.display = 'none';
14958 if (sm) sm.style.display = 'block'; 14959 renderCrReasonList(); 14960 scheduleCommunityCommentModeration(); 14961 }); 14962 var crToVerify = document.getElementById('crToVerify'); 14963 if (crToVerify) 14964 crToVerify.addEventListener('click', function () { 14965 var sm = document.getElementById('crStepMeta'); 14966 var s2 = document.getElementById('crStepVerify'); 14967 var msg = document.getElementById('crMsg'); 14968 var inlineErr = document.getElementById('crMetaInlineErr'); 14969 if (inlineErr) { 14970 inlineErr.textContent = ''; 14971 inlineErr.style.display = 'none'; 14972 } 14973 if (state.crParentReportId) { 14974 var ta0 = document.getElementById('crComment'); 14975 var tx0 = ta0 ? String(ta0.value || '').trim() : ''; 14976 if (tx0.length < 1) { 14977 if (inlineErr) { 14978 inlineErr.textContent = __('js_cr_reply_need_text'); 14979 inlineErr.style.display = ''; 14980 } 14981 return; 14982 } 14983 } else { 14984 var chk = document.querySelector('#crReasonList input[name="crReason"]:checked'); 14985 if (!chk) { 14986 if (inlineErr) { 14987 inlineErr.textContent = 'Escolha um motivo.'; 14988 inlineErr.style.display = ''; 14989 } 14990 return; 14991 } 14992 } 14993 crToVerify.disabled = true; 14994 setCrModerationHint(__('js_cr_mod_analyzing'), ''); 14995 runCommunityCommentModerationSync().then(function (mr) { 14996 crToVerify.disabled = false; 14997 if (!mr.allowed && mr.moderation) { 14998 var loc0 = 14999 typeof window.detectorGetLocale === 'function' ? window.detectorGetLocale() : 'pt'; 15000 var t0 = 15001 loc0 === 'en' && mr.moderation.summary_en 15002 ? mr.moderation.summary_en 15003 : mr.moderation.summary_pt || ''; 15004 if (inlineErr) { 15005 inlineErr.textContent = t0; 15006 inlineErr.style.display = ''; 15007 } 15008 setCrModerationHint(t0, 'cr-mod-hint--bad'); 15009 return; 15010 }
15011 if (sm) sm.style.display = 'none'; 15012 if (s2) s2.style.display = 'block'; 15013 if (msg) { 15014 msg.textContent = ''; 15015 msg.className = 'cr-msg'; 15016 } 15017 syncCrChannelUi(); 15018 var sel = document.getElementById('crChannel'); 15019 var v = sel && sel.value === 'google' ? null : 'crEmail'; 15020 var inp = v ? document.getElementById(v) : null; 15021 if (inp) setTimeout(function () { inp.focus(); }, 80); 15022 }); 15023 }); 15024 var crAnon = document.getElementById('crAnonymous'); 15025 if (crAnon) 15026 crAnon.addEventListener('change', function () { 15027 var w = document.getElementById('crDisplayNameWrap'); 15028 var on = !crAnon.checked; 15029 if (w) w.style.display = on ? 'block' : 'none'; 15030 }); 15031 var crBcast = document.getElementById('crBroadcast'); 15032 if (crBcast) 15033 crBcast.addEventListener('change', function () { 15034 var w = document.getElementById('crBroadcastNameWrap'); 15035 if (w) w.style.display = crBcast.checked ? '' : 'none'; 15036 }); 15037 var crBcastId = document.getElementById('crBroadcastIdentify'); 15038 if (crBcastId) 15039 crBcastId.addEventListener('change', function () { 15040 var w = document.getElementById('crDisplayNameWrap'); 15041 if (w) w.style.display = crBcastId.checked ? 'block' : 'none'; 15042 }); 15043 var crChannel = document.getElementById('crChannel'); 15044 if (crChannel) crChannel.addEventListener('change', syncCrChannelUi); 15045 15046 var crCmtInp = document.getElementById('crComment'); 15047 if (crCmtInp) crCmtInp.addEventListener('input', scheduleCommunityCommentModeration); 15048 var crDnInp = document.getElementById('crDisplayName'); 15049 if (crDnInp) crDnInp.addEventListener('input', scheduleCommunityCommentModeration); 15050 var crReasonListHost = document.getElementById('crReasonList'); 15051 if (crReasonListHost) crReasonListHost.addEventListener('change', scheduleCommunityCommentModeration); 15052 var crAnonMod = document.getElementById('crAnonymous'); 15053 if (crAnonMod) crAnonMod.addEventListener('change', scheduleCommunityCommentModeration); 15054 var crBcastIdMod = document.getElementById('crBroadcastIdentify'); 15055 if (crBcastIdMod) crBcastIdMod.addEventListener('change', scheduleCommunityCommentModeration); 15056 15057 var crSendCode = document.getElementById('crSendCode'); 15058 if (crSendCode) 15059 crSendCode.addEventListener('click', function () { 15060 var sid = state.crSvcId; 15061 var msg = document.getElementById('crMsg'); 15062 var sel = document.getElementById('crChannel'); 15063 var channel = sel ? String(sel.value || 'google') : 'google'; 15064 var phone = ''; 15065 var email = ''; 15066 if (channel === 'email') { 15067 var emEl = document.getElementById('crEmail'); 15068 email = emEl ? String(emEl.value || '').trim() : ''; 15069 if (!email || email.indexOf('@') < 1 || email.indexOf('.') < 0) { 15070 if (msg) { 15071 msg.className = 'cr-msg err'; 15072 msg.textContent = __('js_cr_invalid_email') || 'Informe um e-mail válido.'; 15073 } 15074 return; 15075 } 15076 } 15077 if (!sid) return; 15078 if (channel === 'google') { 15079 if (msg) { 15080 msg.className = 'cr-msg err'; 15081 msg.textContent = __('js_use_google_above'); 15082 } 15083 return; 15084 } 15085 crSendCode.disabled = true; 15086 if (msg) { 15087 msg.textContent = ''; 15088 msg.className = 'cr-msg'; 15089 } 15090 var origin = window.location.origin || ''; 15091 var payload = Object.assign( 15092 { service_id: sid, channel: channel, phone: phone || null, email: email || null }, 15093 getCrMetaPayload(), 15094 ); 15095 fetch(origin + '/api/detector/community-report/start', { 15096 method: 'POST', 15097 headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, 15098 body: JSON.stringify(payload), 15099 credentials: 'same-origin', 15100 }) 15101 .then(function (r) { 15102 return r.json().then(function (data) { 15103 return { r: r, data: data }; 15104 }); 15105 }) 15106 .then(function (pack) { 15107 var d = pack.data || {}; 15108 if (msg) { 15109 msg.className = 'cr-msg ' + (pack.r.ok && d.ok ? 'ok' : 'err'); 15110 var loc = typeof window.detectorGetLocale === 'function' ? window.detectorGetLocale() : 'pt'; 15111 var apiMsg = 15112 loc === 'en' && d.message_en 15113 ? d.message_en 15114 : d.message_pt || 15115 (pack.r.ok ? __('js_cr_code_sent') : __('js_cr_send_fail')); 15116 var txt = apiMsg; 15117 if (d.error === 'require_google') { 15118 txt += __('js_cr_require_google_suffix'); 15119 } 15120 if (d.debug_code) txt += ' [dev: ' + d.debug_code + ']'; 15121 msg.textContent = txt; 15122 } 15123 if (pack.r.ok && d.ok && d.verify_token) { 15124 state.crVerifyToken = d.verify_token; 15125 var panel = document.getElementById('crCo
15125dePanel'); 15126 if (panel) panel.style.display = 'block'; 15127 var cIn = document.getElementById('crCode'); 15128 if (cIn) setTimeout(function () { cIn.focus(); }, 80); 15129 } 15130 }) 15131 .catch(function () { 15132 if (msg) { 15133 msg.className = 'cr-msg err'; 15134 msg.textContent = __('js_err_net_retry'); 15135 } 15136 }) 15137 .finally(function () { 15138 crSendCode.disabled = false; 15139 }); 15140 }); 15141 15142 var crConfirmCode = document.getElementById('crConfirmCode'); 15143 if (crConfirmCode) 15144 crConfirmCode.addEventListener('click', function () { 15145 var sid = state.crSvcId; 15146 var tok = state.crVerifyToken; 15147 var cEl = document.getElementById('crCode'); 15148 var msg = document.getElementById('crMsg'); 15149 var code = cEl ? String(cEl.value || '').replace(/\D/g, '').slice(0, 4) : ''; 15150 if (!sid || !tok) { 15151 if (msg) { 15152 msg.className = 'cr-msg err'; 15153 msg.textContent = __('js_send_code_first'); 15154 } 15155 return; 15156 } 15157 if (code.length !== 4) { 15158 if (msg) { 15159 msg.className = 'cr-msg err'; 15160 msg.textContent = __('js_enter_4_digits'); 15161 } 15162 return; 15163 } 15164 crConfirmCode.disabled = true; 15165 var origin = window.location.origin || ''; 15166 fetch(origin + '/api/detector/community-report/confirm', { 15167 method: 'POST', 15168 headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, 15169 body: JSON.stringify({ 15170 verify_token: tok, 15171 code: code, 15172 broadcast_notify: getCrMetaPayload().broadcast_notify, 15173 broadcast_who: getCrMetaPayload().broadcast_identify ? (getCrMetaPayload().display_name || null) : null, 15174 service_name: (findSvc(sid) || {}).name || sid, 15175 }), 15176 credentials: 'same-origin', 15177 }) 15178 .then(function (r) { 15179 return r.json().then(function (data) { 15180 return { r: r, data: data }; 15181 }); 15182 }) 15183 .then(function (pack) { 15184 var d = pack.data || {}; 15185 if (msg) { 15186 msg.className = 'cr-msg ' + (pack.r.ok && d.ok ? 'ok' : 'err'); 15187 var loc2 = 15188 typeof window.detectorGetLocale === 'function' ? window.detectorGetLocale() : 'pt'; 15189 var apiMsg2 = 15190 loc2 === 'en' && d.message_en 15191 ? d.message_en 15192 : d.message_pt || 15193 (pack.r.ok ? __('js_cr_confirmed') : __('js_cr_confirm_fail')); 15194 msg.textContent = apiMsg2; 15195 } 15196 if (pack.r.ok && d.ok) { 15197 if (!d.parent_report_id && !d.moderation_pending) { 15198 bumpCommunityVoteLocal(sid); 15199 mountShelves(); 15200 mountProblemsCatalog(); 15201 mountCommunityRanking(true); 15202 } 15203 setTimeout(closeCommunityReportModal, 2200); 15204 } 15205 }) 15206 .catch(function () { 15207 if (msg) { 15208 msg.className = 'cr-msg err'; 15209 msg.textContent = __('js_err_net_retry'); 15210 } 15211 }) 15212 .finally(function () { 15213 crConfirmCode.disabled = false; 15214 }); 15215 }); 15216 15217 function initDetectorContactForm() { 15218 var overlay = document.getElementById('det-contact-overlay'); 15219 var form = document.getElementById('det-contact-form'); 15220 var btnOpen = document.getElementById('det-btn-open-contact'); 15221 var btnCancel = document.getElementById('det-contact-cancel'); 15222 var btnSubmit = document.getElementById('det-contact-submit'); 15223 var successEl = document.getElementById('det-contact-success'); 15224 var errorEl = document.getElementById('det-contact-error');
15225 if (!overlay || !form || !btnOpen) return; 15226 15227 function openM() { 15228 overlay.classList.add('visible'); 15229 overlay.setAttribute('aria-hidden', 'false'); 15230 if (successEl) { 15231 successEl.classList.remove('visible'); 15232 successEl.textContent = ''; 15233 } 15234 if (errorEl) { 15235 errorEl.classList.remove('visible'); 15236 errorEl.textContent = ''; 15237 } 15238 form.reset(); 15239 form.querySelectorAll('input, textarea, select').forEach(function (el) { 15240 el.disabled = false; 15241 }); 15242 if (btnSubmit) { 15243 btnSubmit.style.display = ''; 15244 btnSubmit.disabled = false; 15245 } 15246 var em = document.getElementById('det-contact-email'); 15247 if (em) em.focus(); 15248 } 15249 15250 function closeM() { 15251 overlay.classList.remove('visible'); 15252 overlay.setAttribute('aria-hidden', 'true'); 15253 } 15254 15255 btnOpen.addEventListener('click', openM); 15256 if (btnCancel) btnCancel.addEventListener('click', closeM); 15257 overlay.addEventListener('click', function (e) { 15258 if (e.target === overlay) closeM(); 15259 }); 15260 document.addEventListener('keydown', function (e) { 15261 if (e.key === 'Escape' && overlay.classList.contains('visible')) closeM(); 15262 }); 15263 15264 form.addEventListener('submit', function (e) { 15265 e.preventDefault(); 15266 var email = (document.getElementById('det-contact-email').value || '').trim(); 15267 var mensagemRaw = (document.getElementById('det-contact-mensagem').value || '').trim(); 15268 var nome = (document.getElementById('det-contact-nome').value || '').trim(); 15269 var tipoEl = document.getElementById('det-contact-tipo'); 15270 var tipo = tipoEl ? (tipoEl.value || '').trim() : ''; 15271 var empresa = (document.getElementById('det-contact-empresa').value || '').trim(); 15272 var assunto = (document.getElementById('det-contact-assunto').value || '').trim(); 15273 if (!email || mensagemRaw.length < 5 || !tipo) { 15274 if (errorEl) { 15275 errorEl.textContent = 15276 'Preencha o e-mail, o motivo da mensagem e o texto (mÃnimo 5 caracteres).'; 15277 errorEl.classList.add('visible'); 15278 } 15279 return; 15280 } 15281 var tipoLabel = 15282 tipo === 'parceria' 15283 ? 'Parceria / melhorar Ãndices de monitorização' 15284 : tipo === 'problema' 15285 ? 'Problema ou dúvida sobre o Detector' 15286 : 'Outro assunto'; 15287 var bodyLines = [ 15288 'â Mensagem à Catellix · origem: Catellix Detector (formulário público) â', 15289 'Página: https://catellix.com/detector', 15290 '', 15291 'Motivo: ' + tipoLabel, 15292 ]; 15293 if (empresa) bodyLines.push('Empresa / projeto: ' + empresa); 15294 if (assunto) bodyLines.push('Assunto: ' + assunto); 15295 bodyLines.push('', mensagemRaw); 15296 var fullMsg = bodyLines.join('\n'); 15297 if (fullMsg.length > 4000) { 15298 fullMsg = fullMsg.slice(0, 4000); 15299 } 15300 if (btnSubmit) btnSubmit.disabled = true; 15301 if (errorEl) errorEl.classList.remove('visible'); 15302 var origin = window.location.origin || ''; 15303 fetch(origin + '/api/public/chat-contact', { 15304 method: 'POST', 15305 headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, 15306 credentials: 'omit', 15307 body: JSON.stringify({ 15308 departamento: 'suporte', 15309 email: email, 15310 mensagem: fullMsg, 15311 origem: 'detector', 15312 nome: nome || undefined, 15313 assunto: assunto || undefined, 15314 }), 15315 }) 15316 .then(function (r) { 15317 return r.json().then(function (d) { 15318 return { ok: r.ok, data: d }; 15319 }); 15320 }) 15321 .then(function (pack) { 15322 if (btnSubmit) btnSubmit.disabled = false;
15323 if (pack.ok && pack.data.success && pack.data.protocolo) { 15324 if (successEl) { 15325 successEl.innerHTML = 15326 '<strong>Mensagem enviada à Catellix.</strong> Referência: <strong>' + 15327 esc(String(pack.data.protocolo)) + 15328 '</strong>. ' + 15329 esc(pack.data.message || 'Confirme a sua caixa de e-mail.'); 15330 successEl.classList.add('visible'); 15331 } 15332 form.querySelectorAll('input, textarea, select').forEach(function (el) { 15333 el.disabled = true; 15334 }); 15335 if (btnSubmit) btnSubmit.style.display = 'none'; 15336 } else { 15337 if (errorEl) { 15338 errorEl.textContent = 15339 (pack.data && pack.data.detail) || 15340 (pack.data && pack.data.message) || 15341 'Erro ao enviar. Tente novamente ou escreva para [email protected].'; 15342 errorEl.classList.add('visible'); 15343 } 15344 } 15345 }) 15346 .catch(function () { 15347 if (btnSubmit) btnSubmit.disabled = false; 15348 if (errorEl) { 15349 errorEl.textContent = 15350 'Erro de rede. Tente novamente ou contacte [email protected].'; 15351 errorEl.classList.add('visible'); 15352 } 15353 }); 15354 }); 15355 } 15356 15357 function refreshDetectorDynamicI18n() { 15358 fillRankSelectOptions(); 15359 rebuildCategorySelect(); 15360 updateMetricsBar(); 15361 applyRankMoodUI(); 15362 syncSidebarComplaintsToolbarLabels(); 15363 refreshSidebarComplaintsTop10(); 15364 updateCatalogFilterBanner(); 15365 refreshShelfHints(); 15366 applyFilterSections(); 15367 if (state.catalog && state.catalog.length) { 15368 mountShelves(); 15369 mountRankPanels(); 15370 mountProblemsCatalog(); 15371 mountMobileCommunityTop10Board(); 15372 if (!markLazySectionDirty('top10Boards')) mountTop10Boards(); 15373 mountTop10Cards(); 15374 mountDd(); 15375 mountIcmpNoReplySection(); 15376 syncComplaintsTop10BoardsAndCards(); 15377 } 15378 updateRefreshCountdownUi(); 15379 updateTop30OpenLabels(); 15380 updateHeroTagline(); 15381 try { 15382 syncDetectorAdSlotsCountUi(); 15383 } catch (eSlots) { 15384 /* ignore */ 15385 } 15386 try { 15387 renderTop30List((top30State.cachePayload && top30State.cachePayload.items) || []); 15388 } catch (eT30) { 15389 /* ignore */ 15390 } 15391 var crModal = document.getElementById('communityReportModal'); 15392 if (crModal && crModal.classList.contains('open') && state.crSvcId) { 15393 applyCrQuestionLead(!!state.crParentReportId); 15394 applyCrVerifyLeadText(); 15395 } 15396 } 15397 15398 window.detectorOnLocaleChange = function () { 15399 updateDetThemeToggleUi(); 15400 refreshDetectorDynamicI18n(); 15401 try { 15402 syncDetectorAdSlotsCountUi(); 15403 } catch (eSlotsI18n) { 15404 /* ignore */ 15405 } 15406 try { 15407 mergeDetectorCarouselSlotDefaults(); 15408 invalidateCarouselSlotHash(); 15409 invalidateCarouselSlotHash('R'); 15410 initDetectorAdSlotsCarousel(); 15411 if (!isMapaAdSlotsPage) { 15412 initDetectorAdSlotsCarouselRight(); 15413 } 15414 initDetectorTopBarLocalSpin(); 15415 } catch (eLocTools) { 15416 /* ignore */ 15417 } 15418 try { 15419 var adPack = document.getElementById('detAdPackModal'); 15420 if (adPack && !adPack.hidden && adPack.getAttribute('aria-hidden') !== 'true') { 15421 refreshDetectorAdPackModalContent(); 15422 } 15423 } catch (eAdPackLoc) { 15424 /* ignore */ 15425 } 15426 }; 15427 15428 if (!isMapaAdSlotsPage) { 15429 var secProbCatalog = document.getElementById('secProbCatalog'); 15430 if (secProbCatalog && secProbCatalog.dataset.detectorProbClick !== '1') { 15431 secProbCatalog.dataset.detectorProbClick = '1'; 15432 secProbCatalog.addEventListener('click', function (e) { 15433 var btn = e.target && e.target.closest ? e.target.closest('button.rank-line[data-svc-id]') : null; 15434 if (!btn) return; 15435 var id = btn.getAttribute('data-svc-id'); 15436 if (id) openModal(id); 15437 }); 15438 } 15439 15440 initDetectorContactForm(); 15441 (function initDetectorFeedbackReplyButtons() { 15442 document.addEventListener('click', function (e) { 15443 var t = e.target; 15444 var btn = t && t.closest ? t.closest('.det-fb-reply-btn') : null; 15445 if (!btn) return; 15446 var sid = btn.getAttribute('data-det-reply-svc'); 15447 var pid = btn.getAttribute('data-det-reply-parent'); 15448 if (!sid || !pid) return; 15449 e.preventDefault(); 15450 var n = parseInt(pid, 10); 15451 if (!isFinite(n) || n < 1) return; 15452 openCommunityReportModal(sid, n); 15453 }); 15454 })(); 15455 document.addEventListener('click', function (e) { 15456 var a = e.target && e.target.closest && e.target.closest('a.cr-open-det-explainer'); 15457 if (!a) return; 15458 e.preventDefault(); 15459 if (typeof window.detOpenPlatformExplainer === 'function') { 15460 window.detOpenPlatformExplainer(); 15461 } 15462 }); 15463 wireReportPickerModal(); 15464 wireComplaintsHourlyMicroModal(); 15465 wireTop30OutageModal(); 15466 wireDetectorRankingStudioAndReport(); 15467 wireTop10SocialShareModal(); 15468 updateTop30OpenLabels(); 15469 initDetectorBarMascoteVideo(); 15470 initRankSelect(); 15471 wireCategorySelect(); 15472 wireIpStackSelect(); 15473 wireCatalogSearchInput(); 15474 wireCatalogSearchModal(); 15475 wireRankMoodToggle();
15476 wireRefreshIntervalSelect(); 15477 wireSidebarComplaintsRanking(); 15478 startCommunityRankingPoll(); 15479 var btnCatalogHome = document.getElementById('btnCatalogHome'); 15480 if (btnCatalogHome && btnCatalogHome.dataset.detectorWired !== '1') { 15481 btnCatalogHome.dataset.detectorWired = '1'; 15482 btnCatalogHome.addEventListener('click', function () { 15483 resetCatalogHomeView(); 15484 }); 15485 } 15486 wireDetThemeToggle(); 15487 wireDetComplaintsViewportRelocate(); 15488 (function wireMobileGamesTop10ShareDelegate() { 15489 var mount = document.getElementById('detMobileGamesBoardMount'); 15490 if (!mount || mount.dataset.detGamesShareDel === '1') return; 15491 mount.dataset.detGamesShareDel = '1'; 15492 mount.addEventListener( 15493 'click', 15494 function (e) { 15495 var t = e.target; 15496 var btn = t && t.closest ? t.closest('.det-right-games-board__share') : null; 15497 if (!btn || !mount.contains(btn)) return; 15498 e.preventDefault(); 15499 e.stopPropagation(); 15500 openTop10GamesSocialShareModal(); 15501 }, 15502 false 15503 ); 15504 })(); 15505 wireDetRefreshToolbarPlacement(); 15506 if (typeof window.detectorWireLocaleToggle === 'function') { 15507 window.detectorWireLocaleToggle(); 15508 } 15509 if (typeof window.detectorApplyDomI18n === 'function') { 15510 window.detectorApplyDomI18n(document); 15511 } 15512 applyCrQuestionLead(false); 15513 applyCrVerifyLeadText(); 15514 updateCatalogViewToggleBtn(); 15515 updateHeroTagline(); 15516 syncSidebarComplaintsToolbarLabels(); 15517 requestAnimationFrame(function () { 15518 if (communityRowsWithVotesForReport().length > 0) { 15519 renderTop30List(); 15520 clearTop30ErrorState(); 15521 } else { 15522 fetchTop30Services(false) 15523 .then(function (data) { 15524 renderTop30List((data && data.items) || []); 15525 clearTop30ErrorState(); 15526 }) 15527 .catch(function () { 15528 renderTop30List(); 15529 var tList = document.getElementById('detTop30List'); 15530 var hasRows = tList && tList.children && tList.children.length > 0; 15531 if (!hasRows) { 15532 showTop30ErrorState(); 15533 } 15534 }); 15535 } 15536 }); 15537 registerLazySection('top10Boards', mountTop10Boards); 15538 registerLazySection('shelvesHost', mountCatalogGridAndIcmpSection); 15539 loadAll(); 15540 15541 window.addEventListener( 15542 'resize', 15543 function () { 15544 applyViewToShelves(); 15545 var narrowNow = DET_COMPLAINTS_VIEWPORT_MQ.matches; 15546 if (narrowNow !== _detTop10CardsViewportNarrow) { 15547 _detTop10CardsViewportNarrow = narrowNow; 15548 mountTop10Cards(); 15549 } 15550 }, 15551 { passive: true }, 15552 ); 15553 15554 /* ââ Live Report Toasts (SSE) ââ */ 15555 (function initLiveToasts() { 15556 var rail = document.getElementById('liveToastRail'); 15557 if (!rail) return; 15558 var MAX_TOASTS = 4; 15559 var TOAST_MS = 3500; 15560 15561 function showToast(data) { 15562 if (!data || data.type !== 'report') return; 15563 var svcName = data.service_name || data.service_id || '?'; 15564 var who = data.who 15565 ? '<strong>' + esc(data.who) + '</strong>' 15566 : esc(__('js_anonymous')); 15567 var el = document.createElement('div'); 15568 el.className = 'live-toast'; 15569 el.innerHTML = 15570 '<div class="live-toast-icon">⚠</div>' + 15571 '<div class="live-toast-body">' + 15572 '<div class="live-toast-title">' + 15573 esc(__('toast_title')) + 15574 '</div>' + 15575 '<div class="live-toast-svc">' + esc(svcName) + '</div>' + 15576 '<div class="live-toast-who">' + 15577 esc(__('toast_by')) + 15578 ' ' + 15579 who + 15580 '</div>' + 15581 '<div class="live-toast-bar"></div>' + 15582 '</div>'; 15583 rail.appendChild(el); 15584 while (rail.children.length > MAX_TOASTS) { 15585 rail.removeChild(rail.firstChild); 15586 } 15587 setTimeout(function () { 15588 el.classList.add('out'); 15589 setTimeout(function () { 15590 if (el.parentNode) el.parentNode.removeChild(el); 15591 }, 350); 15592 }, TOAST_MS); 15593 } 15594 15595 function connect() { 15596 var origin = window.location.origin || ''; 15597 var es; 15598 try { 15599 es = new EventSource(origin + '/api/detector/live-reports'); 15600 } catch (e) { return; } 15601 es.onmessage = function (ev) { 15602 try { 15603 var d = JSON.parse(ev.data); 15604 if (d.type === 'report' && document.visibilityState === 'visible') { 15605 showToast(d); 15606 } 15607 } catch (e) {} 15608 }; 15609 es.onerror = function () { 15610 es.close(); 15611 setTimeout(connect, 15000); 15612 }; 15613 } 15614 connect(); 15615 })(); 15616 } /* fim: init completo (não usado no WiâFi MAP) */ 15617 15618 /** WiâFi MAP: mesma faixa premium do Detector (R$ 20/dia â `openDetectorAdPackModal('right')`). */ 15619 window.CatellixOpenDetectorAdPackForMapa = function () { 15620 openDetectorAdPackModal('right'); 15621 }; 15622 15623 window.CatellixBootstrapMapaAdSlots = function () { 15624 if (!isMapaAdSlotsPage) { 15625 return Promise.resolve(); 15626 } 15627 var origin = window.location.origin || ''; 15628 return fetch(origin + '/api/detector/catalog', { credentials: 'omit' }) 15629 .then(function (r) { 15630 return r.json().catch(function () { 15631 return {}; 15632 }); 15633 }) 15634 .then(function (cat) { 15635 if (cat && cat.success) { 15636 try { 15637 applyDetectorAdSlotsFromCatalog(cat); 15638 } catch (e1) { 15639 try { 15640 mergeDetectorCarouselSlotDefaults(); 15641 syncDetectorAdSlotsCountUi(); 15642 } catch (e1b) { 15643 /* ignore */ 15644 } 15645 } 15646 } else { 15647 try { 15648 mergeDetectorCarouselSlotDefaults(); 15649 syncDetectorAdSlotsCountUi(); 15650 } catch (e2) { 15651 /* ignore */ 15652 } 15653 } 15654 try { 15655 initDetectorAdSlotsCarousel(); 15656 } catch (e3) { 15657 /* ignore */ 15658 } 15659 }) 15660 .catch(function () { 15661 try { 15662 mergeDetectorCarouselSlotDefaults(); 15663 syncDetectorAdSlotsCountUi(); 15664 } catch (e5) { 15665 /* ignore */ 15666 } 15667 try { 15668 initDetectorAdSlotsCarousel(); 15669 } catch (e6) { 15670 /* ignore */ 15671 } 15672 }); 15673 }; 15674})();
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.