1const FORECAST_API_URL = "/api/forecast"; 2const { 3 DEFAULT_LATEST_RESET_AT, 4 cleanPost, 5 eventTerms, 6 imminentGpt56ReleaseTerms, 7 isActive, 8 isCodex, 9 publishedAt, 10 releaseTerms, 11 vaguepostTerms, 12} = globalThis.CodexForecast; 13const TIBO_PROFILE_URL = "https://x.com/thsottiaux/"; 14const PUBLIC_APP_URL = "https://www.willcodexquotareset.com/"; 15const ANALYTICS_MEASUREMENT_ID = "G-MJH801QS19"; 16const ANALYTICS_CONSENT_KEY = "analytics-consent"; 17const ANALYTICS_DISABLE_KEY = `ga-disable-${ANALYTICS_MEASUREMENT_ID}`; 18 19const fallbackIncidents = [ 20 { 21 name: "Increased latency for Codex compaction for a subset of users", 22 status: "identified", 23 created_at: "2026-06-04T02:57:25Z", 24 }, 25 { 26 name: "Elevated error rates on Codex, ChatGPT and Responses API", 27 status: "resolved", 28 created_at: "2026-06-03T18:09:00Z", 29 }, 30]; 31 32const elements = { 33 advice: document.querySelector("#advice-copy"), 34 analyticsAccept: document.querySelector("#analytics-accept"), 35 analyticsConsent: document.querySelector("#analytics-consent"), 36 analyticsDecline: document.querySelector("#analytics-decline"), 37 analyticsSettings: document.querySelector("#analytics-settings"), 38 breakdown: document.querySelector("#score-breakdown"), 39 daysSinceReset: document.querySelector("#days-since-reset"), 40 gauge: document.querySelector("#gauge"), 41 headerDot: document.querySelector("#header-status-dot"), 42 headerStatus: document.querySelector("#header-status-label"), 43 headlineSignal: document.querySelector("#headline-signal"), 44 headlineSignalCopy: document.querySelector("#headline-signal-copy"), 45 headlineSignalLabel: document.querySelector("#headline-signal-label"), 46 headlineSignalLink: document.querySelector("#headline-signal-link"), 47 headlineSignalReason: document.querySelector("#headline-signal-reason"), 48 lastChecked: document.querySelector("#last-checked-label"), 49 notificationEmail: document.querySelector("#notification-email"), 50 notificationForm: document.querySelector("#notification-form"), 51 notificationStatus: document.querySelector("#notification-status"), 52 notificationWebsite: document.querySelector("#notification-website"), 53 releaseEventCopy: document.querySelector("#release-event-copy"), 54 releaseEventTitle: document.querySelector("#release-event-title"), 55 riskTitle: document.querySelector("#risk-title"), 56 scoreHistory: document.querySelector("#score-history-list"), 57 shareButton: document.querySelector("#share-button"), 58 statusCopy: document.querySelector("#status-copy"), 59 statusTitle: document.querySelector("#status-title"), 60 summary: document.querySelector("#forecast-summary"), 61 tiboCopy: document.querySelector("#tibo-copy"), 62 tiboClassifier: document.querySelector("#tibo-classifier"), 63 tiboLink: document.querySelector("#tibo-link"), 64 tiboTitle: document.querySelector("#tibo-title"), 65 tiboTweet: document.querySelector("#tibo-tweet"), 66 tiboTweetCopy: document.querySelector("#tibo-tweet-copy"), 67 themeIcon: document.querySelector(".theme-icon"), 68 themeLabel: document.querySelector("#theme-label"), 69 themeToggle: document.querySelector("#theme-toggle"), 70}; 71 72function savedAnalyticsConsent() { 73 try { 74 return localStorage.getItem(ANALYTICS_CONSENT_KEY); 75 } catch { 76 return null; 77 } 78} 79 80function loadGoogleAnalytics({ sendPageView = false } = {}) { 81 window[ANALYTICS_DISABLE_KEY] = false; 82 if (window.googleAnalyticsLoaded) { 83 window.gtag("consent", "update", { 84 ad_personalization: "denied", 85 ad_storage: "denied", 86 ad_user_data: "denied", 87 analytics_storage: "granted", 88 }); 89 if (sendPageView) window.gtag("event", "page_view"); 90 return; 91 } 92 window.googleAnalyticsLoaded = true; 93 window.dataLayer = window.dataLayer || []; 94 window.gtag = function gtag() { 95 window.dataLayer.push(arguments); 96 }; 97 98 const tag = document.createElement("script"); 99 tag.async = true; 100 tag.src = `https://www.googletagmanager.com/gtag/js?id=${ANALYTICS_MEASUREMENT_ID}`; 101 document.head.append(tag); 102 103 window.gtag("consent", "default", { 104 ad_personalization: "denied", 105 ad_storage: "denied", 106 ad_user_data: "denied", 107 analytics_storage: "granted", 108 }); 109 window.gtag("js", new Date()); 110 window.gtag("config", ANALYTICS_MEASUREMENT_ID); 111} 112 113function disableGoogleAnalytics() { 114 window[ANALYTICS_DISABLE_KEY] = true; 115 if (window.gtag) { 116 window.gtag("consent", "update", { 117 ad_personalization: "denied", 118 ad_storage: "denied", 119 ad_user_data: "denied", 120 analytics_storage: "denied", 121 }); 122 } 123 124 const cookieNames = new Set(["_ga", `_ga_${ANALYTICS_MEASUREMENT_ID.slice(2)}`]); 125 for (const cookie of String(document.cookie || "").split(";")) { 126 const name = cookie.split("=", 1)[0].trim(); 127 if (name === "_ga" || name.startsWith("_ga_")) cookieNames.add(name); 128 } 129 130 const hostname = window.location.hostname; 131 const rootDomain = hostname.split(".").slice(-2).join("."); 132 const domainAttributes = ["", `; Domain=${hostname}`, `; Domain=.${rootDomain}`]; 133 for (const name of cookieNames) { 134 for (const domain of domainAttributes) { 135 document.cookie = `${name}=; Max-Age=0; Path=/; SameSite=Lax${domain}`; 136 } 137 } 138} 139 140function chooseAnalyticsConsent(choice) { 141 const previousChoice = savedAnalyticsConsent(); 142 try { 143 localStorage.setItem(ANALYTICS_CONSENT_KEY, choice); 144 } catch {} 145 elements.analyticsConsent.hidden = true; 146 if (choice === "granted") { 147 loadGoogleAnalytics({ sendPageView: previousChoice === "denied" }); 148 } else { 149 disableGoogleAnalytics(); 150 } 151} 152 153const analyticsConsent = savedAnalyticsConsent(); 154if (analyticsConsent === "granted") { 155 loadGoogleAnalytics(); 156} else if (analyticsConsent !== "denied") { 157 elements.analyticsConsent.hidden = false;
158} 159 160function savedTheme() { 161 try { 162 return localStorage.getItem("theme"); 163 } catch { 164 return null; 165 } 166} 167 168function renderTheme(theme) { 169 const isDark = theme === "dark"; 170 document.documentElement.dataset.theme = isDark ? "dark" : "light"; 171 elements.themeToggle.setAttribute("aria-pressed", String(isDark)); 172 elements.themeToggle.setAttribute( 173 "aria-label", 174 `Switch to ${isDark ? "light" : "dark"} mode`, 175 ); 176 elements.themeIcon.textContent = isDark ? "â" : "â¾"; 177 elements.themeLabel.textContent = isDark ? "Light mode" : "Dark mode"; 178} 179 180function toggleTheme() { 181 const theme = document.documentElement.dataset.theme === "dark" ? "light" : "dark"; 182 renderTheme(theme); 183 try { 184 localStorage.setItem("theme", theme); 185 } catch {} 186} 187 188renderTheme(savedTheme() === "dark" ? "dark" : "light"); 189 190let latestIncidents = fallbackIncidents; 191let latestForecastHistory = []; 192let latestResetAt = DEFAULT_LATEST_RESET_AT; 193let latestTiboSignal = emptyTiboSignal(); 194let refreshInFlight = false; 195let notificationFormStartedAt = Date.now(); 196 197const notificationMessages = { 198 confirmed: "Alerts are active. You will get an email when likelihood reaches 70% and when a quota reset is announced.", 199 invalid: "That alert link is invalid or has expired.", 200 unsubscribed: "Email alerts have been turned off.", 201}; 202 203function hoursSinceDate(date) { 204 return Math.max(0, (Date.now() - date.getTime()) / 36e5); 205} 206 207function shortAge(hours) { 208 if (hours < 1) return `${Math.max(1, Math.round(hours * 60))}m`; 209 if (hours < 48) return `${Math.round(hours)}h`; 210 return `${Math.round(hours / 24)}d`; 211} 212 213function modelRead(post) { 214 if (post?.tweetAssessment) { 215 return { 216 confidence: post.tweetAssessment.resetSignalStrength / 100, 217 reason: post.tweetAssessment.reason, 218 }; 219 } 220 return post?.vaguepostAssessment || null; 221} 222 223function visiblePostText(text) { 224 return cleanPost(text) 225 .replace(/^(?:@\w+\s+)+/, "") 226 .replace(/^hi[.!]?\s*/i, ""); 227} 228 229function tiboPostUrl(post) { 230 try { 231 const url = new URL(post?.link); 232 const isXHost = url.hostname === "x.com" || url.hostname === "twitter.com"; 233 const isTiboPost = /^\/thsottiaux\/status\/\d+\/?$/.test(url.pathname); 234 return isXHost && isTiboPost ? url.href : null; 235 } catch { 236 return null; 237 } 238} 239 240function renderTiboLink(post) { 241 const postUrl = tiboPostUrl(post); 242 elements.tiboLink.hidden = false; 243 elements.tiboLink.href = postUrl || TIBO_PROFILE_URL; 244 elements.tiboLink.textContent = postUrl ? "Open tweet on X â" : "Open @thsottiaux on X â"; 245} 246 247function renderTiboTweet(post) { 248 const assessment = modelRead(post); 249 elements.tiboTweet.hidden = !post; 250 elements.tiboCopy.hidden = Boolean(post); 251 elements.tiboTweetCopy.textContent = post ? visiblePostText(post.title) : ""; 252 elements.tiboClassifier.hidden = !assessment; 253 elements.tiboClassifier.textContent = assessment 254 ? `Model read · ${Math.round(assessment.confidence * 100)}% · ${assessment.reason}` 255 : ""; 256} 257 258function signalPostDate(post) { 259 const value = 260 post?.resetStatus === "pending" && post?.activationAt 261 ? post.activationAt 262 : post?.resetStatus === "completed" && post?.effectiveAt 263 ? post.effectiveAt 264 : post?.publishedAt || post?.pubDate || 0; 265 const date = value instanceof Date ? value : new Date(value); 266 return Number.isNaN(date.getTime()) ? new Date(0) : date; 267} 268 269function matchedSignalReason(post, terms, label) { 270 const match = cleanPost(post?.title).match(terms)?.[0]; 271 return match ? `Matched ${label} language: "${match}".` : `Matched ${label} language.`; 272} 273 274function latestHeadlineSignal(signal = emptyTiboSignal()) { 275 const candidates = [ 276 ...[ 277 ...(signal.pendingResetAnnouncements || []), 278 ...(signal.completedResetAnnouncements || []).filter( 279 (post) => post.source !== "confirmed-observation", 280 ), 281 ] 282 .filter((post) => hoursSinceDate(signalPostDate(post)) <= 72) 283 .map((post) => ({ 284 label: 285 post.resetStatus === "pending" 286 ? "LATEST RESET ANNOUNCEMENT" 287 : "LATEST COMPLETED RESET POST", 288 post, 289 reason: 290 post.resetStatus === "pending" 291 ? "Explicit upcoming Codex quota-reset announcement. It has not happened yet." 292 : "Explicit completed Codex quota-reset post.", 293 })),
294 ...(signal.releaseHints || []).map((post) => ({ 295 label: "LATEST PRODUCT-RELEASE HINT", 296 post, 297 reason: matchedSignalReason(post, releaseTerms, "product-release"), 298 })), 299 ...(signal.eventHints || []).map((post) => ({ 300 label: "LATEST OPENAI EVENT HINT", 301 post, 302 reason: matchedSignalReason(post, eventTerms, "event"), 303 })), 304 ...(signal.vagueposts || []).map((post) => { 305 const assessment = modelRead(post); 306 return { 307 label: 308 post.tweetAssessment?.category === "reset_proposal" 309 ? "LATEST RESET PROPOSAL" 310 : "LATEST VAGUEPOST SIGNAL", 311 post, 312 reason: assessment 313 ? `Model read · ${Math.round(assessment.confidence * 100)}% · ${assessment.reason}` 314 : "Matched Codex/reset hint language.", 315 }; 316 }), 317 ]; 318 319 return candidates.sort( 320 (a, b) => signalPostDate(b.post).getTime() - signalPostDate(a.post).getTime(), 321 )[0] || null; 322} 323 324function renderHeadlineSignal(signal) { 325 const selection = latestHeadlineSignal(signal); 326 const post = selection?.post; 327 const postUrl = tiboPostUrl(post); 328 elements.headlineSignal.hidden = !post; 329 elements.headlineSignalLabel.textContent = selection?.label || "LATEST TIBO SIGNAL"; 330 elements.headlineSignalCopy.textContent = post ? visiblePostText(post.title) : ""; 331 elements.headlineSignalLink.href = postUrl || TIBO_PROFILE_URL; 332 elements.headlineSignalLink.textContent = postUrl 333 ? "Open tweet on X â" 334 : "Open @thsottiaux on X â"; 335 elements.headlineSignalReason.hidden = !selection?.reason; 336 elements.headlineSignalReason.textContent = selection?.reason || ""; 337} 338 339function emptyTiboSignal() { 340 return { 341 completedResetAnnouncements: [], 342 eventHints: [], 343 pendingResetAnnouncements: [], 344 releaseHints: [], 345 resetAnnouncements: [], 346 vagueposts: [], 347 }; 348} 349 350function hydrateTiboSignal(signal) { 351 if (!signal || typeof signal !== "object") return null; 352 const dateKeys = [ 353 "completedResetAnnouncements", 354 "eventHints", 355 "pendingResetAnnouncements", 356 "releaseHints", 357 "resetAnnouncements", 358 "vagueposts", 359 ]; 360 const hydrated = { ...signal }; 361 362 for (const key of dateKeys) { 363 hydrated[key] = (Array.isArray(signal[key]) ? signal[key] : []).map((post) => ({ 364 ...post, 365 activationAt: post.activationAt ? new Date(post.activationAt) : null, 366 effectiveAt: post.effectiveAt ? new Date(post.effectiveAt) : null, 367 publishedAt: new Date(post.publishedAt || post.pubDate), 368 })); 369 } 370 if (signal.latestResetAt) hydrated.latestResetAt = new Date(signal.latestResetAt); 371 return hydrated; 372} 373 374// Both live server calculations and browser fallbacks use these same pure modules. 375function readTiboSignal(items, now = new Date(Date.now())) { 376 const signal = globalThis.CodexForecast.readTiboSignal(items, { latestResetAt, now }); 377 latestResetAt = signal.latestResetAt; 378 return signal; 379} 380 381function resetAnnouncementStatus(post, now = new Date(Date.now())) { 382 return globalThis.CodexForecast.resetAnnouncementStatus(post, now); 383} 384 385function calculateForecast(incidents) { 386 return globalThis.CodexForecast.calculateForecast( 387 incidents, 388 { ...latestTiboSignal, latestResetAt }, 389 new Date(Date.now()), 390 ); 391} 392 393function getCopy(forecast) { 394 if (forecast.resetAnnounced) { 395 return { 396 advice: "Treat the forecast as certain, but do not count the new quota until it lands.", 397 summary: 398 "Tibo announced a Codex rate-limit reset in the next 48 hours. It has not happened yet, so the reset clock and cooldown have not moved.", 399 title: "Reset announced.", 400 }; 401 } 402 if (forecast.hoursSinceReset < 24 && forecast.evidenceTier !== "proposal") { 403 return { 404 advice: "Tibo already pressed it. Spend responsibly, or do not.", 405 summary: `The latest Codex quota reset was confirmed ${shortAge(forecast.hoursSinceReset)} ago. This percentage estimates the chance of another reset in the next 48 hours.`, 406 title: "It already reset.", 407 }; 408 } 409 if (forecast.score >= 72) { 410 return { 411 advice: "Find a suspiciously token-hungry side project.", 412 summary: "There is explicit talk of another reset, with supporting signals. It is still a proposal, not an announcement.", 413 title: "Use it or potentially lose it.", 414 }; 415 } 416 if (forecast.score >= 48) { 417 return { 418 advice: "Maybe stop hoarding. Keep a meaty task nearby.", 419 summary: "There are enough signals to raise an eyebrow, but nothing is guaranteed.", 420 title: "Worth a tactical token burn.", 421 }; 422 } 423 if (forecast.score >= 26) { 424 return { 425 advice: "Normal building conditions. Check Tibo before panic-spending.", 426 summary: "Some signals are present, but the public data is not making a strong case yet.", 427 title: "Do not force it.", 428 }; 429 } 430 return { 431 advice: "Keep your tokens. The reset button is having a quiet afternoon.", 432 summary: "The incident desk is quiet, or a reset happened too recently.", 433 title: "Probably not today.", 434 }; 435} 436 437function renderForecast(forecast) { 438 const copy = getCopy(forecast); 439 document.querySelector("#score-value").textContent = forecast.score; 440 elements.gauge.style.setProperty("--score", forecast.score); 441 elements.riskTitle.textContent = copy.title; 442 elements.summary.textContent = copy.summary; 443 elements.advice.textContent = copy.advice; 444 elements.daysSinceReset.textContent = forecast.daysSinceReset; 445 elements.breakdown.innerHTML = forecast.breakdown 446 .map( 447 (item) => 448 item.label === "public reset announcement" 449 ? '<span class="chip chip-positive">100% public reset announcement</span>' 450 : `<span class="chip ${item.points > 0 ? "chip-positive" : "chip-negative"}">${ 451 item.points > 0 ? "+" : "" 452 }${item.points} ${item.label}</span>`, 453 ) 454 .join(""); 455} 456 457function shareChangeCopy(history = []) { 458 const labelCopy = { 459 "Codex evidence cap rebalanced": "older Codex evidence made room for fresher reports", 460 "Codex issues in last 3 days": "recent Codex issues", 461 "Codex issues 3-7 days ago": "Codex issues from earlier this week", 462 "OpenAI event hint": "an OpenAI event hint", 463 "OpenAI team vagueposting": "Tibo vagueposted", 464 "high-pain wording": "high-pain Codex wording", 465 "major incident severity": "a major Codex incident", 466 "other incidents": "other OpenAI incidents", 467 "public reset announcement": "Tibo announced a reset", 468 "confirmed reset": "the Codex quota reset landed", 469 "product-release hint": "a product-release hint", 470 "imminent GPT-5.6 release": "an imminent GPT-5.6 release", 471 "recent Codex pain": "recent Codex pain", 472 "recent-reset cooldown": "reset cooldown eased", 473 "reset proposal": "Tibo proposed another reset", 474 "uncertainty ceiling": "the estimate stays below announcement-level confidence",
475 "time since last reset": "another day without a reset", 476 }; 477 const seen = new Set(); 478 479 return history 480 .flatMap((entry) => entry.changes || []) 481 .filter((change) => { 482 if (seen.has(change.label)) return false; 483 seen.add(change.label); 484 return change.label !== "baseline"; 485 }) 486 .slice(0, 2) 487 .map((change) => { 488 const label = labelCopy[change.label] || change.label; 489 return `${label} (${change.delta > 0 ? "+" : ""}${change.delta})`; 490 }) 491 .join("; "); 492} 493 494function shareJoke(score) { 495 if (score >= 72) return "My tokens have entered witness protection."; 496 if (score >= 48) return "I am eyeing every unnecessary refactor."; 497 if (score >= 26) return "My tokens are wearing tiny life jackets."; 498 return "The reset button remains emotionally unavailable."; 499} 500 501function forecastShareText() { 502 const score = Number(document.querySelector("#score-value").textContent) || 0; 503 const headlineSignal = latestHeadlineSignal(latestTiboSignal); 504 const movement = shareChangeCopy(latestForecastHistory); 505 const parts = [`Codex reset forecast: ${score}% in the next 48h.`]; 506 507 if (movement) parts.push(`Lately: ${movement}.`); 508 if (headlineSignal) { 509 parts.push(`Tibo: "${visiblePostText(headlineSignal.post.title).slice(0, 72)}"`); 510 } 511 parts.push(shareJoke(score)); 512 513 const suffix = `\n\n${PUBLIC_APP_URL}`; 514 return `${parts.join("\n\n").slice(0, 280 - suffix.length)}${suffix}`; 515} 516 517async function copyText(text) { 518 const textarea = document.createElement("textarea"); 519 textarea.value = text; 520 textarea.setAttribute("readonly", ""); 521 textarea.style.position = "fixed"; 522 textarea.style.opacity = "0"; 523 document.body.append(textarea); 524 textarea.select(); 525 const copied = document.execCommand("copy"); 526 textarea.remove(); 527 if (copied) return; 528 529 try { 530 await navigator.clipboard.writeText(text); 531 } catch { 532 throw new Error("Copy command failed"); 533 } 534} 535 536async function shareForecast() { 537 const originalText = "Copy forecast"; 538 539 try { 540 await copyText(forecastShareText()); 541 elements.shareButton.textContent = "Copied tweet"; 542 } catch { 543 elements.shareButton.textContent = "Copy failed"; 544 } 545 546 setTimeout(() => { 547 elements.shareButton.textContent = originalText; 548 }, 1800); 549} 550 551function formatHistoryTime(value) { 552 return new Intl.DateTimeFormat([], { 553 day: "numeric", 554 hour: "numeric", 555 minute: "2-digit", 556 month: "short", 557 }).format(new Date(value)); 558} 559 560function historyChangeLabel(change) { 561 const label = typeof change === "string" ? change : change.label; 562 const delta = typeof change === "string" ? 1 : change.delta; 563 const expired = 564 delta < 0 && 565 (change.details || []).some((detail) => detail.action === "Signal expired after 48h"); 566 let labels = { 567 "OpenAI event hint": "Tibo hinted at an OpenAI event", 568 "OpenAI team vagueposting": "Tibo posted a possible reset hint", 569 "public reset announcement": "Tibo announced an upcoming reset", 570 "reset proposal": "Tibo proposed another reset", 571 "product-release hint": "Tibo hinted at a product release", 572 "imminent GPT-5.6 release": "Tibo hinted at an imminent GPT-5.6 release", 573 }; 574 575 if (expired) { 576 labels = { 577 "OpenAI event hint": "Tibo's OpenAI event hint expired after 48h", 578 "OpenAI team vagueposting": "Tibo's possible reset hint expired after 48h", 579 "reset proposal": "Tibo's reset proposal expired after 48h", 580 "product-release hint": "Tibo's product-release hint expired after 48h", 581 "imminent GPT-5.6 release": "Tibo's imminent GPT-5.6 release hint expired after 48h", 582 }; 583 } else if (delta < 0) { 584 labels = { 585 "OpenAI event hint": "Tibo's OpenAI event hint stopped counting", 586 "OpenAI team vagueposting": "Tibo's possible reset hint stopped counting", 587 "reset proposal": "Tibo's reset proposal weakened", 588 "product-release hint": "Tibo's product-release hint stopped counting", 589 "imminent GPT-5.6 release": "Tibo's imminent GPT-5.6 release hint stopped counting", 590 }; 591 } 592 593 return labels[label] || label; 594} 595 596function tweetDetailUrl(detail) { 597 try { 598 const url = new URL(detail?.url); 599 const isXHost = ["x.com", "www.x.com", "twitter.com", "www.twitter.com"].includes( 600 url.hostname, 601 ); 602 const isStatus = /^\/[^/]+\/status\/\d+\/?$/.test(url.pathname); 603 return detail?.kind === "tweet" && isXHost && isStatus ? url.href : null; 604 } catch { 605 return null; 606 } 607} 608 609function renderHistoryTweet(detail, tweetUrl) { 610 const card = document.createElement("div"); 611 card.className = "score-history-tweet"; 612 613 const heading = document.createElement("div"); 614 heading.className = "score-history-tweet-heading"; 615 616 const author = document.createElement("span"); 617 author.className = "score-history-tweet-author"; 618 author.append("Tibo "); 619 620 const handle = document.createElement("b"); 621 handle.textContent = "@thsottiaux"; 622 author.append(handle); 623 624 const link = document.createElement("a"); 625 link.href = tweetUrl; 626 link.rel = "noreferrer"; 627 link.target = "_blank"; 628 link.textContent = "Open on X â"; 629 630 const quote = document.createElement("blockquote"); 631 quote.textContent = detail.name; 632 633 heading.append(author, link); 634 card.append(heading, quote); 635 return card; 636} 637 638function renderForecastHistory(history = []) { 639 latestForecastHistory = history; 640 elements.scoreHistory.replaceChildren(); 641 642 if (!history.length) { 643 const empty = document.createElement("li"); 644 empty.className = "score-history-empty"; 645 empty.textContent = "No score changes recorded in the last 7 days."; 646 elements.scoreHistory.append(empty); 647 return; 648 } 649 650 for (const entry of history) { 651 const item = document.createElement("li"); 652 item.className = "score-history-entry"; 653 654 const meta = document.createElement("div"); 655 meta.className = "score-history-meta"; 656 657 const time = document.createElement("time"); 658 time.dateTime = entry.at; 659 time.textContent = formatHistoryTime(entry.at); 660 661 const score = document.createElement("span"); 662 score.textContent = `${entry.fromScore}% â ${entry.toScore}%`; 663 meta.append(time, score); 664 665 const changes = document.createElement("ul"); 666 changes.className = "score-history-changes"; 667 668 for (const change of entry.changes) { 669 const changeItem = document.createElement("li"); 670 const delta = document.createElement("strong");
671 delta.className = change.delta > 0 ? "history-positive" : "history-negative"; 672 delta.textContent = `${change.delta > 0 ? "+" : ""}${change.delta} pts`; 673 674 const label = document.createElement("span"); 675 label.className = "score-history-label"; 676 label.textContent = historyChangeLabel(change); 677 678 const copy = document.createElement("div"); 679 copy.className = "score-history-copy"; 680 copy.append(label); 681 682 for (const detail of change.details || []) { 683 const tweetUrl = tweetDetailUrl(detail); 684 if (tweetUrl) { 685 copy.append(renderHistoryTweet(detail, tweetUrl)); 686 continue; 687 } 688 689 const description = document.createElement("small"); 690 description.className = "score-history-detail"; 691 description.append(`${detail.action}: `); 692 693 if (detail.url) { 694 const link = document.createElement("a"); 695 link.href = detail.url; 696 link.rel = "noreferrer"; 697 link.target = "_blank"; 698 link.textContent = detail.name; 699 description.append(link); 700 } else { 701 description.append(detail.name); 702 } 703 704 copy.append(description); 705 } 706 707 changeItem.append(delta, copy); 708 changes.append(changeItem); 709 } 710 711 item.append(meta, changes); 712 elements.scoreHistory.append(item); 713 } 714} 715
716function renderStatus(incidents, isFallback = false) { 717 const openIncidents = incidents.filter(isActive); 718 const openCodex = openIncidents.filter(isCodex); 719 const label = `${openIncidents.length} open incident${openIncidents.length === 1 ? "" : "s"}`; 720 721 elements.headerDot.className = `status-dot ${openIncidents.length ? "status-dot-bad" : "status-dot-good"}`; 722 elements.headerStatus.textContent = openIncidents.length 723 ? `OpenAI status: ${label}` 724 : "OpenAI status clear"; 725 elements.statusTitle.textContent = openIncidents.length ? label : "No open incidents"; 726 727 if (openCodex.length) { 728 elements.statusCopy.textContent = `${openCodex[0].name}.${isFallback ? " Cached fallback." : ""}`; 729 } else { 730 elements.statusCopy.textContent = isFallback 731 ? "The status source failed during the last server refresh." 732 : "No open Codex-specific incident in the cached official feed."; 733 } 734} 735 736function renderTiboSignal(signal, isFallback = false) { 737 const pendingReset = [...(signal.pendingResetAnnouncements || [])].sort( 738 (a, b) => signalPostDate(b) - signalPostDate(a), 739 )[0]; 740 const completedResets = [...(signal.completedResetAnnouncements || [])].sort( 741 (a, b) => signalPostDate(b) - signalPostDate(a), 742 ); 743 const completedReset = 744 completedResets.find((post) => post.source !== "confirmed-observation") || 745 completedResets[0]; 746 const vaguepost = (signal.vagueposts || [])[0]; 747 renderHeadlineSignal(signal); 748 749 if (pendingReset) { 750 elements.tiboTitle.textContent = `Tibo announced an upcoming reset ${shortAge( 751 hoursSinceDate(pendingReset.publishedAt), 752 )} ago`; 753 renderTiboTweet(pendingReset); 754 renderTiboLink(pendingReset); 755 return; 756 } 757 758 if (completedReset && hoursSinceDate(signalPostDate(completedReset)) <= 72) { 759 if (completedReset.source === "confirmed-observation") { 760 elements.tiboTitle.textContent = `Codex quota reset confirmed ${shortAge( 761 hoursSinceDate(completedReset.publishedAt), 762 )} ago`; 763 renderTiboTweet(); 764 elements.tiboCopy.textContent = 765 "The reset was confirmed from observed quota state; no completion post is available yet."; 766 elements.tiboLink.hidden = true; 767 return; 768 } 769 770 elements.tiboTitle.textContent = `Tibo posted a completed reset ${shortAge( 771 hoursSinceDate(signalPostDate(completedReset)), 772 )} ago`; 773 renderTiboTweet(completedReset); 774 renderTiboLink(completedReset); 775 return; 776 } 777 778 if (vaguepost) { 779 elements.tiboTitle.textContent = "Possible vaguepost detected"; 780 renderTiboTweet(vaguepost); 781 renderTiboLink(vaguepost); 782 return; 783 } 784 785 renderTiboTweet(); 786 renderTiboLink(); 787 elements.tiboTitle.textContent = isFallback ? "Feed unavailable" : "No fresh reset bait"; 788 elements.tiboCopy.textContent = isFallback 789 ? "The public post mirror failed during the last server refresh. Check @thsottiaux manually." 790 : "Best-effort public feed checked. Nothing obvious in the latest posts."; 791} 792 793function renderReleaseEventSignal(signal, isFallback = false) { 794 const hint = [ 795 ...signal.eventHints.map((post) => ({ post, title: "Possible event hint detected" })), 796 ...signal.releaseHints.map((post) => ({ post, title: "Possible release hint detected" })), 797 ].sort((a, b) => b.post.publishedAt - a.post.publishedAt)[0]; 798 799 if (hint) { 800 elements.releaseEventTitle.textContent = hint.title; 801 elements.releaseEventCopy.textContent = hint.post.title; 802 return; 803 } 804 805 elements.releaseEventTitle.textContent = isFallback ? "Party calendar unavailable" : "No launch confetti yet"; 806 elements.releaseEventCopy.textContent = isFallback 807 ? "The public post mirror failed during the last server refresh." 808 : "No fresh release, party, or event hints in the latest posts."; 809} 810 811async function refreshForecast() { 812 if (refreshInFlight) return; 813 refreshInFlight = true; 814 815 try { 816 const response = await fetch(FORECAST_API_URL, { headers: { Accept: "application/json" } });
817 if (!response.ok) throw new Error(`Forecast API returned ${response.status}`); 818 const data = await response.json(); 819 if (!data.forecast) throw new Error("Forecast API omitted the authoritative forecast"); 820 latestIncidents = data.incidents.length ? data.incidents : fallbackIncidents; 821 latestTiboSignal = hydrateTiboSignal(data.tiboSignal); 822 if (!latestTiboSignal) { 823 throw new Error("Forecast API omitted the authoritative Tibo signal"); 824 } 825 latestResetAt = new Date(data.forecast.latestResetAt); 826 827 renderStatus(latestIncidents, Boolean(data.sourceErrors.status)); 828 renderTiboSignal(latestTiboSignal, Boolean(data.sourceErrors.tibo) || !data.tiboPosts.length); 829 renderReleaseEventSignal( 830 latestTiboSignal, 831 Boolean(data.sourceErrors.tibo) || !data.tiboPosts.length, 832 ); 833 renderForecast(data.forecast); 834 renderForecastHistory(data.history); 835 elements.lastChecked.textContent = `Server checks sources every 30 min · last checked ${new Date( 836 data.fetchedAt, 837 ).toLocaleTimeString([], { 838 hour: "2-digit", 839 minute: "2-digit", 840 })}`; 841 } catch (error) { 842 latestIncidents = fallbackIncidents; 843 latestTiboSignal = readTiboSignal([]); 844 renderStatus(fallbackIncidents, true); 845 renderTiboSignal(latestTiboSignal, true); 846 renderReleaseEventSignal(latestTiboSignal, true); 847 renderForecast(calculateForecast(fallbackIncidents)); 848 renderForecastHistory(); 849 elements.lastChecked.textContent = "Forecast server unavailable; showing bundled snapshot"; 850 console.warn("Forecast fallback in use:", error); 851 } finally { 852 refreshInFlight = false; 853 } 854} 855 856function renderNotificationStatus(message, isError = false, isConfirmed = false) { 857 elements.notificationStatus.hidden = false; 858 elements.notificationStatus.className = `notification-status ${ 859 isError ? "notification-status-error" : "notification-status-success" 860 }${isConfirmed ? " notification-status-confirmed" : ""}`; 861 elements.notificationStatus.textContent = message; 862} 863 864async function subscribeForAlerts(event) { 865 event.preventDefault(); 866 const button = elements.notificationForm.querySelector("button"); 867 button.disabled = true; 868 button.textContent = "Sending..."; 869 870 try { 871 const response = await fetch("/api/subscribe", { 872 method: "POST", 873 headers: { "Content-Type": "application/json" }, 874 body: JSON.stringify({ 875 email: elements.notificationEmail.value, 876 startedAt: notificationFormStartedAt, 877 website: elements.notificationWebsite.value, 878 }), 879 }); 880 const data = await response.json().catch(() => ({})); 881 if (!response.ok) throw new Error(data.error || "Unable to start email alerts."); 882 883 elements.notificationEmail.value = ""; 884 elements.notificationWebsite.value = ""; 885 notificationFormStartedAt = Date.now(); 886 renderNotificationStatus(data.message); 887 } catch (error) { 888 renderNotificationStatus(error.message, true); 889 } finally { 890 button.disabled = false; 891 button.textContent = "Notify me"; 892 } 893} 894 895elements.notificationForm.addEventListener("submit", subscribeForAlerts); 896elements.analyticsAccept.addEventListener("click", () => chooseAnalyticsConsent("granted")); 897elements.analyticsDecline.addEventListener("click", () => chooseAnalyticsConsent("denied")); 898elements.analyticsSettings.addEventListener("click", () => { 899 elements.analyticsConsent.hidden = false; 900}); 901elements.shareButton.addEventListener("click", shareForecast); 902elements.themeToggle.addEventListener("click", toggleTheme); 903 904const notificationState = new URLSearchParams(window.location.search).get("notification"); 905if (notificationMessages[notificationState]) { 906 renderNotificationStatus( 907 notificationMessages[notificationState], 908 notificationState === "invalid", 909 notificationState === "confirmed", 910 ); 911 window.history.replaceState({}, "", `${window.location.pathname}${window.location.hash}`); 912} 913 914refreshForecast();
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.