'use client'; import { useEffect, useState } from 'react'; import { AlertTriangle } from 'lucide-react'; import { SettingsSection, SettingItem, ToggleSwitch } from '@/components/settings/settings-section'; import type { AuditEntry } from '@/lib/admin/types'; import { apiFetch } from '@/lib/browser-navigation'; interface AdminStatus { enabled: boolean; authenticated: boolean; lastLogin: string | null; passwordChangedAt: string | null; } interface ConfigData { appName?: string; jmapServerUrl?: string; settingsSyncEnabled?: boolean; stalwartFeaturesEnabled?: boolean; oauthEnabled?: boolean; devMode?: boolean; } export function DashboardTab() { const [status, setStatus] = useState(null); const [recentActivity, setRecentActivity] = useState([]); const [config, setConfig] = useState(null); const [, setConfigSources] = useState | null>(null); const [warnings, setWarnings] = useState([]); const [pluginCount, setPluginCount] = useState(0); const [themeCount, setThemeCount] = useState(0); const [policyRuleCount, setPolicyRuleCount] = useState(0); const [accountCounts, setAccountCounts] = useState<{ total: number; active7d: number } | null>(null); useEffect(() => { fetchDashboardData(); }, []); async function fetchDashboardData() { const [statusRes, auditRes, configRes, adminConfigRes, pluginRes, themeRes, policyRes, telemetryRes] = await Promise.all([ apiFetch('/api/admin/auth'), apiFetch('/api/admin/audit?limit=10'), apiFetch('/api/config'), apiFetch('/api/admin/config'), apiFetch('/api/admin/plugins').catch(() => null), apiFetch('/api/admin/themes').catch(() => null), apiFetch('/api/admin/policy').catch(() => null), apiFetch('/api/admin/telemetry').catch(() => null), ]); if (statusRes.ok) setStatus(await statusRes.json()); if (auditRes.ok) { const data = await auditRes.json(); setRecentActivity(data.entries || []); } let configData: ConfigData | null = null; if (configRes.ok) { configData = await configRes.json(); setConfig(configData); } if (pluginRes?.ok) { const plugins = await pluginRes.json(); setPluginCount(Array.isArray(plugins) ? plugins.length : 0); } if (themeRes?.ok) { const themes = await themeRes.json(); setThemeCount(Array.isArray(themes) ? themes.length : 0); } if (policyRes?.ok) { const policy = await policyRes.json(); const restrictionCount = policy.restrictions ? Object.keys(policy.restrictions).length : 0; const disabledGates = policy.features ? Object.values(policy.features).filter((v: unknown) => !v).length : 0; setPolicyRuleCount(restrictionCount + disabledGates); } if (telemetryRes?.ok) { const telemetry = await telemetryRes.json(); if (telemetry.accountCounts && typeof telemetry.accountCounts.total === 'number') { setAccountCounts(telemetry.accountCounts); } } const w: string[] = []; if (adminConfigRes.ok) { const sources = await adminConfigRes.json(); setConfigSources(sources); const sessionSecret = sources?.sessionSecret; // Server redacts the raw value for sensitive keys; rely on hasValue, // which is false when unset or matching a known placeholder default. if (!sessionSecret?.hasValue) { w.push('SESSION_SECRET is not set or using a default value. Sessions are insecure.'); } const adminPassword = sources?.adminPassword; if (adminPassword?.value && adminPassword.source === 'env') { w.push('ADMIN_PASSWORD is still set in environment variables. Remove it now that the hash is stored securely.'); } } setWarnings(w); } const jmapUrl = config?.jmapServerUrl || '-'; const jmapHostname = jmapUrl !== '-' ? (() => { try { return new URL(jmapUrl).hostname; } catch { return jmapUrl; } })() : '-'; return (
{warnings.map((msg, i) => (

{msg}

))} {config?.appName || '-'} {jmapHostname} {status?.lastLogin ? new Date(status.lastLogin).toLocaleString() : 'Never'} {}} disabled /> {}} disabled /> {}} disabled /> {}} disabled /> {accountCounts?.total ?? '-'} {accountCounts?.active7d ?? '-'} {pluginCount} {themeCount} {policyRuleCount} {recentActivity.length === 0 ? (
No activity recorded yet
) : ( recentActivity.map((entry, i) => (
{entry.ip} {new Date(entry.ts).toLocaleString()}
)) )}
); } function formatDetail(detail: Record): string { if (!detail || Object.keys(detail).length === 0) return ''; if (detail.key) return `${detail.key}: ${detail.old} → ${detail.new}`; if (detail.reason) return String(detail.reason); if (detail.changes && Array.isArray(detail.changes)) return `${detail.changes.length} setting(s) changed`; return JSON.stringify(detail).slice(0, 80); }