/**
 * Context Overflow page controller.
 *
 * Previously inlined in pages/context_overflow.html. Moved into its own
 * deferred script so all page scripts share `defer` semantics — the
 * inline block ran synchronously, which made it possible (in principle)
 * for callers to reach window.contextOverflowShared before the deferred
 * shared module had finished evaluating.
 *
 * Loads after context-overflow-shared.js (both `defer`, document order).
 *
 * Note: URLValidator is available globally via /static/js/security/url-validator.js
 * — used here to guard the click-to-navigate handler on the scatter chart.
 * SafeLogger is the project's standard log facade (see web/static/js/security/).
 */
(function() {
    'use strict';

    let contextChart = null;
    let latencyChart = null;
    let phaseChart = null;
    let currentPeriod = '30d';
    let currentPage = 1;
    let totalPages = 1;
    let currentPageData = [];  // Current page data for client-side sort/filter
    let sortColumn = null;
    let sortDirection = 'desc';
    let searchTerm = '';

    // Tracks the in-flight loadContextData fetch so we can cancel it when a
    // newer load starts (e.g. rapid time-range button clicks). Without this,
    // a slow response could overwrite a faster, newer response and leave
    // the UI showing data that doesn't match the active period button.
    let currentLoadController = null;

    // Format number with commas. Guards against NaN/Infinity so a
    // corrupted upstream value (e.g. division-by-zero in avg_response_time_ms)
    // can't surface as the literal string 'NaN' or 'Infinity' in the UI.
    function formatNumber(num) {
        if (num == null || !Number.isFinite(Number(num))) return '0';
        return Math.round(num).toString().replace(/\B(?=(?:\d{3})+(?!\d))/g, ",");
    }

    // Escape HTML to prevent XSS when rendering user-controlled data
    function escapeHtml(text) {
        if (text == null) return '';
        const div = document.createElement('div');
        div.textContent = String(text);
        return div.innerHTML;
    }

    // Helper function to format model names for better display
    function formatModelName(model) {
        if (!model) return 'N/A';
        // Shorten long model names for display
        if (model.includes('/')) {
            const parts = model.split('/');
            if (parts[0].toLowerCase() === 'google' && parts[1].includes('gemini')) {
                return `Gemini ${parts[1].replace('gemini-', '').replace('-001', '')}`;
            } else if (parts[0].toLowerCase() === 'anthropic' && parts[1].includes('claude')) {
                return `Claude ${parts[1].replace('claude-', '')}`;
            } else if (parts[0].toLowerCase() === 'openai') {
                return parts[1].toUpperCase();
            } else if (parts[0].toLowerCase() === 'ai21' && parts[1].includes('jamba')) {
                return `Jamba ${parts[1].replace('jamba-', '')}`;
            }
            return `${parts[1].substring(0, 20)}${parts[1].length > 20 ? '...' : ''}`;
        }
        return model.length > 25 ? model.substring(0, 25) + '...' : model;
    }

    // Helper function to format provider names
    function formatProviderName(provider) {
        if (!provider) return 'N/A';
        const providerMap = {
            'openai_endpoint': 'OpenAI Endpoint',
            'anthropic_endpoint': 'Anthropic Endpoint',
            'ollama': 'Ollama',
            'openai': 'OpenAI',
            'anthropic': 'Anthropic',
            'google': 'Google',
            'openrouter': 'OpenRouter',
            'lmstudio': 'LM Studio',
            'llamacpp': 'Llama.cpp',
            'xai': 'xAI',
            'ionos': 'IONOS'
        };
        return providerMap[provider.toLowerCase()] || provider;
    }

    // Load context overflow data. Latest call wins — earlier in-flight calls
    // are aborted at the network level. Late-arriving responses (where the
    // network resolved before abort took effect) are dropped via the
    // post-await aborted check.
    async function loadContextData(period = currentPeriod, page = 1) {
        if (currentLoadController) {
            currentLoadController.abort();
        }
        const controller = new AbortController();
        currentLoadController = controller;

        try {
            document.getElementById('loading').style.display = 'block';
            document.getElementById('content').style.display = 'none';

            const response = await fetch(
                `/metrics/api/context-overflow?period=${period}&page=${page}&per_page=50`,
                { signal: controller.signal }
            );
            if (!response.ok) {
                throw new Error('Failed to load data');
            }

            const data = await response.json();

            // Newer load superseded us between fetch resolving and parse
            // completing — drop this response rather than overwriting whatever
            // the newer load is about to render.
            if (controller.signal.aborted) {
                return;
            }

            if (data.status === 'success') {
                document.getElementById('loading').style.display = 'none';
                document.getElementById('content').style.display = 'block';
                displayContextData(data);
            } else {
                showError();
            }
        } catch (error) {
            // AbortError is expected when a newer load supersedes this one.
            if (error.name === 'AbortError') {
                return;
            }
            SafeLogger.error('Error loading context data:', error);
            showError();
        }
    }

    // Display context overflow data
    function displayContextData(data) {
        const { overview, token_summary,
                model_stats, model_token_stats, recent_truncated,
                chart_data, context_limits, phase_breakdown,
                current_context_window,
                all_requests, pagination } = data;

        // Empty state: brand-new user with zero requests
        if (!token_summary || token_summary.total_requests === 0) {
            document.getElementById('empty-no-data').style.display = 'block';
            document.getElementById('warning-banner').style.display = 'none';
            document.getElementById('context-overflow-section').innerHTML = '';
            return;
        }
        document.getElementById('empty-no-data').style.display = 'none';

        // Show warning if high truncation rate
        if (overview.truncation_rate > 20) {
            document.getElementById('warning-banner').style.display = 'flex';
            document.getElementById('warning-rate').textContent = overview.truncation_rate.toFixed(1);
        } else {
            document.getElementById('warning-banner').style.display = 'none';
        }

        // Empty state: provider doesn't report context_limit (OpenRouter, hosted models without echo)
        const noContextData = !overview.requests_with_context_data
            || overview.requests_with_context_data === 0;
        document.getElementById('empty-no-context-data').style.display = noContextData ? 'block' : 'none';

        // Empty state: has context data but no truncation events (positive signal).
        // Field is `truncated_requests` (matches context_overflow_api.py response);
        // the prior `truncated_count` reading was always undefined, so this banner
        // showed for every user with context data — including users who DID have
        // truncation.
        // Also checks chart_data for high-utilization requests (>80%) since the backend
        // only flags context_truncated at 80% threshold which is the same as the chart warning.
        const highUtilCount = (chart_data || []).filter(d => {
            if (!d.context_limit) return false;
            const original = d.original_prompt_tokens || d.prompt_tokens || 0;
            return original / d.context_limit > 0.8;
        }).length;
        const hasContextNoTrunc = overview.requests_with_context_data > 0
            && (!overview.truncated_requests || overview.truncated_requests === 0)
            && highUtilCount === 0;
        document.getElementById('empty-no-truncation').style.display = hasContextNoTrunc ? 'block' : 'none';

        // Display context overflow section (conditional — renders only when there's truncation to show)
        displayContextOverflowSection(overview, model_stats, model_token_stats, recent_truncated, chart_data, context_limits, phase_breakdown, current_context_window);

        // Populate all requests table with pagination (always shown — raw signal)
        currentPageData = all_requests || [];
        if (pagination) {
            currentPage = pagination.page;
            totalPages = pagination.total_pages;
        }
        renderRequestsTable();
        updatePaginationControls();
    }

    // Display context overflow section conditionally
    function displayContextOverflowSection(overview, modelStats, modelTokenStats, recentTruncated, chartData, _contextLimits, phaseBreakdown, currentContextWindow) {
        const section = document.getElementById('context-overflow-section');

        if (!overview.requests_with_context_data || overview.requests_with_context_data === 0) {
            // Provider doesn't report context_limit — the #empty-no-context-data
            // banner above already covers messaging; leave this section empty
            // so users don't see two stacked "no data" cards.
            section.innerHTML = '';
            return;
        }

        if (overview.requests_with_context_data > 0) {
            // Show context overflow analytics
            section.innerHTML = `
                <!-- Context Overflow Overview -->
                <div class="ldr-card" style="margin-top: 2rem;">
                    <div class="card-header">
                        <h2><i aria-hidden="true" class="fas fa-percentage"></i> Context Overflow Overview</h2>
                    </div>
                    <div class="ldr-card-content">
                        <div class="ldr-overflow-grid">
                            <div class="ldr-overflow-card">
                                <div class="ldr-metric-icon" style="background: var(--warning-color);">
                                    <i aria-hidden="true" class="fas fa-percentage"></i>
                                </div>
                                <div class="ldr-metric-label">Truncation Rate</div>
                                <div class="ldr-metric-value" id="truncation-rate">0%</div>
                                <div style="font-size: 0.875rem; color: var(--text-secondary);">
                                    <span id="truncated-count">0</span> of <span id="total-count">0</span> requests
                                </div>
                            </div>
                            <div class="ldr-overflow-card">
                                <div class="ldr-metric-icon" style="background: var(--error-color);">
                                    <i aria-hidden="true" class="fas fa-cut"></i>
                                </div>
                                <div class="ldr-metric-label">Avg Tokens Lost</div>
                                <div class="ldr-metric-value" id="avg-tokens-lost">0</div>
                                <div style="font-size: 0.875rem; color: var(--text-secondary);">
                                    per truncated request
                                </div>
                            </div>
                            <div class="ldr-overflow-card">
                                <div class="ldr-metric-icon">
                                    <i aria-hidden="true" class="fas fa-microchip"></i>
                                </div>
                                <div class="ldr-metric-label">Models Tracked</div>
                                <div class="ldr-metric-value" id="models-tracked">0</div>
                                <div style="font-size: 0.875rem; color: var(--text-secondary);">
                                    with context data
                                </div>
                            </div>
                            <div class="ldr-overflow-card">
                                <div class="ldr-metric-icon">
                                    <i aria-hidden="true" class="fas fa-database"></i>
                                </div>
                                <div class="ldr-metric-label">Data Coverage</div>
                                <div class="ldr-metric-value" id="data-coverage">0%</div>
                                <div style="font-size: 0.875rem; color: var(--text-secondary);">
                                    requests with context info
                                </div>
                            </div>
                        </div>
                    </div>
                </div>

                <!-- Scatter Chart: Request Size vs Context Limit -->
                <div class="ldr-card" style="margin-top: 2rem;">
                    <div class="card-header">
                        <h2><i aria-hidden="true" class="fas fa-chart-area"></i> Request Size vs Context Limits</h2>
                    </div>
                    <div class="ldr-card-content">
                        <div class="ldr-chart-container">
                            <canvas id="context-chart" aria-label="Scatter plot of LLM request prompt tokens versus configured context window over time. Each point represents a single request, colour and shape coded by utilisation: green circle for safe (under 50%), amber triangle for caution (50 to 80% or unknown tokens), red diamond for critical (over 80%), grey triangle for requests where the provider did not report a context limit. Lower opacity marks local providers."></canvas>
                        </div>
                        <div style="margin-top: 1rem; padding: 1rem; background: var(--bg-tertiary); border-radius: 0.375rem;">
                            <p style="margin: 0; color: var(--text-secondary); font-size: 0.875rem;">
                                <i aria-hidden="true" class="fas fa-info-circle" style="color: var(--primary-color); margin-right: 0.5rem;"></i>
                                <strong>Chart Guide:</strong> Points are coloured and shaped by context utilisation
                                (prompt tokens ÷ configured limit): green circle = safe (&lt; 50%), amber triangle = caution (50–80% or unknown), red diamond = critical (&gt; 80%).
                                Click any point to drill into that research's details.
                            </p>
                        </div>
                    </div>
                </div>

                <!-- Token Usage by Phase -->
                <div class="ldr-card" style="margin-top: 2rem;">
                    <div class="card-header">
                        <h2><i aria-hidden="true" class="fas fa-layer-group"></i> Token Usage by Phase</h2>
                    </div>
                    <div class="ldr-card-content">
                        <div id="phase-breakdown-container">
                            <div class="ldr-chart-container" style="height: 300px;">
                                <canvas id="phase-chart" aria-label="Box plot showing prompt token distribution grouped by research phase"></canvas>
                            </div>
                        </div>
                        <div id="phase-summary-table" style="margin-top: 1rem;"></div>
                    </div>
                </div>

                <!-- Model Context Usage -->
                <div class="ldr-card" style="margin-top: 2rem;">
                    <div class="card-header">
                        <h2><i aria-hidden="true" class="fas fa-robot"></i> Model Context Usage</h2>
                    </div>
                    <div class="ldr-card-content">
                        <div id="model-stats"></div>
                    </div>
                </div>

                <!-- Recent Truncated Requests -->
                <div class="ldr-card" style="margin-top: 2rem;">
                    <div class="card-header">
                        <h2><i aria-hidden="true" class="fas fa-exclamation-triangle"></i> Recent Truncated Requests</h2>
                    </div>
                    <div class="ldr-card-content">
                        <div id="truncated-list"></div>
                    </div>
                </div>

                <!-- Latency vs Context Size -->
                <div class="ldr-card" style="margin-top: 2rem;">
                    <div class="card-header">
                        <h2><i aria-hidden="true" class="fas fa-tachometer-alt"></i> Latency vs Context Size</h2>
                    </div>
                    <div class="ldr-card-content">
                        <div class="ldr-chart-container">
                            <canvas id="latency-chart" aria-label="Scatter plot of response time vs prompt tokens, grouped by model"></canvas>
                        </div>
                        <div style="margin-top: 1rem; padding: 1rem; background: var(--bg-tertiary); border-radius: 0.375rem;">
                            <p style="margin: 0; color: var(--text-secondary); font-size: 0.875rem;">
                                <i aria-hidden="true" class="fas fa-info-circle" style="color: var(--primary-color); margin-right: 0.5rem;"></i>
                                <strong>Chart Guide:</strong> Each point is a single request. X-axis shows prompt token count (input size),
                                Y-axis shows response latency. Separate colour per model. Point opacity indicates context utilisation
                                (darker = higher utilisation). Hover for details.
                            </p>
                        </div>
                    </div>
                </div>
            `;

            // Now populate the dynamically created elements
            document.getElementById('truncation-rate').textContent = `${overview.truncation_rate}%`;
            document.getElementById('truncated-count').textContent = formatNumber(overview.truncated_requests);
            document.getElementById('total-count').textContent = formatNumber(overview.requests_with_context_data);
            document.getElementById('avg-tokens-lost').textContent = formatNumber(Math.round(overview.avg_tokens_truncated));
            document.getElementById('models-tracked').textContent = modelStats.length;

            const coverage = overview.total_requests > 0
                ? Math.round((overview.requests_with_context_data / overview.total_requests) * 100)
                : 0;
            document.getElementById('data-coverage').textContent = `${coverage}%`;

            displayModelStats(modelStats, modelTokenStats);
            displayTruncatedRequests(recentTruncated);
            createContextChart(chartData, currentContextWindow);
            createLatencyChart(chartData);
            createPhaseChart(chartData, phaseBreakdown);
        }
    }

    // Render the requests table from currentPageData (with client-side sort and filter)
    function renderRequestsTable() {
        const tbody = document.getElementById('requests-tbody');
        let data = [...currentPageData];

        // Client-side search filter
        if (searchTerm) {
            data = data.filter(req => {
                const haystack = [
                    req.model, req.provider, req.research_query,
                    req.research_phase, req.research_id
                ].filter(Boolean).join(' ').toLowerCase();
                return haystack.includes(searchTerm);
            });
        }

        // Client-side sort (on current page data)
        if (sortColumn) {
            data.sort((a, b) => {
                let valA = a[sortColumn];
                let valB = b[sortColumn];

                // Handle nulls
                if (valA == null) valA = '';
                if (valB == null) valB = '';

                // Numeric columns
                if (typeof valA === 'number' || typeof valB === 'number') {
                    valA = Number(valA) || 0;
                    valB = Number(valB) || 0;
                    return sortDirection === 'asc' ? valA - valB : valB - valA;
                }

                // Boolean
                if (typeof valA === 'boolean') {
                    return sortDirection === 'asc' ? (valA ? 1 : 0) - (valB ? 1 : 0) : (valB ? 1 : 0) - (valA ? 1 : 0);
                }

                // String comparison
                valA = String(valA).toLowerCase();
                valB = String(valB).toLowerCase();
                const cmp = valA.localeCompare(valB);
                return sortDirection === 'asc' ? cmp : -cmp;
            });
        }

        if (data.length === 0) {
            // eslint-disable-next-line no-unsanitized/property -- only the ternary picks one of two hardcoded strings; no user input interpolated
            tbody.innerHTML = `
                <tr>
                    <td colspan="12" style="text-align: center; padding: 2rem;">
                        ${searchTerm ? 'No matching requests found' : 'No request data available'}
                    </td>
                </tr>
            `;
            return;
        }

        let tableRows = '';
        data.forEach(req => {
            const timestamp = new Date(req.timestamp).toLocaleString();
            const truncatedBadge = req.context_truncated
                ? '<span class="ldr-text-error">Yes</span>'
                : '<span class="ldr-text-success">No</span>';
            const contextLimit = req.context_limit
                ? formatNumber(req.context_limit)
                : '<span class="ldr-text-muted">Unknown</span>';
            const tokensLost = req.tokens_truncated || 0;
            const truncationClass = tokensLost > 0 ? 'ldr-text-error' : 'ldr-text-secondary';
            const researchLink = req.research_id
                ? `<a href="/details/${encodeURIComponent(req.research_id)}" style="color: var(--primary-color); font-family: monospace; font-size: 0.8rem;">${escapeHtml(req.research_id.substring(0, 8))}</a>`
                : 'N/A';

            tableRows += `
                <tr>
                    <td style="white-space: nowrap;">${timestamp}</td>
                    <td>${researchLink}</td>
                    <td title="${escapeHtml(req.model)}">${escapeHtml(formatModelName(req.model))}</td>
                    <td>${escapeHtml(formatProviderName(req.provider))}</td>
                    <td>${escapeHtml(req.research_phase || 'N/A')}</td>
                    <td style="text-align: right;">${formatNumber(req.prompt_tokens)}</td>
                    <td style="text-align: right;">${formatNumber(req.completion_tokens)}</td>
                    <td style="text-align: right; font-weight: bold;">${formatNumber(req.total_tokens)}</td>
                    <td style="text-align: right;">${contextLimit}</td>
                    <td style="text-align: center;">${truncatedBadge}</td>
                    <td style="text-align: right;" class="${truncationClass}">${formatNumber(tokensLost)}</td>
                    <td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis;" title="${escapeHtml(req.research_query)}">${escapeHtml(req.research_query || 'N/A')}</td>
                </tr>
            `;
        });

        // bearer:disable javascript_lang_dangerous_insert_html
        // eslint-disable-next-line no-unsanitized/property -- all user-supplied strings (model, provider, query, research_id) are run through escapeHtml; numeric fields go through formatNumber; classes/badges are hardcoded
        tbody.innerHTML = tableRows;
    }

    // Update pagination controls
    function updatePaginationControls() {
        const prevBtn = document.getElementById('pagination-prev');
        const nextBtn = document.getElementById('pagination-next');
        const info = document.getElementById('pagination-info');

        prevBtn.disabled = currentPage <= 1;
        nextBtn.disabled = currentPage >= totalPages;
        info.textContent = `Page ${currentPage} of ${totalPages}`;
    }

    // Display model-specific stats (context overflow section)
    function displayModelStats(modelStats, modelTokenStats) {
        const container = document.getElementById('model-stats');

        if (modelStats.length === 0) {
            container.innerHTML = `
                <div class="ldr-no-data-message">
                    <i aria-hidden="true" class="fas fa-robot"></i>
                    <p>No model data available</p>
                </div>
            `;
            return;
        }

        container.innerHTML = '';

        const tokenByModel = {};
        (modelTokenStats || []).forEach(ts => {
            tokenByModel[`${ts.model}|${ts.provider}`] = ts;
        });

        modelStats.forEach(stat => {
            const truncationPercent = stat.truncation_rate;
            const card = document.createElement('div');
            card.className = 'ldr-model-card';

            const truncationRateClass = truncationPercent > 20 ? 'ldr-truncation-high' : truncationPercent > 10 ? 'ldr-truncation-medium' : 'ldr-truncation-low';

            const tokenStats = tokenByModel[`${stat.model}|${stat.provider}`] || {};
            const hasUtil = stat.avg_context_limit && tokenStats.avg_prompt;
            const utilPct = hasUtil ? Math.round((tokenStats.avg_prompt / stat.avg_context_limit) * 100) : 0;
            const utilColor = utilPct > 80 ? 'var(--error-color)' : utilPct > 50 ? 'var(--warning-color)' : 'var(--success-color)';

            // bearer:disable javascript_lang_dangerous_insert_html
            // eslint-disable-next-line no-unsanitized/property -- `truncationRateClass` is hardcoded to one of three string literals; model/provider go through escapeHtml; numeric stats go through formatNumber; utilColor is hardcoded to one of three CSS variables
            card.innerHTML = `
                <div class="ldr-model-header">
                    <div>
                        <div class="ldr-model-name" title="${escapeHtml(stat.model)}">${escapeHtml(formatModelName(stat.model))}</div>
                        <div class="ldr-model-provider">${escapeHtml(formatProviderName(stat.provider))}</div>
                    </div>
                    <div style="text-align: right;">
                        <div style="font-size: 1.25rem; font-weight: 600;" class="${truncationRateClass}">
                            ${truncationPercent}%
                        </div>
                        <div style="font-size: 0.75rem;" class="ldr-text-secondary">
                            truncation rate
                        </div>
                    </div>
                </div>
                <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-top: 1rem;">
                    <div>
                        <div style="font-size: 0.75rem;" class="ldr-text-secondary">Total Requests</div>
                        <div style="font-weight: 600;">${formatNumber(stat.total_requests)}</div>
                    </div>
                    <div>
                        <div style="font-size: 0.75rem;" class="ldr-text-secondary">Truncated</div>
                        <div style="font-weight: 600;" class="ldr-text-error">${formatNumber(stat.truncated_count)}</div>
                    </div>
                    <div>
                        <div style="font-size: 0.75rem;" class="ldr-text-secondary">Context Limit</div>
                        <div style="font-weight: 600;">${stat.avg_context_limit ? formatNumber(stat.avg_context_limit) : '<span class="ldr-text-muted" style="font-size: 0.85rem;">Unknown</span>'}</div>
                    </div>
                </div>

                ${tokenStats.max_prompt ? `
                <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-top: 0.75rem;">
                    <div>
                        <div style="font-size: 0.75rem;" class="ldr-text-secondary">Min Prompt</div>
                        <div style="font-weight: 600;">${formatNumber(tokenStats.min_prompt)}</div>
                    </div>
                    <div>
                        <div style="font-size: 0.75rem;" class="ldr-text-secondary">Avg Prompt</div>
                        <div style="font-weight: 600;">${formatNumber(tokenStats.avg_prompt)}</div>
                    </div>
                    <div>
                        <div style="font-size: 0.75rem;" class="ldr-text-secondary">Max Prompt</div>
                        <div style="font-weight: 600;">${formatNumber(tokenStats.max_prompt)}</div>
                    </div>
                </div>` : ''}

                ${hasUtil ? `
                <div style="margin-top: 0.75rem;">
                    <div style="display: flex; justify-content: space-between; font-size: 0.75rem; margin-bottom: 0.25rem;">
                        <span class="ldr-text-secondary">Context utilization</span>
                        <span style="font-weight: 600; color: ${utilColor};">${utilPct}%</span>
                    </div>
                    <div class="ldr-progress-bar" style="height: 6px;">
                        <div class="ldr-progress-fill" style="width: ${Math.min(utilPct, 100)}%; background: ${utilColor};"></div>
                    </div>
                </div>` : ''}

                <div class="ldr-progress-bar" style="margin-top: 0.5rem;">
                    <div class="ldr-progress-fill" style="width: ${Math.min(truncationPercent, 100)}%;"></div>
                </div>

                ${tokenStats.avg_response_time_ms ? `
                <div style="margin-top: 0.5rem; font-size: 0.75rem;" class="ldr-text-secondary">
                    <span><i aria-hidden="true" class="fas fa-clock" style="margin-right: 0.25rem;"></i>Avg ${tokenStats.avg_response_time_ms >= 1000 ? (tokenStats.avg_response_time_ms / 1000).toFixed(1) + 's' : tokenStats.avg_response_time_ms + 'ms'}</span>
                </div>` : ''}
            `;

            container.appendChild(card);
        });
    }

    // Display recent truncated requests
    function displayTruncatedRequests(requests) {
        const container = document.getElementById('truncated-list');

        if (requests.length === 0) {
            container.innerHTML = `
                <div class="ldr-no-data-message">
                    <i aria-hidden="true" class="fas fa-check-circle ldr-icon-success"></i>
                    <p>No truncated requests found</p>
                    <p style="font-size: 0.875rem;">All your requests are within context limits!</p>
                </div>
            `;
            return;
        }

        container.innerHTML = '';
        const table = document.createElement('table');
        table.className = 'ldr-data-table';

        // eslint-disable-next-line no-unsanitized/property -- query/model run through escapeHtml; numeric fields through formatNumber; research_id is encoded via encodeURIComponent in the href
        table.innerHTML = `
            <thead>
                <tr>
                    <th>Time</th>
                    <th>Query</th>
                    <th>Model</th>
                    <th>Prompt Tokens</th>
                    <th>Context Limit</th>
                    <th>Tokens Lost</th>
                    <th>Action</th>
                </tr>
            </thead>
            <tbody>
                ${requests.map(req => `
                    <tr>
                        <td>${new Date(req.timestamp).toLocaleString()}</td>
                        <td style="max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
                            ${escapeHtml(req.research_query || 'N/A')}
                        </td>
                        <td>${escapeHtml(formatModelName(req.model))}</td>
                        <td>${formatNumber(req.prompt_tokens)}</td>
                        <td><span class="ldr-context-limit-badge">${formatNumber(req.context_limit)}</span></td>
                        <td class="ldr-text-error" style="font-weight: 600;">${formatNumber(req.tokens_truncated || 0)}</td>
                        <td>
                            <a href="/details/${encodeURIComponent(req.research_id)}" style="color: var(--primary-color);">
                                View Details
                            </a>
                        </td>
                    </tr>
                `).join('')}
            </tbody>
        `;

        const tableContainer = document.createElement('div');
        tableContainer.className = 'ldr-table-container';
        tableContainer.appendChild(table);
        container.appendChild(tableContainer);
    }

    // Create main context scatter chart
    function createContextChart(chartData, currentContextWindow) {
        const ctx = document.getElementById('context-chart').getContext('2d');

        if (contextChart) {
            contextChart.destroy();
        }

        if (!chartData || chartData.length === 0) {
            contextChart = new Chart(ctx, {
                type: 'scatter',
                data: { datasets: [{ label: 'No data', data: [] }] },
                options: {
                    responsive: true,
                    maintainAspectRatio: false,
                    plugins: { title: { display: true, text: 'No context data available yet' } }
                }
            });
            return;
        }

        // Bucket each point by context utilisation ratio.
        // Reasoning: per-limit dashed reference lines added visual noise without
        // making "is this request safe" easier to read. Color-coding by ratio
        // surfaces the same information per-point. Shape per bucket
        // (circle / triangle / rectRot) gives a redundant encoding for users who
        // can't distinguish red/green/amber. Local-vs-cloud is encoded in
        // opacity (lower for local) so users running Ollama/llama.cpp can see
        // their requests as a distinct visual class.
        // Canonical provider keys per defaults/default_settings.json llm.provider
        // enum: 'lmstudio' / 'llamacpp' (no underscores). main's inline JS used
        // 'lm_studio' / 'llama_cpp' which never matched any real provider value,
        // silently breaking the local-vs-cloud visual distinction since the
        // page first shipped.
        const LOCAL_PROVIDERS = new Set(['ollama', 'lmstudio', 'llamacpp', 'local']);
        const isLocal = p => LOCAL_PROVIDERS.has((p.provider || '').toLowerCase());

        const safeData = [];
        const cautionData = [];
        const criticalData = [];
        // Separate bucket for points where the provider didn't report a
        // context_limit at all — these are a data-quality signal, not a
        // utilisation signal, so they should not be conflated with caution.
        const noLimitData = [];
        chartData.forEach(d => {
            const rawTokens = (d.original_prompt_tokens != null)
                ? d.original_prompt_tokens
                : d.prompt_tokens;
            // Track whether tokens were reported separately from a 0 value;
            // this drives both bucketing and tooltip wording.
            const tokensKnown = rawTokens != null;
            const tokens = tokensKnown ? rawTokens : 0;
            const limit = d.context_limit;
            const local = isLocal(d);
            const point = {
                x: new Date(d.timestamp),
                y: tokens,
                research_id: d.research_id,
                model: d.model,
                provider: d.provider,
                tokens_truncated: d.tokens_truncated,
                context_limit: limit,
                tokens_known: tokensKnown,
                ratio: (limit && tokensKnown) ? tokens / limit : null,
                isLocal: local,
            };
            if (!limit) {
                noLimitData.push(point);
            } else if (!tokensKnown) {
                // Unknown token count — bucket as caution so the point is still
                // visible without implying it overran or fit safely.
                cautionData.push(point);
            } else if (point.ratio < 0.5) {
                safeData.push(point);
            } else if (point.ratio <= 0.8) {
                cautionData.push(point);
            } else {
                criticalData.push(point);
            }
        });

        // Per-point opacity: lower for local providers so they read as a
        // distinct visual class without competing with truncation severity
        // (which is encoded by colour + shape).
        const localAlpha = 0.25;
        const cloudAlpha = 0.7;
        const opacityFor = (rgb, points) =>
            points.map(p => `rgba(${rgb}, ${p.isLocal ? localAlpha : cloudAlpha})`);

        const datasets = [
            {
                label: 'Safe (< 50% of limit)',
                data: safeData,
                backgroundColor: opacityFor('10, 207, 151', safeData),
                borderColor: 'rgba(10, 207, 151, 1)',
                pointStyle: 'circle',
                pointRadius: safeData.map(p => (p.isLocal ? 7 : 6)),
                pointHoverRadius: safeData.map(p => (p.isLocal ? 10 : 9)),
                pointBorderWidth: safeData.map(p => (p.isLocal ? 2.5 : 1)),
            },
            {
                label: 'Caution (50–80% / unknown)',
                data: cautionData,
                backgroundColor: opacityFor('249, 188, 11', cautionData),
                borderColor: 'rgba(249, 188, 11, 1)',
                pointStyle: 'triangle',
                pointRadius: cautionData.map(p => (p.isLocal ? 8 : 7)),
                pointHoverRadius: cautionData.map(p => (p.isLocal ? 11 : 10)),
                pointBorderWidth: cautionData.map(p => (p.isLocal ? 2.5 : 1)),
            },
            {
                label: 'Critical (> 80%)',
                data: criticalData,
                backgroundColor: opacityFor('250, 92, 124', criticalData),
                borderColor: 'rgba(250, 92, 124, 1)',
                pointStyle: 'rectRot',
                pointRadius: criticalData.map(p => (p.isLocal ? 8 : 7)),
                pointHoverRadius: criticalData.map(p => (p.isLocal ? 11 : 10)),
                pointBorderWidth: criticalData.map(p => (p.isLocal ? 2.5 : 1)),
            },
            {
                label: 'No context limit reported',
                data: noLimitData,
                backgroundColor: opacityFor('160, 160, 180', noLimitData),
                borderColor: 'rgba(160, 160, 180, 1)',
                pointStyle: 'triangle',
                pointRadius: noLimitData.map(p => (p.isLocal ? 7 : 6)),
                pointHoverRadius: noLimitData.map(p => (p.isLocal ? 10 : 9)),
                pointBorderWidth: noLimitData.map(p => (p.isLocal ? 2.5 : 1)),
            },
        ];

        // Build horizontal reference lines for each unique context limit so users
        // can see exactly which models are constrained by which limit, plus a
        // distinct solid line for the currently-configured num_ctx setting.
        const uniqueLimits = [...new Set(chartData.map(d => d.context_limit).filter(Boolean))];
        const limitAnnotations = {};
        const limitColors = [
            { border: 'rgba(139, 92, 246, 0.8)', bg: 'rgba(139, 92, 246, 0.1)' },
            { border: 'rgba(59, 130, 246, 0.8)', bg: 'rgba(59, 130, 246, 0.1)' },
            { border: 'rgba(16, 185, 129, 0.8)', bg: 'rgba(16, 185, 129, 0.1)' },
            { border: 'rgba(245, 158, 11, 0.8)', bg: 'rgba(245, 158, 11, 0.1)' },
            { border: 'rgba(239, 68, 68, 0.8)', bg: 'rgba(239, 68, 68, 0.1)' },
        ];
        uniqueLimits.sort((a, b) => a - b);
        uniqueLimits.forEach((limit, i) => {
            const c = limitColors[i % limitColors.length];
            limitAnnotations[`limit_${limit}`] = {
                type: 'line',
                yMin: limit,
                yMax: limit,
                borderColor: c.border,
                borderWidth: 1.5,
                borderDash: [6, 4],
                label: {
                    content: `num_ctx = ${formatNumber(limit)}`,
                    enabled: true,
                    position: 'start',
                    backgroundColor: c.bg,
                    color: c.border,
                    font: { size: 11, weight: '600' },
                    padding: { top: 2, bottom: 2, left: 4, right: 4 },
                }
            };
        });

        if (currentContextWindow && !uniqueLimits.includes(currentContextWindow)) {
            limitAnnotations.current_setting = {
                type: 'line',
                yMin: currentContextWindow,
                yMax: currentContextWindow,
                borderColor: 'rgba(255, 255, 255, 0.9)',
                borderWidth: 2,
                label: {
                    content: `current = ${formatNumber(currentContextWindow)}`,
                    enabled: true,
                    position: 'end',
                    backgroundColor: 'rgba(255, 255, 255, 0.15)',
                    color: 'rgba(255, 255, 255, 0.9)',
                    font: { size: 11, weight: '700' },
                    padding: { top: 2, bottom: 2, left: 4, right: 4 },
                }
            };
        }

        contextChart = new Chart(ctx, {
            type: 'scatter',
            data: { datasets },
            options: {
                responsive: true,
                maintainAspectRatio: false,
                scales: {
                    x: {
                        type: 'time',
                        title: { display: true, text: 'Time' }
                    },
                    y: {
                        title: { display: true, text: 'Token Count' },
                        beginAtZero: true
                    }
                },
                plugins: {
                    tooltip: {
                        callbacks: {
                            label(context) {
                                const point = context.raw;
                                let label = point.tokens_known
                                    ? `${formatNumber(point.y)} prompt tokens`
                                    : 'prompt tokens unknown';
                                if (point.context_limit) {
                                    label += ` / ${formatNumber(point.context_limit)} limit`;
                                    if (point.ratio !== null && point.ratio !== undefined) {
                                        label += ` (${(point.ratio * 100).toFixed(0)}%)`;
                                    }
                                } else {
                                    label += ' — context limit unknown';
                                }
                                if (point.model) {
                                    label += ` — ${point.model}`;
                                }
                                if (point.provider) {
                                    label += ` [${point.isLocal ? 'local' : 'cloud'}]`;
                                }
                                if (point.tokens_truncated) {
                                    label += ` — lost ${formatNumber(point.tokens_truncated)}`;
                                }
                                return label;
                            }
                        }
                    },
                    legend: { position: 'bottom' },
                    annotation: {
                        annotations: limitAnnotations
                    }
                },
                onClick: (event, elements) => {
                    if (elements.length > 0) {
                        const dataPoint = elements[0];
                        const dataset = contextChart.data.datasets[dataPoint.datasetIndex];
                        const point = dataset.data[dataPoint.index];
                        if (point.research_id) {
                            URLValidator.safeAssign(window.location, 'href', `/details/${encodeURIComponent(point.research_id)}`);
                        }
                    }
                }
            }
        });
    }

    // Create phase breakdown: box-plot + jittered scatter per phase
    function createPhaseChart(chartData, phaseBreakdown) {
        const container = document.getElementById('phase-breakdown-container');
        const canvas = document.getElementById('phase-chart');

        if (phaseChart) {
            phaseChart.destroy();
        }

        if (!chartData || chartData.length === 0) {
            container.innerHTML = `
                <div class="ldr-no-data-message">
                    <i aria-hidden="true" class="fas fa-tasks"></i>
                    <p>No phase data available yet</p>
                </div>
            `;
            return;
        }

        // Group points by phase
        const groups = {};
        chartData.forEach(d => {
            const phase = d.research_phase || 'unknown';
            const tokens = d.prompt_tokens || 0;
            if (!groups[phase]) groups[phase] = [];
            groups[phase].push(tokens);
        });

        // Sort phases by median descending
        const median = arr => {
            const s = [...arr].sort((a, b) => a - b);
            const m = Math.floor(s.length / 2);
            return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
        };
        const percentile = (arr, p) => {
            const s = [...arr].sort((a, b) => a - b);
            const pos = (s.length - 1) * p;
            const lo = Math.floor(pos);
            const hi = Math.ceil(pos);
            return s[lo] + (s[hi] - s[lo]) * (pos - lo);
        };

        const phases = Object.keys(groups).sort((a, b) => median(groups[b]) - median(groups[a]));

        if (phases.length === 0) {
            container.innerHTML = `
                <div class="ldr-no-data-message">
                    <i aria-hidden="true" class="fas fa-tasks"></i>
                    <p>No phase data available yet</p>
                </div>
            `;
            return;
        }

        const palette = [
            'rgba(54, 162, 235, 1)',
            'rgba(255, 99, 132, 1)',
            'rgba(75, 192, 192, 1)',
            'rgba(255, 205, 86, 1)',
            'rgba(153, 102, 255, 1)',
            'rgba(255, 159, 64, 1)',
            'rgba(201, 203, 207, 1)',
        ];

        const boxStats = phases.map((phase, i) => {
            const vals = [...groups[phase]].sort((a, b) => a - b);
            return {
                phase,
                n: vals.length,
                min: vals[0],
                q1: percentile(vals, 0.25),
                med: median(vals),
                q3: percentile(vals, 0.75),
                max: vals[vals.length - 1],
                color: palette[i % palette.length],
            };
        });

        // Jittered scatter points (deterministic based on index)
        const scatterPoints = [];
        phases.forEach((phase, phaseIdx) => {
            groups[phase].forEach((v, i) => {
                const jitter = ((i * 7 + 3) % 11) / 11 * 0.3 - 0.15;
                scatterPoints.push({
                    x: phaseIdx + jitter,
                    y: v,
                    phase,
                });
            });
        });

        // Custom plugin to draw box-and-whisker on a bar chart
        const boxPlugin = {
            id: 'boxplot',
            afterDatasetsDraw(chart) {
                const ctx = chart.ctx;
                const meta = chart.getDatasetMeta(0); // the invisible bar dataset
                boxStats.forEach((bp, i) => {
                    const bar = meta.data[i];
                    if (!bar) return;
                    const x = bar.x;
                    const yScale = chart.scales.y;

                    const yMin = yScale.getPixelForValue(bp.min);
                    const yQ1 = yScale.getPixelForValue(bp.q1);
                    const yMed = yScale.getPixelForValue(bp.med);
                    const yQ3 = yScale.getPixelForValue(bp.q3);
                    const yMax = yScale.getPixelForValue(bp.max);

                    const boxW = Math.min(40, meta.data.length > 1
                        ? Math.abs(meta.data[1].x - meta.data[0].x) * 0.5
                        : 40);
                    const half = boxW / 2;

                    ctx.save();
                    ctx.strokeStyle = bp.color;
                    ctx.fillStyle = bp.color.replace('1)', '0.15)');
                    ctx.lineWidth = 1.5;

                    // Whiskers (min → Q1, Q3 → max)
                    ctx.beginPath();
                    ctx.moveTo(x, yMin); ctx.lineTo(x, yQ1);
                    ctx.moveTo(x, yQ3); ctx.lineTo(x, yMax);
                    // Whisker caps
                    ctx.moveTo(x - half * 0.5, yMin); ctx.lineTo(x + half * 0.5, yMin);
                    ctx.moveTo(x - half * 0.5, yMax); ctx.lineTo(x + half * 0.5, yMax);
                    ctx.stroke();

                    // Box (Q1 → Q3)
                    ctx.fillRect(x - half, yQ3, boxW, yQ1 - yQ3);
                    ctx.strokeRect(x - half, yQ3, boxW, yQ1 - yQ3);

                    // Median line
                    ctx.strokeStyle = bp.color;
                    ctx.lineWidth = 2.5;
                    ctx.beginPath();
                    ctx.moveTo(x - half, yMed);
                    ctx.lineTo(x + half, yMed);
                    ctx.stroke();

                    ctx.restore();
                });
            }
        };

        // Invisible bar dataset to establish x-axis category positions
        const barData = boxStats.map(() => 0);

        phaseChart = new Chart(canvas, {
            type: 'bar',
            data: {
                labels: phases.map(p => p || 'unknown'),
                datasets: [
                    {
                        label: '_box',
                        data: barData,
                        backgroundColor: 'transparent',
                        borderColor: 'transparent',
                        borderWidth: 0,
                        barPercentage: 0.6,
                    },
                    {
                        label: 'Requests',
                        type: 'scatter',
                        data: scatterPoints,
                        backgroundColor: 'rgba(120, 120, 140, 0.35)',
                        borderColor: 'transparent',
                        pointRadius: 3,
                        pointHoverRadius: 5,
                        xAxisID: 'x',
                    },
                ],
            },
            plugins: [boxPlugin],
            options: {
                responsive: true,
                maintainAspectRatio: false,
                scales: {
                    y: {
                        beginAtZero: true,
                        title: { display: true, text: 'Prompt Tokens' },
                    },
                    x: {
                        title: { display: true, text: 'Research Phase' },
                    },
                    x2: { display: false },
                },
                plugins: {
                    legend: { display: false },
                    tooltip: {
                        filter: item => item.datasetIndex === 1,
                        callbacks: {
                            label: ctx => `${formatNumber(ctx.raw.y)} prompt tokens · ${ctx.raw.phase}`,
                        },
                    },
                },
            },
        });

        // Phase summary table (uses pre-aggregated phase_breakdown from API)
        const summaryEl = document.getElementById('phase-summary-table');
        if (summaryEl && phaseBreakdown && phaseBreakdown.length > 0) {
            const sorted = [...phaseBreakdown].sort((a, b) => b.total_tokens - a.total_tokens);
            // eslint-disable-next-line no-unsanitized/property -- phase strings escaped via escapeHtml; numeric counts/sums go through formatNumber
            summaryEl.innerHTML = `
                <table class="ldr-data-table" style="width: 100%; font-size: 0.85rem;">
                    <thead>
                        <tr>
                            <th>Phase</th>
                            <th style="text-align: right;">Requests</th>
                            <th style="text-align: right;">Total Tokens</th>
                            <th style="text-align: right;">Avg Tokens</th>
                        </tr>
                    </thead>
                    <tbody>
                        ${sorted.map(p => `
                            <tr>
                                <td>${escapeHtml(p.phase || 'unknown')}</td>
                                <td style="text-align: right;">${formatNumber(p.count)}</td>
                                <td style="text-align: right; font-weight: 600;">${formatNumber(p.total_tokens)}</td>
                                <td style="text-align: right;">${formatNumber(p.avg_tokens)}</td>
                            </tr>
                        `).join('')}
                    </tbody>
                </table>
            `;
        }
    }

    // Create latency vs context size chart (model-specific)
    function createLatencyChart(chartData) {
        const canvas = document.getElementById('latency-chart');
        if (!canvas) return;
        const ctx = canvas.getContext('2d');

        if (latencyChart) {
            latencyChart.destroy();
        }

        if (!chartData || chartData.length === 0) {
            latencyChart = new Chart(ctx, {
                type: 'scatter',
                data: { datasets: [] },
                options: {
                    responsive: true,
                    maintainAspectRatio: false,
                    plugins: { title: { display: true, text: 'No latency data available yet' } }
                }
            });
            return;
        }

        // Filter to points that have response_time_ms
        const withLatency = chartData.filter(d => d.response_time_ms != null && d.response_time_ms > 0);
        if (withLatency.length === 0) {
            latencyChart = new Chart(ctx, {
                type: 'scatter',
                data: { datasets: [] },
                options: {
                    responsive: true,
                    maintainAspectRatio: false,
                    plugins: { title: { display: true, text: 'No response time data recorded yet' } }
                }
            });
            return;
        }

        // Group by model
        const modelGroups = {};
        withLatency.forEach(d => {
            const model = d.model || 'unknown';
            if (!modelGroups[model]) modelGroups[model] = [];
            const prompt = d.prompt_tokens || 0;
            const limit = d.context_limit;
            const util = limit ? Math.min(prompt / limit, 1) : 0;
            modelGroups[model].push({
                x: prompt,
                y: d.response_time_ms,
                model,
                provider: d.provider,
                context_limit: limit,
                truncated: d.truncated,
                utilisation: util,
                research_id: d.research_id,
            });
        });

        // Colour palette per model
        const palette = [
            { bg: 'rgba(54, 162, 235, 0.6)', border: 'rgba(54, 162, 235, 1)' },
            { bg: 'rgba(250, 92, 124, 0.6)', border: 'rgba(250, 92, 124, 1)' },
            { bg: 'rgba(75, 192, 192, 0.6)', border: 'rgba(75, 192, 192, 1)' },
            { bg: 'rgba(249, 188, 11, 0.6)', border: 'rgba(249, 188, 11, 1)' },
            { bg: 'rgba(153, 102, 255, 0.6)', border: 'rgba(153, 102, 255, 1)' },
            { bg: 'rgba(255, 159, 64, 0.6)', border: 'rgba(255, 159, 64, 1)' },
            { bg: 'rgba(46, 204, 113, 0.6)', border: 'rgba(46, 204, 113, 1)' },
            { bg: 'rgba(231, 76, 60, 0.6)', border: 'rgba(231, 76, 60, 1)' },
        ];

        const modelNames = Object.keys(modelGroups).sort((a, b) => modelGroups[b].length - modelGroups[a].length);
        const datasets = modelNames.map((model, i) => {
            const c = palette[i % palette.length];
            const points = modelGroups[model];
            return {
                label: formatModelName(model),
                data: points,
                backgroundColor: points.map(p => {
                    // Darker opacity for higher utilisation
                    const alpha = 0.3 + p.utilisation * 0.5;
                    return c.bg.replace(/[\d.]+\)$/, alpha.toFixed(2) + ')');
                }),
                borderColor: points.map(p =>
                    (p.truncated ? 'rgba(250, 92, 124, 1)' : c.border)
                ),
                pointBorderWidth: points.map(p => (p.truncated ? 2 : 1)),
                pointRadius: 5,
                pointHoverRadius: 8,
            };
        });

        latencyChart = new Chart(ctx, {
            type: 'scatter',
            data: { datasets },
            options: {
                responsive: true,
                maintainAspectRatio: false,
                scales: {
                    x: {
                        title: { display: true, text: 'Prompt Tokens (input size)' },
                        beginAtZero: true,
                    },
                    y: {
                        title: { display: true, text: 'Response Time (ms)' },
                        beginAtZero: true,
                        ticks: {
                            callback(value) {
                                if (value >= 1000) return (value / 1000).toFixed(1) + 's';
                                return value + 'ms';
                            }
                        }
                    }
                },
                plugins: {
                    legend: { position: 'bottom' },
                    tooltip: {
                        callbacks: {
                            label(context) {
                                const p = context.raw;
                                const latency = p.y >= 1000 ? (p.y / 1000).toFixed(1) + 's' : p.y + 'ms';
                                let label = `${formatNumber(p.x)} tokens → ${latency}`;
                                if (p.context_limit) {
                                    const pct = (p.utilisation * 100).toFixed(0);
                                    label += ` (${pct}% of limit)`;
                                }
                                if (p.truncated) label += ' [truncated]';
                                return label;
                            },
                            afterLabel(context) {
                                const p = context.raw;
                                return `Model: ${p.model}`;
                            }
                        }
                    }
                },
                onClick: (event, elements) => {
                    if (elements.length > 0) {
                        const dp = elements[0];
                        const point = latencyChart.data.datasets[dp.datasetIndex].data[dp.index];
                        if (point.research_id) {
                            URLValidator.safeAssign(window.location, 'href', `/details/${encodeURIComponent(point.research_id)}`);
                        }
                    }
                }
            }
        });
    }

    // Show error state
    function showError() {
        document.getElementById('loading').style.display = 'none';
        document.getElementById('content').innerHTML = `
            <div class="ldr-card">
                <div class="ldr-card-content">
                    <div class="ldr-no-data-message">
                        <i aria-hidden="true" class="fas fa-exclamation-circle ldr-icon-error"></i>
                        <p>Error loading token usage data</p>
                        <p style="font-size: 0.875rem;">Please try refreshing the page</p>
                    </div>
                </div>
            </div>
        `;
        document.getElementById('content').style.display = 'block';
    }

    // Handle time range changes
    function handleTimeRangeChange(period) {
        currentPeriod = period;
        currentPage = 1;

        document.querySelectorAll('.ldr-time-range-btn').forEach(btn => {
            btn.classList.remove('active');
        });
        document.querySelector(`[data-period="${period}"]`).classList.add('active');

        loadContextData(period, 1);
    }

    // Initialize when DOM is loaded
    function initialize() {
        // Set up time range buttons
        document.querySelectorAll('.ldr-time-range-btn').forEach(btn => {
            btn.addEventListener('click', function() {
                handleTimeRangeChange(this.getAttribute('data-period'));
            });
        });

        // Set up pagination buttons
        document.getElementById('pagination-prev').addEventListener('click', function() {
            if (currentPage > 1) {
                currentPage--;
                loadContextData(currentPeriod, currentPage);
            }
        });

        document.getElementById('pagination-next').addEventListener('click', function() {
            if (currentPage < totalPages) {
                currentPage++;
                loadContextData(currentPeriod, currentPage);
            }
        });

        // Set up search input
        document.getElementById('requests-search').addEventListener('input', function() {
            searchTerm = this.value.trim().toLowerCase();
            renderRequestsTable();
        });

        // Set up sortable column headers
        document.querySelectorAll('.ldr-sortable').forEach(th => {
            th.addEventListener('click', function() {
                const col = this.getAttribute('data-sort');

                // Toggle direction or set new column
                if (sortColumn === col) {
                    sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
                } else {
                    sortColumn = col;
                    sortDirection = 'desc';
                }

                // Update header classes
                document.querySelectorAll('.ldr-sortable').forEach(h => {
                    h.classList.remove('ldr-sort-asc', 'ldr-sort-desc');
                });
                this.classList.add(sortDirection === 'asc' ? 'ldr-sort-asc' : 'ldr-sort-desc');

                renderRequestsTable();
            });
        });

        // Load initial data
        loadContextData();
    }

    // Test-only export so Vitest can call loadContextData and
    // verify abort-on-supersede behavior without rebuilding the IIFE.
    // Production code never touches this — it's not used internally.
    if (typeof window !== 'undefined') {
        window.contextOverflowController = { loadContextData };
    }

    // With `defer`, this script runs after DOM parsing but before
    // DOMContentLoaded fires. The readyState branch keeps us safe whether
    // the script ends up loading sync (legacy) or async (modern).
    // Skip auto-init under Vitest (happy-dom) — the test sets up the DOM
    // scaffold itself and calls initialize/loadContextData explicitly.
    if (typeof window !== 'undefined' && !window.__VITEST_TEST__) {
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', initialize);
        } else {
            initialize();
        }
    }
})();
