import { useEffect, useMemo, useRef, useState } from "react"; import { LogOut, Cpu, MemoryStick, LayoutDashboard, Activity, Settings, Bell, Play, Square, RotateCcw, } from "lucide-react"; import type { Service, DockerEvent, NotificationLogEntry } from "../../shared/types"; import { useT } from "../i18n"; export type Page = "dashboard" | "monitoring" | "settings"; interface NavButtonProps { icon: React.ElementType; label: string; active: boolean; onClick: () => void; } function NavButton({ icon: Icon, label, active, onClick }: NavButtonProps) { return ( ); } function timeAgo(ts: number): string { const diff = Math.floor((Date.now() / 1000) - ts); if (diff < 60) return `${diff}s ago`; if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; return `${Math.floor(diff / 86400)}d ago`; } function eventIcon(action: string) { switch (action) { case "start": return ; case "stop": case "die": return ; case "restart": return ; default: return ; } } interface NotificationBellProps { notifications: NotificationLogEntry[]; services: Service[]; token: string; onOpenServiceDetail: (uid: string, tab?: "info" | "config" | "env" | "stats") => void; } function levelDot(level: NotificationLogEntry["level"]): string { if (level === "error") return "bg-red-400"; if (level === "warning") return "bg-amber-400"; return "bg-cyan-400"; } const LAST_READ_KEY = "df:lastReadNotifId"; function NotificationBell({ notifications, services, token, onOpenServiceDetail }: NotificationBellProps) { const { t } = useT(); const [open, setOpen] = useState(false); const [persisted, setPersisted] = useState([]); const [lastReadId, setLastReadId] = useState(() => { try { return parseInt(localStorage.getItem(LAST_READ_KEY) || "0") || 0; } catch { return 0; } }); const ref = useRef(null); // Preload from server so the bell has history immediately after a page reload useEffect(() => { const headers: Record = {}; if (token) headers["Authorization"] = `Bearer ${token}`; fetch("/api/notifications?limit=20", { headers }) .then((r) => r.ok ? r.json() : []) .then((d: NotificationLogEntry[]) => setPersisted(d)) .catch(() => {}); }, [token]); // Merge persisted + live, dedupe by id, keep newest 20 const recent = useMemo(() => { const seen = new Set(); const merged: NotificationLogEntry[] = []; for (const n of [...notifications, ...persisted]) { if (seen.has(n.id)) continue; seen.add(n.id); merged.push(n); } return merged.slice(0, 20); }, [notifications, persisted]); const unread = recent.filter((n) => n.id > lastReadId).length; useEffect(() => { const handler = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as HTMLElement)) { setOpen(false); } }; document.addEventListener("mousedown", handler); return () => document.removeEventListener("mousedown", handler); }, []); const handleToggle = () => { if (!open && recent.length > 0) { // Mark current set as read (persist) const newest = recent[0].id; setLastReadId(newest); try { localStorage.setItem(LAST_READ_KEY, String(newest)); } catch {} } setOpen((v) => !v); }; return (
{open && (
{t("header.recentNotifications")}
{recent.length === 0 ? (
{t("header.noNotifications")}
) : ( recent.map((n) => { const isKnownService = services.some((s) => s.uid === n.service); const isUnread = n.id > lastReadId; return (
{ onOpenServiceDetail(n.service, "stats"); setOpen(false); } : undefined} className={`flex items-start gap-2.5 px-3 py-2 ${isKnownService ? "cursor-pointer hover:bg-slate-700/40" : ""} transition-colors ${isUnread ? "" : "opacity-55"}`} >
{n.title}
{n.service}
{timeAgo(n.timestamp)}
); }) )}
)}
); } interface HeaderBarProps { services: Service[]; filteredServices: Service[]; token: string; totalStats: { cpu: number; mem: number }; activePage: Page; onPageChange: (page: Page) => void; events: DockerEvent[]; notifications: NotificationLogEntry[]; onOpenServiceDetail: (uid: string, tab?: "info" | "config" | "env" | "stats") => void; } export function HeaderBar({ services, filteredServices, token, totalStats, activePage, onPageChange, events, notifications, onOpenServiceDetail, }: HeaderBarProps) { const { t, lang, setLang } = useT(); return (
{/* Left: Navigation */} {/* Center: Logo */}
ContainerFlow
ContainerFlow v{__APP_VERSION__}
{/* Right: contextual controls + notifications + logout */}
{activePage === "dashboard" && ( <> {/* Total resource usage */}
{totalStats.cpu.toFixed(1)}% - {totalStats.mem >= 1024 ? `${(totalStats.mem / 1024).toFixed(1)} GB` : `${totalStats.mem.toFixed(0)} MB`}
)} {/* Language toggle */}
{/* Logout (only if auth is active) */} {token && ( )}
); }