This script adds a custom Startrack Leaderboards tab to Nitro Type, providing advanced leaderboard functionality with multiple timeframes, intelligent caching, and a polished UI that closely matches the original Nitro Type leaderboard design.
// ==UserScript==
// @name Nitro Type - Leaderboards
// @namespace https://nitrotype.info
// @version 11.0.1
// @description This script adds a custom Startrack Leaderboards tab to Nitro Type, providing advanced leaderboard functionality with multiple timeframes, intelligent caching, and a polished UI that closely matches the original Nitro Type leaderboard design.
// @author Combined Logic (SuperJoelzy + Captain.Loveridge)
// @license MIT
// @match https://www.nitrotype.com/*
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant unsafeWindow
// @connect ntstartrack.org
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
// ─── Mod Menu Manifest Bridge ───────────────────────────────────────────────
const NTCFG_LEADERBOARDS_MANIFEST_ID = "leaderboards";
const NTCFG_LEADERBOARDS_MANIFEST_KEY = `ntcfg:manifest:${NTCFG_LEADERBOARDS_MANIFEST_ID}`;
const NTCFG_LEADERBOARDS_VALUE_PREFIX = `ntcfg:${NTCFG_LEADERBOARDS_MANIFEST_ID}:`;
const NTCFG_LEADERBOARDS_BRIDGE_VERSION = "1.0.0-bridge.1";
const SETTINGS_STORAGE_VERSION = 1;
const STORAGE_VERSION_KEY = `${NTCFG_LEADERBOARDS_VALUE_PREFIX}__storage_version`;
const STORAGE_MIGRATED_AT_KEY = `${NTCFG_LEADERBOARDS_VALUE_PREFIX}__migrated_at`;
const STORAGE_CLEANUP_AFTER_KEY = `${NTCFG_LEADERBOARDS_VALUE_PREFIX}__cleanup_after`;
const LEGACY_CLEANUP_GRACE_MS = 30 * 24 * 60 * 60 * 1000;
const LEADERBOARDS_SHARED_SETTINGS = {
DEFAULT_VIEW: {
type: 'select',
label: 'Default View',
default: 'individual',
group: 'Views',
description: 'Which leaderboard view to show by default.',
options: [
{ label: 'Individual', value: 'individual' },
{ label: 'Team', value: 'team' }
]
},
DEFAULT_TIMEFRAME: {
type: 'select',
label: 'Default Timeframe',
default: 'season',
group: 'Views',
description: 'Which timeframe to show by default.',
options: [
{ label: 'Season', value: 'season' },
{ label: 'Last 24 Hours', value: '24hr' },
{ label: 'Last 7 Days', value: '7day' }
]
},
HIGHLIGHT_POSITION_CHANGE: {
type: 'boolean',
label: 'Highlight Position Changes',
default: true,
group: 'Views',
description: 'Show position change arrows next to leaderboard entries.'
},
SHOW_MANUAL_REFRESH: {
type: 'boolean',
label: 'Show Manual Refresh Button',
default: true,
group: 'Views',
description: 'Display the manual refresh button on the leaderboard.'
},
CACHE_DURATION_MINUTES: {
type: 'number',
label: 'Cache Duration (minutes)',
default: 60,
group: 'Sync',
description: 'How long to keep cached leaderboard data before re-fetching.',
min: 1,
max: 120,
step: 1
},
ANTI_FLICKER_MODE: {
type: 'boolean',
label: 'Anti-Flicker Mode',
default: true,
group: 'Presentation',
description: 'Hide page content briefly while the leaderboard loads to prevent flickering.'
},
SHOW_ROUTE_TAB: {
type: 'boolean',
label: 'Show Leaderboards Nav Tab',
default: true,
group: 'Presentation',
description: 'Display the Leaderboards tab in the top navigation bar.'
},
SHOW_DROPDOWN_LINK: {
type: 'boolean',
label: 'Show Leaderboards in Dropdown',
default: false,
group: 'Presentation',
description: 'Add a Leaderboards link to the account dropdown menu.'
},
HIDE_CLASS_TAB: {
type: 'boolean',
label: 'Hide Class Tab',
default: false,
group: 'Presentation',
description: 'Hide the Class/Classes tab from the navigation bar.'
},
DEBUG_LOGGING: {
type: 'boolean',
label: 'Debug Logging',
default: false,
group: 'Advanced',
description: 'Enable verbose console logging for troubleshooting.'
}
};
const getNtcfgLeaderboardsStorageKey = (settingKey) => `${NTCFG_LEADERBOARDS_VALUE_PREFIX}${settingKey}`;
const canUseGMStorage = () => typeof GM_getValue === 'function' && typeof GM_setValue === 'function';
const readCanonicalValue = (storageKey) => {
if (!canUseGMStorage()) return undefined;
try {
return GM_getValue(storageKey);
} catch {
return undefined;
}
};
const writeCanonicalValue = (storageKey, value) => {
if (!canUseGMStorage()) return;
try {
GM_setValue(storageKey, value);
} catch {
// ignore storage sync failures
}
};
const readStorageMetaNumber = (storageKey, fallback = 0) => {
const raw = readCanonicalValue(storageKey);
const parsed = Number(raw);
return Number.isFinite(parsed) ? parsed : fallback;
};
const dispatchLeaderboardsActionResult = (requestId, status, error = '') => {
if (!requestId) return;
try {
document.dispatchEvent(new CustomEvent('ntcfg:action-result', {
detail: {
requestId,
script: NTCFG_LEADERBOARDS_MANIFEST_ID,
status,
error
}
}));
} catch {
// ignore dispatch failures
}
};
const coerceNtcfgLeaderboardsValue = (settingKey, value) => {
const meta = LEADERBOARDS_SHARED_SETTINGS[settingKey];
if (!meta) return value;
if (meta.type === 'boolean') {
if (typeof value === 'string') {
const raw = value.trim().toLowerCase();
if (raw === 'false' || raw === '0' || raw === 'off') return false;
if (raw === 'true' || raw === '1' || raw === 'on') return true;
}
return !!value;
}
if (meta.type === 'number') {
const fallback = Number(meta.default);
const parsed = Number(value);
let normalized = Number.isFinite(parsed) ? parsed : fallback;
const min = Number(meta.min);
const max = Number(meta.max);
const step = Number(meta.step);
if (Number.isFinite(step) && step >= 1) {
normalized = Math.round(normalized);
}
if (Number.isFinite(min)) {
normalized = Math.max(min, normalized);
}
if (Number.isFinite(max)) {
normalized = Math.min(max, normalized);
}
return normalized;
}
if (meta.type === 'select') {
const raw = String(value ?? '').trim();
const options = Array.isArray(meta.options) ? meta.options : [];
return options.some((option) => String(option.value) === raw) ? raw : meta.default;
}
return String(value ?? meta.default);
};
const readNtcfgLeaderboardsValue = (settingKey) => {
const meta = LEADERBOARDS_SHARED_SETTINGS[settingKey];
if (!meta) return undefined;
try {
const canonical = readCanonicalValue(getNtcfgLeaderboardsStorageKey(settingKey));
if (canonical !== undefined) {
return coerceNtcfgLeaderboardsValue(settingKey, canonical);
}
const raw = localStorage.getItem(getNtcfgLeaderboardsStorageKey(settingKey));
if (raw == null) return meta.default;
const parsed = JSON.parse(raw);
return coerceNtcfgLeaderboardsValue(settingKey, parsed);
} catch {
return meta.default;
}
};
const writeNtcfgLeaderboardsValue = (settingKey, value) => {
try {
writeCanonicalValue(getNtcfgLeaderboardsStorageKey(settingKey), value);
const serialized = JSON.stringify(value);
if (localStorage.getItem(getNtcfgLeaderboardsStorageKey(settingKey)) !== serialized) {
localStorage.setItem(getNtcfgLeaderboardsStorageKey(settingKey), serialized);
}
} catch {
// ignore storage sync failures
}
};
const registerNtcfgLeaderboardsManifest = () => {
try {
const manifest = {
id: NTCFG_LEADERBOARDS_MANIFEST_ID,
name: 'Startrack Leaderboard Integration',
version: NTCFG_LEADERBOARDS_BRIDGE_VERSION,
scriptVersion: typeof GM_info !== 'undefined' ? GM_info.script.version : '',
storageVersion: SETTINGS_STORAGE_VERSION,
supportsGlobalReset: true,
description: 'Custom Startrack-powered leaderboards with multiple timeframes and caching.',
sections: [
{ id: 'views', title: 'Views', subtitle: 'Default view and display preferences.', resetButton: true },
{ id: 'sync', title: 'Sync', subtitle: 'Cache and data refresh settings.', resetButton: true },
{ id: 'presentation', title: 'Presentation', subtitle: 'Visual behavior and navigation controls.', resetButton: true },
{ id: 'advanced', title: 'Advanced', subtitle: 'Debug and diagnostic controls.', resetButton: true }
],
settings: LEADERBOARDS_SHARED_SETTINGS
};
const serialized = JSON.stringify(manifest);
if (localStorage.getItem(NTCFG_LEADERBOARDS_MANIFEST_KEY) !== serialized) {
localStorage.setItem(NTCFG_LEADERBOARDS_MANIFEST_KEY, serialized);
}
} catch {
// ignore manifest registration failures
}
};
// Direct apply: writes to localStorage (used for same-tab ntcfg:change events from mod menu)
const applyNtcfgLeaderboardsValueDirect = (settingKey, value) => {
const meta = LEADERBOARDS_SHARED_SETTINGS[settingKey];
if (!meta) return;
const normalized = coerceNtcfgLeaderboardsValue(settingKey, value);
writeNtcfgLeaderboardsValue(settingKey, normalized);
// Sync DEBUG_LOGGING bidirectionally with the existing ntStartrackDebug localStorage key
if (settingKey === 'DEBUG_LOGGING') {
syncLeaderboardsDebugToggle(normalized);
}
applyLeaderboardsSettingSideEffects(settingKey);
};
// Deduped apply: compares before writing (used for cross-tab storage events)
const applyNtcfgLeaderboardsValueIfChanged = (settingKey, value) => {
const meta = LEADERBOARDS_SHARED_SETTINGS[settingKey];
if (!meta) return;
const normalized = coerceNtcfgLeaderboardsValue(settingKey, value);
const currentValue = readNtcfgLeaderboardsValue(settingKey);
if (JSON.stringify(currentValue) !== JSON.stringify(normalized)) {
writeNtcfgLeaderboardsValue(settingKey, normalized);
if (settingKey === 'DEBUG_LOGGING') {
syncLeaderboardsDebugToggle(normalized);
}
applyLeaderboardsSettingSideEffects(settingKey);
}
};
// Bidirectional sync for DEBUG_LOGGING <-> ntStartrackDebug localStorage key
const syncLeaderboardsDebugToggle = (enabled) => {
try {
localStorage.setItem('ntStartrackDebug', enabled ? '1' : '0');
} catch {
// ignore storage errors
}
};
// Helper to check if a specific leaderboards feature is enabled via ntcfg
function isNtcfgLeaderboardsFeatureEnabled(settingKey) {
const val = readNtcfgLeaderboardsValue(settingKey);
return val !== false;
}
// Seed localStorage values from defaults for any settings not yet stored
const seedNtcfgLeaderboardsDefaults = () => {
Object.keys(LEADERBOARDS_SHARED_SETTINGS).forEach((settingKey) => {
const meta = LEADERBOARDS_SHARED_SETTINGS[settingKey];
const storageKey = getNtcfgLeaderboardsStorageKey(settingKey);
if (readCanonicalValue(storageKey) === undefined && localStorage.getItem(storageKey) == null) {
writeNtcfgLeaderboardsValue(settingKey, meta.default);
}
// Sync DEBUG_LOGGING bidirectionally with existing ntStartrackDebug key
if (settingKey === 'DEBUG_LOGGING') {
const debugVal = localStorage.getItem('ntStartrackDebug');
if (debugVal === '1') {
writeNtcfgLeaderboardsValue(settingKey, true);
}
}
});
};
const syncAllNtcfgLeaderboardsSettings = () => {
Object.keys(LEADERBOARDS_SHARED_SETTINGS).forEach((settingKey) => {
writeNtcfgLeaderboardsValue(settingKey, readNtcfgLeaderboardsValue(settingKey));
});
};
// Listen for mod menu changes (same tab)
document.addEventListener('ntcfg:change', (event) => {
if (event?.detail?.script !== NTCFG_LEADERBOARDS_MANIFEST_ID) return;
applyNtcfgLeaderboardsValueDirect(event.detail.key, event.detail.value);
});
document.addEventListener('ntcfg:action', (event) => {
const detail = event?.detail || {};
if (detail.script !== '*') return;
if (detail.key !== 'clear-settings' || detail.scope !== 'prefs+caches') return;
try {
Object.keys(LEADERBOARDS_SHARED_SETTINGS).forEach((settingKey) => {
writeNtcfgLeaderboardsValue(settingKey, LEADERBOARDS_SHARED_SETTINGS[settingKey].default);
});
try {
Object.keys(localStorage).forEach((key) => {
if (key.startsWith('ntStartrackCache_') ||
key.startsWith('ntStartrackCacheExp_') ||
key.startsWith('ntStartrackCacheTime_') ||
key === 'ntStartrackSeasonCache' ||
key === 'ntStartrackDailySyncCT' ||
key === 'ntStartrackHourlySyncCT' ||
key === 'ntCarDataMap' ||
key === 'ntCarDataMapTimestamp' ||
key === 'ntStartrackDebug' ||
key === 'ntStartrackDebugSeasonOnly') {
localStorage.removeItem(key);
}
});
} catch {
// ignore localStorage cleanup failures
}
writeCanonicalValue(STORAGE_VERSION_KEY, SETTINGS_STORAGE_VERSION);
writeCanonicalValue(STORAGE_MIGRATED_AT_KEY, Date.now());
writeCanonicalValue(STORAGE_CLEANUP_AFTER_KEY, Date.now() + LEGACY_CLEANUP_GRACE_MS);
registerNtcfgLeaderboardsManifest();
syncAllNtcfgLeaderboardsSettings();
applyAllLeaderboardsSettingSideEffects();
document.dispatchEvent(new CustomEvent('ntcfg:manifest-updated', {
detail: { script: NTCFG_LEADERBOARDS_MANIFEST_ID }
}));
dispatchLeaderboardsActionResult(detail.requestId, 'success');
} catch (error) {
dispatchLeaderboardsActionResult(detail.requestId, 'error', error?.message || String(error));
}
});
// Listen for cross-tab changes
window.addEventListener('storage', (event) => {
const storageKey = String(event?.key || '');
if (!storageKey.startsWith(NTCFG_LEADERBOARDS_VALUE_PREFIX) || event.newValue == null) return;
const settingKey = storageKey.slice(NTCFG_LEADERBOARDS_VALUE_PREFIX.length);
if (!LEADERBOARDS_SHARED_SETTINGS[settingKey]) return;
try {
applyNtcfgLeaderboardsValueIfChanged(settingKey, JSON.parse(event.newValue));
} catch {
// ignore invalid synced payloads
}
});
const ensureLeaderboardsStorageMigration = () => {
const currentVersion = readStorageMetaNumber(STORAGE_VERSION_KEY, 0);
const migratedAt = readStorageMetaNumber(STORAGE_MIGRATED_AT_KEY, 0);
const now = Date.now();
if (currentVersion < SETTINGS_STORAGE_VERSION) {
syncAllNtcfgLeaderboardsSettings();
writeCanonicalValue(STORAGE_VERSION_KEY, SETTINGS_STORAGE_VERSION);
if (!migratedAt) {
writeCanonicalValue(STORAGE_MIGRATED_AT_KEY, now);
writeCanonicalValue(STORAGE_CLEANUP_AFTER_KEY, now + LEGACY_CLEANUP_GRACE_MS);
}
}
const cleanupAfter = readStorageMetaNumber(STORAGE_CLEANUP_AFTER_KEY, 0);
if (cleanupAfter && now >= cleanupAfter) {
try {
localStorage.removeItem('ntStartrackDebug');
} catch {
// ignore legacy cleanup failures
}
}
};
// Register manifest and seed defaults
ensureLeaderboardsStorageMigration();
registerNtcfgLeaderboardsManifest();
seedNtcfgLeaderboardsDefaults();
syncAllNtcfgLeaderboardsSettings();
// Alive signal — write BEFORE dispatching manifest-updated so the mod menu sees this script as alive when it re-renders.
try { localStorage.setItem('ntcfg:alive:' + NTCFG_LEADERBOARDS_MANIFEST_ID, String(Date.now())); } catch { /* ignore */ }
try {
document.dispatchEvent(new CustomEvent('ntcfg:manifest-updated', {
detail: { script: NTCFG_LEADERBOARDS_MANIFEST_ID }
}));
} catch {
// ignore event dispatch failures
}
function initNTRouteHelper(targetWindow = window) {
const hostWindow = targetWindow || window;
const existing = hostWindow.NTRouteHelper;
if (existing && existing.__ntRouteHelperReady && typeof existing.subscribe === 'function') {
return existing;
}
const helper = existing || {};
const listeners = helper.listeners instanceof Set ? helper.listeners : new Set();
let currentKey = `${hostWindow.location.pathname}${hostWindow.location.search}${hostWindow.location.hash}`;
const notify = (reason) => {
const nextKey = `${hostWindow.location.pathname}${hostWindow.location.search}${hostWindow.location.hash}`;
if (reason !== 'init' && nextKey === currentKey) return;
const previousKey = currentKey;
currentKey = nextKey;
listeners.forEach((listener) => {
try {
listener({
reason,
previous: previousKey,
current: nextKey,
pathname: hostWindow.location.pathname
});
} catch (error) {
console.error('[NTRouteHelper] listener error', error);
}
});
helper.currentKey = currentKey;
};
if (!helper.__ntRouteHelperWrapped) {
const wrapHistoryMethod = (methodName) => {
const current = hostWindow.history[methodName];
if (typeof current !== 'function' || current.__ntRouteHelperWrapped) return;
const wrapped = function() {
const result = current.apply(this, arguments);
queueMicrotask(() => notify(methodName));
return result;
};
wrapped.__ntRouteHelperWrapped = true;
wrapped.__ntRouteHelperOriginal = current;
hostWindow.history[methodName] = wrapped;
};
wrapHistoryMethod('pushState');
wrapHistoryMethod('replaceState');
hostWindow.addEventListener('popstate', () => queueMicrotask(() => notify('popstate')));
helper.__ntRouteHelperWrapped = true;
}
helper.listeners = listeners;
helper.currentKey = currentKey;
helper.version = '1.0.0';
helper.__ntRouteHelperReady = true;
helper.subscribe = function(listener, options = {}) {
if (typeof listener !== 'function') return () => {};
listeners.add(listener);
if (options.immediate !== false) {
try {
listener({
reason: 'init',
previous: currentKey,
current: currentKey,
pathname: hostWindow.location.pathname
});
} catch (error) {
console.error('[NTRouteHelper] immediate listener error', error);
}
}
return () => listeners.delete(listener);
};
helper.notify = notify;
hostWindow.NTRouteHelper = helper;
return helper;
}
// ─── End Mod Menu Manifest Bridge ───────────────────────────────────────────
// =================================================================
// 1. CSS & ANTI-FLICKER SYSTEM
// =================================================================
const globalStyles = `
/* Curtain Logic */
html.is-leaderboard-route main.structure-content {
opacity: 0 !important;
visibility: hidden !important;
}
html.is-leaderboard-route main.structure-content.custom-loaded {
opacity: 1 !important;
visibility: visible !important;
transition: opacity 0.15s ease-in;
}
/* Spin Animation for Refresh Icon */
@keyframes nt-spin { 100% { transform: rotate(360deg); } }
.icon-spin { animation: nt-spin 1s linear infinite; }
/* Refresh Button Styling */
#manual-refresh-btn {
transition: opacity 0.2s, color 0.2s;
cursor: pointer;
vertical-align: middle;
}
#manual-refresh-btn:hover {
opacity: 1 !important;
color: #fff !important;
}
/* Position Change Arrows */
.position-change {
font-size: 10px;
font-weight: bold;
margin-right: 4px;
vertical-align: middle;
display: inline-flex;
align-items: center;
gap: 1px;
}
.position-change--up {
color: #28a745;
}
.position-change--down {
color: #dc3545;
}
`;
const styleEl = document.createElement('style');
styleEl.textContent = globalStyles;
(document.head || document.documentElement).appendChild(styleEl);
// Hide Class Tab CSS — injected at document-start to prevent flicker
const hideClassTabStyle = document.createElement('style');
hideClassTabStyle.id = 'ntcfg-hide-class-tab';
hideClassTabStyle.textContent = `
a[href*="/class"], a[href*="/classes"] {
display: none !important;
opacity: 0 !important;
}
.nav-list-item:has(a[href*="/class"]),
.nav-list-item:has(a[href*="/classes"]) {
display: none !important;
width: 0 !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
}
`;
function syncHideClassTabState() {
if (readNtcfgLeaderboardsValue('HIDE_CLASS_TAB')) {
if (!document.getElementById('ntcfg-hide-class-tab')) {
(document.head || document.documentElement).appendChild(hideClassTabStyle);
}
} else {
document.getElementById('ntcfg-hide-class-tab')?.remove();
}
}
syncHideClassTabState();
// Listen for live toggle changes
document.addEventListener('ntcfg:change', (event) => {
if (event?.detail?.script !== NTCFG_LEADERBOARDS_MANIFEST_ID) return;
if (event.detail.key === 'HIDE_CLASS_TAB') {
syncHideClassTabState();
}
});
function updateRouteStatus() {
if (location.pathname === '/leaderboards' && isNtcfgLeaderboardsFeatureEnabled('ANTI_FLICKER_MODE')) {
document.documentElement.classList.add('is-leaderboard-route');
} else {
document.documentElement.classList.remove('is-leaderboard-route');
const main = document.querySelector('main.structure-content');
if (main) main.classList.remove('custom-loaded');
}
}
updateRouteStatus();
const ntRouteHelper = initNTRouteHelper(window);
// =================================================================
// 2. SHARED OPTIMIZATION LAYER (Compat across scripts)
// =================================================================
function initNTShared() {
const shared = window.NTShared || {};
shared.version = '2.0.0';
const existingCache = shared.cache;
const hasMapCache = existingCache instanceof Map;
const cacheMap = shared.cacheMap || (hasMapCache ? existingCache : new Map());
const cacheObj = shared.cacheObj || (!hasMapCache && existingCache && typeof existingCache === 'object' ? existingCache : {
individual: { data: null, timestamp: 0, expiresAt: 0 },
team: { data: null, timestamp: 0, expiresAt: 0 },
isbot: new Map()
});
if (!cacheObj.individual) cacheObj.individual = { data: null, timestamp: 0, expiresAt: 0 };
if (!cacheObj.team) cacheObj.team = { data: null, timestamp: 0, expiresAt: 0 };
if (!cacheObj.isbot) cacheObj.isbot = new Map();
const isbot = shared.isbot || cacheObj.isbot || new Map();
cacheObj.isbot = isbot;
if (!shared.cache) shared.cache = cacheObj;
shared.cacheMap = cacheMap;
shared.cacheObj = cacheObj;
shared.isbot = isbot;
shared._makeKey = shared._makeKey || function(view, timeframe, startKey, endKey) {
return `${view}_${timeframe}_${startKey}_${endKey}`;
};
const BOT_STATUS_TTL_MS = shared.BOT_STATUS_TTL_MS || (24 * 60 * 60 * 1000);
const BOT_STATUS_MAX_ENTRIES = shared.BOT_STATUS_MAX_ENTRIES || 5000;
shared.BOT_STATUS_TTL_MS = BOT_STATUS_TTL_MS;
shared.BOT_STATUS_MAX_ENTRIES = BOT_STATUS_MAX_ENTRIES;
const syncState = shared._syncState || {
initialized: false,
tabId: `tab_${Math.random().toString(36).slice(2)}_${Date.now()}`,
broadcastChannel: null,
storageKey: 'ntSharedCacheSyncV1'
};
shared._syncState = syncState;
function emitLocalUpdate(detail) {
window.dispatchEvent(new CustomEvent('nt-cache-updated', { detail: detail }));
}
function normalizeBotEntry(raw, now) {
if (!raw) return null;
if (raw && typeof raw === 'object' && raw.status && typeof raw.ts === 'number') return raw;
return { status: raw, ts: now };
}
function pruneBotCache() {
const now = Date.now();
isbot.forEach((value, key) => {
const entry = normalizeBotEntry(value, now);
if (!entry) {
isbot.delete(key);
return;
}
if (now - entry.ts > BOT_STATUS_TTL_MS) {
isbot.delete(key);
return;
}
if (entry !== value) isbot.set(key, entry);
});
if (isbot.size <= BOT_STATUS_MAX_ENTRIES) return;
const entries = Array.from(isbot.entries())
.map(([key, value]) => [key, normalizeBotEntry(value, now)])
.filter(([, value]) => !!value)
.sort((a, b) => a[1].ts - b[1].ts);
const overflow = entries.length - BOT_STATUS_MAX_ENTRIES;
for (let i = 0; i < overflow; i++) {
isbot.delete(entries[i][0]);
}
}
function postCrossTabUpdate(payload) {
if (!payload || !syncState.initialized) return;
const message = Object.assign({}, payload, {
__ntSharedSync: true,
origin: syncState.tabId,
sentAt: Date.now()
});
if (syncState.broadcastChannel) {
try { syncState.broadcastChannel.postMessage(message); } catch (e) {}
}
try {
localStorage.setItem(syncState.storageKey, JSON.stringify(message));
localStorage.removeItem(syncState.storageKey);
} catch (e) {}
}
function setLegacyCacheInternal(type, data, expiresAt, options = {}) {
if (!cacheObj[type]) cacheObj[type] = { data: null, timestamp: 0, expiresAt: 0 };
const now = options.timestamp || Date.now();
cacheObj[type].data = data;
cacheObj[type].timestamp = now;
cacheObj[type].expiresAt = expiresAt || (now + 3600000);
emitLocalUpdate({ type, data, expiresAt: cacheObj[type].expiresAt, source: options.source || 'local' });
if (options.broadcast !== false) {
postCrossTabUpdate({
kind: 'legacy',
type: type,
data: data,
expiresAt: cacheObj[type].expiresAt,
timestamp: now
});
}
}
function setKeyedCacheInternal(key, data, expiresAt, options = {}) {
const now = options.timestamp || Date.now();
const ttl = expiresAt || (now + 3600000);
cacheMap.set(key, { data: data, timestamp: now, expiresAt: ttl });
emitLocalUpdate({ key, data, expiresAt: ttl, source: options.source || 'local' });
if (options.broadcast !== false) {
postCrossTabUpdate({
kind: 'keyed',
key: key,
data: data,
expiresAt: ttl,
timestamp: now
});
}
}
function setBotStatusInternal(username, status, options = {}) {
if (!username) return;
const key = username.toLowerCase();
const now = options.timestamp || Date.now();
isbot.set(key, { status: status, ts: now });
pruneBotCache();
emitLocalUpdate({ type: 'isbot', username: key, data: status, source: options.source || 'local' });
if (options.broadcast !== false) {
postCrossTabUpdate({
kind: 'isbot',
username: key,
status: status,
timestamp: now
});
}
}
function applyCrossTabMessage(message, source) {
if (!message || message.origin === syncState.tabId) return;
if (message.kind === 'legacy' && message.type) {
setLegacyCacheInternal(message.type, message.data, message.expiresAt, {
broadcast: false,
timestamp: message.timestamp,
source: source || 'remote'
});
return;
}
if (message.kind === 'keyed' && message.key) {
setKeyedCacheInternal(message.key, message.data, message.expiresAt, {
broadcast: false,
timestamp: message.timestamp,
source: source || 'remote'
});
return;
}
if (message.kind === 'isbot' && message.username) {
setBotStatusInternal(message.username, message.status, {
broadcast: false,
timestamp: message.timestamp,
source: source || 'remote'
});
}
}
if (!syncState.initialized) {
syncState.initialized = true;
if (typeof BroadcastChannel !== 'undefined') {
try {
syncState.broadcastChannel = new BroadcastChannel('ntSharedCacheSyncV1');
syncState.broadcastChannel.onmessage = function(event) {
const message = event ? event.data : null;
if (!message || !message.__ntSharedSync) return;
applyCrossTabMessage(message, 'broadcast');
};
} catch (e) {}
}
window.addEventListener('storage', (event) => {
if (!event || event.key !== syncState.storageKey || !event.newValue) return;
try {
const message = JSON.parse(event.newValue);
if (!message || !message.__ntSharedSync) return;
applyCrossTabMessage(message, 'storage');
} catch (e) {}
});
}
shared.setLegacyCache = function(type, data, expiresAt) {
setLegacyCacheInternal(type, data, expiresAt);
};
shared.setCache = function(key, data, expiresAt) {
if (typeof key === 'string' && cacheObj[key]) {
setLegacyCacheInternal(key, data, expiresAt);
return;
}
setKeyedCacheInternal(key, data, expiresAt);
};
shared.getCache = function(key, maxAge) {
const now = Date.now();
const maxAgeMs = typeof maxAge === 'number' ? maxAge : Number.POSITIVE_INFINITY;
if (typeof key === 'string' && cacheObj[key]) {
const cached = cacheObj[key];
if (!cached || !cached.data) return null;
const age = now - (cached.timestamp || 0);
if (age < maxAgeMs && now < (cached.expiresAt || 0)) return cached.data;
return null;
}
const cached = cacheMap.get(key);
if (!cached || !cached.data) return null;
const age = now - (cached.timestamp || 0);
if (age < maxAgeMs && now < cached.expiresAt) return cached.data;
return null;
};
shared.getTimestamp = function(key) {
if (typeof key === 'string' && cacheObj[key]) return cacheObj[key].timestamp || null;
const cached = cacheMap.get(key);
return cached?.timestamp || null;
};
shared.getLegacyCache = function(type, maxAge) {
return shared.getCache(type, maxAge);
};
shared.getBotStatus = function(username) {
if (!username) return null;
const key = username.toLowerCase();
const value = isbot.get(key);
if (!value) return null;
const entry = normalizeBotEntry(value, Date.now());
if (!entry) return null;
if (Date.now() - entry.ts > BOT_STATUS_TTL_MS) {
isbot.delete(key);
return null;
}
if (entry !== value) isbot.set(key, entry);
return entry.status;
};
shared.setBotStatus = function(username, status) {
setBotStatusInternal(username, status);
};
window.NTShared = shared;
return shared;
}
initNTShared();
// --- CONFIGURATION ---
const TAB_CLASS = 'nt-custom-leaderboards';
const LEADERBOARD_PATH = '/leaderboards';
const CACHE_KEY = 'ntStartrackCache_';
const SHARED_CACHE_PREFIX = 'ntStartrackCacheExp_';
const CACHE_TIMESTAMP_KEY = 'ntStartrackCacheTime_';
const SEASON_CACHE_KEY = 'ntStartrackSeasonCache';
const SHARED_LEADERBOARD_SCHEMA_VERSION = 1;
const SHARED_LEADERBOARD_BOT_POLICY = 'server_filtered';
function getCacheDurationMs() {
const minutes = readNtcfgLeaderboardsValue('CACHE_DURATION_MINUTES');
return (typeof minutes === 'number' && minutes > 0 ? minutes : 60) * 60 * 1000;
}
const DAILY_SYNC_KEY = 'ntStartrackDailySyncCT';
const HOURLY_SYNC_KEY = 'ntStartrackHourlySyncCT';
const SEASON_REFRESH_MS = 6 * 60 * 60 * 1000;
const DEBUG_FLAG_KEY = 'ntStartrackDebug';
const DEBUG_SEASON_ONLY_KEY = 'ntStartrackDebugSeasonOnly';
const BOOTSTRAP_SCRIPT_PATH_FALLBACK = '/index/faf5993982a8235165748e39847ee73ba1dea710-1847/bootstrap.js';
const BACKGROUND_SYNC_DELAY_MS = 350;
const ASYNC_DELAY = 50;
const DEBUG_PARAM_ENABLED = /(?:\?|&)ntdebug=1(?:&|$)/.test(window.location.search);
const DEBUG_ENABLED = DEBUG_PARAM_ENABLED || localStorage.getItem(DEBUG_FLAG_KEY) === '1' || readNtcfgLeaderboardsValue('DEBUG_LOGGING') === true;
const DEBUG_SEASON_ONLY = localStorage.getItem(DEBUG_SEASON_ONLY_KEY) === '1';
// --- STATE ---
let cacheQueue = [];
let isCaching = false;
let forceBackgroundUpdate = false;
let initialCacheComplete = false;
let carDataMap = {};
let carDataLoaded = false;
let carDataLoadAttempts = 0;
const MAX_CAR_LOAD_ATTEMPTS = 10;
let lastCheckedHour = null;
let hourlyCheckInterval = null;
let pageRenderInProgress = false;
let seasonBootstrapFetchInFlight = false;
let seasonBootstrapLastAttemptMs = 0;
// Dynamic season data (loaded from NTBOOTSTRAP)
let currentSeason = {
seasonID: null,
name: 'Season',
startCT: null,
endCT: null,
startStampUTC: null,
endStampUTC: null
};
let state = {
view: readNtcfgLeaderboardsValue('DEFAULT_VIEW') || 'individual',
timeframe: readNtcfgLeaderboardsValue('DEFAULT_TIMEFRAME') || 'season',
currentDate: getCurrentCT(),
dateRange: { start: null, end: null }
};
function shouldLogDebug(payload) {
if (!DEBUG_ENABLED) return false;
if (!DEBUG_SEASON_ONLY) return true;
if (!payload || typeof payload !== 'object') return false;
const tf = payload.timeframe || payload.targetTimeframe || payload.stateTimeframe;
return tf === 'season' || payload.reason === 'season';
}
function debugLog(event, payload) {
if (!shouldLogDebug(payload)) return;
const ts = new Date().toISOString();
if (payload === undefined) {
console.log(`[Startrack][DBG ${ts}] ${event}`);
return;
}
console.log(`[Startrack][DBG ${ts}] ${event}`, payload);
}
function shortCacheKey(cacheKey) {
if (!cacheKey) return '';
return cacheKey.replace(CACHE_KEY, '');
}
function getCacheFreshness(cacheKey, ttlMs = getCacheDurationMs()) {
const timestampKey = CACHE_TIMESTAMP_KEY + cacheKey;
const rawTimestamp = localStorage.getItem(timestampKey);
if (!rawTimestamp) {
return { hasTimestamp: false, fresh: false, ageMs: null, ttlMs: ttlMs };
}
const timestamp = parseInt(rawTimestamp, 10);
const ageMs = Date.now() - timestamp;
return { hasTimestamp: true, fresh: ageMs < ttlMs, ageMs: ageMs, ttlMs: ttlMs, timestamp: timestamp };
}
const timeframes = [
{ key: 'season', label: 'Season', hasNav: false },
{ key: '24hr', label: 'Last 24 Hours', hasNav: false },
{ key: '60min', label: '60 Minutes', hasNav: false },
{ key: '7day', label: 'Last 7 Days', hasNav: false },
{ key: 'daily', label: 'Daily', hasNav: true },
{ key: 'weekly', label: 'Weekly', hasNav: true },
{ key: 'monthly', label: 'Monthly', hasNav: true }
// { key: 'custom', label: 'Custom', hasNav: false } // TODO: Add custom date picker UI
];
// =================================================================
// 3. TIMEZONE & SEASON UTILITIES
// =================================================================
function getCurrentCT() {
const now = new Date();
const ctString = now.toLocaleString("en-US", { timeZone: "America/Chicago" });
return new Date(ctString);
}
function getCTDateKey() {
const ct = getCurrentCT();
const y = ct.getFullYear();
const m = String(ct.getMonth() + 1).padStart(2, '0');
const d = String(ct.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
}
function getCTHourKey() {
const ct = getCurrentCT();
const y = ct.getFullYear();
const m = String(ct.getMonth() + 1).padStart(2, '0');
const d = String(ct.getDate()).padStart(2, '0');
const h = String(ct.getHours()).padStart(2, '0');
return `${y}-${m}-${d}-${h}`;
}
function getMsUntilNextCTMidnight() {
const ct = getCurrentCT();
const next = new Date(ct);
next.setHours(24, 0, 0, 0);
return Math.max(0, next.getTime() - ct.getTime());
}
function getCacheTTLForTimeframe(timeframe) {
if (timeframe === 'season') return SEASON_REFRESH_MS;
if (timeframe === 'daily' || timeframe === 'weekly' || timeframe === 'monthly') {
return getMsUntilNextCTMidnight();
}
if (timeframe === '60min' || timeframe === '24hr' || timeframe === '7day') {
return getCacheDurationMs();
}
return getCacheDurationMs();
}
function getCurrentHour() { return new Date().getHours(); }
function getSharedRowLimit(view) { return view === 'individual' ? 300 : 50; }
function getSharedCacheKey(tempState) {
return getCacheKey(tempState).replace(CACHE_KEY, SHARED_CACHE_PREFIX);
}
function sanitizeLeaderboardRows(view, rows) {
if (!Array.isArray(rows)) return [];
let removedBotRows = 0;
const sanitized = rows.filter((entry) => {
if (!entry || typeof entry !== 'object') return false;
if (entry.bot === 1) {
removedBotRows += 1;
return false;
}
if (view === 'individual') {
return !!String(entry.Username || entry.username || '').trim();
}
return !!String(entry.TeamTag || entry.teamTag || entry.TeamName || entry.teamName || '').trim();
});
if (removedBotRows > 0) {
debugLog('Unexpected bot rows removed from payload', {
view,
removedBotRows
});
}
return sanitized;
}
function buildSharedLeaderboardPayload(view, timeframe, rows, writtenAt, expiresAt) {
return {
schemaVersion: SHARED_LEADERBOARD_SCHEMA_VERSION,
source: `leaderboards@${typeof GM_info !== 'undefined' ? GM_info.script.version : 'unknown'}`,
botPolicy: SHARED_LEADERBOARD_BOT_POLICY,
view,
timeframe,
rowLimit: getSharedRowLimit(view),
writtenAt,
expiresAt,
rows: Array.isArray(rows) ? rows.slice(0, getSharedRowLimit(view)) : []
};
}
function saveSharedPayloadToLocalStorage(cacheKey, payload) {
if (!cacheKey || !payload) return;
try {
localStorage.setItem(cacheKey, JSON.stringify(payload));
localStorage.setItem(CACHE_TIMESTAMP_KEY + cacheKey, String(payload.writtenAt || Date.now()));
} catch (error) {
debugLog('Shared payload localStorage write failed', {
key: shortCacheKey(cacheKey),
message: error && error.message ? error.message : String(error)
});
}
}
// Convert Unix timestamp (UTC seconds) to CT-formatted string for Startrack API
function utcToCTString(unixTimestamp) {
const date = new Date(unixTimestamp * 1000);
const ctString = date.toLocaleString("en-US", { timeZone: "America/Chicago" });
const ctDate = new Date(ctString);
return formatDate(ctDate);
}
// Convert Unix timestamp to user's local timezone for display
function utcToLocalDate(unixTimestamp) {
return new Date(unixTimestamp * 1000);
}
// Check if cache should be refreshed (within 1 week of season end)
function shouldRefreshSeasonCache(endStampUTC) {
if (!endStampUTC) return true;
const oneWeekMs = 7 * 24 * 60 * 60 * 1000;
const endMs = endStampUTC * 1000;
const now = Date.now();
return (endMs - now) < oneWeekMs;
}
function hasValidSeasonData() {
return !!(
currentSeason &&
currentSeason.startCT &&
currentSeason.endCT &&
currentSeason.startStampUTC &&
currentSeason.endStampUTC
);
}
function parseActiveSeasonsFromBootstrapPayload(bootstrapData) {
if (!Array.isArray(bootstrapData)) return null;
const seasonsData = bootstrapData.find(item => Array.isArray(item) && item[0] === 'ACTIVE_SEASONS');
if (!seasonsData || !Array.isArray(seasonsData[1]) || seasonsData[1].length === 0) return null;
return seasonsData[1];
}
function parseActiveSeasonsFromBootstrapScript(scriptText) {
if (!scriptText || typeof scriptText !== 'string') return null;
const match = scriptText.match(/\["ACTIVE_SEASONS",(\[[\s\S]*?\])\],\["ACTIVE_EVENTS"/);
if (!match || !match[1]) return null;
try {
const parsed = JSON.parse(match[1]);
return Array.isArray(parsed) && parsed.length > 0 ? parsed : null;
} catch (e) {
return null;
}
}
function pickActiveSeason(seasons) {
if (!Array.isArray(seasons) || seasons.length === 0) return null;
const now = Math.floor(Date.now() / 1000);
const active = seasons.find(s => now >= s.startStamp && now < s.endStamp);
if (active) return active;
return seasons.slice().sort((a, b) => (b.startStamp || 0) - (a.startStamp || 0))[0] || null;
}
function applySeasonFromRecord(seasonRecord, source) {
if (!seasonRecord) return false;
currentSeason = {
seasonID: seasonRecord.seasonID,
name: seasonRecord.name,
startStampUTC: seasonRecord.startStamp,
endStampUTC: seasonRecord.endStamp,
startCT: utcToCTString(seasonRecord.startStamp),
endCT: utcToCTString(seasonRecord.endStamp)
};
localStorage.setItem(SEASON_CACHE_KEY, JSON.stringify(currentSeason));
debugLog('Season loaded', {
source: source,
seasonName: currentSeason.name,
targetTimeframe: 'season'
});
debugLog('Season source selected', {
source: source,
seasonName: currentSeason.name,
seasonID: currentSeason.seasonID,
startStampUTC: currentSeason.startStampUTC,
endStampUTC: currentSeason.endStampUTC,
targetTimeframe: 'season'
});
return true;
}
function getBootstrapScriptUrl() {
const scriptEl = document.querySelector('script[src*="/bootstrap.js"]');
if (scriptEl && scriptEl.src) return scriptEl.src;
return new URL(BOOTSTRAP_SCRIPT_PATH_FALLBACK, window.location.origin).href;
}
function fetchSeasonDataFromBootstrapScript() {
const now = Date.now();
const retryDelayMs = 5 * 60 * 1000;
if (seasonBootstrapFetchInFlight) return;
if (now - seasonBootstrapLastAttemptMs < retryDelayMs) return;
seasonBootstrapFetchInFlight = true;
seasonBootstrapLastAttemptMs = now;
const bootstrapUrl = getBootstrapScriptUrl();
debugLog('Season bootstrap script fetch start', {
source: 'bootstrap-script',
url: bootstrapUrl,
targetTimeframe: 'season'
});
GM_xmlhttpRequest({
method: 'GET',
url: bootstrapUrl,
onload: function(response) {
seasonBootstrapFetchInFlight = false;
if (response.status !== 200 || !response.responseText) {
debugLog('Season bootstrap script fetch failed', {
source: 'bootstrap-script',
status: response.status,
targetTimeframe: 'season'
});
return;
}
const seasons = parseActiveSeasonsFromBootstrapScript(response.responseText);
const activeSeason = pickActiveSeason(seasons || []);
if (!activeSeason) {
debugLog('Season bootstrap script parse returned no seasons', {
source: 'bootstrap-script',
targetTimeframe: 'season'
});
return;
}
if (!applySeasonFromRecord(activeSeason, 'bootstrap-script')) {
return;
}
if (location.pathname === LEADERBOARD_PATH && state.timeframe === 'season') {
fetchLeaderboardData(true);
} else {
startBackgroundSync('season-bootstrap', {
timeframes: ['season'],
includeNav: false,
includeCurrent: true,
force: true
});
}
},
onerror: function() {
seasonBootstrapFetchInFlight = false;
debugLog('Season bootstrap script fetch transport error', {
source: 'bootstrap-script',
targetTimeframe: 'season'
});
}
});
}
// Load season data from NTBOOTSTRAP, cache, or bootstrap script fallback
function loadSeasonData() {
// 1. Check localStorage cache first
try {
const cached = localStorage.getItem(SEASON_CACHE_KEY);
if (cached) {
const seasonCache = JSON.parse(cached);
const shouldRefresh = shouldRefreshSeasonCache(seasonCache.endStampUTC);
debugLog('Season cache check', {
source: 'localStorage',
seasonName: seasonCache.name,
endStampUTC: seasonCache.endStampUTC,
shouldRefresh: shouldRefresh,
targetTimeframe: 'season'
});
if (!shouldRefresh) {
currentSeason = seasonCache;
debugLog('Season cache hit (fresh)', {
source: 'localStorage',
seasonName: currentSeason.name,
targetTimeframe: 'season'
});
debugLog('Season source selected', {
source: 'localStorage-fresh',
seasonName: currentSeason.name,
targetTimeframe: 'season'
});
return true;
}
}
} catch (e) {
console.warn('[Startrack] Error reading season cache:', e);
debugLog('Season cache read error', {
message: e && e.message ? e.message : String(e),
targetTimeframe: 'season'
});
}
// 2. Parse NTBOOTSTRAP for ACTIVE_SEASONS
if (typeof NTBOOTSTRAP === 'function') {
try {
const bootstrapData = NTBOOTSTRAP();
const seasons = parseActiveSeasonsFromBootstrapPayload(bootstrapData);
const activeSeason = pickActiveSeason(seasons || []);
if (activeSeason && applySeasonFromRecord(activeSeason, 'NTBOOTSTRAP')) {
return true;
}
} catch (e) {
console.warn('[Startrack] Error parsing NTBOOTSTRAP for seasons:', e);
debugLog('Season bootstrap parse error', {
message: e && e.message ? e.message : String(e),
targetTimeframe: 'season'
});
}
}
// 3. Fallback: use expired cache if available while we request fresh season metadata
try {
const cached = localStorage.getItem(SEASON_CACHE_KEY);
if (cached) {
currentSeason = JSON.parse(cached);
debugLog('Season cache hit (expired fallback)', {
source: 'localStorage',
seasonName: currentSeason.name,
targetTimeframe: 'season'
});
debugLog('Season source selected', {
source: 'localStorage-expired-fallback',
seasonName: currentSeason.name,
targetTimeframe: 'season'
});
fetchSeasonDataFromBootstrapScript();
return hasValidSeasonData();
}
} catch (e) {}
// 4. No season metadata available yet, fetch asynchronously from bootstrap script
fetchSeasonDataFromBootstrapScript();
console.warn('[Startrack] Season metadata not available yet; skipping season fetch until bootstrap data is loaded');
debugLog('Season source selected', {
source: 'pending-bootstrap-script',
seasonName: currentSeason.name || 'Season',
targetTimeframe: 'season'
});
return hasValidSeasonData();
}
// Format season dates for display (user's local timezone)
function getSeasonDisplayDates() {
if (!currentSeason.startStampUTC || !currentSeason.endStampUTC) {
return { startDisplay: 'Unknown', endDisplay: 'Unknown' };
}
const startLocal = utcToLocalDate(currentSeason.startStampUTC);
const endLocal = utcToLocalDate(currentSeason.endStampUTC);
const options = { month: 'short', day: 'numeric', year: 'numeric' };
return {
startDisplay: startLocal.toLocaleDateString(undefined, options),
endDisplay: endLocal.toLocaleDateString(undefined, options)
};
}
// =================================================================
// 4. POSITION CHANGE HELPER
// =================================================================
function getPositionChangeHTML(change) {
if (!isNtcfgLeaderboardsFeatureEnabled('HIGHLIGHT_POSITION_CHANGE')) return '';
const parsedChange = parseInt(change, 10);
if (isNaN(parsedChange) || parsedChange === 0) {
return `<div class="rank-change rank-change--none"><svg class="icon icon-arrow-up"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/dist/site/images/icons/icons.css.svg#icon-arrow-up"></use></svg><div>-</div><svg class="icon icon-arrow-down"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/dist/site/images/icons/icons.css.svg#icon-arrow-down"></use></svg></div>`;
}
if (parsedChange > 0) {
return `<div class="rank-change rank-change--up"><svg class="icon icon-arrow-up"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/dist/site/images/icons/icons.css.svg#icon-arrow-up"></use></svg><div>${parsedChange}</div><svg class="icon icon-arrow-down"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/dist/site/images/icons/icons.css.svg#icon-arrow-down"></use></svg></div>`;
} else {
return `<div class="rank-change rank-change--down"><svg class="icon icon-arrow-up"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/dist/site/images/icons/icons.css.svg#icon-arrow-up"></use></svg><div>${Math.abs(parsedChange)}</div><svg class="icon icon-arrow-down"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/dist/site/images/icons/icons.css.svg#icon-arrow-down"></use></svg></div>`;
}
}
// =================================================================
// 5. CAR DATA LOADING (DEFERRED WITH RETRY)
// =================================================================
function loadCarData(callback) {
// Check cache timestamp first
const cacheTimestamp = localStorage.getItem('ntCarDataMapTimestamp');
const cacheAge = cacheTimestamp ? Date.now() - parseInt(cacheTimestamp) : Infinity;
const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000;
// Try loading from cache if it's less than 7 days old
if (cacheAge < SEVEN_DAYS) {
const cached = localStorage.getItem('ntCarDataMap');
if (cached) {
try {
carDataMap = JSON.parse(cached);
if (Object.keys(carDataMap).length > 0) {
carDataLoaded = true;
debugLog('Car data loaded from cache', {
count: Object.keys(carDataMap).length
});
if (callback) callback(true);
return;
}
} catch (e) {
console.warn('[Startrack] Failed to parse cached car data');
}
}
}
// Try to load from NTBOOTSTRAP
if (typeof NTBOOTSTRAP === 'function') {
try {
const bootstrapData = NTBOOTSTRAP();
const carsData = bootstrapData.find(item => item[0] === 'CARS');
if (carsData && carsData[1] && carsData[1].length > 0) {
carsData[1].forEach(car => {
if (car.carID && car.options && car.options.smallSrc) {
carDataMap[car.carID] = car.options.smallSrc;
}
});
if (Object.keys(carDataMap).length > 0) {
localStorage.setItem('ntCarDataMap', JSON.stringify(carDataMap));
localStorage.setItem('ntCarDataMapTimestamp', Date.now().toString());
carDataLoaded = true;
debugLog('Car data loaded from NTBOOTSTRAP', {
count: Object.keys(carDataMap).length
});
if (callback) callback(true);
return;
}
}
} catch (e) {
console.warn('[Startrack] Error loading from NTBOOTSTRAP:', e);
}
}
// NTBOOTSTRAP not available yet - retry
carDataLoadAttempts++;
if (carDataLoadAttempts < MAX_CAR_LOAD_ATTEMPTS) {
debugLog('Car data retry scheduled', {
attempt: carDataLoadAttempts,
maxAttempts: MAX_CAR_LOAD_ATTEMPTS
});
setTimeout(() => loadCarData(callback), 500);
return;
}
// Max attempts reached - try expired cache as fallback
const cached = localStorage.getItem('ntCarDataMap');
if (cached) {
try {
carDataMap = JSON.parse(cached);
if (Object.keys(carDataMap).length > 0) {
carDataLoaded = true;
debugLog('Car data loaded from expired cache fallback', {
count: Object.keys(carDataMap).length
});
if (callback) callback(true);
return;
}
} catch (e) {}
}
console.error('[Startrack] Failed to load car data after all attempts');
carDataLoaded = true; // Mark as loaded to prevent infinite retries
if (callback) callback(false);
}
function getCarImage(carID, carHueAngle) {
const smallSrc = carDataMap[carID];
if (smallSrc) {
// If car has a hue angle, use painted version
if (carHueAngle !== null && carHueAngle !== undefined && carHueAngle !== 0) {
// Format: /cars/painted/{smallSrc without .png}_{hue}.png
const baseImage = smallSrc.replace('.png', '');
const url = `https://www.nitrotype.com/cars/painted/${baseImage}_${carHueAngle}.png`;
// Debug: uncomment to trace image URLs
// console.log(`[Startrack] Car ${carID}: painted → ${url}`);
return url;
}
// Unpainted car - use smallSrc directly
// Debug: uncomment to trace image URLs
// console.log(`[Startrack] Car ${carID}: unpainted → /cars/${smallSrc}`);
return `https://www.nitrotype.com/cars/${smallSrc}`;
}
// Fallback - unpainted rental car (identifiable error state)
debugLog('Unknown car ID fallback used', {
carID: carID,
loadedCars: Object.keys(carDataMap).length
});
return `https://www.nitrotype.com/cars/9_small_1.png`;
}
// =================================================================
// 6. OTHER UTILITIES
// =================================================================
function startHourlyCheck() {
lastCheckedHour = getCurrentHour();
if (hourlyCheckInterval) clearInterval(hourlyCheckInterval);
hourlyCheckInterval = setInterval(() => {
const currentHour = getCurrentHour();
if (currentHour !== lastCheckedHour && location.pathname === LEADERBOARD_PATH) {
lastCheckedHour = currentHour;
state.currentDate = getCurrentCT();
if (['60min', '24hr', '7day'].includes(state.timeframe)) fetchLeaderboardData(true);
} else if (currentHour !== lastCheckedHour) {
lastCheckedHour = currentHour;
}
}, 60000);
}
function stopHourlyCheck() {
if (hourlyCheckInterval) { clearInterval(hourlyCheckInterval); hourlyCheckInterval = null; }
}
function formatDate(date) {
if (!date) return '';
const d = new Date(date);
const y = d.getFullYear();
const m = ('0' + (d.getMonth() + 1)).slice(-2);
const day = ('0' + d.getDate()).slice(-2);
const h = ('0' + d.getHours()).slice(-2);
const min = ('0' + d.getMinutes()).slice(-2);
const s = ('0' + d.getSeconds()).slice(-2);
return `${y}-${m}-${day} ${h}:${min}:${s}`;
}
function getStartOfDay(date) { const d = new Date(date); d.setHours(0, 0, 0, 0); return d; }
function getEndOfDay(date) { const d = new Date(date); d.setHours(23, 59, 59, 999); return d; }
function calculateDateRange(tempState) {
let start, end;
const current = new Date(tempState.currentDate);
const timeframe = tempState.timeframe || state.timeframe;
const now = getCurrentCT();
if (timeframe === 'season') {
// Use dynamic season data only when metadata is valid
if (!hasValidSeasonData()) return { start: null, end: null };
return { start: currentSeason.startCT, end: currentSeason.endCT };
}
else if (timeframe === '60min') { end = now; start = new Date(now.getTime() - (60 * 60 * 1000)); }
else if (timeframe === '24hr') { end = now; start = new Date(now.getTime() - (24 * 60 * 60 * 1000)); }
else if (timeframe === '7day') { end = now; start = new Date(now.getTime() - (7 * 24 * 60 * 60 * 1000)); }
else if (timeframe === 'daily') { start = getStartOfDay(current); end = getEndOfDay(current); }
else if (timeframe === 'weekly') {
const dayOfWeek = current.getDay();
start = getStartOfDay(current);
start.setDate(start.getDate() - dayOfWeek);
end = new Date(start);
end.setDate(end.getDate() + 6);
end = getEndOfDay(end);
} else if (timeframe === 'monthly') {
start = new Date(current.getFullYear(), current.getMonth(), 1);
end = new Date(current.getFullYear(), current.getMonth() + 1, 0);
end = getEndOfDay(end);
} else if (timeframe === 'custom') {
start = tempState.dateRange?.start || getStartOfDay(now);
end = tempState.dateRange?.end || getEndOfDay(now);
}
return { start: formatDate(start), end: formatDate(end) };
}
function navigateDate(direction) {
const current = state.currentDate;
const date = new Date(current);
if (state.timeframe === 'daily') date.setDate(current.getDate() + direction);
else if (state.timeframe === 'weekly') date.setDate(current.getDate() + (7 * direction));
else if (state.timeframe === 'monthly') date.setMonth(current.getMonth() + direction);
state.currentDate = date;
fetchLeaderboardData();
}
function setIndicator(message, isUpdating = true) {
const indicatorEl = document.getElementById('update-indicator');
if (indicatorEl) {
indicatorEl.textContent = message;
indicatorEl.style.color = isUpdating ? '#FFC107' : '#28A745';
const refreshBtn = document.getElementById('manual-refresh-btn');
if (refreshBtn && !isUpdating) {
const svg = refreshBtn.querySelector('svg');
if (svg) svg.classList.remove('icon-spin');
}
document.querySelectorAll('[data-timeframe]').forEach(btn => {
btn.classList.remove('is-active', 'is-frozen');
if (btn.dataset.timeframe === state.timeframe) btn.classList.add('is-active', 'is-frozen');
});
document.querySelectorAll('[data-view]').forEach(btn => {
btn.classList.remove('is-active');
if (btn.dataset.view === state.view) btn.classList.add('is-active');
});
}
}
// Generate a cache key for both localStorage and RAM cache
function getCacheKey(tempState) {
const s = tempState || state;
const ranges = calculateDateRange(s);
let startKey = ranges.start;
let endKey = ranges.end;
if (s.timeframe === '60min' || s.timeframe === '24hr' || s.timeframe === '7day') {
const roundToHour = (dateStr) => {
const date = new Date(dateStr.replace(' ', 'T'));
date.setMinutes(0, 0, 0);
return formatDate(date);
};
startKey = roundToHour(ranges.start);
endKey = roundToHour(ranges.end);
}
return `${CACHE_KEY}${s.view}_${s.timeframe}_${startKey}_${endKey}`;
}
// --- HTML BUILDING ---
function buildLeaderboardHTML() {
const currentTF = timeframes.find(t => t.key === state.timeframe);
const hasNav = currentTF?.hasNav || false;
const isCustom = state.timeframe === 'custom';
return `
<section class="card card--b card--o card--shadow card--f card--grit well well--b well--l">
<div class="card-cap bg--gradient">
<h1 class="h2 tbs">Startrack Leaderboards</h1>
</div>
<div class="well--p well--l_p">
<div class="row row--o well well--b well--l">
<div class="tabs tabs--a tabs--leaderboards">
<button class="tab" data-view="individual">
<div class="bucket bucket--c bucket--xs"><div class="bucket-media"><svg class="icon icon-racer"><use xlink:href="/dist/site/images/icons/icons.css.svg#icon-racer"></use></svg></div><div class="bucket-content">Top Racers</div></div>
</button>
<button class="tab" data-view="team">
<div class="bucket bucket--c bucket--xs"><div class="bucket-media"><svg class="icon icon-team"><use xlink:href="/dist/site/images/icons/icons.css.svg#icon-team"></use></svg></div><div class="bucket-content">Top Teams</div></div>
</button>
</div>
<div class="card card--d card--o card--sq card--f">
<div class="well--p well--pt">
<div class="row row--o has-btn">
${timeframes.map(tf => `<button type="button" class="btn btn--dark btn--outline btn--thin" data-timeframe="${tf.key}">${tf.label}</button>`).join('')}
</div>
<div class="divider divider--a mbf"></div>
<div class="seasonLeader seasonLeader--default" style="position: relative;">
<div class="split split--start row">
<div class="split-cell">
<h1 class="seasonLeader-title" id="date-title">Loading...</h1>
<p class="seasonLeader-date" id="date-range"></p>
</div>
</div>
<div style="position: absolute; bottom: 10px; width: 100%; text-align: center; left: 0; pointer-events: none;">
<div style="pointer-events: auto; display: inline-flex; align-items: center; justify-content: center;">
<span id="update-indicator" class="mrxs tsm">Loading...</span>
${isNtcfgLeaderboardsFeatureEnabled('SHOW_MANUAL_REFRESH') ? `<button id="manual-refresh-btn" class="btn btn--bare" title="Manual Refresh - Syncs All Tabs" style="padding: 2px; opacity: 0.6; line-height: 0;">
<svg style="width: 14px; height: 14px; fill: currentColor;" viewBox="0 0 24 24">
<path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 3.33-3.85 5.63-7.29 5.63-4.14 0-7.5-3.36-7.5-7.5s3.36-7.5 7.5-7.5c2.07 0 3.94.83 5.36 2.24L13 12h7V5l-2.35 1.35z"/>
</svg>
</button>` : ''}
</div>
</div>
</div>
${hasNav ? `
<div class="row row--o mtm">
<button class="btn btn--secondary btn--thin" id="nav-prev">< Previous</button>
<button class="btn btn--secondary btn--thin" id="nav-today">Today</button>
<button class="btn btn--secondary btn--thin" id="nav-next">Next ></button>
</div>
` : ''}
${isCustom ? `
<div class="row row--o mtm">
<label class="tsm tc-ts">Start:</label>
<input type="date" id="start-date" class="input input--mini mlxs mrm" value="${state.dateRange.start ? state.dateRange.start.toISOString().split('T')[0] : ''}">
<label class="tsm tc-ts">End:</label>
<input type="date" id="end-date" class="input input--mini mlxs mrm" value="${state.dateRange.end ? state.dateRange.end.toISOString().split('T')[0] : ''}">
<button class="btn btn--primary btn--thin" id="update-custom">Update</button>
</div>
` : ''}
<div id="leaderboard-table-container">
<div class="tac pxl mtl">
<div class="loading-spinner loading-spinner--ts" style="margin: 0 auto;"></div>
<div class="mtm">Loading content...</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
`;
}
// --- CACHE MGMT ---
function cleanOldCache() {
try {
const keys = Object.keys(localStorage);
const cacheKeys = keys.filter(k => k.startsWith(CACHE_KEY));
if (cacheKeys.length > 50) {
cacheKeys.sort().slice(0, cacheKeys.length - 50).forEach(key => {
localStorage.removeItem(key);
localStorage.removeItem(CACHE_TIMESTAMP_KEY + key);
});
}
// Remove orphaned timestamp keys
keys.filter(k => k.startsWith(CACHE_TIMESTAMP_KEY)).forEach(tsKey => {
const baseKey = tsKey.slice(CACHE_TIMESTAMP_KEY.length);
if (!localStorage.getItem(baseKey)) localStorage.removeItem(tsKey);
});
} catch (e) {}
}
function isCacheFresh(cacheKey, ttlMs = getCacheDurationMs()) {
const timestampKey = CACHE_TIMESTAMP_KEY + cacheKey;
const timestamp = localStorage.getItem(timestampKey);
if (!timestamp) return false;
return (Date.now() - parseInt(timestamp)) < ttlMs;
}
function saveToCache(cacheKey, data) {
try {
localStorage.setItem(cacheKey, data);
const savedAt = Date.now();
localStorage.setItem(CACHE_TIMESTAMP_KEY + cacheKey, savedAt.toString());
debugLog('Cache write success', {
key: shortCacheKey(cacheKey),
size: typeof data === 'string' ? data.length : null
});
} catch (quotaError) {
cleanOldCache();
try {
localStorage.setItem(cacheKey, data);
localStorage.setItem(CACHE_TIMESTAMP_KEY + cacheKey, Date.now().toString());
debugLog('Cache write success after cleanup', {
key: shortCacheKey(cacheKey),
size: typeof data === 'string' ? data.length : null
});
} catch (e2) {}
}
}
// --- RENDER TABLE ---
function renderTable(data, specificTime = null) {
const container = document.getElementById('leaderboard-table-container');
if (!container) return;
if (!data || data.length === 0) {
container.innerHTML = '<div class="tac pxl tsm tc-ts">No data available.</div>';
setIndicator('Updated', false);
return;
}
const top100 = data.slice(0, 100);
const isIndividual = state.view === 'individual';
const hasPositionData = top100.some(item => item.position_change !== null && item.position_change !== undefined);
let html = '<table class="table table--selectable table--striped table--fixed table--leaderboard">';
html += '<thead class="table-head"><tr class="table-row">';
const rankChangeHeader = hasPositionData ? '<th scope="col" class="table-cell table-cell--rank-change"></th>' : '';
html += isIndividual
? `${rankChangeHeader}<th scope="col" class="table-cell table-cell--place"></th><th scope="col" class="table-cell table-cell--racer">Racer</th><th scope="col" class="table-cell table-cell--speed">WPM</th><th scope="col" class="table-cell table-cell--races">Accuracy</th><th scope="col" class="table-cell table-cell--races">Races</th><th scope="col" class="table-cell table-cell--points">Points</th>`
: `${rankChangeHeader}<th scope="col" class="table-cell table-cell--place"></th><th scope="col" class="table-cell table-cell--tag">Tag</th><th scope="col" class="table-cell table-cell--team">Team</th><th scope="col" class="table-cell table-cell--speed">WPM</th><th scope="col" class="table-cell table-cell--races">Accuracy</th><th scope="col" class="table-cell table-cell--races">Races</th><th scope="col" class="table-cell table-cell--points">Points</th>`;
html += '</tr></thead><tbody class="table-body">';
top100.forEach((item, index) => {
const rank = index + 1;
let rowClass = 'table-row';
const posChangeHTML = getPositionChangeHTML(item.position_change);
const rankChangeCellHtml = hasPositionData ? `<td class="table-cell tac table-cell--rank-change">${posChangeHTML}</td>` : '';
// Rank visual HTML
let medalHTML = `<div class="mhc"><span class="h3 tc-ts">${rank}</span></div>`;
if (rank === 1) {
rowClass = 'table-row table-row--gold';
medalHTML = `<div class="mhc"><img class="db" src="/dist/site/images/medals/gold-sm.png"></div>`;
} else if (rank === 2) {
rowClass = 'table-row table-row--silver';
medalHTML = `<div class="mhc"><img class="db" src="/dist/site/images/medals/silver-sm.png"></div>`;
} else if (rank === 3) {
rowClass = 'table-row table-row--bronze';
medalHTML = `<div class="mhc"><img class="db" src="/dist/site/images/medals/bronze-sm.png"></div>`;
}
const wpm = parseFloat(item.WPM).toFixed(1);
const acc = (parseFloat(item.Accuracy) * 100).toFixed(2);
const points = Math.round(parseFloat(item.Points)).toLocaleString();
if (isIndividual) {
html += `<tr class="${rowClass}" data-username="${item.Username || ''}" style="cursor: pointer;">${rankChangeCellHtml}<td class="table-cell table-cell--place tac">${medalHTML}</td>`;
const teamTag = item.TeamTag || '--';
const displayName = item.CurrentDisplayName || item.Username;
const tagColor = item.tagColor || 'fff';
const isGold = item.membership === 'gold';
const carImage = getCarImage(item.carID || 1, item.carHueAngle);
const title = item.title || 'Untitled';
html += `
<td class="table-cell table-cell--racer">
<div class="bucket bucket--s bucket--c">
<div class="bucket-media bucket-media--w90"><img class="db" src="${carImage}"></div>
<div class="bucket-content">
<div class="df df--align-center">
${isGold ? '<div class="prxxs"><img alt="NT Gold" class="icon icon-nt-gold-s" src="https://www.nitrotype.com/dist/site/images/themes/profiles/gold/nt-gold-icon.png"></div>' : ''}
<div class="prxs df df--align-center" title="${displayName}">
<a href="https://www.nitrotype.com/team/${teamTag}" class="link link--bare mrxxs twb" style="color: #${tagColor};">[${teamTag}]</a>
<span class="type-ellip ${isGold ? 'type-gold' : ''} tss">${displayName}</span>
</div>
</div>
<div class="tsxs tc-fuel tsi db">"${title}"</div>
</div>
</div>
</td>
<td class="table-cell table-cell--speed">${wpm}</td>
<td class="table-cell table-cell--races">${acc}%</td>
<td class="table-cell table-cell--races">${item.Races}</td>
<td class="table-cell table-cell--points">${points}</td>
`;
} else {
const teamTag = item.TeamTag || '----';
const teamName = item.TeamName || `${teamTag} Team`;
const tagColor = item.tagColor || 'B3C8DD';
html += `<tr class="${rowClass}" data-teamtag="${item.TeamTag || ''}" style="cursor: pointer;">${rankChangeCellHtml}<td class="table-cell table-cell--place tac">${medalHTML}</td>`;
html += `
<td class="table-cell table-cell--tag"><span class="twb" style="color: #${tagColor};">[${teamTag}]</span></td>
<td class="table-cell table-cell--team"><span class="tc-lemon">"${teamName}"</span></td>
<td class="table-cell table-cell--speed">${wpm}</td>
<td class="table-cell table-cell--races">${acc}%</td>
<td class="table-cell table-cell--races">${item.Races}</td>
<td class="table-cell table-cell--points">${points}</td>
`;
}
html += '</tr>';
});
html += '</tbody></table>';
container.innerHTML = html;
if (isIndividual) {
document.querySelectorAll('.table-row[data-username]').forEach(row => {
row.addEventListener('click', (e) => {
if (!e.target.closest('a[href*="/team/"]')) window.location.href = `https://www.nitrotype.com/racer/${row.dataset.username}`;
});
});
} else {
document.querySelectorAll('.table-row[data-teamtag]').forEach(row => {
row.addEventListener('click', () => window.location.href = `https://www.nitrotype.com/team/${row.dataset.teamtag}`);
});
}
const cacheKey = getCacheKey();
let timeToDisplay = specificTime || localStorage.getItem(CACHE_TIMESTAMP_KEY + cacheKey);
if (!timeToDisplay) timeToDisplay = Date.now().toString();
const updateTime = new Date(parseInt(timeToDisplay));
setIndicator(`Last updated: ${updateTime.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`, false);
if (!initialCacheComplete && !isCaching) {
initialCacheComplete = true;
isCaching = true;
setTimeout(() => { populateCacheQueue(); cacheAllViews(); }, 1000);
}
}
function updateDateDisplay() {
const titleEl = document.getElementById('date-title');
const rangeEl = document.getElementById('date-range');
if (!titleEl || !rangeEl) return;
const ranges = calculateDateRange(state);
const hasRange = !!(ranges && ranges.start && ranges.end);
const start = hasRange ? new Date(ranges.start.replace(' ', 'T')) : null;
const end = hasRange ? new Date(ranges.end.replace(' ', 'T')) : null;
if (state.timeframe === 'season') {
// Use dynamic season name and dates
titleEl.textContent = currentSeason.name || 'Season';
const seasonDates = getSeasonDisplayDates();
rangeEl.textContent = `${seasonDates.startDisplay} - ${seasonDates.endDisplay}`;
}
else if (state.timeframe === 'daily') { titleEl.textContent = 'Daily'; rangeEl.textContent = start ? start.toLocaleDateString() : 'Loading...'; }
else if (state.timeframe === 'weekly') { titleEl.textContent = 'Weekly'; rangeEl.textContent = (start && end) ? `${start.toLocaleDateString()} - ${end.toLocaleDateString()}` : 'Loading...'; }
else if (state.timeframe === 'monthly') { titleEl.textContent = 'Monthly'; rangeEl.textContent = start ? start.toLocaleDateString(undefined, { month: 'long', year: 'numeric' }) : 'Loading...'; }
else if (state.timeframe === 'custom') { titleEl.textContent = 'Custom Range'; rangeEl.textContent = (start && end) ? `${start.toLocaleDateString()} - ${end.toLocaleDateString()}` : 'Loading...'; }
else { titleEl.textContent = timeframes.find(t => t.key === state.timeframe)?.label || 'Leaderboards'; rangeEl.textContent = ''; }
}
function populateCacheQueue(options = {}) {
cacheQueue = [];
const views = options.views || ['individual', 'team'];
const currentCT = getCurrentCT();
const now = getCurrentCT();
now.setMinutes(0, 0, 0);
const timeframesFilter = Array.isArray(options.timeframes) ? new Set(options.timeframes) : null;
const includeNav = options.includeNav !== false;
const includeCurrent = options.includeCurrent === true ||
(options.includeCurrent === undefined && location.pathname !== LEADERBOARD_PATH);
const shouldInclude = (key) => !timeframesFilter || timeframesFilter.has(key);
const priorityTimeframes = ['season', '24hr', '60min', '7day'];
const prioritySet = new Set(priorityTimeframes);
// Always fetch Season first: individual -> team
if (shouldInclude('season')) {
if (hasValidSeasonData()) {
cacheQueue.push({ view: 'individual', timeframe: 'season', currentDate: now });
cacheQueue.push({ view: 'team', timeframe: 'season', currentDate: now });
} else {
debugLog('Season queue skipped (metadata unavailable)', {
reason: options.reason || 'unknown',
targetTimeframe: 'season'
});
}
}
timeframes
.filter(t => prioritySet.has(t.key) && t.key !== 'season' && shouldInclude(t.key))
.forEach(tf => {
views.forEach(view => cacheQueue.push({ view: view, timeframe: tf.key, currentDate: now }));
});
if (includeNav) {
const dynamicTFs = timeframes.filter(t => t.hasNav && shouldInclude(t.key));
views.forEach(view => {
dynamicTFs.forEach(tf => {
let date = new Date(currentCT.getFullYear(), currentCT.getMonth(), currentCT.getDate());
cacheQueue.push({ view: view, timeframe: tf.key, currentDate: date });
});
});
}
if (!includeCurrent) {
const currentKey = getCacheKey();
cacheQueue = cacheQueue.filter(item => getCacheKey(item) !== currentKey);
}
debugLog('Cache queue prepared', {
reason: options.reason || 'unknown',
includeNav: includeNav,
includeCurrent: includeCurrent,
force: options.force === true,
count: cacheQueue.length,
order: cacheQueue.map(item => `${item.view}:${item.timeframe}`),
targetTimeframe: cacheQueue[0]?.timeframe || null
});
}
function cacheAllViews() {
try {
if (cacheQueue.length === 0) {
debugLog('Cache queue drained', { reason: 'complete' });
isCaching = false;
forceBackgroundUpdate = false;
return;
}
const nextItem = cacheQueue.shift();
const nextKey = getCacheKey(nextItem);
const ttl = getCacheTTLForTimeframe(nextItem.timeframe);
const scheduleNext = () => setTimeout(cacheAllViews, BACKGROUND_SYNC_DELAY_MS);
const freshness = getCacheFreshness(nextKey, ttl);
if (!forceBackgroundUpdate && localStorage.getItem(nextKey) && isCacheFresh(nextKey, ttl)) {
debugLog('Cache queue item skipped (fresh cache)', {
view: nextItem.view,
timeframe: nextItem.timeframe,
key: shortCacheKey(nextKey),
ageMs: freshness.ageMs,
ttlMs: ttl,
queueRemaining: cacheQueue.length,
targetTimeframe: nextItem.timeframe
});
scheduleNext();
return;
}
debugLog('Cache queue item fetching', {
view: nextItem.view,
timeframe: nextItem.timeframe,
key: shortCacheKey(nextKey),
forceBackgroundUpdate: forceBackgroundUpdate,
ageMs: freshness.ageMs,
ttlMs: ttl,
queueRemaining: cacheQueue.length,
targetTimeframe: nextItem.timeframe
});
fetchFreshData(nextKey, nextItem.view, nextItem.timeframe, nextItem.currentDate, scheduleNext);
} catch (error) {
console.error('Error in cache queue:', error);
debugLog('Cache queue error', { message: error && error.message ? error.message : String(error) });
isCaching = false;
forceBackgroundUpdate = false;
}
}
function startBackgroundSync(reason, options = {}) {
try {
loadSeasonData();
if (isCaching) {
debugLog('Background sync skipped (already running)', { reason: reason });
return false;
}
forceBackgroundUpdate = options.force === true;
populateCacheQueue({ ...options, reason: reason });
isCaching = true;
cacheAllViews();
debugLog('Background sync started', {
reason: reason,
force: forceBackgroundUpdate,
options: options
});
return true;
} catch (error) {
console.error('[Startrack] Background sync failed to start:', error);
debugLog('Background sync failed', {
reason: reason,
message: error && error.message ? error.message : String(error)
});
return false;
}
}
function maybeStartDailyBackgroundSync() {
try {
const todayKey = getCTDateKey();
const lastKey = localStorage.getItem(DAILY_SYNC_KEY);
if (lastKey === todayKey) {
debugLog('Daily sync skipped (already ran for CT day)', {
todayKey: todayKey,
lastKey: lastKey,
targetTimeframe: 'season'
});
return;
}
if (startBackgroundSync('daily', { includeCurrent: true })) {
localStorage.setItem(DAILY_SYNC_KEY, todayKey);
debugLog('Daily sync marker updated', {
todayKey: todayKey,
targetTimeframe: 'season'
});
}
} catch (error) {
console.warn('[Startrack] Daily sync check failed:', error);
debugLog('Daily sync error', {
message: error && error.message ? error.message : String(error),
targetTimeframe: 'season'
});
}
}
function maybeStartHourlyBackgroundSync() {
try {
const hourKey = getCTHourKey();
const lastKey = localStorage.getItem(HOURLY_SYNC_KEY);
if (lastKey === hourKey) {
debugLog('Hourly sync skipped (already ran for CT hour)', {
hourKey: hourKey,
lastKey: lastKey,
targetTimeframe: 'season'
});
return;
}
const started = startBackgroundSync('hourly', {
timeframes: ['season', '60min', '24hr'],
includeNav: false,
includeCurrent: true,
force: true
});
if (started) {
localStorage.setItem(HOURLY_SYNC_KEY, hourKey);
debugLog('Hourly sync marker updated', {
hourKey: hourKey,
targetTimeframe: 'season'
});
}
} catch (error) {
console.warn('[Startrack] Hourly sync check failed:', error);
debugLog('Hourly sync error', {
message: error && error.message ? error.message : String(error),
targetTimeframe: 'season'
});
}
}
function startGlobalCacheMaintenance() {
if (window.__ntGlobalCacheInterval) return;
window.__ntGlobalCacheInterval = setInterval(() => {
maybeStartDailyBackgroundSync();
maybeStartHourlyBackgroundSync();
}, 60000);
}
function stopGlobalCacheMaintenance() {
if (!window.__ntGlobalCacheInterval) return;
clearInterval(window.__ntGlobalCacheInterval);
window.__ntGlobalCacheInterval = null;
}
function syncBackgroundWorkToRoute() {
maybeStartDailyBackgroundSync();
maybeStartHourlyBackgroundSync();
startGlobalCacheMaintenance();
}
function initDebugAPI() {
const debugApi = {
enable: function() {
localStorage.setItem(DEBUG_FLAG_KEY, '1');
console.log('[Startrack][DBG] Enabled. Reload the page to start logging.');
},
disable: function() {
localStorage.removeItem(DEBUG_FLAG_KEY);
localStorage.removeItem(DEBUG_SEASON_ONLY_KEY);
console.log('[Startrack][DBG] Disabled. Reload the page to stop logging.');
},
seasonOnly: function(enabled = true) {
if (enabled) localStorage.setItem(DEBUG_SEASON_ONLY_KEY, '1');
else localStorage.removeItem(DEBUG_SEASON_ONLY_KEY);
console.log(`[Startrack][DBG] seasonOnly=${enabled ? 'ON' : 'OFF'}. Reload the page to apply.`);
},
status: function() {
const status = {
debugEnabled: DEBUG_ENABLED,
debugSeasonOnly: DEBUG_SEASON_ONLY,
debugParamEnabled: DEBUG_PARAM_ENABLED,
currentState: { view: state.view, timeframe: state.timeframe },
ctDateKey: getCTDateKey(),
ctHourKey: getCTHourKey(),
lastDailySyncKey: localStorage.getItem(DAILY_SYNC_KEY),
lastHourlySyncKey: localStorage.getItem(HOURLY_SYNC_KEY),
seasonName: currentSeason.name,
seasonStartUTC: currentSeason.startStampUTC,
seasonEndUTC: currentSeason.endStampUTC
};
console.log('[Startrack][DBG] Status', status);
return status;
},
inspect: function(view = 'individual', timeframe = 'season') {
const tempState = { view: view, timeframe: timeframe, currentDate: getCurrentCT() };
const key = getCacheKey(tempState);
const ttl = getCacheTTLForTimeframe(timeframe);
const freshness = getCacheFreshness(key, ttl);
const result = {
view: view,
timeframe: timeframe,
key: shortCacheKey(key),
hasData: !!localStorage.getItem(key),
hasTimestamp: freshness.hasTimestamp,
ageMs: freshness.ageMs,
ttlMs: freshness.ttlMs,
fresh: freshness.fresh
};
console.log('[Startrack][DBG] Inspect', result);
return result;
}
};
window.NTStartrackDebug = debugApi;
if (typeof unsafeWindow !== 'undefined' && unsafeWindow) {
unsafeWindow.NTStartrackDebug = debugApi;
}
if (DEBUG_ENABLED) {
console.log(`[Startrack][DBG] Enabled${DEBUG_SEASON_ONLY ? ' (season-only)' : ''}. API: window.NTStartrackDebug.status(), .inspect(), .seasonOnly(), .disable()`);
}
}
function fetchLeaderboardData(forceRefresh = false) {
if (state.timeframe === 'season' && !hasValidSeasonData()) {
loadSeasonData();
debugLog('Leaderboard fetch deferred (season metadata unavailable)', {
view: state.view,
timeframe: state.timeframe,
forceRefresh: forceRefresh,
stateTimeframe: state.timeframe
});
updateDateDisplay();
setIndicator('Season metadata loading...', true);
const container = document.getElementById('leaderboard-table-container');
if (container) {
container.innerHTML = `<div class="tac pxl mtl"><div class="loading-spinner loading-spinner--ts" style="margin: 0 auto;"></div><div class="mtm">Loading season metadata...</div></div>`;
}
return;
}
const cacheKey = getCacheKey();
const ttl = getCacheTTLForTimeframe(state.timeframe);
const freshness = getCacheFreshness(cacheKey, ttl);
debugLog('Leaderboard fetch start', {
view: state.view,
timeframe: state.timeframe,
forceRefresh: forceRefresh,
key: shortCacheKey(cacheKey),
hasLocalStorage: !!localStorage.getItem(cacheKey),
hasTimestamp: freshness.hasTimestamp,
ageMs: freshness.ageMs,
ttlMs: ttl,
freshByTimestamp: freshness.fresh,
stateTimeframe: state.timeframe
});
// Check RAM cache first (now properly keyed)
if (window.NTShared && window.NTShared.getCache && !forceRefresh) {
const sharedData = window.NTShared.getCache(cacheKey);
if (sharedData) {
debugLog('Leaderboard served from RAM cache', {
view: state.view,
timeframe: state.timeframe,
key: shortCacheKey(cacheKey),
count: Array.isArray(sharedData) ? sharedData.length : null,
stateTimeframe: state.timeframe
});
updateDateDisplay();
const sharedTS = window.NTShared.getTimestamp(cacheKey);
renderTable(sharedData, sharedTS);
return;
}
}
// Check localStorage cache
const cachedData = localStorage.getItem(cacheKey);
updateDateDisplay();
if (cachedData) {
try {
const data = JSON.parse(cachedData);
renderTable(data);
if (isCacheFresh(cacheKey, ttl) && !forceRefresh) {
debugLog('Leaderboard served from localStorage cache', {
view: state.view,
timeframe: state.timeframe,
key: shortCacheKey(cacheKey),
count: Array.isArray(data) ? data.length : null,
ageMs: freshness.ageMs,
ttlMs: ttl,
stateTimeframe: state.timeframe
});
return;
}
debugLog('Leaderboard localStorage cache stale, revalidating', {
view: state.view,
timeframe: state.timeframe,
key: shortCacheKey(cacheKey),
ageMs: freshness.ageMs,
ttlMs: ttl,
stateTimeframe: state.timeframe
});
setIndicator('Updating...', true);
fetchFreshData(cacheKey);
return;
}
catch (e) {
debugLog('Leaderboard cache parse failed', {
key: shortCacheKey(cacheKey),
message: e && e.message ? e.message : String(e),
stateTimeframe: state.timeframe
});
localStorage.removeItem(cacheKey);
}
}
debugLog('Leaderboard cache miss, fetching network', {
view: state.view,
timeframe: state.timeframe,
key: shortCacheKey(cacheKey),
stateTimeframe: state.timeframe
});
const container = document.getElementById('leaderboard-table-container');
if (container) container.innerHTML = `<div class="tac pxl mtl"><div class="loading-spinner loading-spinner--ts" style="margin: 0 auto;"></div><div class="mtm">Loading data...</div></div>`;
setIndicator('Updating...', true);
fetchFreshData(cacheKey);
}
function fetchFreshData(cacheKey, view = state.view, timeframe = state.timeframe, currentDate = state.currentDate, callback) {
const tempState = { view, timeframe, currentDate };
const requestStartedAt = Date.now();
let ranges = calculateDateRange(tempState);
if (!ranges || !ranges.start || !ranges.end) {
debugLog('Network fetch skipped (missing date range)', {
view: view,
timeframe: timeframe,
key: shortCacheKey(cacheKey),
stateTimeframe: state.timeframe
});
if (view === state.view && timeframe === state.timeframe) {
setIndicator('Season metadata loading...', true);
}
if (callback) callback();
return;
}
if (['60min', '24hr', '7day'].includes(timeframe)) {
const roundToHour = (d) => { const date = new Date(d.replace(' ', 'T')); date.setMinutes(0, 0, 0); return formatDate(date); };
ranges = { start: roundToHour(ranges.start), end: roundToHour(ranges.end) };
}
const apiUrl = view === 'individual' ? 'https://ntstartrack.org/api/individual-leaderboard' : 'https://ntstartrack.org/api/team-leaderboard';
const url = `${apiUrl}?start_time=${encodeURIComponent(ranges.start)}&end_time=${encodeURIComponent(ranges.end)}&showbot=FALSE&include_position_change=true&cb=${new Date().getTime()}`;
debugLog('Network fetch start', {
view: view,
timeframe: timeframe,
key: shortCacheKey(cacheKey),
start: ranges.start,
end: ranges.end,
url: url,
stateTimeframe: state.timeframe
});
if (view === state.view && timeframe === state.timeframe) setIndicator('Updating...', true);
GM_xmlhttpRequest({
method: 'GET',
url: url,
onload: function(response) {
if (response.status === 200) {
setTimeout(() => {
try {
if (!response.responseText || response.responseText.trim().length === 0) throw new Error('Empty');
let data = JSON.parse(response.responseText);
if (!Array.isArray(data)) throw new Error('Invalid format');
data = sanitizeLeaderboardRows(view, data);
const now = Date.now();
const ttl = getCacheTTLForTimeframe(timeframe);
const legacyLimit = view === 'individual' ? 300 : 50;
const legacyData = data.slice(0, legacyLimit);
const top100 = data.slice(0, 100);
saveToCache(cacheKey, JSON.stringify(top100));
// Save to RAM cache with proper key
if (window.NTShared && window.NTShared.setCache) {
window.NTShared.setCache(cacheKey, top100, now + ttl);
}
// Provide legacy 7-day cache for badge scripts
if (timeframe === '7day' && window.NTShared && window.NTShared.setLegacyCache) {
window.NTShared.setLegacyCache(view, legacyData, now + ttl);
}
if (timeframe === '7day') {
const sharedCacheKey = getSharedCacheKey(tempState);
const sharedPayload = buildSharedLeaderboardPayload(view, timeframe, legacyData, now, now + ttl);
if (window.NTShared && window.NTShared.setCache) {
window.NTShared.setCache(sharedCacheKey, sharedPayload, sharedPayload.expiresAt);
}
saveSharedPayloadToLocalStorage(sharedCacheKey, sharedPayload);
}
debugLog('Network fetch success', {
view: view,
timeframe: timeframe,
key: shortCacheKey(cacheKey),
status: response.status,
rows: data.length,
renderedRows: top100.length,
elapsedMs: Date.now() - requestStartedAt,
ttlMs: ttl,
stateTimeframe: state.timeframe
});
if (view === state.view && timeframe === state.timeframe) renderTable(data, now);
if (callback) callback();
} catch (e) {
console.error(e);
debugLog('Network fetch parse/render error', {
view: view,
timeframe: timeframe,
key: shortCacheKey(cacheKey),
message: e && e.message ? e.message : String(e),
elapsedMs: Date.now() - requestStartedAt,
stateTimeframe: state.timeframe
});
if (view === state.view && timeframe === state.timeframe) {
setIndicator(`Update failed`, false);
document.documentElement.classList.remove('is-leaderboard-route');
const main = document.querySelector('main.structure-content');
if (main) main.classList.remove('custom-loaded');
}
if (callback) callback();
}
}, ASYNC_DELAY);
} else {
debugLog('Network fetch failed status', {
view: view,
timeframe: timeframe,
key: shortCacheKey(cacheKey),
status: response.status,
elapsedMs: Date.now() - requestStartedAt,
stateTimeframe: state.timeframe
});
if (view === state.view && timeframe === state.timeframe) {
setIndicator(`Update failed`, false);
document.documentElement.classList.remove('is-leaderboard-route');
const main = document.querySelector('main.structure-content');
if (main) main.classList.remove('custom-loaded');
}
if (callback) callback();
}
},
onerror: function() {
debugLog('Network fetch transport error', {
view: view,
timeframe: timeframe,
key: shortCacheKey(cacheKey),
elapsedMs: Date.now() - requestStartedAt,
stateTimeframe: state.timeframe
});
if (view === state.view && timeframe === state.timeframe) {
setIndicator('Update failed', false);
document.documentElement.classList.remove('is-leaderboard-route');
const main = document.querySelector('main.structure-content');
if (main) main.classList.remove('custom-loaded');
}
if (callback) callback();
}
});
}
function attachListeners() {
document.querySelectorAll('[data-view]').forEach(btn => {
btn.addEventListener('click', (e) => {
const view = e.currentTarget.dataset.view;
if (state.view !== view) { state.view = view; fetchLeaderboardData(false); }
});
});
document.querySelectorAll('[data-timeframe]').forEach(btn => {
btn.addEventListener('click', (e) => {
const tf = e.currentTarget.dataset.timeframe;
if (state.timeframe !== tf) { state.timeframe = tf; state.currentDate = getCurrentCT(); fetchLeaderboardData(false); }
});
});
document.getElementById('nav-prev')?.addEventListener('click', () => navigateDate(-1));
document.getElementById('nav-next')?.addEventListener('click', () => navigateDate(1));
document.getElementById('nav-today')?.addEventListener('click', () => { state.currentDate = getCurrentCT(); fetchLeaderboardData(true); });
const refreshBtn = document.getElementById('manual-refresh-btn');
if (refreshBtn) {
refreshBtn.addEventListener('click', (e) => {
const icon = e.currentTarget.querySelector('svg');
if(icon) icon.classList.add('icon-spin');
fetchLeaderboardData(true);
debugLog('Manual refresh requested full background sync', {
reason: 'manual-refresh-button'
});
forceBackgroundUpdate = true;
populateCacheQueue();
if (!isCaching) {
isCaching = true;
cacheAllViews();
}
});
}
document.getElementById('update-custom')?.addEventListener('click', () => {
const startVal = document.getElementById('start-date')?.value;
const endVal = document.getElementById('end-date')?.value;
if (startVal && endVal) {
state.dateRange.start = getStartOfDay(new Date(startVal + 'T00:00:00'));
state.dateRange.end = getEndOfDay(new Date(endVal + 'T00:00:00'));
fetchLeaderboardData(true);
}
});
}
function renderLeaderboardPage(forceRefresh = false) {
if (pageRenderInProgress) return;
pageRenderInProgress = true;
const mainContent = document.querySelector('main.structure-content');
if (!mainContent) {
pageRenderInProgress = false;
return;
}
try {
// Load season data first (this can use NTBOOTSTRAP or cache)
loadSeasonData();
// Build HTML immediately
mainContent.innerHTML = buildLeaderboardHTML();
requestAnimationFrame(() => { mainContent.classList.add('custom-loaded'); });
attachListeners();
setActiveTab();
setTabTitle();
startHourlyCheck();
// Defer car loading with retry mechanism
loadCarData((success) => {
if (success) {
debugLog('Car data ready, fetching leaderboard', { success: true });
}
// Fetch leaderboard data after car data is loaded (or failed)
fetchLeaderboardData(forceRefresh);
});
} catch (error) {
console.error('Error rendering leaderboard page:', error);
document.documentElement.classList.remove('is-leaderboard-route');
if (mainContent) mainContent.classList.remove('custom-loaded');
} finally {
pageRenderInProgress = false;
}
}
function setActiveTab() {
document.querySelectorAll('.nav-list-item').forEach(li => li.classList.remove('is-current'));
const tab = document.querySelector('.' + TAB_CLASS);
if (tab) tab.classList.add('is-current');
}
function setTabTitle() {
if (window.location.pathname === LEADERBOARD_PATH) document.title = 'Leaderboards | Nitro Type';
}
function insertLeaderboardDropdownLink() {
if (!isNtcfgLeaderboardsFeatureEnabled('SHOW_DROPDOWN_LINK')) return;
// Don't duplicate
if (document.querySelector('.ntcfg-leaderboards-dropdown-item')) return;
const dropdown = document.querySelector('.dropdown--account .dropdown-items');
if (!dropdown) return;
// Insert above "My Public Profile" (icon-eye), or fall back to end of list
const profileItem = Array.from(dropdown.querySelectorAll('.dropdown-item')).find(
li => li.querySelector('a[href*="/racer/"]')
);
const li = document.createElement('li');
li.className = 'list-item dropdown-item ntcfg-leaderboards-dropdown-item';
if (location.pathname === LEADERBOARD_PATH) li.classList.add('is-current');
li.innerHTML = `<a class="dropdown-link" href="${LEADERBOARD_PATH}"><svg class="icon icon-trophy mrxs" style="width:16px;height:16px;vertical-align:middle;"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/dist/site/images/icons/icons.css.svg#icon-trophy"></use></svg>Leaderboards</a>`;
if (profileItem) {
profileItem.before(li);
} else {
dropdown.appendChild(li);
}
}
function removeLeaderboardDropdownLink() {
document.querySelectorAll('.ntcfg-leaderboards-dropdown-item').forEach((el) => el.remove());
}
function insertLeaderboardTab() {
if (!isNtcfgLeaderboardsFeatureEnabled('SHOW_ROUTE_TAB')) return;
if (document.querySelector(`a[href="${LEADERBOARD_PATH}"]`)) return;
const navList = document.querySelector('.nav-list');
if (!navList) return;
const li = document.createElement('li');
li.className = `nav-list-item ${TAB_CLASS}`;
li.innerHTML = `<a href="${LEADERBOARD_PATH}" class="nav-link"><span class="has-notify">Leaderboards</span></a>`;
const news = Array.from(navList.children).find(li => li.textContent.trim().includes('News'));
if (news) news.before(li);
else navList.appendChild(li);
}
function removeLeaderboardTab() {
document.querySelectorAll('.' + TAB_CLASS).forEach((el) => el.remove());
}
function rerenderLeaderboardPageFromSettings() {
if (location.pathname === LEADERBOARD_PATH) {
renderLeaderboardPage(true);
}
}
function applyLeaderboardsSettingSideEffects(settingKey) {
if (!settingKey) return;
if (settingKey === 'HIDE_CLASS_TAB') {
syncHideClassTabState();
return;
}
if (settingKey === 'SHOW_ROUTE_TAB') {
if (isNtcfgLeaderboardsFeatureEnabled('SHOW_ROUTE_TAB')) insertLeaderboardTab();
else removeLeaderboardTab();
return;
}
if (settingKey === 'SHOW_DROPDOWN_LINK') {
if (isNtcfgLeaderboardsFeatureEnabled('SHOW_DROPDOWN_LINK')) insertLeaderboardDropdownLink();
else removeLeaderboardDropdownLink();
return;
}
if (settingKey === 'SHOW_MANUAL_REFRESH' || settingKey === 'HIGHLIGHT_POSITION_CHANGE') {
rerenderLeaderboardPageFromSettings();
}
}
function applyAllLeaderboardsSettingSideEffects() {
syncHideClassTabState();
if (isNtcfgLeaderboardsFeatureEnabled('SHOW_ROUTE_TAB')) insertLeaderboardTab();
else removeLeaderboardTab();
if (isNtcfgLeaderboardsFeatureEnabled('SHOW_DROPDOWN_LINK')) insertLeaderboardDropdownLink();
else removeLeaderboardDropdownLink();
rerenderLeaderboardPageFromSettings();
}
function handlePage() {
insertLeaderboardTab();
insertLeaderboardDropdownLink();
if (location.pathname === LEADERBOARD_PATH) {
if (document.getElementById('leaderboard-table-container')) { setActiveTab(); return; }
const main = document.querySelector('main.structure-content');
if (main && (main.children.length === 0 || main.querySelector('.error'))) renderLeaderboardPage();
} else {
document.querySelector('.' + TAB_CLASS)?.classList.remove('is-current');
stopHourlyCheck();
}
}
function fastInject() {
insertLeaderboardTab();
insertLeaderboardDropdownLink();
if (location.pathname === LEADERBOARD_PATH) {
const waitForMain = new MutationObserver(() => {
const main = document.querySelector('main.structure-content');
if (main) {
if (main.children.length === 0 || main.querySelector('.error') || main.textContent.includes("Page Not Found")) {
renderLeaderboardPage();
waitForMain.disconnect();
}
}
});
waitForMain.observe(document.documentElement, { childList: true, subtree: true });
}
}
initDebugAPI();
ntRouteHelper.subscribe(() => {
updateRouteStatus();
syncBackgroundWorkToRoute();
handlePage();
}, { immediate: false });
fastInject();
syncBackgroundWorkToRoute();
handlePage();
const navObserver = new MutationObserver((mutations) => {
if (!document.querySelector(`a[href="${LEADERBOARD_PATH}"]`)) {
insertLeaderboardTab();
insertLeaderboardDropdownLink();
}
if (location.pathname === LEADERBOARD_PATH) {
const main = document.querySelector('main.structure-content');
if (main && (main.children.length === 0 || main.querySelector('.error') || main.textContent.includes("Page Not Found"))) {
if (!main.classList.contains('custom-loaded')) {
renderLeaderboardPage();
}
}
}
handlePage();
});
navObserver.observe(document.documentElement, { childList: true, subtree: true });
})();