/**
 * Fetch JSON with a hard timeout so a wedged /api/reports/executive build
 * (the route runs heavy DB aggregates) rejects into an error state instead of
 * leaving the Executive screens blank forever.
 *
 * Uses AbortController + setTimeout rather than AbortSignal.timeout() — the
 * latter is unavailable on older embedded/kiosk browsers and would throw
 * synchronously, turning a working dashboard into a permanent error panel.
 *
 * The live route nests the payload under { success, metrics }; callers get the
 * inner object (or the raw body if not nested).
 */
export async function fetchJsonWithTimeout<T>(url: string, timeoutMs: number): Promise<T> {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
        const res = await fetch(url, { cache: 'no-store', signal: controller.signal });
        if (!res.ok) throw new Error(`metrics ${res.status}`);
        const json = await res.json();
        return (json.metrics ?? json) as T;
    } finally {
        clearTimeout(timer);
    }
}
