// Home Screen Section Benchmarks — timed query overview for admin config (function () { 'use strict'; const LOG = (...args) => console.log('[KefinHomeScreenBenchmark]', ...args); const STORAGE_KEY = 'kefin.homeScreen.benchmark.selectedIds'; const DEFAULT_QUERY_LIMIT = window.KefinHomeScreenEditorConstants?.DEFAULT_SECTION_QUERY_LIMIT ?? 16; const CATEGORIES = [ { id: 'default', label: 'Default', groupsKey: 'HOME_SECTION_GROUPS' }, { id: 'custom', label: 'Custom', groupsKey: 'CUSTOM_SECTION_GROUPS' }, { id: 'discovery', label: 'Discovery', groupsKey: 'DISCOVERY_SECTION_GROUPS' }, { id: 'seasonal', label: 'Seasonal', groupsKey: 'SEASONAL_SECTION_GROUPS' } ]; const MS_BANDS = [ { max: 500, className: 'kefin-bench-ms-fast' }, { max: 1000, className: 'kefin-bench-ms-mid' }, { max: 2000, className: 'kefin-bench-ms-slow' }, { max: Infinity, className: 'kefin-bench-ms-worst' } ]; /** @type {{ aborted: boolean, controller: AbortController|null, onSkipRemaining?: Function } | null} */ let activeRun = null; let navCancelBound = false; /** * Survives re-renders of the tab so results are not lost when the user * switches tabs or reopens the config modal. * @type {{ entries: Array, results: Map } | null} */ let lastRun = null; let lastViewMode = 'picker'; function hasStoredResults() { return !!lastRun?.entries?.length; } function flattenGroups(groups) { if (!Array.isArray(groups)) return []; const out = []; groups.forEach((group) => { if (group && Array.isArray(group.sections)) { out.push(...group.sections); } }); return out; } function getSectionsForCategories(config, enabledCategoryIds) { const enabled = new Set(enabledCategoryIds); const sections = []; CATEGORIES.forEach((cat) => { if (!enabled.has(cat.id)) return; const groups = config?.[cat.groupsKey] || []; flattenGroups(groups).forEach((section) => { if (!section?.id) return; sections.push({ section, category: cat.label, categoryId: cat.id }); }); }); return sections; } function loadStoredSelectedIds() { try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return null; const parsed = JSON.parse(raw); return Array.isArray(parsed) ? parsed.filter((id) => typeof id === 'string') : null; } catch (e) { return null; } } function saveStoredSelectedIds(ids) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(ids)); } catch (e) { LOG('Could not persist selection', e); } } function msBandClass(ms) { if (ms == null || Number.isNaN(ms)) return ''; for (const band of MS_BANDS) { if (ms < band.max) return band.className; } return MS_BANDS[MS_BANDS.length - 1].className; } function withBenchmarkQueryOptions(query) { const queryOptions = { ...(query?.queryOptions || {}) }; if (queryOptions.Limit == null || queryOptions.Limit === '') { queryOptions.Limit = DEFAULT_QUERY_LIMIT; } return { ...query, queryOptions }; } function uniqueItemsById(items) { const seen = new Set(); const out = []; (items || []).forEach((item) => { const id = item?.Id ?? item?.id; if (id != null) { if (seen.has(id)) return; seen.add(id); } out.push(item); }); return out; } function analyzeItemFields(items) { let hasMediaStreams = false; let hasPeople = false; let hasStudios = false; (items || []).forEach((item) => { if (item?.MediaStreams?.length) hasMediaStreams = true; if (item?.People?.length) hasPeople = true; if (item?.Studios?.length) hasStudios = true; }); return { hasMediaStreams, hasPeople, hasStudios }; } function jsonByteSize(value) { try { const json = typeof value === 'string' ? value : JSON.stringify(value); return new TextEncoder().encode(json || '').byteLength; } catch (e) { return 0; } } function formatBytes(bytes) { if (!Number.isFinite(bytes)) return '—'; if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; } function getAuthHeaders() { if (window.apiHelper?.getAuthHeader) { return { Authorization: window.apiHelper.getAuthHeader(), Accept: 'application/json' }; } return { Accept: 'application/json' }; } async function timedFetchJson(url, signal) { const start = performance.now(); const response = await fetch(url, { headers: getAuthHeaders(), signal }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const text = await response.text(); const data = JSON.parse(text); return { data, ms: performance.now() - start, responseBytes: jsonByteSize(text) }; } async function timedDataSource(dataSource, options) { const start = performance.now(); const result = await window.apiHelper.fetchFromDataSource(dataSource, options || {}, false); const ms = performance.now() - start; const items = result?.Items || result || []; return { items: Array.isArray(items) ? items : [], ms, responseBytes: jsonByteSize(result) }; } async function fetchQueryItems(query, section, signal) { const userId = window.ApiClient?.getCurrentUserId?.(); const serverUrl = window.ApiClient?.serverAddress?.(); if (!userId || !serverUrl) { throw new Error('ApiClient not ready'); } const previewQuery = withBenchmarkQueryOptions(query); const useSpotlight = section.renderMode === 'Spotlight' || section.spotlight === true; if (previewQuery.dataSource) { return await timedDataSource(previewQuery.dataSource, previewQuery.queryOptions); } if (!window.apiHelper?.buildQueryFromSection) { throw new Error('apiHelper.buildQueryFromSection not available'); } const built = window.apiHelper.buildQueryFromSection( previewQuery, userId, serverUrl, useSpotlight ); if (built && typeof built === 'object' && built.dataSource) { return await timedDataSource(built.dataSource, built.options); } if (!built || typeof built !== 'string') { throw new Error('Could not build query URL'); } const { data, ms, responseBytes } = await timedFetchJson(built, signal); const items = data?.Items || (Array.isArray(data) ? data : []); return { items, ms, responseBytes }; } /** * Discovery sections ship as templates without queries: their source (genre, person, * studio, collection, ...) is picked at render time. Reuse the home screen resolver so * the benchmark measures the same queries the home screen would actually issue. */ async function resolveSectionQueries(entry) { const section = entry.section; const isTemplate = !section.queries?.length && !section.items?.length; if (!isTemplate || !section.type) return { section, error: null }; const resolve = window.homeScreen3?.resolveDiscoverySection; if (!resolve) { return { section, error: 'Discovery resolver unavailable' }; } try { const resolved = await resolve(section, { pairSpotlight: false, resolveViewMore: false }); if (!resolved?.queries?.length) { return { section, error: `Could not resolve ${section.type} source` }; } return { section: resolved, error: null }; } catch (e) { return { section, error: `Source resolve failed: ${e?.message || e}` }; } } async function benchmarkSection(section, signal) { if (section.items?.length) { return { totalMs: 0, items: section.items, responseBytes: 0, queryMetrics: [], error: null, skippedQueries: true }; } const queries = section.queries || []; if (!queries.length) { return { totalMs: 0, items: [], responseBytes: 0, queryMetrics: [], error: 'No queries', skippedQueries: true }; } let totalMs = 0; let responseBytes = 0; const allItems = []; const queryMetrics = []; for (const query of queries) { if (signal?.aborted) { return { totalMs, items: allItems, responseBytes, queryMetrics, error: 'Cancelled', cancelled: true }; } const result = await fetchQueryItems(query, section, signal); const { items, ms } = result; const queryBytes = result.responseBytes || 0; totalMs += ms; responseBytes += queryBytes; allItems.push(...items); queryMetrics.push({ ms, itemCount: items.length, responseBytes: queryBytes }); } return { totalMs, items: uniqueItemsById(allItems), responseBytes, queryMetrics, error: null }; } function chipHtml(on, label) { const cls = on ? 'kefin-bench-chip-on' : 'kefin-bench-chip-off'; const text = on ? 'On' : 'Off'; return `${label}: ${text}`; } function escapeHtml(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function ensureBenchmarkStyles() { if (document.getElementById('kefin-bench-styles')) return; const style = document.createElement('style'); style.id = 'kefin-bench-styles'; style.textContent = ` .kefin-bench-wrap { font-size: 0.95em; } .kefin-bench-cats { display: flex; flex-wrap: wrap; gap: 1em; margin: 1.25em 0; } .kefin-bench-cat-btn { padding: 0.75em 1.75em; font-size: 1em; line-height: 1.2; border-radius: 999px; border: 2px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.04); color: rgba(255,255,255,0.5); cursor: pointer; font-weight: 600; transition: background 0.15s, border-color 0.15s, color 0.15s; } .kefin-bench-cat-btn:hover:not(:disabled) { color: rgba(255,255,255,0.85); border-color: rgba(255,255,255,0.4); } .kefin-bench-cat-btn.is-active { border-color: var(--theme-primary-color, #00a4dc); color: #fff; background: var(--theme-primary-color, #00a4dc); } .kefin-bench-cat-btn.is-active:hover:not(:disabled) { border-color: var(--theme-primary-color, #00a4dc); color: #fff; filter: brightness(1.1); } .kefin-bench-cat-btn:disabled { opacity: 0.5; cursor: default; } .kefin-bench-actions { display: flex; gap: 0.75em; align-items: center; flex-wrap: wrap; margin-top: 1em; } .kefin-bench-wrap [hidden] { display: none !important; } .kefin-bench-panel { min-height: 120px; } .kefin-bench-picker-toolbar { display: flex; align-items: center; gap: 0.5em; margin-bottom: 0.65em; flex-wrap: wrap; } .kefin-bench-picker-count { font-size: 0.85em; color: rgba(255,255,255,0.55); margin-left: 0.25em; } #kefin-bench-picker-grid { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 0.4em; overflow-y: auto; padding: 0.5em; border: 1px solid rgba(255,255,255,0.12); border-radius: 6px; background: rgba(0,0,0,0.15); } @media (max-width: 1100px) { #kefin-bench-picker-grid { grid-template-columns: 1fr 1fr 1fr; } } @media (max-width: 800px) { #kefin-bench-picker-grid { grid-template-columns: 1fr 1fr; } } @media (max-width: 520px) { #kefin-bench-picker-grid { grid-template-columns: 1fr; } } .kefin-bench-picker-cell { height: 24px; overflow: hidden; display: flex; align-items: center; gap: 0.5em; padding: 0 0.4em; border-radius: 4px; min-width: 0; cursor: pointer; } .kefin-bench-picker-cell:hover { background: rgba(255,255,255,0.07); } .kefin-bench-picker-cell input { flex-shrink: 0; margin: 0; } .kefin-bench-picker-label { flex: 1; min-width: 0; display: flex; align-items: center; gap: 0.4em; overflow: hidden; } .kefin-bench-picker-name { font-size: 1em; color: rgba(255,255,255,0.92); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .kefin-bench-picker-badge { flex-shrink: 0; font-size: 0.65em; padding: 0.1em 0.4em; border-radius: 3px; background: rgba(0,164,220,0.2); color: rgba(255,255,255,0.7); } .kefin-bench-table-wrap { overflow-x: auto; border: 1px solid rgba(255,255,255,0.1); border-radius: 6px; } .kefin-bench-summary-wrap { overflow-x: auto; margin-bottom: 1em; border: 1px solid rgba(255,255,255,0.1); border-radius: 6px; } .kefin-bench-summary-table td { font-size: 1.1em; font-weight: 600; text-align: center; } .kefin-bench-summary-table th { text-align: center; white-space: nowrap; } .kefin-bench-summary-table td[title] { cursor: help; } .kefin-bench-table { width: 100%; border-collapse: collapse; font-size: 0.9em; /* Fixed layout keeps column widths stable as rows fill in */ table-layout: fixed; } .kefin-bench-table:not(.kefin-bench-summary-table) { min-width: 780px; } .kefin-bench-table th, .kefin-bench-table td { padding: 0.55em 0.65em; text-align: left; border-bottom: 1px solid rgba(255,255,255,0.08); overflow: hidden; text-overflow: ellipsis; } .kefin-bench-table tbody tr { height: 2.9em; } .kefin-bench-col-name, .kefin-bench-col-plain { white-space: nowrap; } .kefin-bench-table th { font-weight: 600; color: rgba(255,255,255,0.85); background: rgba(0,0,0,0.2); } .kefin-bench-table tr.kefin-bench-row-running td { color: rgba(255,255,255,0.6); } .kefin-bench-ms-fast { background: rgba(47,158,68,0.35); } .kefin-bench-ms-mid { background: rgba(255,212,59,0.25); } .kefin-bench-ms-slow { background: rgba(253,126,20,0.3); } .kefin-bench-ms-worst { background: rgba(224,49,49,0.35); } .kefin-bench-chips { display: flex; flex-wrap: nowrap; gap: 0.35em; } .kefin-bench-chip { font-size: 0.75em; padding: 0.15em 0.45em; border-radius: 4px; border: 1px solid rgba(255,255,255,0.2); white-space: nowrap; } .kefin-bench-chip-on { background: rgba(47,158,68,0.25); } .kefin-bench-chip-off { background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.5); } `; document.head.appendChild(style); } function buildPickerGridHtml(sections, checkedSet) { if (!sections.length) { return '

No sections in the selected categories.

'; } return sections .map((entry) => { const id = entry.section.id; const checked = checkedSet ? checkedSet.has(id) : true; const name = escapeHtml(entry.section.name || id); const cat = escapeHtml(entry.category); return ` `; }) .join(''); } function buildTableShell() { return `
<500ms 500–1000ms 1000–2000ms 2000ms+ >100 items >5 MB
0 0 0 0 0 0
Section Category Time (ms) Response size Items Fields Status
`; } function rowHtml(entry, result) { const name = result?.resolvedName || entry.section.name || entry.section.id; const status = result?.status || 'pending'; const ms = result?.totalMs; const msClass = status === 'done' ? msBandClass(ms) : ''; const msText = status === 'running' ? '…' : status === 'skipped' ? '—' : status === 'error' ? '—' : status === 'done' ? Math.round(ms) : '—'; const items = result?.itemCount ?? '—'; const responseSize = status === 'done' ? formatBytes(result.responseBytes) : '—'; const fields = status === 'done' ? `
${chipHtml(result.hasMediaStreams, 'MediaStreams')} ${chipHtml(result.hasPeople, 'People')} ${chipHtml(result.hasStudios, 'Studios')}
` : '—'; const statusText = status === 'running' ? 'Running…' : status === 'skipped' ? 'Skipped' : status === 'error' ? (result.error || 'Error') : status === 'done' ? (result.skippedQueries ? 'Static / no query' : 'OK') : 'Pending'; return ` ${escapeHtml(name)} ${escapeHtml(entry.category)} ${msText} ${responseSize} ${items} ${fields} ${escapeHtml(statusText)} `; } function cancelActiveRun(markSkipped) { if (!activeRun) return; activeRun.aborted = true; if (activeRun.controller) { try { activeRun.controller.abort(); } catch (e) { /* ignore */ } } if (markSkipped && activeRun.onSkipRemaining) { activeRun.onSkipRemaining(); } activeRun = null; } function bindNavCancel() { if (navCancelBound) return; navCancelBound = true; const onNavigate = () => { if (!activeRun) return; if (window.location.hash === activeRun.startHash) return; cancelActiveRun(true); }; window.addEventListener('hashchange', onNavigate); if (window.KefinTweaksUtils?.onViewPage) { window.KefinTweaksUtils.onViewPage(() => onNavigate()); } } function updateSummaryCells(scopeEl, entries, results) { const buckets = { fast: new Map(), mid: new Map(), slow: new Map(), worst: new Map(), 'large-items': new Map(), 'large-response': new Map() }; entries.forEach((entry) => { const result = results.get(entry.section.id); if (!result?.queryMetrics?.length) return; const name = result.resolvedName || entry.section.name || entry.section.id; const tally = (key) => { buckets[key].set(name, (buckets[key].get(name) || 0) + 1); }; result.queryMetrics.forEach((query) => { if (query.ms < 500) tally('fast'); else if (query.ms < 1000) tally('mid'); else if (query.ms < 2000) tally('slow'); else tally('worst'); if (query.itemCount > 100) tally('large-items'); if (query.responseBytes > 5 * 1024 * 1024) tally('large-response'); }); }); const summary = scopeEl?.querySelector('.kefin-bench-summary-row'); if (!summary) return; Object.keys(buckets).forEach((key) => { const cell = summary.querySelector(`[data-summary="${key}"]`); if (!cell) return; const tallied = [...buckets[key].entries()]; const total = tallied.reduce((sum, [, count]) => sum + count, 0); cell.textContent = total; if (total) { // One line per section, with a query count when a section contributes more than once cell.title = tallied .map(([name, count]) => (count > 1 ? `${name} (${count} queries)` : name)) .join('\n'); } else { cell.removeAttribute('title'); } }); } function renderResultsTable(resultsEl, entries, results) { resultsEl.innerHTML = buildTableShell(); const tbody = resultsEl.querySelector('.kefin-bench-tbody'); tbody.innerHTML = entries .map((entry) => rowHtml(entry, results.get(entry.section.id) || { status: 'pending' })) .join(''); updateSummaryCells(resultsEl, entries, results); return tbody; } async function runBenchmarks(selectedEntries, tbody, ui, results) { const controller = new AbortController(); const signal = controller.signal; const updateSummary = () => { updateSummaryCells(tbody.closest('#kefin-bench-results'), selectedEntries, results); }; const updateRow = (entry, result) => { results.set(entry.section.id, result); const tr = tbody.querySelector(`tr[data-section-id="${entry.section.id}"]`); if (tr) { tr.outerHTML = rowHtml(entry, result); } updateSummary(); }; const skipRemaining = () => { selectedEntries.forEach((entry) => { if (results.get(entry.section.id)?.status === 'running' || !results.has(entry.section.id)) { updateRow(entry, { status: 'skipped', totalMs: null, itemCount: '—' }); } }); ui.setRunning(false); }; activeRun = { aborted: false, controller, startHash: window.location.hash, onSkipRemaining: skipRemaining }; ui.setRunning(true); for (const entry of selectedEntries) { if (activeRun?.aborted || signal.aborted) { updateRow(entry, { status: 'skipped', totalMs: null, itemCount: '—' }); continue; } updateRow(entry, { status: 'running' }); try { const resolved = await resolveSectionQueries(entry); if (resolved.error) { updateRow(entry, { status: 'error', error: resolved.error, totalMs: null, itemCount: '—' }); continue; } const bench = await benchmarkSection(resolved.section, signal); if (activeRun?.aborted || signal.aborted || bench.cancelled) { updateRow(entry, { status: 'skipped', totalMs: null, itemCount: '—' }); continue; } const fields = analyzeItemFields(bench.items); updateRow(entry, { status: bench.error && !bench.skippedQueries ? 'error' : 'done', resolvedName: resolved.section !== entry.section ? resolved.section.name : null, totalMs: bench.totalMs, itemCount: bench.items.length, hasMediaStreams: fields.hasMediaStreams, hasPeople: fields.hasPeople, hasStudios: fields.hasStudios, responseBytes: bench.responseBytes, queryMetrics: bench.queryMetrics, skippedQueries: bench.skippedQueries, error: bench.error }); } catch (e) { if (signal.aborted || activeRun?.aborted) { updateRow(entry, { status: 'skipped', totalMs: null, itemCount: '—' }); } else { updateRow(entry, { status: 'error', error: e?.message || String(e), totalMs: null, itemCount: '—' }); } } await new Promise((r) => setTimeout(r, 0)); } activeRun = null; ui.setRunning(false); } /** * Render benchmark UI into host element. * @param {HTMLElement} hostEl * @param {{ getConfig?: () => Object }} options */ function renderHomeScreenBenchmarkUI(hostEl, options = {}) { if (!hostEl) return; cancelActiveRun(true); const getConfig = options.getConfig || (() => window.KefinHomeScreen?.getConfig?.() || {}); ensureBenchmarkStyles(); hostEl.innerHTML = `
Home Screen Section Benchmarks
Test your home sections for slow queries or otherwise unintended results. Timings reflect server round-trip per query (not full card render).
`; const catsEl = hostEl.querySelector('#kefin-bench-cats'); const runBtn = hostEl.querySelector('#kefin-bench-run'); const stopBtn = hostEl.querySelector('#kefin-bench-stop'); const viewResultsBtn = hostEl.querySelector('#kefin-bench-view-results'); const editBtn = hostEl.querySelector('#kefin-bench-edit'); const panelEl = hostEl.querySelector('#kefin-bench-panel'); const resultsEl = hostEl.querySelector('#kefin-bench-results'); const enabledCats = new Set(CATEGORIES.map((c) => c.id)); function getCurrentSections() { const config = typeof getConfig === 'function' ? getConfig() : {}; return getSectionsForCategories(config, [...enabledCats]); } function getCheckedIdsFromPanel() { return Array.from(panelEl.querySelectorAll('.kefin-bench-pick:checked')) .map((cb) => cb.value); } function updatePickerCount() { const countEl = panelEl.querySelector('.kefin-bench-picker-count'); if (!countEl) return; const total = panelEl.querySelectorAll('.kefin-bench-pick').length; const checked = panelEl.querySelectorAll('.kefin-bench-pick:checked').length; countEl.textContent = `${checked} of ${total} selected`; } function bindPickerToolbar() { panelEl.querySelector('#kefin-bench-run-top') ?.addEventListener('click', () => startRun()); const selectAllBtn = panelEl.querySelector('#kefin-bench-select-all'); const deselectAllBtn = panelEl.querySelector('#kefin-bench-deselect-all'); selectAllBtn?.addEventListener('click', () => { panelEl.querySelectorAll('.kefin-bench-pick').forEach((cb) => { cb.checked = true; }); updatePickerCount(); }); deselectAllBtn?.addEventListener('click', () => { panelEl.querySelectorAll('.kefin-bench-pick').forEach((cb) => { cb.checked = false; }); updatePickerCount(); }); panelEl.querySelectorAll('.kefin-bench-pick').forEach((cb) => { cb.addEventListener('change', () => updatePickerCount()); }); } function showPickerPanel(preserveCheckedIds) { const sections = getCurrentSections(); const stored = loadStoredSelectedIds(); const storedSet = stored ? new Set(stored) : null; const checkedSet = preserveCheckedIds instanceof Set ? preserveCheckedIds : storedSet; panelEl.innerHTML = `
${buildPickerGridHtml(sections, checkedSet)}
`; bindPickerToolbar(); updatePickerCount(); } function showPicker() { cancelActiveRun(true); lastViewMode = 'picker'; resultsEl.innerHTML = ''; catsEl.hidden = false; panelEl.hidden = false; editBtn.hidden = true; viewResultsBtn.hidden = !hasStoredResults(); ui.setRunning(false); } function showResults(entries, results) { lastViewMode = 'results'; catsEl.hidden = true; panelEl.hidden = true; editBtn.hidden = false; viewResultsBtn.hidden = true; return renderResultsTable(resultsEl, entries, results); } const ui = { setRunning(running) { stopBtn.hidden = !running; hostEl.querySelectorAll('.kefin-bench-run-btn').forEach((btn) => { btn.disabled = running; }); } }; function startRun() { if (activeRun) return; const sections = getCurrentSections(); if (!sections.length) { window.KefinTweaksToaster?.toast?.('No sections in the selected categories.'); return; } const selectedIds = getCheckedIdsFromPanel(); if (!selectedIds.length) { window.KefinTweaksToaster?.toast?.('Select at least one section to benchmark.'); return; } saveStoredSelectedIds(selectedIds); const idSet = new Set(selectedIds); const selected = sections.filter((e) => idSet.has(e.section.id)); if (!selected.length) return; lastRun = { entries: selected, results: new Map() }; const tbody = showResults(lastRun.entries, lastRun.results); runBenchmarks(lastRun.entries, tbody, ui, lastRun.results); } CATEGORIES.forEach((cat) => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'kefin-bench-cat-btn is-active'; btn.dataset.cat = cat.id; btn.setAttribute('aria-pressed', 'true'); btn.textContent = cat.label; btn.addEventListener('click', () => { if (activeRun) return; const prevChecked = new Set(getCheckedIdsFromPanel()); if (enabledCats.has(cat.id)) { enabledCats.delete(cat.id); btn.classList.remove('is-active'); btn.setAttribute('aria-pressed', 'false'); } else { enabledCats.add(cat.id); btn.classList.add('is-active'); btn.setAttribute('aria-pressed', 'true'); } showPickerPanel(prevChecked); }); catsEl.appendChild(btn); }); stopBtn.addEventListener('click', () => cancelActiveRun(true)); editBtn.addEventListener('click', () => showPicker()); runBtn.addEventListener('click', () => startRun()); viewResultsBtn.addEventListener('click', () => { if (!hasStoredResults()) return; showResults(lastRun.entries, lastRun.results); }); showPickerPanel(); if (lastViewMode === 'results' && hasStoredResults()) { showResults(lastRun.entries, lastRun.results); } else { showPicker(); } } window.KefinHomeScreenBenchmark = { renderHomeScreenBenchmarkUI, cancelActiveRun }; bindNavCancel(); LOG('loaded'); })();