"use client"; import { useEffect, useState, useCallback } from "react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { Settings, Database, Bell, Globe, Sliders, Plug, Save, CheckCircle2, AlertTriangle, } from "lucide-react"; import { fetchSiteSettings, updateSiteSettings, fetchNotifications, updateNotifications, fetchIntegrations, updateIntegrations, fetchPagePrefs, updatePagePrefs, type SiteSettings, type NotificationSettings, type IntegrationSettings, type PagePrefs, type SensorDevice, } from "@/lib/api"; import { SensorTable } from "@/components/settings/SensorTable"; import { SensorSheet } from "@/components/settings/SensorSheet"; import { SensorDetailSheet } from "@/components/settings/SensorDetailSheet"; import { ThresholdEditor } from "@/components/settings/ThresholdEditor"; const SITE_ID = "sg-01"; // ── Helpers ──────────────────────────────────────────────────────────────────── function FieldGroup({ label, hint, children }: { label: string; hint?: string; children: React.ReactNode }) { return (
{children} {hint &&

{hint}

}
); } function TextInput({ value, onChange, disabled, placeholder, type = "text" }: { value: string; onChange?: (v: string) => void; disabled?: boolean; placeholder?: string; type?: string; }) { return ( onChange?.(e.target.value)} className="w-full border border-border rounded-md px-3 py-1.5 text-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-50" /> ); } function NumberInput({ value, onChange, min, max }: { value: number; onChange: (v: number) => void; min?: number; max?: number }) { return ( onChange(Number(e.target.value))} className="w-full border border-border rounded-md px-3 py-1.5 text-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary" /> ); } function Toggle({ checked, onChange, label }: { checked: boolean; onChange: (v: boolean) => void; label?: string }) { return (
{label && {label}}
); } function SaveButton({ saving, saved, onClick }: { saving: boolean; saved: boolean; onClick: () => void }) { return ( ); } function SectionCard({ title, children, action }: { title: string; children: React.ReactNode; action?: React.ReactNode }) { return (
{title} {action}
{children}
); } // ── Site Tab ─────────────────────────────────────────────────────────────────── function SiteTab() { const [form, setForm] = useState({ name: "", timezone: "", description: "" }); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); useEffect(() => { fetchSiteSettings(SITE_ID).then(setForm).catch(() => {}); }, []); const save = async () => { setSaving(true); try { const updated = await updateSiteSettings(SITE_ID, form); setForm(updated); setSaved(true); setTimeout(() => setSaved(false), 3000); } catch { /* ignore */ } finally { setSaving(false); } }; const TIMEZONES = [ "Asia/Singapore", "Asia/Tokyo", "Asia/Hong_Kong", "Asia/Kuala_Lumpur", "Europe/London", "Europe/Paris", "America/New_York", "America/Los_Angeles", "UTC", ]; return (
setForm(f => ({ ...f, name: v }))} /> setForm(f => ({ ...f, description: v }))} placeholder="Optional description" />
{[ { label: "Site ID", value: SITE_ID }, { label: "Halls", value: "2 (Hall A, Hall B)" }, { label: "Total Racks", value: "80 (40 per hall)" }, { label: "UPS Units", value: "2" }, { label: "Generators", value: "1" }, { label: "CRAC Units", value: "2" }, ].map(({ label, value }) => (

{label}

{value}

))}
); } // ── Sensors Tab ──────────────────────────────────────────────────────────────── function SensorsTab() { const [editingSensor, setEditingSensor] = useState(null); const [addOpen, setAddOpen] = useState(false); const [detailId, setDetailId] = useState(null); const [tableKey, setTableKey] = useState(0); // force re-render after save const handleSaved = () => { setAddOpen(false); setEditingSensor(null); setTableKey(k => k + 1); }; return (
Device-level sensor registry. Each device can be enabled/disabled, and protocol configuration is stored for all supported connection types. Currently only MQTT is active.
setAddOpen(true)} onEdit={s => setEditingSensor(s)} onDetail={id => setDetailId(id)} /> { setAddOpen(false); setEditingSensor(null); }} onSaved={handleSaved} /> setDetailId(null)} />
); } // ── Notifications Tab ────────────────────────────────────────────────────────── function NotificationsTab() { const [form, setForm] = useState({ critical_alarms: true, warning_alarms: true, generator_events: true, maintenance_reminders: true, webhook_url: "", email_recipients: "", }); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); useEffect(() => { fetchNotifications(SITE_ID).then(setForm).catch(() => {}); }, []); const save = async () => { setSaving(true); try { const updated = await updateNotifications(SITE_ID, form); setForm(updated); setSaved(true); setTimeout(() => setSaved(false), 3000); } catch { /* ignore */ } finally { setSaving(false); } }; const toggles: { key: keyof NotificationSettings; label: string; desc: string }[] = [ { key: "critical_alarms", label: "Critical alarms", desc: "Notify on all critical severity alarms" }, { key: "warning_alarms", label: "Warning alarms", desc: "Notify on warning severity alarms" }, { key: "generator_events", label: "Generator events", desc: "Generator start, stop, and fault events" }, { key: "maintenance_reminders", label: "Maintenance reminders", desc: "Upcoming scheduled maintenance windows" }, ]; return (
{toggles.map(({ key, label, desc }) => (

{label}

{desc}

setForm(f => ({ ...f, [key]: v }))} />
))}
setForm(f => ({ ...f, webhook_url: v }))} placeholder="https://hooks.example.com/alarm" type="url" /> setForm(f => ({ ...f, email_recipients: v }))} placeholder="ops@example.com, oncall@example.com" />
); } // ── Page Preferences Tab ─────────────────────────────────────────────────────── function PagePrefsTab() { const [form, setForm] = useState({ default_time_range_hours: 6, refresh_interval_seconds: 30 }); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); useEffect(() => { fetchPagePrefs(SITE_ID).then(setForm).catch(() => {}); }, []); const save = async () => { setSaving(true); try { const updated = await updatePagePrefs(SITE_ID, form); setForm(updated); setSaved(true); setTimeout(() => setSaved(false), 3000); } catch { /* ignore */ } finally { setSaving(false); } }; return (
Per-page configuration (visible panels, default overlays, etc.) is coming in a future update.
); } // ── Integrations Tab ─────────────────────────────────────────────────────────── function IntegrationsTab() { const [form, setForm] = useState({ mqtt_host: "mqtt", mqtt_port: 1883 }); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); useEffect(() => { fetchIntegrations(SITE_ID).then(setForm).catch(() => {}); }, []); const save = async () => { setSaving(true); try { const updated = await updateIntegrations(SITE_ID, form); setForm(updated); setSaved(true); setTimeout(() => setSaved(false), 3000); } catch { /* ignore */ } finally { setSaving(false); } }; const future = [ { name: "Slack", desc: "Post alarm notifications to a Slack channel" }, { name: "PagerDuty", desc: "Create incidents for critical alarms" }, { name: "Email SMTP", desc: "Send alarm emails via custom SMTP server" }, { name: "Syslog", desc: "Forward alarms to syslog / SIEM" }, ]; return (
setForm(f => ({ ...f, mqtt_host: v }))} /> setForm(f => ({ ...f, mqtt_port: v }))} min={1} max={65535} />
Changing the MQTT broker requires a backend restart to take effect.
{future.map(({ name, desc }) => (

{name}

{desc}

Coming soon
))}
); } // ── Page ─────────────────────────────────────────────────────────────────────── export default function SettingsPage() { return (

Settings

Site-wide configuration for Singapore DC01

Site Sensors Thresholds Notifications Page Prefs Integrations
); }