first commit

This commit is contained in:
mega 2026-03-19 11:32:17 +00:00
commit 4b98219bf7
144 changed files with 31561 additions and 0 deletions

View file

@ -0,0 +1,124 @@
"use client";
import Link from "next/link";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import { acknowledgeAlarm, type Alarm } from "@/lib/api";
import { useState } from "react";
import { ChevronRight } from "lucide-react";
interface Props {
alarms: Alarm[];
loading?: boolean;
onAcknowledge?: () => void;
onAlarmClick?: (alarm: Alarm) => void;
}
const severityStyles: Record<string, { badge: string; dot: string }> = {
critical: { badge: "bg-destructive/20 text-destructive border-destructive/30", dot: "bg-destructive" },
warning: { badge: "bg-amber-500/20 text-amber-400 border-amber-500/30", dot: "bg-amber-400" },
info: { badge: "bg-primary/20 text-primary border-primary/30", dot: "bg-primary" },
};
function timeAgo(iso: string) {
const secs = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);
if (secs < 60) return `${secs}s ago`;
if (secs < 3600) return `${Math.floor(secs / 60)}m ago`;
return `${Math.floor(secs / 3600)}h ago`;
}
export function AlarmFeed({ alarms, loading, onAcknowledge, onAlarmClick }: Props) {
const [acking, setAcking] = useState<number | null>(null);
async function handleAck(id: number) {
setAcking(id);
try {
await acknowledgeAlarm(id);
onAcknowledge?.();
} catch { /* ignore */ }
finally { setAcking(null); }
}
const activeCount = alarms.filter((a) => a.state === "active").length;
return (
<Card className="flex flex-col">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-sm font-semibold">Active Alarms</CardTitle>
<div className="flex items-center gap-2">
{!loading && (
<Badge variant={activeCount > 0 ? "destructive" : "outline"} className="text-[10px] h-4 px-1.5">
{activeCount > 0 ? `${activeCount} active` : "All clear"}
</Badge>
)}
<Link href="/alarms" className="text-[10px] text-muted-foreground hover:text-primary transition-colors">
View all
</Link>
</div>
</div>
</CardHeader>
<CardContent className="flex-1 space-y-3 overflow-y-auto max-h-72">
{loading ? (
<Skeleton className="h-80 w-full" />
) : alarms.length === 0 ? (
<div className="flex items-center justify-center h-24 text-sm text-muted-foreground">
No active alarms
</div>
) : (
alarms.map((alarm) => {
const style = severityStyles[alarm.severity] ?? severityStyles.info;
const clickable = !!onAlarmClick;
const locationLabel = [alarm.rack_id, alarm.room_id].find(Boolean);
return (
<div
key={alarm.id}
onClick={() => onAlarmClick?.(alarm)}
className={cn(
"flex gap-2.5 group rounded-md px-1 py-1 -mx-1 transition-colors",
clickable && "cursor-pointer hover:bg-muted/40"
)}
>
<div className="mt-1.5 shrink-0">
<span className={cn("block w-2 h-2 rounded-full", style.dot)} />
</div>
<div className="flex-1 min-w-0">
<p className="text-xs leading-snug text-foreground">{alarm.message}</p>
<div className="mt-0.5 flex items-center gap-1.5">
{locationLabel && (
<span className="text-[10px] text-muted-foreground font-medium">{locationLabel}</span>
)}
{locationLabel && <span className="text-[10px] text-muted-foreground/50">·</span>}
<span className="text-[10px] text-muted-foreground">{timeAgo(alarm.triggered_at)}</span>
</div>
</div>
<div className="flex flex-col items-end gap-1 shrink-0">
<Badge variant="outline" className={cn("text-[9px] h-4 px-1 uppercase tracking-wide", style.badge)}>
{alarm.severity}
</Badge>
{alarm.state === "active" && (
<Button
variant="ghost"
size="sm"
className="h-4 px-1 text-[9px] opacity-0 group-hover:opacity-100 transition-opacity"
disabled={acking === alarm.id}
onClick={(e) => { e.stopPropagation(); handleAck(alarm.id); }}
>
Ack
</Button>
)}
{clickable && (
<ChevronRight className="w-3 h-3 text-muted-foreground/40 group-hover:text-muted-foreground transition-colors mt-auto" />
)}
</div>
</div>
);
})
)}
</CardContent>
</Card>
);
}