'use client'; import { useState, useEffect, useCallback, type ReactNode } from 'react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; import BackupManagerModal from './BackupManagerModal'; import DoctorModal from './DoctorModal'; import type { DoctorLevel } from '@/lib/admin/doctor'; import * as Icons from './Icons'; interface Props { onLogout: () => void; /** The active route's panel — Pages, Journal Studio, Settings or Analytics. */ children: ReactNode; } const TABS = [ { label: 'Pages', href: '/admin/pages', match: /^\/admin(\/pages)?$/ }, { label: 'Journal', href: '/admin/journal', match: /^\/admin\/journal/ }, { label: 'Settings', href: '/admin/settings', match: /^\/admin\/settings/ }, { label: 'Analytics', href: '/admin/analytics', match: /^\/admin\/analytics/ }, ]; export default function AdminDashboard({ onLogout, children }: Props) { const pathname = usePathname(); const [saving, setSaving] = useState(false); // Diagnostics & Backup state const [showStatus, setShowStatus] = useState(false); const [showBackupModal, setShowBackupModal] = useState(false); const [showDoctor, setShowDoctor] = useState(false); // The badge's job is "does anything need me?", and the doctor is the only // check that can answer that beyond "is Immich up" (#491). const [doctorLevel, setDoctorLevel] = useState(null); const [status, setStatus] = useState(null); const [statusLoading, setStatusLoading] = useState(false); // Distinct from `status === null`: the check itself did not run. Without this // an expired session or a 500 rendered exactly like a real outage — every // indicator flipped to its alarming value at once (#341). const [statusError, setStatusError] = useState(false); async function handleLogout() { await fetch('/api/admin/auth', { method: 'DELETE' }); onLogout(); } async function handleReload() { setSaving(true); try { await fetch('/api/admin/reload', { method: 'POST' }); // Refresh status after reload await fetchStatus(); } finally { setSaving(false); } } const fetchStatus = async () => { setStatusLoading(true); try { const res = await fetch('/api/admin/status'); if (res.ok) { const data = await res.json(); setStatus(data); setStatusError(false); } else { setStatus(null); setStatusError(true); } } catch { setStatus(null); setStatusError(true); } finally { setStatusLoading(false); } }; /** * Render one diagnostic as ok / bad / unknown. "Unknown" covers both the * in-flight check and a check that failed to run — neither is evidence that * the thing being checked is broken. */ const indicator = (healthy: boolean, okLabel: string, badLabel: string) => { if (statusLoading) return { className: 'unknown', label: 'Checking…' }; if (statusError || !status) return { className: 'unknown', label: 'Unknown' }; return healthy ? { className: 'ok', label: okLabel } : { className: 'error', label: badLabel }; }; const fetchDoctor = useCallback(async () => { try { const res = await fetch('/api/admin/doctor'); if (!res.ok) return; const data = await res.json(); setDoctorLevel(data.level ?? null); } catch { // A failed check must not colour the badge — "unknown" is not "broken". } }, []); useEffect(() => { fetchStatus(); fetchDoctor(); }, [fetchDoctor]); const immichIndicator = indicator( status?.immich?.status === 'connected', 'Connected', 'Disconnected', ); const configIndicator = !statusLoading && !statusError && status?.config?.status === 'setup' ? { className: 'unknown', label: 'Not set up' } : indicator(status?.config?.status === 'valid', 'Valid', 'Degraded'); // An installation that was never finished is not a fault. It used to render // as "System Degraded" with no further hint, which sent operators looking for // a network problem that did not exist (#507). const setupIncomplete = !statusLoading && !statusError && status?.setup?.complete === false; const missingCredentials = status?.setup?.credentials === 'missing'; return (

Immich Folio

{/* Diagnostics Badge */}
{showStatus && ( <>
setShowStatus(false)} />

System Diagnostics

{setupIncomplete && (

{missingCredentials ? 'No Immich URL or API key configured. Set IMMICH_API_URL and IMMICH_API_KEY, or run the ' : 'No content/gallery.yaml yet — the public site has nothing to show. Add a page below, or run the '} setup wizard {missingCredentials ? '.' : ' (the one-time token is printed to the server log).'}

)}
Immich Connection {immichIndicator.label}
Config Integrity {configIndicator.label}
Config Backups {status?.backups?.count ?? 0} Backups
Latest Backup {status?.backups?.lastBackup ? new Date(status.backups.lastBackup).toLocaleDateString() : 'None'}
In-Memory Cache {status?.cache?.size ?? 0} items
)}
{children}
{ setShowDoctor(false); fetchDoctor(); }} /> setShowBackupModal(false)} onRestoreSuccess={() => { fetchStatus(); }} />
); }