import { useCallback, useEffect, useRef, useState } from 'react'; import { ApiError, request } from './client'; import { getDashboardHealth, getDashboardVersion, type DashboardVersion } from './dashboard'; import { getMediaSources, type MediaSource } from './libraries'; import { getFFmpegProfiles, getFillerPresets, getWatermarks, type FFmpegProfile, type FillerPreset, type Watermark } from './pickers'; import type { components } from './generated/v1'; export type FfmpegSettings = components['schemas']['FFmpegSettingsResponseModel']; export type UpdateFfmpegSettingsRequest = components['schemas']['UpdateFFmpegSettingsRequest']; export type PlayoutSettings = components['schemas']['PlayoutSettingsResponseModel']; export type UpdatePlayoutSettingsRequest = components['schemas']['UpdatePlayoutSettingsRequest']; export type XmltvSettings = components['schemas']['XmltvSettingsResponseModel']; export type UpdateXmltvSettingsRequest = components['schemas']['UpdateXmltvSettingsRequest']; export type ScannerSettings = components['schemas']['ScannerSettingsResponseModel']; export type UpdateScannerSettingsRequest = components['schemas']['UpdateScannerSettingsRequest']; export type LoggingSettings = components['schemas']['LoggingSettingsResponseModel']; export type UpdateLoggingSettingsRequest = components['schemas']['UpdateLoggingSettingsRequest']; export type UiSettings = components['schemas']['UiSettingsResponseModel']; export type UpdateUiSettingsRequest = components['schemas']['UpdateUiSettingsRequest']; export type HdhrSettings = components['schemas']['HdhrSettingsResponseModel']; export type UpdateHdhrSettingsRequest = components['schemas']['UpdateHdhrSettingsRequest']; export type Resolution = components['schemas']['ResolutionResponseModel']; export type CreateResolutionRequest = components['schemas']['CreateResolutionRequest']; export type LogEventLevel = components['schemas']['LogEventLevel']; export type XmltvTimeZone = components['schemas']['XmltvTimeZone']; export type XmltvBlockBehavior = components['schemas']['XmltvBlockBehavior']; export type OutputFormatKind = components['schemas']['OutputFormatKind']; export type HealthCheck = components['schemas']['HealthCheckResponseModel']; export function getFfmpegSettings(): Promise { return request('/api/settings/ffmpeg'); } export function updateFfmpegSettings(body: UpdateFfmpegSettingsRequest): Promise { return request('/api/settings/ffmpeg', { body, method: 'PUT' }); } export function getPlayoutSettings(): Promise { return request('/api/settings/playout'); } export function updatePlayoutSettings(body: UpdatePlayoutSettingsRequest): Promise { return request('/api/settings/playout', { body, method: 'PUT' }); } export function getXmltvSettings(): Promise { return request('/api/settings/xmltv'); } export function updateXmltvSettings(body: UpdateXmltvSettingsRequest): Promise { return request('/api/settings/xmltv', { body, method: 'PUT' }); } export function getScannerSettings(): Promise { return request('/api/settings/scanner'); } export function updateScannerSettings(body: UpdateScannerSettingsRequest): Promise { return request('/api/settings/scanner', { body, method: 'PUT' }); } export function getLoggingSettings(): Promise { return request('/api/settings/logging'); } export function updateLoggingSettings(body: UpdateLoggingSettingsRequest): Promise { return request('/api/settings/logging', { body, method: 'PUT' }); } export function getUiSettings(): Promise { return request('/api/settings/ui'); } export function updateUiSettings(body: UpdateUiSettingsRequest): Promise { return request('/api/settings/ui', { body, method: 'PUT' }); } export function getHdhrSettings(): Promise { return request('/api/settings/hdhr'); } export function updateHdhrSettings(body: UpdateHdhrSettingsRequest): Promise { return request('/api/settings/hdhr', { body, method: 'PUT' }); } export function getResolutions(): Promise { return request('/api/settings/resolutions'); } export function createResolution(body: CreateResolutionRequest): Promise { return request('/api/settings/resolutions', { body, method: 'POST' }); } export function deleteResolution(id: number): Promise { return request(`/api/settings/resolutions/${id}`, { method: 'DELETE' }); } export interface SettingsScreenData { ffmpeg: FfmpegSettings; playout: PlayoutSettings; xmltv: XmltvSettings; scanner: ScannerSettings; logging: LoggingSettings; ui: UiSettings; hdhr: HdhrSettings; resolutions: Resolution[]; ffmpegProfiles: FFmpegProfile[]; watermarks: Watermark[]; fillerPresets: FillerPreset[]; mediaSources: MediaSource[]; version: DashboardVersion | null; health: HealthCheck[]; // Per-resource tier-2 failure flags: reference data that failed to load. The screen // still renders (with empty/null fallbacks above); the affected card shows an inline // "Couldn't load X" note instead of data. referenceFailures: ReferenceFailures; } export interface ReferenceFailures { ffmpegProfiles: boolean; fillerPresets: boolean; health: boolean; mediaSources: boolean; version: boolean; watermarks: boolean; } export type SettingsScreenQueryState = | { data: SettingsScreenData; error: string | null; refresh: () => void; refreshResolutions: () => Promise; status: 'success'; } | { data: null; error: string; refresh: () => void; refreshResolutions: () => Promise; status: 'error' } | { data: null; error: null; refresh: () => void; refreshResolutions: () => Promise; status: 'loading' }; type SettingsScreenState = | { data: SettingsScreenData; error: string | null; status: 'success' } | { data: null; error: string; status: 'error' } | { data: null; error: null; status: 'loading' }; // A settled tier-2 (reference-data) result: falls back to `fallback` on failure and // records the failure so the UI can show an inline note instead of data. function settled(result: PromiseSettledResult, fallback: T): { failed: boolean; value: T } { return result.status === 'fulfilled' ? { failed: false, value: result.value } : { failed: true, value: fallback }; } async function loadSettingsScreenData(): Promise { // Tier 1 (critical): the settings groups themselves + resolutions. A failure here // rejects and produces the screen-level error state (with retry). const tier1 = Promise.all([ getFfmpegSettings(), getPlayoutSettings(), getXmltvSettings(), getScannerSettings(), getLoggingSettings(), getUiSettings(), getHdhrSettings(), getResolutions() ]); // Tier 2 (reference data): each settles independently. A failure never blocks the // screen - the affected card degrades to an inline "Couldn't load X" note. const tier2 = Promise.allSettled([ getFFmpegProfiles(), getWatermarks(), getFillerPresets(), getMediaSources(), getDashboardVersion(), getDashboardHealth() ] as const); const [ffmpeg, playout, xmltv, scanner, logging, ui, hdhr, resolutions] = await tier1; const [profilesResult, watermarksResult, fillerPresetsResult, mediaSourcesResult, versionResult, healthResult] = await tier2; const ffmpegProfiles = settled(profilesResult, []); const watermarks = settled(watermarksResult, []); const fillerPresets = settled(fillerPresetsResult, []); const mediaSources = settled(mediaSourcesResult, []); const version = settled(versionResult, null); const health = settled(healthResult, []); return { ffmpeg, ffmpegProfiles: ffmpegProfiles.value, fillerPresets: fillerPresets.value, health: health.value, hdhr, logging, mediaSources: mediaSources.value, playout, referenceFailures: { ffmpegProfiles: ffmpegProfiles.failed, fillerPresets: fillerPresets.failed, health: health.failed, mediaSources: mediaSources.failed, version: version.failed, watermarks: watermarks.failed }, resolutions, scanner, ui, version: version.value, watermarks: watermarks.value, xmltv }; } export function useSettingsScreenQuery(): SettingsScreenQueryState { const [state, setState] = useState({ data: null, error: null, status: 'loading' }); const activeRef = useRef(true); useEffect(() => { activeRef.current = true; return () => { activeRef.current = false; }; }, []); const load = useCallback(() => { loadSettingsScreenData() .then((data) => { if (activeRef.current) { setState({ data, error: null, status: 'success' }); } }) .catch((error: unknown) => { if (activeRef.current) { setState({ data: null, error: messageFromSettingsError(error, 'Unable to load settings'), status: 'error' }); } }); }, []); useEffect(() => { load(); }, [load]); const refresh = useCallback(() => { setState({ data: null, error: null, status: 'loading' }); load(); }, [load]); const refreshResolutions = useCallback(async () => { const resolutions = await getResolutions(); if (!activeRef.current) { return; } setState((current) => { if (current.status !== 'success') { return current; } return { data: { ...current.data, resolutions }, error: current.error, status: 'success' }; }); }, []); if (state.status === 'success') { return { data: state.data, error: state.error, refresh, refreshResolutions, status: 'success' }; } if (state.status === 'error') { return { data: null, error: state.error, refresh, refreshResolutions, status: 'error' }; } return { data: null, error: null, refresh, refreshResolutions, status: 'loading' }; } export function messageFromSettingsError(error: unknown, fallback = 'Unable to load settings'): string { if (error instanceof ApiError) { return error.detail ?? error.message; } if (error instanceof Error) { return error.message; } return fallback; }