"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 (
{label}
{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 (
onChange(!checked)}
className={cn(
"relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors",
checked ? "bg-primary" : "bg-muted",
)}
>
{label && {label} }
);
}
function SaveButton({ saving, saved, onClick }: { saving: boolean; saved: boolean; onClick: () => void }) {
return (
{saving ? (
) : saved ? (
) : (
)}
{saving ? "Saving..." : saved ? "Saved" : "Save Changes"}
);
}
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" />
setForm(f => ({ ...f, timezone: 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"
>
{TIMEZONES.map(tz => {tz} )}
{[
{ 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 }) => (
))}
);
}
// ── 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 }) => (
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 (
setForm(f => ({ ...f, default_time_range_hours: 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"
>
{[1, 3, 6, 12, 24].map(h => {h} hour{h !== 1 ? "s" : ""} )}
setForm(f => ({ ...f, refresh_interval_seconds: 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"
>
{[10, 15, 30, 60, 120].map(s => Every {s} seconds )}
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 }) => (
))}
);
}
// ── Page ───────────────────────────────────────────────────────────────────────
export default function SettingsPage() {
return (
Settings
Site-wide configuration for Singapore DC01
Site
Sensors
Thresholds
Notifications
Page Prefs
Integrations
);
}