const { useState, useEffect, useRef, useCallback, createContext, useContext } = React; // ============================================================================ // TOAST NOTIFICATION SYSTEM — replaces all alert() calls // ============================================================================ const ToastContext = createContext(null); function useToast() { return useContext(ToastContext); } function ToastProvider({ children }) { const [toasts, setToasts] = useState([]); const addToast = useCallback((message, type = "info", duration = 4000) => { const id = Date.now() + Math.random(); setToasts(prev => [...prev, { id, message, type, exiting: false }]); if (duration > 0) { setTimeout(() => { setToasts(prev => prev.map(t => t.id === id ? { ...t, exiting: true } : t)); setTimeout(() => setToasts(prev => prev.filter(t => t.id !== id)), 250); }, duration); } }, []); const dismissToast = useCallback((id) => { setToasts(prev => prev.map(t => t.id === id ? { ...t, exiting: true } : t)); setTimeout(() => setToasts(prev => prev.filter(t => t.id !== id)), 250); }, []); return ( {children}
{toasts.map(t => { const colors = { success: "bg-green-800 border-green-600 text-green-100", error: "bg-red-900 border-red-700 text-red-100", warning: "bg-amber-900 border-amber-600 text-amber-100", info: "bg-gray-800 border-gray-600 text-gray-100", }; return (
{t.message}
); })}
); } // ============================================================================ // CONFIRM / PROMPT MODALS — replaces window.confirm() and window.prompt() // ============================================================================ function ConfirmModal({ open, title, message, onConfirm, onCancel, confirmText = "Confirm", danger = false }) { const btnRef = useRef(null); useEffect(() => { if (open && btnRef.current) btnRef.current.focus(); }, [open]); if (!open) return null; return (

{title}

{message}

); } function PromptModal({ open, title, message, placeholder, onSubmit, onCancel, required = false }) { const [value, setValue] = useState(""); const inputRef = useRef(null); useEffect(() => { if (open) { setValue(""); setTimeout(() => inputRef.current?.focus(), 50); } }, [open]); if (!open) return null; const handleSubmit = () => { if (required && !value.trim()) return; onSubmit(value); }; return (

{title}

{message}

setValue(e.target.value)} onKeyDown={e => e.key === "Enter" && handleSubmit()} placeholder={placeholder || ""} className="w-full bg-gray-700 text-white rounded px-3 py-2 text-sm border border-gray-600 focus:border-amber-500 focus:outline-none mb-4" />
); } // ============================================================================ // COOKIE HELPERS — session persistence across page refreshes // ============================================================================ function setCookie(name, value, hours) { const expires = new Date(Date.now() + hours * 3600000).toUTCString(); document.cookie = name + "=" + encodeURIComponent(JSON.stringify(value)) + ";expires=" + expires + ";path=/;SameSite=Strict"; } function getCookie(name) { const match = document.cookie.match(new RegExp("(^| )" + name + "=([^;]+)")); if (!match) return null; try { return JSON.parse(decodeURIComponent(match[2])); } catch (e) { return null; } } function deleteCookie(name) { document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;SameSite=Strict"; } // ============================================================================ // TYPING INDICATOR — animated dots shown while waiting for LLM response // ============================================================================ function TypingIndicator({ visible }) { if (!visible) return null; return (
); } // ============================================================================ // TOOL LINE STRIPPING — hide [Executing: ...] lines for non-admin users // ============================================================================ function stripToolLines(content) { if (!content) return ""; // Remove lines like [Executing: ...], [Tool completed ...], [Export started: ...] return content.replace(/^\[(?:Executing|Tool completed|Export started)[^\]]*\].*$/gm, "").trimStart(); } // Markdown rendering for chat messages (sanitized with DOMPurify) marked.setOptions({ breaks: true, gfm: true }); // DOMPurify config — allow safe HTML subset const PURIFY_CONFIG = typeof DOMPurify !== "undefined" ? { ALLOWED_TAGS: ["p", "br", "strong", "em", "b", "i", "code", "pre", "ul", "ol", "li", "a", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "table", "thead", "tbody", "tr", "th", "td", "hr", "div", "span", "sup", "sub", "del", "img"], ALLOWED_ATTR: ["href", "title", "class", "target", "rel", "src", "alt", "width", "height"], ADD_ATTR: ["target"], } : {}; function MarkdownMessage({ content, className }) { const containerRef = useRef(null); const html = React.useMemo(() => { if (!content) return ""; try { let parsed = marked.parse(content) .replace(//g, '
') .replace(/<\/table>/g, '
'); // Add copy button to code blocks parsed = parsed.replace(/
/g, '
');
      // Sanitize HTML
      if (typeof DOMPurify !== "undefined") {
        return DOMPurify.sanitize(parsed, { ...PURIFY_CONFIG, ALLOWED_TAGS: [...PURIFY_CONFIG.ALLOWED_TAGS, "button"] });
      }
      return parsed;
    } catch (e) {
      return content.replace(/&/g, "&").replace(//g, ">").replace(/\n/g, "
"); } }, [content]); // Attach copy handlers to code block buttons after render useEffect(() => { if (!containerRef.current) return; const buttons = containerRef.current.querySelectorAll(".code-copy-btn"); buttons.forEach(btn => { btn.onclick = () => { const pre = btn.closest("pre"); if (!pre) return; const code = pre.querySelector("code"); const text = (code || pre).textContent.replace(/^Copy\n?/, ""); navigator.clipboard.writeText(text).then(() => { btn.textContent = "Copied!"; setTimeout(() => { btn.textContent = "Copy"; }, 1500); }).catch(() => {}); }; }); }, [html]); return React.createElement("div", { ref: containerRef, className: "markdown-message " + (className || ""), dangerouslySetInnerHTML: { __html: html } }); } // ============================================================================ // MOCK DATA // ============================================================================ const MOCK_FLEET = [ { id: "fleet-001", name: "Production (US-East)", agents: 24, status: "healthy", lastSeen: "now", utilization: 87 }, { id: "fleet-002", name: "Staging (US-West)", agents: 8, status: "healthy", lastSeen: "1m", utilization: 42 }, { id: "fleet-003", name: "Development", agents: 12, status: "degraded", lastSeen: "3m", utilization: 56 }, ]; const MOCK_ALERTS = [ { id: "ALR-1001", severity: "critical", message: "Model inference latency exceeded 2s on fleet-001", timestamp: "14:32", agent: "ml-node-07", resolved: false }, { id: "ALR-1002", severity: "warning", message: "Agent node fleet-002-04 memory usage at 89%", timestamp: "14:15", agent: "fleet-002-04", resolved: false }, { id: "ALR-1003", severity: "info", message: "Daily backup completed successfully", timestamp: "13:45", agent: "backup-job-01", resolved: true }, ]; const MOCK_COST = [ { period: "Jan", compute: 45000, storage: 8200, network: 3100 }, { period: "Feb", compute: 52000, storage: 9100, network: 3400 }, { period: "Mar", compute: 48500, storage: 8900, network: 3200 }, ]; const MOCK_TASK_LOG = [ { id: "TASK-2847", status: "completed", title: "Process order batch #521", agent: "order-processor-03", duration: "2m 14s", timestamp: "14:28" }, { id: "TASK-2846", status: "in_progress", title: "Analyze customer feedback", agent: "sentiment-analyzer-01", duration: "4m 32s", timestamp: "14:12" }, { id: "TASK-2845", status: "queued", title: "Generate daily report", agent: "report-gen-02", duration: "pending", timestamp: "14:05" }, ]; const MOCK_PROJECTS = [ { id: "PRJ-88", name: "Q1 Automation Initiative", status: "in_progress", progress: 65, owner: "alex.kim", team: 7 }, { id: "PRJ-87", name: "Model Fine-tuning Sprint", status: "in_progress", progress: 42, owner: "jordan.lee", team: 5 }, { id: "PRJ-86", name: "Infrastructure Upgrade", status: "on_hold", progress: 30, owner: "alex.kim", team: 3 }, ]; const MOCK_AGENTS = [ { id: "agent-001", name: "Order Processor", tasks: 247, status: "idle", model: "claude-opus-4", efficiency: 94 }, { id: "agent-002", name: "Sentiment Analyzer", tasks: 183, status: "busy", model: "claude-sonnet-4", efficiency: 88 }, { id: "agent-003", name: "Report Generator", tasks: 92, status: "idle", model: "claude-haiku-4", efficiency: 96 }, ]; // Mock data removed — business tabs now use live analytics via chat // ============================================================================ // UTILITY COMPONENTS // ============================================================================ function StatusDot({ status }) { const colorMap = { healthy: "bg-green-500", degraded: "bg-amber-500", critical: "bg-red-500", idle: "bg-teal-500", busy: "bg-blue-500", active: "bg-green-500", inactive: "bg-gray-400", complete: "bg-green-500", in_progress: "bg-amber-500", pending: "bg-gray-400", queued: "bg-blue-500", in_service: "bg-green-500", awaiting_parts: "bg-amber-500", shipped_vendor: "bg-blue-500", complete_service: "bg-green-500", processing: "bg-amber-500", quoted: "bg-gray-400", invoiced: "bg-blue-500", tagging: "bg-amber-500", qc_pass: "bg-green-500", qc_fail: "bg-red-500", config: "bg-amber-500", proposal: "bg-gray-400", negotiation: "bg-amber-500", discovery: "bg-blue-500", closed_won: "bg-green-500", discrepancy: "bg-red-500", }; return
; } function MetricBar({ label, value, total, color = "bg-amber-500" }) { const percentage = Math.round((value / total) * 100); return (
{label} {percentage}%
); } function SeverityBadge({ severity }) { const classMap = { critical: "bg-red-900 text-red-200", warning: "bg-amber-900 text-amber-200", info: "bg-blue-900 text-blue-200", }; return ( {severity.charAt(0).toUpperCase() + severity.slice(1)} ); } function ModelBadge({ model }) { const colors = { "claude-opus-4": "bg-violet-900 text-violet-200", "claude-sonnet-4": "bg-purple-900 text-purple-200", "claude-haiku-4": "bg-teal-900 text-teal-200", }; return ( {model} ); } function TaskStatusIcon({ status }) { const icons = { completed: "✓", in_progress: "◐", queued: "→", }; const colors = { completed: "text-green-400", in_progress: "text-amber-400", queued: "text-blue-400", }; return {icons[status] || "·"}; } // ============================================================================ // HELPER FUNCTIONS // ============================================================================ function hasRole(user, ...roles) { if (!user?.roles) return false; if (user.roles.includes("admin")) return true; return roles.some(r => user.roles.includes(r)); } // ============================================================================ // MAIN COMPONENTS // ============================================================================ // ============================================================================ // METRIC PARSERS — extract numbers from MCP tool output // ============================================================================ function parseTopCPU(topOutput) { if (!topOutput) return 0; const match = topOutput.match(/%Cpu\(s\):\s+([\d.]+)\s+us,\s+([\d.]+)\s+sy/); if (match) return Math.round(parseFloat(match[1]) + parseFloat(match[2])); // Fallback: try to find idle percentage const idleMatch = topOutput.match(/([\d.]+)\s+id/); if (idleMatch) return Math.round(100 - parseFloat(idleMatch[1])); return 0; } function parseFreeMemory(freeOutput) { if (!freeOutput) return { total: "0", used: "0", percent: 0 }; const lines = freeOutput.split("\n"); const memLine = lines.find((l) => l.trim().startsWith("Mem:")); if (!memLine) return { total: "0", used: "0", percent: 0 }; const parts = memLine.trim().split(/\s+/); // Normalize values to bytes to handle mixed units (e.g., 31G vs 5200M) const toBytes = (s) => { const n = parseFloat(s) || 0; const u = (s || "").slice(-1).toUpperCase(); if (u === "T") return n * 1e12; if (u === "G") return n * 1e9; if (u === "M") return n * 1e6; if (u === "K") return n * 1e3; return n; }; const total = toBytes(parts[1]) || 1; const used = toBytes(parts[2]) || 0; return { total: parts[1], used: parts[2], percent: Math.round((used / total) * 100) }; } function parseDfDisk(dfOutput) { if (!dfOutput) return { total: "0", used: "0", percent: 0 }; const lines = dfOutput.split("\n"); const rootLine = lines.find((l) => l.trim().endsWith(" /") || l.includes("/$")); if (!rootLine) { // fallback: first non-header line const dataLine = lines.find((l, i) => i > 0 && l.trim().length > 0); if (!dataLine) return { total: "0", used: "0", percent: 0 }; const parts = dataLine.trim().split(/\s+/); return { total: parts[1] || "0", used: parts[2] || "0", percent: parseInt(parts[4]) || 0 }; } const parts = rootLine.trim().split(/\s+/); return { total: parts[1] || "0", used: parts[2] || "0", percent: parseInt(parts[4]) || 0 }; } function parseGPUMetrics(gpuString) { if (!gpuString) return null; const parts = gpuString.split(",").map((s) => s.trim()); return { utilization: parseInt(parts[0]) || 0, vramUsed: parseInt(parts[1]) || 0, vramTotal: parseInt(parts[2]) || 0, temperature: parseInt(parts[3]) || 0, }; } function parseServiceList(servicesText) { if (!servicesText) return []; return servicesText.split("\n") .filter((line) => line.includes(".service")) .map((line) => { const parts = line.trim().split(/\s+/); const name = (parts[0] || "").replace(".service", ""); return { name, loadState: parts[1] || "unknown", activeState: parts[2] || "unknown", subState: parts[3] || "", description: parts.slice(4).join(" "), }; }) .filter((s) => s.name); } function parseMetricsResult(resultData) { // MCP tool returns either a JSON dict or a text array let data = {}; if (typeof resultData === "string") { try { data = JSON.parse(resultData); } catch { return { cpu: 0, memory: { percent: 0 }, disk: { percent: 0 }, gpu: null }; } } else if (Array.isArray(resultData)) { // Try parsing first element as JSON try { data = JSON.parse(resultData[0]); } catch { data = { raw: resultData.join("\n") }; } } else if (typeof resultData === "object") { data = resultData; } return { cpu: parseTopCPU(data.cpu || ""), memory: parseFreeMemory(data.memory || ""), disk: parseDfDisk(data.disk || ""), gpu: parseGPUMetrics(data.gpu), network: data.network_sockets || "", hostname: data.hostname || "", }; } // ============================================================================ // AGENTS TAB COMPONENTS // ============================================================================ function MetricGauge({ label, value, thresholds = { warn: 70, danger: 90 } }) { const color = value > thresholds.danger ? "text-red-400" : value > thresholds.warn ? "text-amber-400" : "text-green-400"; const bgColor = value > thresholds.danger ? "bg-red-500" : value > thresholds.warn ? "bg-amber-500" : "bg-green-500"; return (
{value}%
{label}
); } function TemperatureIndicator({ temp }) { const color = temp > 80 ? "text-red-400" : temp > 60 ? "text-amber-400" : "text-green-400"; return {temp}°C; } const PROFILE_TO_AGENT_TYPE_MAP = { "ollama-only": "monitoring", "os-coding": "coding", "claude-code": "coding", }; function AgentTypeBadge({ agentType }) { const colors = { monitoring: "bg-gray-700 text-gray-200", inference: "bg-blue-900 text-blue-200", coding: "bg-violet-900 text-violet-200", triage: "bg-amber-900 text-amber-200", analytics: "bg-emerald-900 text-emerald-200", "analytics-secure": "bg-red-900 text-red-200", }; const labels = { monitoring: "monitoring", inference: "inference", coding: "coding", triage: "triage", analytics: "analytics", "analytics-secure": "analytics-secure", }; return ( {labels[agentType] || agentType || "unknown"} ); } // ── Tab Icon Library ──────────────────────────────────────────────── // Store as path data arrays — rendered in TabIcon component const TAB_ICON_PATHS = { truck: [{t:"path",d:"M1 3h15v13H1z"},{t:"path",d:"M16 8h4l3 3v5h-7V8z"},{t:"circle",cx:5.5,cy:18.5,r:2.5},{t:"circle",cx:18.5,cy:18.5,r:2.5}], package: [{t:"path",d:"M16.5 9.4l-9-5.19M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"},{t:"path",d:"M3.27 6.96L12 12.01l8.73-5.05M12 22.08V12"}], warehouse: [{t:"path",d:"M22 8.35V20a2 2 0 01-2 2H4a2 2 0 01-2-2V8.35A2 2 0 013.26 6.5l8-3.2a2 2 0 011.48 0l8 3.2A2 2 0 0122 8.35z"},{t:"path",d:"M6 18h12M6 14h12"}], wrench: [{t:"path",d:"M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94l-6.91 6.91a2.12 2.12 0 01-3-3l6.91-6.91a6 6 0 017.94-7.94l-3.76 3.76z"}], settings: [{t:"circle",cx:12,cy:12,r:3},{t:"path",d:"M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z"}], cog: [{t:"circle",cx:12,cy:12,r:8},{t:"circle",cx:12,cy:12,r:2},{t:"path",d:"M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"}], "chart-bar": [{t:"path",d:"M12 20V10M18 20V4M6 20v-4"}], "chart-line": [{t:"path",d:"M23 6l-9.5 9.5-5-5L1 18"},{t:"path",d:"M17 6h6v6"}], "chart-pie": [{t:"path",d:"M21.21 15.89A10 10 0 118 2.83"},{t:"path",d:"M22 12A10 10 0 0012 2v10z"}], users: [{t:"path",d:"M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"},{t:"circle",cx:9,cy:7,r:4},{t:"path",d:"M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75"}], user: [{t:"path",d:"M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"},{t:"circle",cx:12,cy:7,r:4}], briefcase: [{t:"rect",x:2,y:7,width:20,height:14,rx:2},{t:"path",d:"M16 21V5a2 2 0 00-2-2h-4a2 2 0 00-2 2v16"}], dollar: [{t:"path",d:"M12 1v22M17 5H9.5a3.5 3.5 0 000 7h5a3.5 3.5 0 010 7H6"}], "credit-card": [{t:"rect",x:1,y:4,width:22,height:16,rx:2},{t:"path",d:"M1 10h22"}], receipt: [{t:"path",d:"M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1-2-1z"},{t:"path",d:"M8 10h8M8 14h4"}], shield: [{t:"path",d:"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"}], lock: [{t:"rect",x:3,y:11,width:18,height:11,rx:2},{t:"path",d:"M7 11V7a5 5 0 0110 0v4"}], key: [{t:"path",d:"M21 2l-2 2m-7.61 7.61a5.5 5.5 0 11-7.778 7.778 5.5 5.5 0 017.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"}], server: [{t:"rect",x:2,y:2,width:20,height:8,rx:2},{t:"rect",x:2,y:14,width:20,height:8,rx:2},{t:"path",d:"M6 6h.01M6 18h.01"}], database: [{t:"ellipse",cx:12,cy:5,rx:9,ry:3},{t:"path",d:"M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"},{t:"path",d:"M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"}], globe: [{t:"circle",cx:12,cy:12,r:10},{t:"path",d:"M2 12h20M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"}], clipboard: [{t:"path",d:"M16 4h2a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2h2"},{t:"rect",x:8,y:2,width:8,height:4,rx:1}], folder: [{t:"path",d:"M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"}], search: [{t:"circle",cx:11,cy:11,r:8},{t:"path",d:"M21 21l-4.35-4.35"}], bell: [{t:"path",d:"M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"},{t:"path",d:"M13.73 21a2 2 0 01-3.46 0"}], lightning: [{t:"path",d:"M13 2L3 14h9l-1 8 10-12h-9l1-8z"}], refresh: [{t:"path",d:"M23 4v6h-6M1 20v-6h6"},{t:"path",d:"M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"}], home: [{t:"path",d:"M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"},{t:"path",d:"M9 22V12h6v10"}], "brain-chip": [{t:"rect",x:5,y:5,width:14,height:14,rx:2},{t:"path",d:"M9 5V2M15 5V2M9 19v3M15 19v3M5 9H2M5 15H2M19 9h3M19 15h3"},{t:"circle",cx:9,cy:5,r:0.5},{t:"circle",cx:15,cy:5,r:0.5},{t:"circle",cx:9,cy:19,r:0.5},{t:"circle",cx:15,cy:19,r:0.5},{t:"circle",cx:5,cy:9,r:0.5},{t:"circle",cx:5,cy:15,r:0.5},{t:"circle",cx:19,cy:9,r:0.5},{t:"circle",cx:19,cy:15,r:0.5},{t:"path",d:"M9.5 8.5v7M14.5 8.5v7M9.5 12h5"}], star: [{t:"path",d:"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"}], flag: [{t:"path",d:"M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"},{t:"path",d:"M4 22v-7"}], inbox: [{t:"path",d:"M22 12h-6l-2 3H10l-2-3H2"},{t:"path",d:"M5.45 5.11L2 12v6a2 2 0 002 2h16a2 2 0 002-2v-6l-3.45-6.89A2 2 0 0016.76 4H7.24a2 2 0 00-1.79 1.11z"}], layers: [{t:"path",d:"M12 2L2 7l10 5 10-5-10-5z"},{t:"path",d:"M2 17l10 5 10-5"},{t:"path",d:"M2 12l10 5 10-5"}], monitor: [{t:"rect",x:2,y:3,width:20,height:14,rx:2},{t:"path",d:"M8 21h8M12 17v4"}], "file-text": [{t:"path",d:"M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"},{t:"path",d:"M14 2v6h6M16 13H8M16 17H8M10 9H8"}], terminal: [{t:"path",d:"M4 17l6-5-6-5"},{t:"path",d:"M12 19h8"}], }; function ICPLogo({ className = "h-10", invert = true }) { return ICP; } function TabIcon({ name, className = "w-4 h-4" }) { const shapes = TAB_ICON_PATHS[name]; if (!shapes) return null; return ( {shapes.map((s, i) => { if (s.t === "path") return ; if (s.t === "circle") return ; if (s.t === "rect") return ; if (s.t === "ellipse") return ; return null; })} ); } function HostMetricCard({ host, metrics, sessionToken, onHealthCheck }) { const m = metrics || { cpu: 0, memory: { percent: 0 }, disk: { percent: 0 }, gpu: null }; const [checking, setChecking] = useState(false); const [showFindings, setShowFindings] = useState(false); const [findings, setFindings] = useState([]); const [findingsCount, setFindingsCount] = useState(0); const runHealthCheck = async () => { setChecking(true); try { const res = await fetch("/api/console/exec", { method: "POST", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, body: JSON.stringify({ host: host.id, tool: "get_host_info", args: {} }), }); if (res.ok) { const data = await res.json(); const result = data.results?.[host.id]; if (onHealthCheck) onHealthCheck(host.id, result); } } catch {} finally { setChecking(false); } }; // Poll findings count for this agent useEffect(() => { const poll = async () => { try { const res = await fetch(`/api/findings?source_host=${host.id}&status=active&limit=50`, { headers: { "X-Session-Token": sessionToken }, }); if (res.ok) { const data = await res.json(); setFindingsCount(data.total || 0); if (showFindings) setFindings(data.findings || []); } } catch {} }; poll(); const iv = setInterval(poll, 30000); return () => clearInterval(iv); }, [host.id, sessionToken, showFindings]); const handleDismiss = async (id) => { await fetch(`/api/findings/${id}/dismiss`, { method: "POST", headers: { "X-Session-Token": sessionToken } }); }; const handleStar = async (id) => { await fetch(`/api/findings/${id}/star`, { method: "POST", headers: { "X-Session-Token": sessionToken } }); }; const handleResolve = async (id) => { await fetch(`/api/findings/${id}/resolve`, { method: "POST", headers: { "X-Session-Token": sessionToken } }); }; return (
0 ? "border-amber-600" : "border-gray-700 hover:border-amber-500"}`}>

{host.name}

{host.ip && {host.ip}}
{m.gpu && }
{host.uptime || ""} {(host.model || host.ollama_model) && {host.model || host.ollama_model}}
{/* Inline findings panel */} {showFindings && (
{findings.length === 0 ? (

No active findings for this agent.

) : (
{findings.map((f) => ( ))}
)}
)}
); } function FleetSubTab({ hosts, hostMetrics, sessionToken }) { const [healthDetail, setHealthDetail] = useState(null); const handleHealthCheck = (hostId, result) => { setHealthDetail({ hostId, result, timestamp: new Date() }); }; return (
{hosts.map((host) => ( ))}
{hosts.length === 0 && (
No agents registered in fleet
)} {healthDetail && (

Health Check: {healthDetail.hostId}

            {healthDetail.result?.ok
              ? (Array.isArray(healthDetail.result.result) ? healthDetail.result.result.join("\n") : JSON.stringify(healthDetail.result.result, null, 2))
              : `Error: ${healthDetail.result?.error || "Unknown error"}`}
          
)}
); } function GPUSubTab({ hosts, hostMetrics }) { const gpuHosts = hosts.filter((h) => h.has_gpu || hostMetrics[h.id]?.gpu); if (gpuHosts.length === 0) { return
No GPU agents in fleet
; } return (
{gpuHosts.map((host) => { const gpu = hostMetrics[host.id]?.gpu; return (

{host.name}

GPU
{gpu ? (
{gpu.vramUsed}
/ {gpu.vramTotal} MiB VRAM
Temp
) : (
Waiting for GPU metrics...
)}
); })}
); } function ServicesSubTab({ hosts, hostServices, sessionToken, user }) { const [actionResult, setActionResult] = useState(null); const [acting, setActing] = useState(null); const handleServiceAction = async (hostname, serviceName, action) => { setActing(`${hostname}:${serviceName}:${action}`); setActionResult(null); try { const res = await fetch("/api/console/exec", { method: "POST", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, body: JSON.stringify({ host: hostname, tool: "manage_service", args: { name: serviceName, action } }), }); if (res.ok) { const data = await res.json(); const result = data.results?.[hostname]; setActionResult({ hostname, service: serviceName, action, ok: result?.ok, message: result?.ok ? `${action} successful` : (result?.error || "Failed") }); } } catch (e) { setActionResult({ hostname, service: serviceName, action, ok: false, message: e.message }); } finally { setActing(null); } }; const canManage = hasRole(user, "fleet_ops", "admin"); return (
{actionResult && (
[{actionResult.hostname}] {actionResult.service}: {actionResult.message}
)} {hosts.map((host) => { const services = hostServices[host.id] || []; return (

{host.name}

{services.length} services
{services.length > 0 ? (
{services.map((svc) => { const isRunning = svc.activeState === "running"; const isFailed = svc.activeState === "failed"; const statusColor = isFailed ? "border-red-600 bg-red-900/20" : isRunning ? "border-gray-600" : "border-gray-700 bg-gray-900/30"; const isActing = acting === `${host.id}:${svc.name}:restart` || acting === `${host.id}:${svc.name}:stop`; return (
{svc.name}
{canManage && (
{isRunning && ( )}
)}
); })}
) : (
Loading services...
)}
); })}
); } function AlertsSubTab({ alerts, sessionToken, user }) { const canAck = hasRole(user, "fleet_ops", "admin"); const handleAcknowledge = async (alertId) => { try { await fetch(`/api/alerts/${alertId}/acknowledge`, { method: "POST", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, }); } catch {} }; return (
{alerts.length === 0 && (
No alerts
)} {alerts.map((alert) => (

{alert.message}

{alert.source_host} • {new Date(alert.timestamp).toLocaleString()} {alert.acknowledged && ✓ {alert.acknowledged_by}}

{canAck && !alert.acknowledged && ( )}
))}
); } function ActivitySubTab({ hosts, agentStatus, sessionToken, user, onChatAgent, distVersion }) { const canManage = hasRole(user, "fleet_ops", "admin"); const isAdmin = hasRole(user, "admin"); const [acting, setActing] = useState(null); const toast = useToast(); const handleRestartAgent = async (hostname, service) => { setActing(`${hostname}:${service}`); try { await fetch("/api/console/exec", { method: "POST", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, body: JSON.stringify({ host: hostname, tool: "manage_service", args: { name: service, action: "restart" } }), }); } catch {} finally { setActing(null); } }; const handleForceUpdate = async (hostname) => { setActing(`${hostname}:update`); try { const res = await fetch(`/api/agent/${hostname}/update`, { method: "POST", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, }); if (res.ok) { toast.success(`Update triggered for ${hostname} — agent restarting`); // Keep button disabled for 30s while agent downloads, deploys, and restarts setTimeout(() => setActing((prev) => prev === `${hostname}:update` ? null : prev), 30000); return; } else { const err = await res.json().catch(() => ({})); toast.error(err.detail || `Update failed: ${res.status}`); } } catch (e) { toast.error(`Update failed: ${e.message}`); } setActing(null); }; // Merge host info with agent status const agents = hosts.map((h) => { const status = (agentStatus || []).find((a) => a.hostname === h.id); return { ...h, agentOnline: status?.status || "unknown", lastHeartbeat: status?.last_heartbeat, version: status?.version || h.version || "unknown" }; }); return (
{canManage && } {agents.map((agent) => ( {canManage && ( )} ))}
Agent Type Provider / Model Version Status Last HeartbeatActions
{agent.name} {agent.hostname && agent.hostname !== agent.name && {agent.hostname}} {agent.ip && {agent.ip}}
{agent.provider ? `${agent.provider}/${agent.model || "?"}` : agent.ollama_model || "—"} {agent.version || "—"} {agent.agentOnline} {agent.lastHeartbeat ? new Date(agent.lastHeartbeat).toLocaleTimeString() : "—"}
{onChatAgent && ( )} {distVersion && agent.version && agent.version !== "unknown" && distVersion !== "0.0.0" && agent.version < distVersion && ( )}
); } // ── Direct Agent Chat Panel ───────────────────────────────── function AgentChatPanel({ hostname, agentType, provider, model, sessionToken, onClose }) { const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [isLoading, setIsLoading] = useState(false); const [mode, setMode] = useState("agent"); const [elapsed, setElapsed] = useState(0); const abortRef = useRef(null); const scrollRef = useRef(null); const inputRef = useRef(null); const elapsedRef = useRef(null); // Resizable panel const [panelWidth, setPanelWidth] = useState(() => { try { return parseInt(localStorage.getItem("hal_agent_chat_width")) || 384; } catch { return 384; } }); const resizingRef = useRef(false); useEffect(() => { const onMove = (e) => { if (!resizingRef.current) return; const newW = Math.max(320, Math.min(800, window.innerWidth - e.clientX)); setPanelWidth(newW); }; const onUp = () => { if (resizingRef.current) { resizingRef.current = false; document.body.style.cursor = ""; document.body.style.userSelect = ""; try { localStorage.setItem("hal_agent_chat_width", String(panelWidth)); } catch {} } }; window.addEventListener("mousemove", onMove); window.addEventListener("mouseup", onUp); return () => { window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseup", onUp); }; }, [panelWidth]); // Elapsed time counter while loading useEffect(() => { if (isLoading) { setElapsed(0); elapsedRef.current = setInterval(() => setElapsed((e) => e + 1), 1000); } else { if (elapsedRef.current) clearInterval(elapsedRef.current); setElapsed(0); } return () => { if (elapsedRef.current) clearInterval(elapsedRef.current); }; }, [isLoading]); // Auto-scroll to bottom on new messages useEffect(() => { if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight; }, [messages]); // Focus input on mount and when agent changes useEffect(() => { if (inputRef.current) inputRef.current.focus(); setMessages([]); setInput(""); setIsLoading(false); }, [hostname]); const handleSend = async (e) => { e.preventDefault(); const text = input.trim(); if (!text || isLoading) return; const userMsg = { id: Date.now(), role: "user", content: text }; const assistantId = Date.now() + 1; const assistantMsg = { id: assistantId, role: "assistant", content: "" }; setMessages((prev) => [...prev, userMsg, assistantMsg]); setInput(""); setIsLoading(true); const controller = new AbortController(); abortRef.current = controller; // Build conversation for API (all messages in order) const apiMessages = [...messages, userMsg].map((m) => ({ role: m.role, content: m.content })); try { const res = await fetch(`/api/agent/${hostname}/chat`, { method: "POST", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, body: JSON.stringify({ messages: apiMessages, mode }), signal: controller.signal, }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.detail || `Error: ${res.status}`); } const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() || ""; for (const line of lines) { if (!line.startsWith("data: ")) continue; try { const data = JSON.parse(line.slice(6)); if (data.type === "chunk" || data.type === "error") { setMessages((prev) => prev.map((msg) => msg.id === assistantId ? { ...msg, content: msg.content + data.content } : msg ) ); } else if (data.type === "done") { break; } } catch {} } } } catch (err) { if (err.name !== "AbortError") { setMessages((prev) => prev.map((msg) => msg.id === assistantId ? { ...msg, content: msg.content || `*Error: ${err.message}*` } : msg ) ); } } finally { setIsLoading(false); abortRef.current = null; setTimeout(() => inputRef.current?.focus(), 50); } }; const handleStop = () => { if (abortRef.current) abortRef.current.abort(); }; const handleClear = () => { if (abortRef.current) abortRef.current.abort(); setMessages([]); setIsLoading(false); }; return (
{/* Resize handle — desktop only */}
{ e.preventDefault(); resizingRef.current = true; document.body.style.cursor = "col-resize"; document.body.style.userSelect = "none"; }} /> {/* Header */}
{hostname}
{provider}/{model}
{/* Messages */}
{messages.length === 0 && (

Direct chat with {hostname}

{mode === "agent" ? "Agent prompt active" : "Raw mode — no system prompt"}

)} {messages.filter((msg) => !(msg.role === "assistant" && !msg.content && isLoading)).map((msg) => (
{msg.role === "assistant" && msg.content ? ( ) : ( {msg.content} )}
))} {isLoading && (
{elapsed > 0 && ( {elapsed}s{elapsed > 10 && provider === "ollama" ? " · CPU inference may be slow" : ""} )}
)}
{/* Input */}
setInput(e.target.value)} placeholder={`Chat with ${hostname}...`} className="flex-1 bg-gray-800 text-gray-200 rounded px-3 py-2 text-sm border border-gray-600 focus:border-amber-500 focus:outline-none" disabled={isLoading} autoComplete="off" /> {isLoading ? ( ) : ( )}
); } function FileAttachmentInput({ sessionToken, files, onFilesChange, disabled }) { const [uploading, setUploading] = useState(false); const fileInputRef = useRef(null); const toast = useToast(); const ALLOWED_EXT = [".csv", ".json", ".xml", ".txt", ".tsv", ".yaml", ".yml", ".log"]; const MAX_SIZE = 50 * 1024 * 1024; const handleFileSelect = async (e) => { const selected = Array.from(e.target.files || []); if (!selected.length) return; const valid = []; for (const file of selected) { const ext = "." + file.name.split(".").pop().toLowerCase(); if (!ALLOWED_EXT.includes(ext)) { toast("File type " + ext + " not allowed. Allowed: " + ALLOWED_EXT.join(", "), "warning"); continue; } if (file.size > MAX_SIZE) { toast(file.name + " exceeds 50MB limit.", "warning"); continue; } valid.push(file); } if (!valid.length) return; setUploading(true); const uploaded = [...files]; for (const file of valid) { const formData = new FormData(); formData.append("file", file); try { const res = await fetch("/api/chat/upload", { method: "POST", headers: { "X-Session-Token": sessionToken }, body: formData, }); if (!res.ok) { const err = await res.json().catch(() => ({})); toast("Upload failed: " + file.name + " — " + (err.detail || res.status), "error"); continue; } const data = await res.json(); uploaded.push({ file_id: data.file_id, filename: data.filename, size: data.size }); } catch (err) { toast("Upload error: " + file.name + " — " + err.message, "error"); } } onFilesChange(uploaded); setUploading(false); if (fileInputRef.current) fileInputRef.current.value = ""; }; const removeFile = (file_id) => { onFilesChange(files.filter((f) => f.file_id !== file_id)); }; const clearAll = () => { onFilesChange([]); }; const formatSize = (bytes) => { if (bytes < 1024) return bytes + "B"; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(0) + "KB"; return (bytes / (1024 * 1024)).toFixed(1) + "MB"; }; return (
{files.length > 0 && (
{files.map((f) => ( {f.filename} ({formatSize(f.size)}) ))}
)}
); } // ── WorkflowCard Component ───────────────────────────────── function WorkflowCard({ workflow, sessionToken, user }) { const [wf, setWf] = React.useState(workflow); const [approving, setApproving] = React.useState(false); const [promptModal, setPromptModal] = React.useState(null); // {type, stepIndex} const toast = useToast(); const stepStatusColors = { completed: "bg-green-900/60 text-green-200", approved: "bg-green-900/60 text-green-200", in_progress: "bg-amber-900/60 text-amber-200", awaiting_approval: "bg-violet-900/60 text-violet-200", pending: "bg-gray-700/60 text-gray-400", failed: "bg-red-900/60 text-red-200", rejected: "bg-red-900/60 text-red-200", skipped: "bg-gray-700/60 text-gray-500", }; const stepDotColors = { completed: "bg-green-500", approved: "bg-green-500", in_progress: "bg-amber-500 animate-pulse", awaiting_approval: "bg-violet-500 animate-pulse", pending: "bg-gray-600", failed: "bg-red-500", rejected: "bg-red-500", skipped: "bg-gray-600", }; const wfStatusColors = { in_progress: "bg-amber-900/60 text-amber-200", completed: "bg-green-900/60 text-green-200", failed: "bg-red-900/60 text-red-200", cancelled: "bg-gray-700/60 text-gray-400", pending: "bg-blue-900/60 text-blue-200", }; const canApproveStep = (step) => { if (step.status !== "awaiting_approval") return false; const userRoles = user?.roles || []; return userRoles.includes(step.required_role) || userRoles.includes("admin"); }; const doApprove = async (stepIndex, comment) => { setApproving(true); try { const res = await fetch(`/api/workflows/${wf.id}/steps/${stepIndex}/approve`, { method: "POST", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, body: JSON.stringify({ comment: comment || "" }), }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.detail || "Approval failed"); } const data = await res.json(); if (data.workflow) setWf(data.workflow); toast("Step approved successfully", "success"); } catch (err) { toast("Failed to approve: " + err.message, "error"); } finally { setApproving(false); } }; const doReject = async (stepIndex, reason) => { setApproving(true); try { const res = await fetch(`/api/workflows/${wf.id}/steps/${stepIndex}/reject`, { method: "POST", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, body: JSON.stringify({ comment: reason }), }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.detail || "Rejection failed"); } const data = await res.json(); if (data.workflow) setWf(data.workflow); toast("Step rejected", "warning"); } catch (err) { toast("Failed to reject: " + err.message, "error"); } finally { setApproving(false); } }; const handleApprove = (stepIndex) => setPromptModal({ type: "approve", stepIndex }); const handleReject = (stepIndex) => setPromptModal({ type: "reject", stepIndex }); return (

{wf.title}

{wf.id}

{(wf.status || "").replace(/_/g, " ")}
{(wf.steps || []).map((step, i) => (
{step.type} {step.title} {step.status === "completed" && step.approved_by && ( ✓ {step.approved_by} )} {step.status === "rejected" && ( ✗ rejected )}
{canApproveStep(step) && (
)}
))}
{wf.context && Object.keys(wf.context).length > 0 && (
{Object.entries(wf.context).map(([k, v]) => ( {k}: {v} ))}
)} { const { type, stepIndex } = promptModal; setPromptModal(null); if (type === "approve") doApprove(stepIndex, val); else doReject(stepIndex, val); }} onCancel={() => setPromptModal(null)} />
); } // ── Inject CSS animations if not already present ────────── if (!document.getElementById("hal-animations")) { const style = document.createElement("style"); style.id = "hal-animations"; style.textContent = ` @keyframes hal-spin { to { transform: rotate(360deg); } } @keyframes hal-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } } `; document.head.appendChild(style); } // ── ExportStatusCard Component ──────────────────────────── function ExportStatusCard({ jobId, format, sessionToken }) { const [status, setStatus] = React.useState("pending"); const [filename, setFilename] = React.useState(null); const [error, setError] = React.useState(null); const [rowCount, setRowCount] = React.useState(null); const intervalRef = React.useRef(null); React.useEffect(() => { let mounted = true; const poll = () => { fetch(`/api/exports/${jobId}`, { headers: { "X-Session-Token": sessionToken }, }) .then((r) => r.ok ? r.json() : Promise.reject()) .then((data) => { if (!mounted) return; setStatus(data.status); setFilename(data.filename); setRowCount(data.row_count); if (data.error) setError(data.error); if (data.status === "completed" || data.status === "failed") { clearInterval(intervalRef.current); } }) .catch(() => {}); }; poll(); intervalRef.current = setInterval(poll, 3000); return () => { mounted = false; clearInterval(intervalRef.current); }; }, [jobId, sessionToken]); const handleDownload = () => { fetch(`/api/exports/${jobId}/download`, { headers: { "X-Session-Token": sessionToken }, }) .then((r) => { if (!r.ok) throw new Error("Download failed"); return r.blob(); }) .then((blob) => { const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename || `export.${format}`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }) .catch((err) => console.error("Download error:", err)); }; const formatIcons = { xlsx: "\u{1F4CA}", csv: "\u{1F4CB}", pdf: "\u{1F4C4}", docx: "\u{1F4C4}" }; const formatLabels = { xlsx: "Excel", csv: "CSV", pdf: "PDF", docx: "Word" }; const icon = formatIcons[format] || "\u{1F4C1}"; const label = formatLabels[format] || format?.toUpperCase(); return (
{icon}
{label} Export
{status === "pending" || status === "running" ? (
Preparing file...
) : status === "completed" ? (
{rowCount != null ? `${rowCount.toLocaleString()} rows` : "Ready"} {filename && — {filename}}
) : (
Failed{error ? `: ${error}` : ""}
)}
{status === "completed" && ( )}
); } // ── FindingCard Component ────────────────────────────────── const SEVERITY_COLORS = { CRITICAL: { border: "border-red-500", bg: "bg-red-900/20", text: "text-red-400", dot: "bg-red-500" }, HIGH: { border: "border-orange-500", bg: "bg-orange-900/20", text: "text-orange-400", dot: "bg-orange-500" }, MEDIUM: { border: "border-amber-500", bg: "bg-amber-900/20", text: "text-amber-400", dot: "bg-amber-500" }, LOW: { border: "border-blue-500", bg: "bg-blue-900/20", text: "text-blue-400", dot: "bg-blue-500" }, }; function FindingCard({ finding, sessionToken, onDismiss, onStar, onResolve }) { const [expanded, setExpanded] = useState(false); const sev = SEVERITY_COLORS[finding.severity] || SEVERITY_COLORS.LOW; const isResolved = finding.status === "resolved"; const isRecurring = (finding.occurrence_count || 1) >= 3; const timeAgo = (ts) => { if (!ts) return ""; const diff = Date.now() - new Date(ts).getTime(); const mins = Math.floor(diff / 60000); if (mins < 1) return "just now"; if (mins < 60) return `${mins}m ago`; const hrs = Math.floor(mins / 60); if (hrs < 24) return `${hrs}h ago`; return `${Math.floor(hrs / 24)}d ago`; }; const handleDismiss = async () => { try { await fetch(`/api/findings/${finding.id}/dismiss`, { method: "POST", headers: { "X-Session-Token": sessionToken }, }); onDismiss?.(finding.id); } catch {} }; const handleStar = async () => { try { await fetch(`/api/findings/${finding.id}/star`, { method: "POST", headers: { "X-Session-Token": sessionToken }, }); onStar?.(finding.id); } catch {} }; const handleResolve = async () => { try { await fetch(`/api/findings/${finding.id}/resolve`, { method: "POST", headers: { "X-Session-Token": sessionToken }, }); onResolve?.(finding.id); } catch {} }; return (
{finding.severity} {finding.category} {(finding.occurrence_count || 1) > 1 && ( {"\u00d7"}{finding.occurrence_count} )} {isRecurring && Recurring} {finding.escalated && Escalated} {isResolved && Resolved} {finding.watchdog_check_id && {"\u23f1"}}
{!isResolved && ( )}

{finding.finding}

{timeAgo(finding.last_seen || finding.timestamp)} {finding.first_seen && finding.first_seen !== finding.last_seen && ( First: {timeAgo(finding.first_seen)} )} {finding.source_host && {finding.source_host}} {isResolved && finding.resolved_at && Resolved {timeAgo(finding.resolved_at)}}
{finding.details && ( )} {expanded && finding.details && (() => { const lines = finding.details.split("\n"); // Detect pipe-delimited table: header row with |, separator row with -+- const sepIdx = lines.findIndex(l => /^[-+]+$/.test(l.replace(/\s/g, ""))); if (sepIdx > 0 && lines[sepIdx - 1].includes("|")) { const headers = lines[sepIdx - 1].split("|").map(h => h.trim()).filter(Boolean); const dataRows = lines.slice(sepIdx + 1).filter(l => l.includes("|")).map(l => l.split("|").map(c => c.trim()).filter(Boolean)); const footer = lines.slice(sepIdx + 1).filter(l => !l.includes("|") && l.trim()).join("\n"); return (
{headers.map((h, i) => )} {dataRows.map((row, ri) => ( {row.map((cell, ci) => )} ))}
{h}
{cell}
{footer &&

{footer}

}
); } return
{finding.details}
; })()}
); } // ── FindingsSidebar Component ───────────────────────────── function FindingsSidebar({ tabId, sessionToken, isOpen, onToggle }) { const [findings, setFindings] = useState([]); const [total, setTotal] = useState(0); const [statusFilter, setStatusFilter] = useState("active"); const [severityFilter, setSeverityFilter] = useState(""); const knownIdsRef = useRef(new Set()); const toast = useToast(); const [sidebarWidth, setSidebarWidth] = useState(() => { try { const w = parseInt(localStorage.getItem("hal-findings-width")); return w >= 350 && w <= 900 ? w : 480; } catch { return 480; } }); const resizingRef = useRef(false); // Persist width to localStorage useEffect(() => { try { localStorage.setItem("hal-findings-width", String(sidebarWidth)); } catch {} }, [sidebarWidth]); useEffect(() => { const handleMouseMove = (e) => { if (!resizingRef.current) return; const newWidth = window.innerWidth - e.clientX; setSidebarWidth(Math.max(350, Math.min(900, newWidth))); }; const handleMouseUp = () => { resizingRef.current = false; document.body.style.cursor = ""; document.body.style.userSelect = ""; }; window.addEventListener("mousemove", handleMouseMove); window.addEventListener("mouseup", handleMouseUp); return () => { window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseup", handleMouseUp); }; }, []); const fetchFindings = () => { if (!sessionToken || !tabId) return; const statusParam = statusFilter === "all" ? "" : `&status=${statusFilter}`; const sevParam = severityFilter ? `&severity=${severityFilter}` : ""; fetch(`/api/findings?tab_id=${tabId}&limit=50${statusParam}${sevParam}`, { headers: { "X-Session-Token": sessionToken }, }) .then((r) => r.ok ? r.json() : Promise.reject()) .then((data) => { const newFindings = data.findings || []; setFindings(newFindings); setTotal(data.total || 0); // Toast for new findings newFindings.forEach((f) => { if (f.is_new && !knownIdsRef.current.has(f.id)) { toast(`New finding: ${f.finding.substring(0, 80)}`, f.severity === "CRITICAL" || f.severity === "HIGH" ? "error" : "warning", 6000); } }); knownIdsRef.current = new Set(newFindings.map((f) => f.id)); }) .catch(() => {}); }; useEffect(() => { if (!isOpen) return; fetchFindings(); const interval = setInterval(fetchFindings, 30000); return () => clearInterval(interval); }, [sessionToken, tabId, isOpen, statusFilter, severityFilter]); const handleDismiss = (id) => setFindings((prev) => prev.filter((f) => f.id !== id)); const handleStar = (id) => setFindings((prev) => prev.map((f) => f.id === id ? { ...f, starred: !f.starred } : f)); const handleResolve = (id) => setFindings((prev) => prev.map((f) => f.id === id ? { ...f, status: "resolved" } : f)); if (!isOpen) return null; const filterBtn = (label, value) => ( ); return (
{/* Resize handle — desktop only */}
{ e.preventDefault(); resizingRef.current = true; document.body.style.cursor = "col-resize"; document.body.style.userSelect = "none"; }} />
Findings {total > 0 && ( {total} )}
{filterBtn("Active", "active")} {filterBtn("Resolved", "resolved")} {filterBtn("All", "all")}
Sev: {[{ k: "", l: "All" }, { k: "CRITICAL", l: "Crit", c: "red" }, { k: "HIGH", l: "High", c: "orange" }, { k: "MEDIUM", l: "Med", c: "amber" }, { k: "LOW", l: "Low", c: "blue" }].map((b) => ( ))}
{findings.length === 0 ? (
{"\uD83D\uDD0D"}
{statusFilter === "active" ? "No active findings" : statusFilter === "resolved" ? "No resolved findings" : "No findings yet"}
) : ( findings.map((f) => ( )) )}
); } // ── Insights Tab ────────────────────────────────────────── // Consolidated findings view — all findings from business tabs + infrastructure agents function InsightsTab({ sessionToken, user }) { const [findings, setFindings] = useState([]); const [total, setTotal] = useState(0); const [statusFilter, setStatusFilter] = useState("active"); const [severityFilter, setSeverityFilter] = useState(""); const [loading, setLoading] = useState(true); const lastFindingIdsRef = useRef(new Set()); const fetchFindings = useCallback(async () => { try { const params = new URLSearchParams({ limit: "100" }); if (statusFilter) params.set("status", statusFilter); if (severityFilter) params.set("severity", severityFilter); const res = await fetch(`/api/findings?${params}`, { headers: { "X-Session-Token": sessionToken } }); if (!res.ok) return; const data = await res.json(); const newFindings = data.findings || []; // Toast for new HIGH/CRITICAL findings const currentIds = new Set(newFindings.map((f) => f.id)); if (lastFindingIdsRef.current.size > 0) { newFindings.forEach((f) => { if (f.is_new && !lastFindingIdsRef.current.has(f.id) && (f.severity === "CRITICAL" || f.severity === "HIGH")) { if (typeof toast !== "undefined") toast.error(`New ${f.severity}: ${f.finding.slice(0, 80)}`); } }); } lastFindingIdsRef.current = currentIds; setFindings(newFindings); setTotal(data.total || 0); } catch (e) { /* ignore */ } setLoading(false); }, [sessionToken, statusFilter, severityFilter]); useEffect(() => { fetchFindings(); const iv = setInterval(fetchFindings, 30000); return () => clearInterval(iv); }, [fetchFindings]); const handleDismiss = async (id) => { await fetch(`/api/findings/${id}/dismiss`, { method: "POST", headers: { "X-Session-Token": sessionToken } }); fetchFindings(); }; const handleStar = async (id) => { await fetch(`/api/findings/${id}/star`, { method: "POST", headers: { "X-Session-Token": sessionToken } }); fetchFindings(); }; const handleResolve = async (id) => { await fetch(`/api/findings/${id}/resolve`, { method: "POST", headers: { "X-Session-Token": sessionToken } }); fetchFindings(); }; // Summary counts const counts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 }; findings.forEach((f) => { if (counts[f.severity] !== undefined) counts[f.severity]++; }); const sevBtns = [ { key: "", label: "All" }, { key: "CRITICAL", label: "Critical", color: "red" }, { key: "HIGH", label: "High", color: "orange" }, { key: "MEDIUM", label: "Medium", color: "amber" }, { key: "LOW", label: "Low", color: "blue" }, ]; const statusBtns = [ { key: "active", label: "Active" }, { key: "resolved", label: "Resolved" }, { key: "", label: "All" }, ]; return (
{/* Header */}

Insights

{counts.CRITICAL > 0 && {counts.CRITICAL} Critical} {counts.HIGH > 0 && {counts.HIGH} High} {counts.MEDIUM > 0 && {counts.MEDIUM} Medium} {counts.LOW > 0 && {counts.LOW} Low} {total === 0 && !loading && No findings}
{/* Filters */}
Severity: {sevBtns.map((b) => ( ))}
Status: {statusBtns.map((b) => ( ))}
{total} total
{/* Findings list */}
{loading &&

Loading...

} {!loading && findings.length === 0 && (
✔️

All clear

No findings match the current filters.

)} {findings.map((f) => ( ))}
); } // ── Agent Findings Sub-Tab ─────────────────────────────────── // Per-agent infrastructure findings in the Agents/Dashboard tab function AgentFindingsSubTab({ sessionToken }) { const [findings, setFindings] = useState([]); const [severityFilter, setSeverityFilter] = useState(""); const [statusFilter, setStatusFilter] = useState("active"); const fetchFindings = useCallback(async () => { try { const params = new URLSearchParams({ category: "infrastructure", limit: "100" }); if (statusFilter) params.set("status", statusFilter); if (severityFilter) params.set("severity", severityFilter); const res = await fetch(`/api/findings?${params}`, { headers: { "X-Session-Token": sessionToken } }); if (!res.ok) return; const data = await res.json(); setFindings(data.findings || []); } catch (e) { /* ignore */ } }, [sessionToken, statusFilter, severityFilter]); useEffect(() => { fetchFindings(); const iv = setInterval(fetchFindings, 30000); return () => clearInterval(iv); }, [fetchFindings]); const handleDismiss = async (id) => { await fetch(`/api/findings/${id}/dismiss`, { method: "POST", headers: { "X-Session-Token": sessionToken } }); fetchFindings(); }; const handleStar = async (id) => { await fetch(`/api/findings/${id}/star`, { method: "POST", headers: { "X-Session-Token": sessionToken } }); fetchFindings(); }; const handleResolve = async (id) => { await fetch(`/api/findings/${id}/resolve`, { method: "POST", headers: { "X-Session-Token": sessionToken } }); fetchFindings(); }; // Group by source_host const grouped = {}; findings.forEach((f) => { const host = f.source_host || "unknown"; if (!grouped[host]) grouped[host] = []; grouped[host].push(f); }); const sevBtns = [ { key: "", label: "All" }, { key: "CRITICAL", label: "Critical", color: "red" }, { key: "HIGH", label: "High", color: "orange" }, { key: "MEDIUM", label: "Medium", color: "amber" }, { key: "LOW", label: "Low", color: "blue" }, ]; return (
Severity: {sevBtns.map((b) => ( ))}
Status: {["active", "resolved", ""].map((s) => ( ))}
{Object.keys(grouped).length === 0 && (

No infrastructure findings.

)} {Object.entries(grouped).sort(([a], [b]) => a.localeCompare(b)).map(([host, hostFindings]) => (

{host} ({hostFindings.length})

{hostFindings.map((f) => ( ))}
))}
); } // ── Slash Command Helpers ────────────────────────────────── const SLASH_COMMANDS = [ { cmd: "/clear", desc: "Reset conversation" }, { cmd: "/memory", desc: "View/manage saved notes" }, { cmd: "/status", desc: "Context and system status" }, { cmd: "/summarize", desc: "Compress conversation history" }, ]; function parseSlashCommand(input) { const trimmed = input.trim(); if (!trimmed.startsWith("/")) return null; const spaceIdx = trimmed.indexOf(" "); let command, args; if (spaceIdx === -1) { command = trimmed.toLowerCase(); args = ""; } else { command = trimmed.slice(0, spaceIdx).toLowerCase(); args = trimmed.slice(spaceIdx + 1).trim(); } // Validate it's a known command root const knownRoots = SLASH_COMMANDS.map(c => c.cmd); if (!knownRoots.includes(command)) return null; return { command, args }; } function SlashCommandMenu({ input, onSelect }) { if (!input.startsWith("/") || input.includes(" ")) return null; const filter = input.toLowerCase(); const matches = SLASH_COMMANDS.filter(c => c.cmd.startsWith(filter)); if (matches.length === 0) return null; return (
{matches.map(c => ( ))}
); } function formatCommandResponse(data, setMessages, user, sessionToken) { const sysMsg = (content) => ({ id: Date.now(), role: "system", content, timestamp: new Date(), isCommand: true, }); if (data.type === "clear") { setMessages([{ id: 1, role: "assistant", content: `Chat cleared. What would you like to do, ${user?.display_name || "User"}?`, timestamp: new Date(), }]); return; } if (data.type === "status") { const lines = [ `Context: ${data.context_used}/${data.context_limit} messages (${data.history_total} total stored)`, `Memories: ${data.memory_count} saved notes`, `Active workflows: ${data.active_workflows}`, `Connected agents: ${data.connected_agents}`, ]; setMessages(prev => [...prev, sysMsg(lines.join("\n"))]); return; } if (data.type === "memory_list") { if (!data.memories || data.memories.length === 0) { setMessages(prev => [...prev, sysMsg("No memories saved yet. Use /memory add to save one.")]); return; } const lines = data.memories.map(m => `[${m.category}] ${m.content} (id: ${m.id}, ${m.source})` ); setMessages(prev => [...prev, sysMsg("Saved memories:\n" + lines.join("\n"))]); return; } if (data.type === "memory_added") { setMessages(prev => [...prev, sysMsg(`Saved: ${data.memory.content}`)]); return; } if (data.type === "memory_removed") { setMessages(prev => [...prev, sysMsg(data.message)]); return; } if (data.type === "summarized") { setMessages(prev => [ sysMsg(`Conversation compressed. ${data.context_freed} messages freed.`), ]); return; } if (data.type === "error") { setMessages(prev => [...prev, sysMsg(`Error: ${data.message}`)]); return; } } function MainChat({ user, sessionToken }) { const isAdmin = user?.roles?.includes("admin") || user?.roles?.includes("fleet_ops"); const toast = useToast(); const [showClearConfirm, setShowClearConfirm] = useState(false); const [messages, setMessages] = useState([ { id: 1, role: "assistant", content: `Welcome, ${user?.display_name || "User"}! I'm HAL, your business intelligence assistant. I can help you manage your fleet, process orders, coordinate services, and more. What would you like to do?`, timestamp: new Date(), }, ]); const [input, setInput] = useState(""); const [isLoading, setIsLoading] = useState(false); const [attachedFiles, setAttachedFiles] = useState([]); const messagesEndRef = useRef(null); const abortRef = useRef(null); const chatInputRef = useRef(null); useEffect(() => { setTimeout(() => chatInputRef.current?.focus(), 100); }, []); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }; useEffect(() => { scrollToBottom(); }, [messages]); // Fetch chat history on mount useEffect(() => { const fetchChatHistory = async () => { if (!sessionToken) return; try { const response = await fetch("/api/chat/history", { headers: { "X-Session-Token": sessionToken }, }); if (response.ok) { const data = await response.json(); if (data.messages && data.messages.length > 0) { const parsedMessages = data.messages.map((msg, idx) => ({ id: idx + 1, role: msg.role, content: msg.content, timestamp: new Date(msg.timestamp || Date.now()), workflows: msg.workflows || undefined, exports: msg.exports || undefined, })); setMessages(parsedMessages); } } } catch (err) { console.error("Failed to fetch chat history:", err); } }; fetchChatHistory(); }, [sessionToken]); const handleSend = async () => { if ((!input.trim() && !attachedFiles.length) || isLoading) return; // Check for slash commands const parsed = parseSlashCommand(input); if (parsed) { const cmdMsg = { id: messages.length + 1, role: "user", content: input, timestamp: new Date(), isCommand: true }; setMessages(prev => [...prev, cmdMsg]); setInput(""); setIsLoading(true); try { const res = await fetch("/api/chat/command", { method: "POST", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, body: JSON.stringify({ command: parsed.command, args: parsed.args }), }); const data = await res.json(); formatCommandResponse(data, setMessages, user, sessionToken); } catch (err) { setMessages(prev => [...prev, { id: Date.now(), role: "system", content: `Command failed: ${err.message}`, timestamp: new Date(), isCommand: true }]); } finally { setIsLoading(false); setTimeout(() => chatInputRef.current?.focus(), 50); } return; } const userMessage = { id: messages.length + 1, role: "user", content: input || "[Attached file(s)]", timestamp: new Date(), files: attachedFiles.length > 0 ? [...attachedFiles] : undefined, }; const fileIds = attachedFiles.map((f) => f.file_id); setMessages((prev) => [...prev, userMessage]); setInput(""); setAttachedFiles([]); setIsLoading(true); const assistantId = messages.length + 2; const controller = new AbortController(); abortRef.current = controller; try { const res = await fetch("/api/chat/send", { method: "POST", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken, }, body: JSON.stringify({ content: input, file_ids: fileIds.length ? fileIds : undefined }), signal: controller.signal, }); if (!res.ok) { const errBody = await res.json().catch(() => ({})); throw new Error(errBody.detail || `API error: ${res.status}`); } // Create placeholder assistant message setMessages((prev) => [...prev, { id: assistantId, role: "assistant", content: "", timestamp: new Date() }]); // Stream SSE response const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() || ""; for (const line of lines) { if (!line.startsWith("data: ")) continue; try { const data = JSON.parse(line.slice(6)); // Tool execution events — only show to admin/fleet_ops users if (data.type === "tool_start" || data.type === "tool_result") { if (isAdmin) { setMessages((prev) => prev.map((msg) => msg.id === assistantId ? { ...msg, content: msg.content + data.content } : msg ) ); } } else if (data.type === "chunk" || data.type === "error") { setMessages((prev) => prev.map((msg) => msg.id === assistantId ? { ...msg, content: msg.content + data.content } : msg ) ); } else if (data.type === "workflow_update" && data.workflow) { setMessages((prev) => prev.map((msg) => msg.id === assistantId ? { ...msg, workflows: [...(msg.workflows || []), data.workflow] } : msg ) ); } else if (data.type === "export_started" && data.job_id) { setMessages((prev) => prev.map((msg) => msg.id === assistantId ? { ...msg, exports: [...(msg.exports || []), { job_id: data.job_id, format: data.format }] } : msg ) ); } else if (data.type === "done") { break; } } catch {} } } } catch (err) { if (err.name === "AbortError") { // User cancelled — keep what was streamed so far setMessages((prev) => prev.map((msg) => msg.id === assistantId && !msg.content ? { ...msg, content: "(cancelled)" } : msg ) ); } else { setMessages((prev) => [...prev, { id: assistantId, role: "assistant", content: `Sorry, I encountered an error: ${err.message}`, timestamp: new Date(), }]); } } finally { abortRef.current = null; setIsLoading(false); setTimeout(() => chatInputRef.current?.focus(), 50); } }; const handleStop = () => { if (abortRef.current) abortRef.current.abort(); }; const handleClearHistory = async () => { if (sessionToken) { try { await fetch("/api/chat/history", { method: "DELETE", headers: { "X-Session-Token": sessionToken }, }); } catch (err) { console.error("Failed to clear chat history:", err); } } setMessages([ { id: 1, role: "assistant", content: `Welcome, ${user?.display_name || "User"}! I'm HAL, your business intelligence assistant. I can help you manage your fleet, process orders, coordinate services, and more. What would you like to do?`, timestamp: new Date(), }, ]); }; return (
{ setShowClearConfirm(false); handleClearHistory(); }} onCancel={() => setShowClearConfirm(false)} />

Chat with HAL

{messages.filter((msg) => { // Hide empty assistant messages (typing indicator covers this state) const visibleContent = isAdmin ? (msg.content || "").trim() : stripToolLines(msg.content); if (msg.role === "assistant" && !visibleContent && !(msg.workflows?.length) && !(msg.exports?.length)) return false; return true; }).map((msg) => (
{msg.role === "user" ?

{msg.content}

: } {msg.files && msg.files.length > 0 && (
{msg.files.map((f) => ( {f.filename} ))}
)} {msg.workflows && msg.workflows.length > 0 && msg.workflows.map((wf) => ( ))} {msg.exports && msg.exports.length > 0 && msg.exports.map((exp) => ( ))}

{msg.timestamp.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}

))}
setInput(cmd + " ")} /> setInput(e.target.value)} onKeyPress={(e) => e.key === "Enter" && !isLoading && handleSend()} placeholder={attachedFiles.length > 0 ? "Describe what to do with the files..." : "Ask HAL anything... (/ for commands)"} disabled={isLoading} className="flex-1 bg-gray-800 text-white rounded px-3 py-2 text-sm border border-gray-700 focus:border-amber-500 focus:outline-none disabled:opacity-50" /> {isLoading ? ( ) : ( )}
); } function TabChatPanel({ tabId, tabLabel, user, sessionToken, accessLevel }) { const isAdmin = user?.roles?.includes("admin") || user?.roles?.includes("fleet_ops"); const [isOpen, setIsOpen] = useState(false); const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [isLoading, setIsLoading] = useState(false); const [attachedFiles, setAttachedFiles] = useState([]); const messagesEndRef = useRef(null); const abortRef = useRef(null); const chatInputRef = useRef(null); useEffect(() => { if (isOpen) setTimeout(() => chatInputRef.current?.focus(), 100); }, [isOpen]); // Fetch tab-specific chat history when opened useEffect(() => { if (!sessionToken || !isOpen) return; fetch(`/api/chat/history?tab=${tabId}`, { headers: { "X-Session-Token": sessionToken }, }) .then((r) => r.ok ? r.json() : Promise.reject("Failed")) .then((data) => { if (data.messages && data.messages.length > 0) { setMessages(data.messages.map((msg, idx) => ({ id: idx + 1, role: msg.role, content: msg.content, timestamp: new Date(msg.timestamp || Date.now()), }))); } else { setMessages([{ id: 1, role: "assistant", content: `Hi ${user?.display_name || "there"}! I'm HAL, ready to help with ${tabLabel}.${accessLevel === "restricted" ? " (You have read-only access.)" : ""} What do you need?`, timestamp: new Date(), }]); } }) .catch(() => { setMessages([{ id: 1, role: "assistant", content: `Hi! I'm HAL, assisting with ${tabLabel}. How can I help?`, timestamp: new Date(), }]); }); }, [sessionToken, isOpen, tabId]); useEffect(() => { if (isOpen) messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages, isOpen]); const handleSend = async () => { if ((!input.trim() && !attachedFiles.length) || isLoading) return; // Check for slash commands const parsed = parseSlashCommand(input); if (parsed) { const cmdMsg = { id: messages.length + 1, role: "user", content: input, timestamp: new Date(), isCommand: true }; setMessages(prev => [...prev, cmdMsg]); setInput(""); setIsLoading(true); try { const res = await fetch("/api/chat/command", { method: "POST", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, body: JSON.stringify({ command: parsed.command, args: parsed.args, tab_id: tabId }), }); const data = await res.json(); formatCommandResponse(data, setMessages, user, sessionToken); } catch (err) { setMessages(prev => [...prev, { id: Date.now(), role: "system", content: `Command failed: ${err.message}`, timestamp: new Date(), isCommand: true }]); } finally { setIsLoading(false); setTimeout(() => chatInputRef.current?.focus(), 50); } return; } const fileIds = attachedFiles.map((f) => f.file_id); const userMessage = { id: messages.length + 1, role: "user", content: input || "[Attached file(s)]", timestamp: new Date(), files: attachedFiles.length > 0 ? [...attachedFiles] : undefined, }; setMessages((prev) => [...prev, userMessage]); setInput(""); setAttachedFiles([]); setIsLoading(true); const assistantId = messages.length + 2; const controller = new AbortController(); abortRef.current = controller; // Add placeholder assistant message setMessages((prev) => [...prev, { id: assistantId, role: "assistant", content: "", timestamp: new Date() }]); try { const res = await fetch("/api/chat/send", { method: "POST", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, body: JSON.stringify({ content: input, tab_id: tabId, file_ids: fileIds.length ? fileIds : undefined }), signal: controller.signal, }); if (!res.ok) { const errBody = await res.json().catch(() => ({})); throw new Error(errBody.detail || `Error ${res.status}`); } const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() || ""; for (const line of lines) { if (!line.startsWith("data: ")) continue; try { const data = JSON.parse(line.slice(6)); if (data.type === "tool_start" || data.type === "tool_result") { if (isAdmin) { setMessages((prev) => prev.map((msg) => msg.id === assistantId ? { ...msg, content: msg.content + data.content } : msg )); } } else if (data.type === "chunk" || data.type === "error") { setMessages((prev) => prev.map((msg) => msg.id === assistantId ? { ...msg, content: msg.content + data.content } : msg )); } else if (data.type === "workflow_update" && data.workflow) { setMessages((prev) => prev.map((msg) => msg.id === assistantId ? { ...msg, workflows: [...(msg.workflows || []), data.workflow] } : msg )); } else if (data.type === "export_started" && data.job_id) { setMessages((prev) => prev.map((msg) => msg.id === assistantId ? { ...msg, exports: [...(msg.exports || []), { job_id: data.job_id, format: data.format }] } : msg )); } } catch {} } } } catch (err) { if (err.name !== "AbortError") { setMessages((prev) => prev.map((msg) => msg.id === assistantId ? { ...msg, content: `Error: ${err.message}` } : msg )); } } finally { abortRef.current = null; setIsLoading(false); setTimeout(() => chatInputRef.current?.focus(), 50); } }; const handleStop = () => { if (abortRef.current) abortRef.current.abort(); }; if (!isOpen) { return ( ); } return (
{/* Header */}
HAL · {tabLabel} {accessLevel === "restricted" && ( Read-only )}
{/* Messages */}
{messages.filter((msg) => { const visibleContent = isAdmin ? (msg.content || "").trim() : stripToolLines(msg.content); if (msg.role === "assistant" && !visibleContent && !(msg.workflows?.length) && !(msg.exports?.length)) return false; return true; }).map((msg) => (
{msg.role === "user" ? {msg.content} : } {msg.files && msg.files.length > 0 && (
{msg.files.map((f) => ( {f.filename} ))}
)} {msg.workflows && msg.workflows.length > 0 && msg.workflows.map((wf) => ( ))} {msg.exports && msg.exports.length > 0 && msg.exports.map((exp) => ( ))} {msg.timestamp &&

{msg.timestamp.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}

}
))}
{/* Input */}
setInput(cmd + " ")} /> setInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); } }} placeholder={attachedFiles.length > 0 ? "Describe what to do with the files..." : `Ask about ${tabLabel}... (/ for commands)`} className="flex-1 bg-gray-900 border border-gray-700 rounded px-3 py-1.5 text-sm text-white focus:outline-none focus:border-amber-500" disabled={isLoading} /> {isLoading ? ( ) : ( )}
); } function ConsoleTab({ fleet: mockFleet, agents: mockAgents, sessionToken, user }) { const isAdmin = true; // ConsoleTab is only visible to admin/fleet_ops // Restore session history from localStorage const savedSession = (() => { try { const raw = localStorage.getItem("hal_console_history"); return raw ? JSON.parse(raw) : null; } catch { return null; } })(); const [history, setHistory] = useState(() => { const welcome = { type: "system", text: "HAL Fleet Console v1.0 — Type 'help' for available commands.", timestamp: new Date() }; if (savedSession?.terminal?.length) { return [...savedSession.terminal, { type: "system", text: "— session restored —", timestamp: new Date() }]; } return [welcome]; }); const [inputValue, setInputValue] = useState(""); const [commandHistory, setCommandHistory] = useState(savedSession?.commands || []); const [historyIndex, setHistoryIndex] = useState(-1); const [targetHost, setTargetHost] = useState("all"); const [isExecuting, setIsExecuting] = useState(false); const [liveHosts, setLiveHosts] = useState([]); const terminalRef = useRef(null); const inputRef = useRef(null); // Persist session history to localStorage useEffect(() => { try { localStorage.setItem("hal_console_history", JSON.stringify({ commands: commandHistory.slice(-100), terminal: history.slice(-500), })); } catch {} }, [commandHistory, history]); // Fetch live hosts on mount (same pattern as DashboardTab) useEffect(() => { fetch("/api/hosts", { headers: { "X-Session-Token": sessionToken } }) .then(r => r.ok ? r.json() : Promise.reject()) .then(data => { const hosts = data.hosts || data || []; if (hosts.length > 0) setLiveHosts(hosts); }) .catch(() => {}); }, [sessionToken]); // Build fleet list: prefer live hosts, fall back to mock const fleet = liveHosts.length > 0 ? liveHosts.map(h => ({ id: h.hostname || h.id, name: h.display_name || h.hostname || h.name || h.id, status: h.status === "online" || h.status === "healthy" ? "healthy" : h.status || "unknown", utilization: h.cpu_percent || 0, agents: 1, lastSeen: h.last_seen || "unknown", })) : mockFleet; const isLive = liveHosts.length > 0; useEffect(() => { if (terminalRef.current) { terminalRef.current.scrollTop = terminalRef.current.scrollHeight; } }, [history]); const getTargetLabel = () => targetHost === "all" ? "all" : fleet.find(f => f.id === targetHost)?.name || targetHost; // Map console commands to MCP tool + args const mapCommandToTool = (command, args) => { switch (command) { case "status": return { tool: "get_host_info", args: {} }; case "metrics": return { tool: "get_metrics", args: {} }; case "uptime": return { tool: "run_command", args: { command: "uptime" } }; case "ps": return { tool: "list_services", args: {} }; case "services": return { tool: "list_services", args: { filter_pattern: args[0] || "" } }; case "restart": return args[0] ? { tool: "manage_service", args: { name: args[0], action: "restart" } } : null; case "stop": return args[0] ? { tool: "manage_service", args: { name: args[0], action: "stop" } } : null; case "logs": return { tool: "tail_logs", args: { service: args[0] || "", lines: 50 } }; case "df": case "disk": return { tool: "run_command", args: { command: "df -h" } }; case "ping": return { tool: "run_command", args: { command: "echo pong" } }; default: return null; } }; // Call the real API, return formatted output entries // Non-streaming API call (single host or fallback) const callConsoleAPI = async (host, tool, toolArgs) => { const res = await fetch("/api/console/exec", { method: "POST", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, body: JSON.stringify({ host, tool, args: toolArgs }), }); if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error(body.detail || `API error: ${res.status}`); } const data = await res.json(); const entries = []; for (const [hostname, result] of Object.entries(data.results || {})) { const label = fleet.find(f => f.id === hostname)?.name || hostname; if (result.ok) { const text = Array.isArray(result.result) ? result.result.join("\n") : String(result.result); entries.push({ type: "output", text: `[${label}] ${text}` }); } else { entries.push({ type: "error", text: `[${label}] Error: ${result.error}` }); } } return entries; }; // Streaming API call for multi-host — results appear host-by-host const callConsoleStream = async (host, tool, toolArgs) => { const res = await fetch("/api/console/stream", { method: "POST", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, body: JSON.stringify({ host, tool, args: toolArgs }), }); if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error(body.detail || `API error: ${res.status}`); } const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() || ""; for (const line of lines) { if (!line.startsWith("data: ")) continue; try { const data = JSON.parse(line.slice(6)); if (data.done) return; const now = new Date(); if (data.ok) { const text = Array.isArray(data.result) ? data.result.join("\n") : String(data.result); setHistory(prev => [...prev.filter(e => e.text !== "Executing..."), { type: "output", text: `[${data.display_name || data.hostname}] ${text}`, timestamp: now }]); } else { setHistory(prev => [...prev.filter(e => e.text !== "Executing..."), { type: "error", text: `[${data.display_name || data.hostname}] Error: ${data.error}`, timestamp: now }]); } } catch {} } } }; // Mock fallback for when API is unreachable const mockExecute = (command, args, targetFleet) => { const now = new Date(); switch (command) { case "status": case "uptime": return [{ type: "output", text: targetFleet.map(h => `[${h.name}] ${h.status.padEnd(10)} CPU: ${h.utilization}% Agents: ${h.agents} Last seen: ${h.lastSeen}` ).join("\n") || "No hosts match target." }]; case "agents": return [{ type: "output", text: mockAgents.map(a => `${a.name.padEnd(22)} ${a.status.padEnd(8)} Model: ${a.model || "n/a"} Tasks: ${a.tasks}` ).join("\n") || "No agents found." }]; case "restart": case "stop": { const svc = args[0]; if (!svc) return [{ type: "error", text: `Usage: ${command} ` }]; const lines = targetFleet.map(h => ({ type: "output", text: `[${h.name}] ${command === "restart" ? "Restarting" : "Stopping"} ${svc}...` })); lines.push({ type: "system", text: `${command === "restart" ? "Restart" : "Stop"} signal sent to ${targetFleet.length} host(s) for service '${svc}'.` }); return lines; } case "logs": { const svc = args[0] || "system"; const host = targetFleet[0]?.name || "unknown"; return [{ type: "output", text: [ `[${host}] ${new Date(now - 30000).toLocaleTimeString()} ${svc}[1842]: Started successfully`, `[${host}] ${new Date(now - 25000).toLocaleTimeString()} ${svc}[1842]: Listening on port 8080`, `[${host}] ${new Date(now - 12000).toLocaleTimeString()} ${svc}[1842]: Request processed in 42ms`, `[${host}] ${new Date(now - 5000).toLocaleTimeString()} ${svc}[1842]: Health check OK`, ].join("\n") }]; } case "df": case "disk": return [{ type: "output", text: ["Filesystem Size Used Avail Use% Mounted on", ...targetFleet.map(h => `/dev/sda1 100G ${Math.round(h.utilization * 0.8)}G ${100 - Math.round(h.utilization * 0.8)}G ${Math.round(h.utilization * 0.8)}% / [${h.name}]`) ].join("\n") }]; case "ps": case "services": return [{ type: "output", text: "PID USER CPU% MEM% COMMAND\n1 root 0.0 0.1 /sbin/init\n842 claude-agent 2.1 3.4 /opt/hal/agent/hal-agent\n1203 claude-agent 0.8 1.2 ollama serve\n1544 root 0.3 0.5 /usr/sbin/nginx\n1891 claude-agent 1.5 2.8 python3 mcp_server.py" }]; case "metrics": return [{ type: "output", text: targetFleet.map(h => `[${h.name}] CPU: ${h.utilization}% Status: ${h.status}`).join("\n") }]; case "ping": if (targetFleet.length === 0) return [{ type: "error", text: "No hosts match target." }]; return targetFleet.map(h => ({ type: "output", text: `[${h.name}] PING OK — ${Math.floor(Math.random() * 15) + 1}ms` })); default: return null; // Not a known command } }; const executeCommand = async (raw) => { const cmd = raw.trim(); if (!cmd) return; const now = new Date(); const prompt = `hal@${getTargetLabel()}> `; setCommandHistory(prev => [...prev, cmd]); setHistoryIndex(-1); setInputValue(""); // Add the input line immediately setHistory(prev => [...prev, { type: "input", text: `${prompt}${cmd}`, timestamp: now }]); const parts = cmd.split(/\s+/); const command = parts[0].toLowerCase(); const cmdArgs = parts.slice(1); // Client-only commands if (command === "clear") { setHistory([{ type: "system", text: "Terminal cleared.", timestamp: now }]); return; } if (command === "help") { setHistory(prev => [...prev, { type: "output", text: [ "Available commands:", " status Show fleet host info", " metrics Show host CPU/memory/disk metrics", " agents List active agents", " restart Restart a service on target host(s)", " stop Stop a service on target host(s)", " logs [svc] Tail recent logs (optional service filter)", " services [pat] List systemd services (optional filter)", " df Show disk usage", " uptime Show host uptime", " ps List services", " ping Ping target host(s)", " ! Run a raw shell command on target host(s)", " history Show command history", " clear Clear terminal", " help Show this message", "", "Use the host selector above to target specific hosts.", ].join("\n"), timestamp: now }]); return; } if (command === "history") { const cmds = commandHistory.length ? commandHistory.map((c, i) => ` ${String(i + 1).padStart(4)} ${c}`).join("\n") : " (no history)"; setHistory(prev => [...prev, { type: "output", text: cmds, timestamp: now }]); return; } if (command === "agents") { const agentList = (isLive ? fleet : mockAgents).map(a => `${(a.name || "unknown").padEnd(22)} ${(a.status || "unknown").padEnd(8)}` ).join("\n") || "No agents found."; setHistory(prev => [...prev, { type: "output", text: agentList, timestamp: now }]); return; } // Validate service commands if ((command === "restart" || command === "stop") && !cmdArgs[0]) { setHistory(prev => [...prev, { type: "error", text: `Usage: ${command} `, timestamp: now }]); return; } // Determine MCP tool mapping const isRawCommand = cmd.startsWith("!"); const mapped = isRawCommand ? { tool: "run_command", args: { command: cmd.slice(1).trim() } } : mapCommandToTool(command, cmdArgs); if (!mapped) { setHistory(prev => [...prev, { type: "error", text: `Unknown command: '${command}'. Type 'help' for available commands.`, timestamp: now }]); return; } // Resolve target host ID for the API const apiHost = targetHost === "all" ? "all" : targetHost; // Show executing indicator setIsExecuting(true); setHistory(prev => [...prev, { type: "system", text: "Executing...", timestamp: now }]); // Use streaming for multi-host, regular API for single-host const useStream = apiHost === "all" && fleet.length > 1; try { if (useStream) { await callConsoleStream(apiHost, mapped.tool, mapped.args); } else { const entries = await callConsoleAPI(apiHost, mapped.tool, mapped.args); setHistory(prev => { const withoutExec = prev.filter(e => e.text !== "Executing..."); return [...withoutExec, ...entries.map(e => ({ ...e, timestamp: new Date() }))]; }); } } catch (e) { // API unreachable — fall back to mock const targetFleet = targetHost === "all" ? fleet : fleet.filter(f => f.id === targetHost); const mockEntries = mockExecute(command, cmdArgs, targetFleet); setHistory(prev => { const withoutExec = prev.filter(e => e.text !== "Executing..."); if (mockEntries) { return [...withoutExec, { type: "system", text: "(mock — API unreachable)", timestamp: new Date() }, ...mockEntries.map(e => ({ ...e, timestamp: new Date() })), ]; } return [...withoutExec, { type: "error", text: `Failed: ${e.message}`, timestamp: new Date() }]; }); } finally { setIsExecuting(false); setTimeout(() => inputRef.current?.focus(), 50); } }; const handleKeyDown = (e) => { if (e.key === "Enter" && !isExecuting) { executeCommand(inputValue); } else if (e.key === "ArrowUp") { e.preventDefault(); if (commandHistory.length === 0) return; const newIndex = historyIndex === -1 ? commandHistory.length - 1 : Math.max(0, historyIndex - 1); setHistoryIndex(newIndex); setInputValue(commandHistory[newIndex]); } else if (e.key === "ArrowDown") { e.preventDefault(); if (historyIndex === -1) return; const newIndex = historyIndex + 1; if (newIndex >= commandHistory.length) { setHistoryIndex(-1); setInputValue(""); } else { setHistoryIndex(newIndex); setInputValue(commandHistory[newIndex]); } } }; const quickCommands = [ { label: "System Status", cmd: "status" }, { label: "Tail Logs", cmd: "logs" }, { label: "List Services", cmd: "services" }, { label: "Disk Usage", cmd: "df" }, { label: "Ping", cmd: "ping" }, ]; const typeColors = { input: "text-green-400", output: "text-gray-300", error: "text-red-400", system: "text-amber-400", }; return (
{/* Header row */}

Fleet Console

{isLive ? ( {fleet.length} live ) : ( demo mode )}
{quickCommands.map((qc) => ( ))}
{/* Terminal area */}
inputRef.current?.focus()} className="flex-1 bg-gray-950 rounded-lg border border-gray-700 p-4 overflow-y-auto font-mono text-sm cursor-text min-h-0" > {history.map((entry, i) => (
{entry.text}
))} {/* Input line */}
hal@{getTargetLabel()}> setInputValue(e.target.value)} onKeyDown={handleKeyDown} disabled={isExecuting} className="flex-1 bg-transparent text-green-400 font-mono text-sm outline-none caret-green-400 disabled:opacity-50" spellCheck={false} autoFocus />
); } function DashboardTab({ sessionToken, user }) { const [activeSubTab, setActiveSubTab] = useState("fleet"); const [hosts, setHosts] = useState([]); const [hostMetrics, setHostMetrics] = useState({}); const [hostServices, setHostServices] = useState({}); const [alerts, setAlerts] = useState([]); const [agentStatus, setAgentStatus] = useState([]); const [loading, setLoading] = useState(true); const [lastRefresh, setLastRefresh] = useState(null); const [fetchError, setFetchError] = useState(null); const [chatAgent, setChatAgent] = useState(null); const [distVersion, setDistVersion] = useState(null); const activeSubTabRef = useRef("fleet"); // Keep ref in sync useEffect(() => { activeSubTabRef.current = activeSubTab; }, [activeSubTab]); // Fetch host list from /api/hosts const fetchHosts = useCallback(async () => { try { const res = await fetch("/api/hosts", { headers: { "X-Session-Token": sessionToken } }); if (res.ok) { setFetchError(null); const data = await res.json(); const rawHosts = data.hosts || data || []; const mapped = rawHosts.map((h) => ({ id: h.hostname || h.id, name: h.display_name || h.hostname || h.name || h.id, hostname: h.hostname || h.id, status: h.status === "online" || h.status === "healthy" ? "healthy" : h.status === "degraded" ? "degraded" : "offline", profile: h.profile || "unknown", agent_type: h.agent_type || PROFILE_TO_AGENT_TYPE_MAP[h.profile] || "monitoring", provider: h.provider || "ollama", model: h.model || h.ollama_model || "", ip: h.ip || h.address || "", ollama_model: h.ollama_model || "", uptime: h.uptime || "", has_gpu: h.has_gpu || false, })); setHosts(mapped); setLastRefresh(new Date()); } } catch (e) { setFetchError("Fleet API unreachable"); } finally { setLoading(false); } }, [sessionToken]); // Fetch metrics for a single host via /api/console/exec const fetchMetricsForHost = useCallback(async (hostname) => { try { const res = await fetch("/api/console/exec", { method: "POST", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, body: JSON.stringify({ host: hostname, tool: "get_metrics", args: {} }), }); if (!res.ok) return; const data = await res.json(); const result = data.results?.[hostname]; if (result?.ok) { const parsed = parseMetricsResult(result.result); setHostMetrics((prev) => ({ ...prev, [hostname]: parsed })); } } catch {} }, [sessionToken]); // Fetch services for a single host const fetchServicesForHost = useCallback(async (hostname) => { try { const res = await fetch("/api/console/exec", { method: "POST", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, body: JSON.stringify({ host: hostname, tool: "list_services", args: {} }), }); if (!res.ok) return; const data = await res.json(); const result = data.results?.[hostname]; if (result?.ok) { const text = Array.isArray(result.result) ? result.result.join("\n") : String(result.result); // Try to parse as JSON first (structured response) let parsed; try { const obj = JSON.parse(text); parsed = parseServiceList(obj.services || text); } catch { parsed = parseServiceList(text); } setHostServices((prev) => ({ ...prev, [hostname]: parsed })); } } catch {} }, [sessionToken]); // Fetch alerts from /api/alerts const fetchAlerts = useCallback(async () => { try { const res = await fetch("/api/alerts", { headers: { "X-Session-Token": sessionToken } }); if (res.ok) { const data = await res.json(); setAlerts(data.alerts || []); } } catch {} }, [sessionToken]); // Fetch agent status from /api/agents/status const fetchAgentStatus = useCallback(async () => { try { const res = await fetch("/api/agents/status", { headers: { "X-Session-Token": sessionToken } }); if (res.ok) { const data = await res.json(); setAgentStatus(data.agents || []); if (data.dist_version) setDistVersion(data.dist_version); } } catch {} }, [sessionToken]); // Host list polling (30s) useEffect(() => { fetchHosts(); const interval = setInterval(fetchHosts, 30000); return () => clearInterval(interval); }, [fetchHosts]); // Metrics polling (30s, staggered, only when fleet/gpu sub-tab active) useEffect(() => { if (hosts.length === 0) return; const tab = activeSubTabRef.current; if (tab !== "fleet" && tab !== "gpu") return; // Fetch all immediately hosts.forEach((h) => fetchMetricsForHost(h.id)); const interval = 30000; const stagger = Math.max(Math.floor(interval / hosts.length), 2000); const timers = hosts.map((host, i) => setTimeout(() => { fetchMetricsForHost(host.id); const repeater = setInterval(() => fetchMetricsForHost(host.id), interval); timers[i] = repeater; }, i * stagger) ); return () => timers.forEach((t) => clearTimeout(t) || clearInterval(t)); }, [hosts.length, activeSubTab, fetchMetricsForHost]); // Services polling (60s, only when services sub-tab active) useEffect(() => { if (hosts.length === 0 || activeSubTab !== "services") return; hosts.forEach((h) => fetchServicesForHost(h.id)); const interval = setInterval(() => hosts.forEach((h) => fetchServicesForHost(h.id)), 60000); return () => clearInterval(interval); }, [hosts.length, activeSubTab, fetchServicesForHost]); // Alerts polling (15s when active, 60s for badge) useEffect(() => { fetchAlerts(); const ms = activeSubTab === "alerts" ? 15000 : 60000; const interval = setInterval(fetchAlerts, ms); return () => clearInterval(interval); }, [activeSubTab, fetchAlerts]); // Agent status polling (60s, when activity tab active) useEffect(() => { if (activeSubTab !== "activity") return; fetchAgentStatus(); const interval = setInterval(fetchAgentStatus, 60000); return () => clearInterval(interval); }, [activeSubTab, fetchAgentStatus]); const unackedAlertCount = alerts.filter((a) => !a.acknowledged).length; const hasGPU = hosts.some((h) => h.has_gpu || hostMetrics[h.id]?.gpu); const subTabs = [ { id: "fleet", label: "Fleet" }, { id: "services", label: "Services" }, { id: "alerts", label: "Alerts", badge: unackedAlertCount || null }, ...(hasGPU ? [{ id: "gpu", label: "GPU" }] : []), { id: "activity", label: "Activity" }, ]; return (
{hosts.length > 0 && ( {hosts.length} {hosts.length === 1 ? "agent" : "agents"} )} {hosts.length === 0 && !loading && !fetchError && ( No agents registered )} {fetchError && ( {fetchError} )} {lastRefresh && ( Updated {lastRefresh.toLocaleTimeString()} )}
{subTabs.map((tab) => ( ))}
{activeSubTab === "fleet" && } {activeSubTab === "services" && } {activeSubTab === "alerts" && } {activeSubTab === "gpu" && } {activeSubTab === "activity" && ( setChatAgent(agent)} /> )}
{chatAgent && ( setChatAgent(null)} /> )}
); } // ============================================================================ // BUSINESS OPERATION TABS // ============================================================================ function BusinessTab({ tabId, tabLabel, user, sessionToken, accessLevel, hasTabChat }) { const isAdmin = user?.roles?.includes("admin") || user?.roles?.includes("fleet_ops"); const toast = useToast(); const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [isLoading, setIsLoading] = useState(false); const [attachedFiles, setAttachedFiles] = useState([]); const [showFindings, setShowFindings] = useState(false); const [findingsCount, setFindingsCount] = useState(0); const [findingsPollFails, setFindingsPollFails] = useState(0); const findingsAutoOpenedRef = useRef(false); const messagesEndRef = useRef(null); const abortRef = useRef(null); const chatInputRef = useRef(null); useEffect(() => { setTimeout(() => chatInputRef.current?.focus(), 200); }, []); // Fetch tab-specific chat history on mount useEffect(() => { if (!sessionToken) return; fetch(`/api/chat/history?tab=${tabId}`, { headers: { "X-Session-Token": sessionToken }, }) .then((r) => r.ok ? r.json() : Promise.reject("Failed")) .then((data) => { if (data.messages && data.messages.length > 0) { setMessages(data.messages.map((msg, idx) => ({ id: idx + 1, role: msg.role, content: msg.content, timestamp: new Date(msg.timestamp || Date.now()), workflows: msg.workflows || undefined, exports: msg.exports || undefined, }))); } else { setMessages([{ id: 1, role: "assistant", content: `Hi ${user?.display_name || "there"}! I'm HAL, your ${tabLabel} assistant. I can query live data, generate reports, and help with analysis.${accessLevel === "restricted" ? " (You have read-only access.)" : ""}\n\nTry asking me something like:\n- "Show me a summary of current activity"\n- "How many items are in process?"\n- "What are the top trends this week?"`, timestamp: new Date(), }]); } }) .catch(() => { setMessages([{ id: 1, role: "assistant", content: `Hi! I'm HAL, assisting with ${tabLabel}. How can I help?`, timestamp: new Date(), }]); }); }, [sessionToken, tabId]); // Poll findings count for badge useEffect(() => { if (!sessionToken || !tabId) return; const poll = () => { fetch(`/api/findings?tab_id=${tabId}&limit=1&status=active`, { headers: { "X-Session-Token": sessionToken } }) .then((r) => r.ok ? r.json() : Promise.reject()) .then((data) => { const count = data.total || 0; setFindingsCount(count); setFindingsPollFails(0); // Auto-open panel on first poll if there are active findings if (count > 0 && !findingsAutoOpenedRef.current) { findingsAutoOpenedRef.current = true; setShowFindings(true); } }) .catch(() => { setFindingsPollFails(prev => prev + 1); }); }; poll(); const interval = setInterval(poll, 30000); return () => clearInterval(interval); }, [sessionToken, tabId]); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); const handleSend = async () => { if ((!input.trim() && !attachedFiles.length) || isLoading) return; // Check for slash commands const parsed = parseSlashCommand(input); if (parsed) { const cmdMsg = { id: messages.length + 1, role: "user", content: input, timestamp: new Date(), isCommand: true }; setMessages(prev => [...prev, cmdMsg]); setInput(""); setIsLoading(true); try { const res = await fetch("/api/chat/command", { method: "POST", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, body: JSON.stringify({ command: parsed.command, args: parsed.args, tab_id: tabId }), }); const data = await res.json(); formatCommandResponse(data, setMessages, user, sessionToken); } catch (err) { setMessages(prev => [...prev, { id: Date.now(), role: "system", content: `Command failed: ${err.message}`, timestamp: new Date(), isCommand: true }]); } finally { setIsLoading(false); setTimeout(() => chatInputRef.current?.focus(), 50); } return; } const fileIds = attachedFiles.map((f) => f.file_id); const userMessage = { id: messages.length + 1, role: "user", content: input || "[Attached file(s)]", timestamp: new Date(), files: attachedFiles.length > 0 ? [...attachedFiles] : undefined, }; setMessages((prev) => [...prev, userMessage]); setInput(""); setAttachedFiles([]); setIsLoading(true); const assistantId = messages.length + 2; const controller = new AbortController(); abortRef.current = controller; setMessages((prev) => [...prev, { id: assistantId, role: "assistant", content: "", timestamp: new Date() }]); try { const res = await fetch("/api/chat/send", { method: "POST", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, body: JSON.stringify({ content: input, tab_id: tabId, file_ids: fileIds.length ? fileIds : undefined }), signal: controller.signal, }); if (!res.ok) { const errBody = await res.json().catch(() => ({})); throw new Error(errBody.detail || `Error ${res.status}`); } const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() || ""; for (const line of lines) { if (!line.startsWith("data: ")) continue; try { const data = JSON.parse(line.slice(6)); if (data.type === "tool_start" || data.type === "tool_result") { if (isAdmin) { setMessages((prev) => prev.map((msg) => msg.id === assistantId ? { ...msg, content: msg.content + data.content } : msg )); } } else if (data.type === "chunk" || data.type === "error") { setMessages((prev) => prev.map((msg) => msg.id === assistantId ? { ...msg, content: msg.content + data.content } : msg )); } else if (data.type === "workflow_update" && data.workflow) { setMessages((prev) => prev.map((msg) => msg.id === assistantId ? { ...msg, workflows: [...(msg.workflows || []), data.workflow] } : msg )); } else if (data.type === "export_started" && data.job_id) { setMessages((prev) => prev.map((msg) => msg.id === assistantId ? { ...msg, exports: [...(msg.exports || []), { job_id: data.job_id, format: data.format }] } : msg )); } } catch {} } } } catch (err) { if (err.name !== "AbortError") { setMessages((prev) => prev.map((msg) => msg.id === assistantId ? { ...msg, content: `Error: ${err.message}` } : msg )); } } finally { abortRef.current = null; setIsLoading(false); setTimeout(() => chatInputRef.current?.focus(), 50); } }; const handleStop = () => { if (abortRef.current) abortRef.current.abort(); }; return (
{/* Chat column */}
{/* Messages — scrollable area */}
{messages.filter((msg) => { const visibleContent = isAdmin ? (msg.content || "").trim() : stripToolLines(msg.content); if (msg.role === "assistant" && !visibleContent && !(msg.workflows?.length) && !(msg.exports?.length)) return false; return true; }).map((msg) => (
{msg.role === "user" ? {msg.content} : } {msg.files && msg.files.length > 0 && (
{msg.files.map((f) => ( {f.filename} ))}
)} {msg.workflows && msg.workflows.length > 0 && msg.workflows.map((wf) => ( ))} {msg.exports && msg.exports.length > 0 && msg.exports.map((exp) => ( ))} {msg.timestamp &&

{msg.timestamp.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}

}
))}
{/* Input bar — pinned to bottom */}
setInput(cmd + " ")} /> setInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); } }} placeholder={attachedFiles.length > 0 ? "Describe what to do with the files..." : `Ask HAL about ${tabLabel}... (/ for commands)`} className="flex-1 bg-gray-900 border border-gray-700 rounded-lg px-4 py-2.5 text-sm text-white focus:outline-none focus:border-amber-500" disabled={isLoading} /> {isLoading ? ( ) : ( )}
{/* Findings sidebar */} setShowFindings(false)} />
); } function MFAAdminSubTab({ sessionToken }) { const [mfaConfig, setMfaConfig] = React.useState(null); const [users, setUsers] = React.useState([]); const [saving, setSaving] = React.useState(false); const VALID_ROLES = ["admin", "fleet_ops", "distribution_ops", "config_center", "services", "front_office", "sales", "manager", "executive"]; const fetchConfig = React.useCallback(async () => { try { const r = await fetch("/api/admin/mfa-config", { headers: { "X-Session-Token": sessionToken } }); if (r.ok) setMfaConfig(await r.json()); } catch {} }, [sessionToken]); const fetchUsers = React.useCallback(async () => { try { const r = await fetch("/api/admin/users", { headers: { "X-Session-Token": sessionToken } }); if (r.ok) { const d = await r.json(); setUsers(d.users || []); } } catch {} }, [sessionToken]); React.useEffect(() => { fetchConfig(); fetchUsers(); }, []); const saveConfig = async () => { setSaving(true); try { await fetch("/api/admin/mfa-config", { method: "PUT", headers: { "X-Session-Token": sessionToken, "Content-Type": "application/json" }, body: JSON.stringify(mfaConfig), }); fetchConfig(); } catch {} setSaving(false); }; const resetUserMFA = async (username) => { if (!confirm(`Reset all MFA for ${username}? They will need to re-enroll.`)) return; try { const r = await fetch(`/api/admin/users/${username}/mfa/reset`, { method: "POST", headers: { "X-Session-Token": sessionToken }, }); if (r.ok) fetchUsers(); } catch {} }; if (!mfaConfig) return
Loading MFA configuration...
; return (

Multi-Factor Authentication

{/* Global Settings */}

Global Settings

setMfaConfig({...mfaConfig, trusted_device_days: parseInt(e.target.value) || 7})} className="bg-gray-700 text-gray-200 text-sm rounded px-2 py-1 border border-gray-600 w-20" />
setMfaConfig({...mfaConfig, grace_logins: parseInt(e.target.value) || 3})} className="bg-gray-700 text-gray-200 text-sm rounded px-2 py-1 border border-gray-600 w-20" />
{/* Allowed methods */}
{/* WebAuthn RP settings */}
setMfaConfig({...mfaConfig, webauthn_rp_id: e.target.value})} className="bg-gray-700 text-gray-200 text-sm rounded px-2 py-1 border border-gray-600 w-48" />
setMfaConfig({...mfaConfig, webauthn_rp_name: e.target.value})} className="bg-gray-700 text-gray-200 text-sm rounded px-2 py-1 border border-gray-600 w-48" />
{/* Per-role enforcement */}
{VALID_ROLES.map(role => (
{role}
))}
{/* User MFA Status */}

User MFA Status

{users.map(u => { const mfa = u.mfa || {}; const totpOk = mfa.totp_enabled && mfa.totp_verified; const waCount = (mfa.webauthn_credentials || []).length; const devCount = (mfa.trusted_devices || []).length; return ( ); })}
User TOTP WebAuthn Devices Required Actions
{u.display_name || u.username} {totpOk ? Enabled : Off} {waCount > 0 ? {waCount} key{waCount > 1 ? "s" : ""} : None} {devCount} {u.roles && u.roles.some(r => (mfaConfig.role_enforcement || {})[r] === "required") || mfaConfig.enforcement === "required" ? Required : Optional} {(totpOk || waCount > 0) && ( )}
); } function LogsSubTab({ sessionToken }) { const [logConfig, setLogConfig] = React.useState(null); const [logEntries, setLogEntries] = React.useState([]); const [logSource, setLogSource] = React.useState("dashboard"); const [logSubFile, setLogSubFile] = React.useState("mcp-server"); const [logLevel, setLogLevel] = React.useState("ALL"); const [logSearch, setLogSearch] = React.useState(""); const [logLines, setLogLines] = React.useState(200); const [logLoading, setLogLoading] = React.useState(false); const [logAutoRefresh, setLogAutoRefresh] = React.useState(false); const [complianceConfig, setComplianceConfig] = React.useState(null); const [complianceEntries, setComplianceEntries] = React.useState([]); const [showCompliance, setShowCompliance] = React.useState(false); const [expandedCompliance, setExpandedCompliance] = React.useState(null); const [agents, setAgents] = React.useState([]); const fetchLogConfig = React.useCallback(async () => { try { const r = await fetch("/api/admin/log-config", { headers: { "X-Session-Token": sessionToken } }); if (r.ok) setLogConfig(await r.json()); } catch {} }, [sessionToken]); const fetchAgents = React.useCallback(async () => { try { const r = await fetch("/api/hosts", { headers: { "X-Session-Token": sessionToken } }); if (r.ok) { const data = await r.json(); setAgents((data.hosts || []).filter(h => h.status === "online")); } } catch {} }, [sessionToken]); const fetchLogs = React.useCallback(async () => { setLogLoading(true); try { const isAgent = !["dashboard", "coordinator", "console_audit"].includes(logSource); const params = new URLSearchParams({ file: isAgent ? logSubFile : logSource, lines: logLines.toString(), ...(logLevel !== "ALL" && { level: logLevel }), ...(logSearch && { search: logSearch }), }); const url = isAgent ? `/api/agent/${logSource}/logs?${params}` : `/api/logs/coordinator?${params}`; const r = await fetch(url, { headers: { "X-Session-Token": sessionToken } }); if (r.ok) { const data = await r.json(); setLogEntries(data.entries || []); } } catch {} setLogLoading(false); }, [sessionToken, logSource, logSubFile, logLevel, logSearch, logLines]); const fetchComplianceConfig = React.useCallback(async () => { try { const r = await fetch("/api/admin/compliance-config", { headers: { "X-Session-Token": sessionToken } }); if (r.ok) setComplianceConfig(await r.json()); } catch {} }, [sessionToken]); const fetchComplianceLogs = React.useCallback(async () => { try { const r = await fetch("/api/admin/compliance-logs?lines=100", { headers: { "X-Session-Token": sessionToken } }); if (r.ok) { const data = await r.json(); setComplianceEntries(data.entries || []); } } catch {} }, [sessionToken]); React.useEffect(() => { fetchLogConfig(); fetchAgents(); fetchComplianceConfig(); }, []); React.useEffect(() => { fetchLogs(); }, [logSource, logSubFile]); React.useEffect(() => { if (!logAutoRefresh) return; const iv = setInterval(fetchLogs, 5000); return () => clearInterval(iv); }, [logAutoRefresh, fetchLogs]); const applyLogConfig = async () => { if (!logConfig) return; try { await fetch("/api/admin/log-config", { method: "PUT", headers: { "X-Session-Token": sessionToken, "Content-Type": "application/json" }, body: JSON.stringify(logConfig), }); fetchLogConfig(); } catch {} }; const updateComplianceConfig = async (updates) => { const newCfg = { ...complianceConfig, ...updates }; try { await fetch("/api/admin/compliance-config", { method: "PUT", headers: { "X-Session-Token": sessionToken, "Content-Type": "application/json" }, body: JSON.stringify(newCfg), }); setComplianceConfig(newCfg); } catch {} }; const levelColors = { DEBUG: "text-gray-500", INFO: "text-gray-300", WARNING: "text-yellow-400", ERROR: "text-red-400", CRITICAL: "text-red-300 font-bold" }; const levelOptions = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]; const isAgent = !["dashboard", "coordinator", "console_audit"].includes(logSource); return (

Logging

{/* Log Level Controls */}

Log Levels

{agents.map(a => (
))}
{/* Log Viewer */}

Log Viewer

{/* Source tabs */}
{["dashboard", "coordinator", "console_audit"].map(s => ( ))} {agents.map(a => ( ))}
{/* Agent sub-file selector */} {isAgent && (
{["mcp-server", "local-agent", "analytics-audit"].map(f => ( ))}
)} {/* Filters */}
setLogSearch(e.target.value)} onKeyDown={e => e.key === "Enter" && fetchLogs()} className="bg-gray-700 text-gray-200 text-xs rounded px-2 py-1 border border-gray-600 flex-1" /> setLogLines(Math.min(parseInt(e.target.value) || 200, 2000))} className="bg-gray-700 text-gray-200 text-xs rounded px-2 py-1 border border-gray-600 w-20" /> lines
{/* Log output */}
{logEntries.length === 0 &&

No log entries{logLoading ? " (loading...)" : ""}

} {logEntries.map((e, i) => (
{(e.ts || "").replace("T", " ").slice(0, 19)} {" "} {(e.level || "").padEnd(8)} {" "} {e.logger || ""} {" "} {e.msg || JSON.stringify(e)}
))}
{/* Compliance Panel */}
setShowCompliance(!showCompliance)}>

Compliance Logging

{complianceConfig?.enabled ? "Enabled" : "Disabled"} {showCompliance ? "▲" : "▼"}
{showCompliance && complianceConfig && (
updateComplianceConfig({ response_preview_length: parseInt(e.target.value) || 500 })} className="bg-gray-700 text-gray-200 text-sm rounded px-2 py-1 border border-gray-600 w-24" />
updateComplianceConfig({ exclude_users: e.target.value.split(",").map(s => s.trim()).filter(Boolean) })} className="bg-gray-700 text-gray-200 text-sm rounded px-2 py-1 border border-gray-600 w-48" />
{complianceConfig.enabled && (

Recent Compliance Entries

{complianceEntries.length === 0 &&

No compliance entries

} {complianceEntries.map((e, i) => ( setExpandedCompliance(expandedCompliance === i ? null : i)}> {expandedCompliance === i && ( )} ))}
Time User Tab Prompt Model Duration
{expandedCompliance === i ? "▼" : "▶"} {(e.ts || "").replace("T", " ").slice(0, 19)} {e.username} {e.tab_id} {(e.prompt || "").slice(0, 80)} {e.model} {e.duration_ms ? `${(e.duration_ms/1000).toFixed(1)}s` : ""}
Prompt:
{e.prompt}
Response:
{e.response || e.response_preview || "(no response logged)"}
)}
)}
); } function AdminPanel({ sessionToken, user, tabId, tabLabel, accessLevel, hasTabChat }) { const toast = useToast(); const [adminSubTab, setAdminSubTab] = useState("users"); const [llmConfig, setLlmConfig] = useState(null); const [llmSaving, setLlmSaving] = useState(false); const [llmError, setLlmError] = useState(""); const [llmSuccess, setLlmSuccess] = useState(""); const [llmTestResult, setLlmTestResult] = useState(null); const [llmTesting, setLlmTesting] = useState(false); // Tab Config state const [tabCfg, setTabCfg] = useState(null); const [tabCfgSaving, setTabCfgSaving] = useState(false); const [tabCfgError, setTabCfgError] = useState(""); const [tabCfgSuccess, setTabCfgSuccess] = useState(""); const [selectedTabId, setSelectedTabId] = useState(""); // Agent Config state const [agentCfgs, setAgentCfgs] = useState({ configs: {} }); const [agentCfgSaving, setAgentCfgSaving] = useState(false); const [agentCfgError, setAgentCfgError] = useState(""); const [agentCfgSuccess, setAgentCfgSuccess] = useState(""); const [selectedAgentHost, setSelectedAgentHost] = useState(""); const [agentStatusList, setAgentStatusList] = useState([]); // Add / Remove agent state const [showAddAgent, setShowAddAgent] = useState(false); const AGENT_TYPE_DEFAULTS = { monitoring: { provider: "ollama", model: "qwen3:8b", profile: "ollama-only" }, inference: { provider: "ollama", model: "qwen3:14b", profile: "ollama-only" }, coding: { provider: "anthropic", model: "claude-sonnet-4-5-20250929", profile: "claude-code" }, triage: { provider: "ollama", model: "qwen3:8b", profile: "ollama-only" }, analytics: { provider: "anthropic", model: "claude-sonnet-4-5-20250929", profile: "ollama-only" }, "analytics-secure": { provider: "vllm", model: "Qwen/Qwen3-32B-AWQ", profile: "ollama-only" }, }; const [newAgent, setNewAgent] = useState({ hostname: "", ip: "", port: "8900", profile: "ollama-only", agent_type: "monitoring", provider: "ollama", model: "" }); // Watchdog Config state const [watchdogCfg, setWatchdogCfg] = useState(null); const [watchdogSaving, setWatchdogSaving] = useState(false); const [selectedCheckId, setSelectedCheckId] = useState(""); const [testResult, setTestResult] = useState(null); const [testing, setTesting] = useState(false); const [selectedDiscoveryId, setSelectedDiscoveryId] = useState(""); const handleAddAgent = async () => { if (!newAgent.hostname || !newAgent.ip) { setAgentCfgError("Hostname and IP are required"); return; } setAgentCfgError(""); try { const res = await fetch("/api/hosts", { method: "POST", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, body: JSON.stringify({ hostname: newAgent.hostname, ip: newAgent.ip, port: parseInt(newAgent.port), profile: newAgent.profile, agent_type: newAgent.agent_type, provider: newAgent.provider, model: newAgent.model }), }); if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.detail || "Failed to add agent"); } setShowAddAgent(false); setNewAgent({ hostname: "", ip: "", port: "8900", profile: "ollama-only", agent_type: "monitoring", provider: "ollama", model: "" }); setAgentCfgSuccess("Agent added to fleet."); setTimeout(() => setAgentCfgSuccess(""), 3000); // Refresh agent list const statusData = await fetch("/api/agents/status", { headers: { "X-Session-Token": sessionToken } }).then(r => r.ok ? r.json() : { agents: [] }); setAgentStatusList(statusData.agents || statusData || []); } catch (err) { setAgentCfgError(err.message); } }; const handleRemoveAgent = async (hostname) => { if (!confirm(`Remove agent "${hostname}" from fleet? This only removes the registration — the agent's services will keep running on the host.`)) return; setAgentCfgError(""); try { const res = await fetch(`/api/hosts/${hostname}`, { method: "DELETE", headers: { "X-Session-Token": sessionToken }, }); if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.detail || "Failed to remove agent"); } setSelectedAgentHost(""); setAgentCfgSuccess("Agent removed from fleet."); setTimeout(() => setAgentCfgSuccess(""), 3000); // Refresh agent list const statusData = await fetch("/api/agents/status", { headers: { "X-Session-Token": sessionToken } }).then(r => r.ok ? r.json() : { agents: [] }); setAgentStatusList(statusData.agents || statusData || []); } catch (err) { setAgentCfgError(err.message); } }; useEffect(() => { if (!sessionToken) return; fetch("/api/admin/llm-config", { headers: { "X-Session-Token": sessionToken } }) .then((r) => r.ok ? r.json() : Promise.reject("Failed to load LLM config")) .then((data) => setLlmConfig(data)) .catch(() => setLlmConfig({ ...{provider:"anthropic",model:"claude-sonnet-4-5-20250929",api_key:"",base_url:"",temperature:0.7,max_tokens:4096,system_prompt:"",smart_routing:true,api_key_set:false} })); }, [sessionToken]); // Fetch tab config useEffect(() => { if (!sessionToken) return; fetch("/api/admin/tab-config", { headers: { "X-Session-Token": sessionToken } }) .then((r) => r.ok ? r.json() : Promise.reject("Failed")) .then((data) => { setTabCfg(data); const tabIds = Object.keys(data.tabs || {}); if (tabIds.length > 0 && !selectedTabId) setSelectedTabId(tabIds[0]); }) .catch(() => setTabCfg(null)); }, [sessionToken]); // Fetch agent configs + agent status useEffect(() => { if (!sessionToken) return; Promise.all([ fetch("/api/admin/agent-config", { headers: { "X-Session-Token": sessionToken } }) .then(r => r.ok ? r.json() : { configs: {} }), fetch("/api/agents/status", { headers: { "X-Session-Token": sessionToken } }) .then(r => r.ok ? r.json() : { agents: [] }), ]).then(([cfgData, statusData]) => { setAgentCfgs(cfgData); const agents = statusData.agents || []; setAgentStatusList(agents); if (agents.length > 0 && !selectedAgentHost) setSelectedAgentHost(agents[0].hostname || agents[0].name || agents[0].id); }); }, [sessionToken]); // Fetch watchdog config useEffect(() => { if (!sessionToken) return; fetch("/api/admin/watchdog-config", { headers: { "X-Session-Token": sessionToken } }) .then(r => r.ok ? r.json() : null) .then(data => { if (data) setWatchdogCfg(data); }) .catch(() => {}); }, [sessionToken]); const handleSaveAgentConfig = async () => { if (!selectedAgentHost) return; setAgentCfgSaving(true); setAgentCfgError(""); setAgentCfgSuccess(""); try { const cfg = (agentCfgs?.configs || {})[selectedAgentHost] || {}; const res = await fetch(`/api/admin/agent-config/${selectedAgentHost}`, { method: "PUT", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, body: JSON.stringify(cfg), }); if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.detail || "Failed to save"); } setAgentCfgSuccess("Agent config saved. Agent will pick it up within 15 minutes."); setTimeout(() => setAgentCfgSuccess(""), 5000); } catch (err) { setAgentCfgError(err.message); } finally { setAgentCfgSaving(false); } }; const updateAgentCfg = (field, value) => { if (!selectedAgentHost || !agentCfgs) return; const current = (agentCfgs.configs || {})[selectedAgentHost] || { mode: "observer", prompt: "", interval: 300, enabled: true }; setAgentCfgs({ ...agentCfgs, configs: { ...agentCfgs.configs, [selectedAgentHost]: { ...current, [field]: value }, }, }); }; const handleSaveTabConfig = async () => { if (!tabCfg || !selectedTabId) return; setTabCfgSaving(true); setTabCfgError(""); setTabCfgSuccess(""); try { const tabData = { ...tabCfg.tabs[selectedTabId] }; const isNew = tabData._isNew; delete tabData._isNew; if (isNew) { // Create new business tab const res = await fetch("/api/admin/tab-config", { method: "POST", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, body: JSON.stringify({ tab_id: selectedTabId, ...tabData }), }); if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.detail || "Failed to create"); } // Clear _isNew flag in local state setTabCfg({ ...tabCfg, tabs: { ...tabCfg.tabs, [selectedTabId]: tabData } }); setTabCfgSuccess("Business tab created."); } else { // Update existing tab const res = await fetch("/api/admin/tab-config", { method: "PUT", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, body: JSON.stringify({ tabs: { [selectedTabId]: tabData } }), }); if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.detail || "Failed to save"); } setTabCfgSuccess("Tab configuration saved."); } setTimeout(() => setTabCfgSuccess(""), 3000); } catch (err) { setTabCfgError(err.message); } finally { setTabCfgSaving(false); } }; const updateSelectedTab = (field, value) => { if (!tabCfg || !selectedTabId) return; setTabCfg({ ...tabCfg, tabs: { ...tabCfg.tabs, [selectedTabId]: { ...tabCfg.tabs[selectedTabId], [field]: value } } }); }; const updateRoleAccess = (role, level) => { if (!tabCfg || !selectedTabId) return; const current = tabCfg.tabs[selectedTabId].role_access || {}; const updated = { ...current }; if (level === "none") { delete updated[role]; } else { updated[role] = level; } updateSelectedTab("role_access", updated); }; const handleSaveLlmConfig = async () => { setLlmSaving(true); setLlmError(""); setLlmSuccess(""); try { const res = await fetch("/api/admin/llm-config", { method: "PUT", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, body: JSON.stringify(llmConfig), }); if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.detail || "Failed to save"); } setLlmSuccess("Configuration saved."); setTimeout(() => setLlmSuccess(""), 3000); } catch (err) { setLlmError(err.message); } finally { setLlmSaving(false); } }; const handleTestLlm = async () => { setLlmTesting(true); setLlmTestResult(null); try { const res = await fetch("/api/admin/llm-test", { method: "POST", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, }); if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.detail || "Test failed"); } const data = await res.json(); setLlmTestResult({ ok: true, preview: data.response_preview }); } catch (err) { setLlmTestResult({ ok: false, error: err.message }); } finally { setLlmTesting(false); } }; // ── System Prompts state ── const [systemPrompts, setSystemPrompts] = useState(null); const [promptsSaving, setPromptsSaving] = useState(false); const [promptsError, setPromptsError] = useState(""); const [promptsSuccess, setPromptsSuccess] = useState(""); const [selectedPromptUser, setSelectedPromptUser] = useState(""); const [previewResult, setPreviewResult] = useState(null); const [previewLoading, setPreviewLoading] = useState(false); useEffect(() => { if (!sessionToken) return; fetch("/api/admin/system-prompts", { headers: { "X-Session-Token": sessionToken } }) .then((r) => r.ok ? r.json() : Promise.reject("Failed")) .then((data) => setSystemPrompts(data)) .catch(() => setSystemPrompts({ base: "", roles: {}, users: {} })); }, [sessionToken]); const handleSaveSystemPrompts = async () => { setPromptsSaving(true); setPromptsError(""); setPromptsSuccess(""); try { const res = await fetch("/api/admin/system-prompts", { method: "PUT", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, body: JSON.stringify(systemPrompts), }); if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.detail || "Failed to save"); } setPromptsSuccess("System prompts saved."); setTimeout(() => setPromptsSuccess(""), 3000); } catch (err) { setPromptsError(err.message); } finally { setPromptsSaving(false); } }; const handlePreviewPrompt = async (username) => { setPreviewLoading(true); setPreviewResult(null); try { const res = await fetch("/api/admin/system-prompts/preview", { method: "POST", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, body: JSON.stringify({ username }), }); if (!res.ok) throw new Error("Preview failed"); const data = await res.json(); setPreviewResult(data); } catch (err) { setPreviewResult({ error: err.message }); } finally { setPreviewLoading(false); } }; const [users, setUsers] = useState([]); // Fetch users from API useEffect(() => { if (!sessionToken) return; fetch("/api/admin/users", { headers: { "X-Session-Token": sessionToken } }) .then((r) => r.ok ? r.json() : Promise.reject("Failed")) .then((data) => { const list = Array.isArray(data) ? data : (data.users || []); setUsers(list); }) .catch(() => {}); }, [sessionToken]); const [showNewUserForm, setShowNewUserForm] = useState(false); const [newUser, setNewUser] = useState({ username: "", password: "", display_name: "", email: "", department: "", roles: [], ldap_bypass: false, }); const [passwordRules, setPasswordRules] = useState(null); const [createUserError, setCreateUserError] = useState(""); const [ldapLookupStatus, setLdapLookupStatus] = useState(""); // "", "looking", "found", "not_found", "error" const [ldapLookupError, setLdapLookupError] = useState(""); // LDAP lookup when username changes (debounced) useEffect(() => { if (!sessionToken || !ldapStatus?.enabled || newUser.ldap_bypass || !newUser.username || newUser.username.length < 2) { setLdapLookupStatus(""); setLdapLookupError(""); return; } const timer = setTimeout(() => { setLdapLookupStatus("looking"); setLdapLookupError(""); fetch(`/api/admin/ldap/lookup/${encodeURIComponent(newUser.username)}`, { headers: { "X-Session-Token": sessionToken } }) .then((r) => r.ok ? r.json() : Promise.reject("Failed")) .then((data) => { if (data.found) { setLdapLookupStatus("found"); setNewUser((prev) => ({ ...prev, display_name: prev.display_name || data.display_name || "", email: prev.email || data.email || "", department: prev.department || data.department || "", })); } else if (data.error && !data.error.includes("not found")) { setLdapLookupStatus("error"); setLdapLookupError(data.error); } else { setLdapLookupStatus("not_found"); } }) .catch(() => setLdapLookupStatus("")); }, 600); return () => clearTimeout(timer); }, [newUser.username, ldapStatus?.enabled, newUser.ldap_bypass, sessionToken]); // Fetch password rules useEffect(() => { if (!sessionToken) return; fetch("/api/admin/password-rules", { headers: { "X-Session-Token": sessionToken } }) .then((r) => r.ok ? r.json() : Promise.reject("Failed")) .then((data) => setPasswordRules(data)) .catch(() => setPasswordRules({ min_length: 12, require_uppercase: true, require_lowercase: true, require_digit: true, require_special: true })); }, [sessionToken]); // LDAP status state const [ldapStatus, setLdapStatus] = useState(null); useEffect(() => { if (!sessionToken) return; fetch("/api/admin/ldap/status", { headers: { "X-Session-Token": sessionToken } }) .then((r) => r.ok ? r.json() : Promise.reject("Failed")) .then((data) => setLdapStatus(data)) .catch(() => setLdapStatus({ enabled: false, status: "unknown" })); }, [sessionToken]); const [editingUser, setEditingUser] = useState(null); // username being edited const [editFields, setEditFields] = useState({}); const handleStartEdit = (user) => { setEditingUser(user.username); setEditFields({ display_name: user.display_name || "", email: user.email || "", department: user.department || "" }); }; const handleSaveEdit = async (username) => { try { const res = await fetch(`/api/admin/users/${username}`, { method: "PATCH", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, body: JSON.stringify(editFields), }); if (res.ok) { setUsers(users.map((u) => u.username === username ? { ...u, ...editFields } : u)); setEditingUser(null); } } catch (err) {} }; const handleDeleteUser = async (username) => { if (!confirm(`Delete user "${username}"? This cannot be undone.`)) return; try { const res = await fetch(`/api/admin/users/${username}`, { method: "DELETE", headers: { "X-Session-Token": sessionToken }, }); if (res.ok) { setUsers(users.filter((u) => u.username !== username)); } else { const err = await res.json().catch(() => ({})); toast(err.detail || "Failed to delete user", "error"); } } catch (err) { toast("Failed to delete user", "error"); } }; const handleToggleLdapBypass = (u) => { const newVal = !u.ldap_bypass; fetch(`/api/admin/users/${u.username}`, { method: "PATCH", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, body: JSON.stringify({ ldap_bypass: newVal }), }) .then((r) => r.ok ? r.json() : Promise.reject("Failed")) .then(() => setUsers(users.map((x) => x.username === u.username ? { ...x, ldap_bypass: newVal } : x))) .catch(() => {}); }; const [show2FAModal, setShow2FAModal] = useState(false); const [selected2FAUser, setSelected2FAUser] = useState(null); const [twoFAMethod, setTwoFAMethod] = useState("totp"); const [twoFAQRCode, setTwoFAQRCode] = useState(null); const [twoFAVerifyCode, setTwoFAVerifyCode] = useState(""); const [twoFAModalError, setTwoFAModalError] = useState(""); const [twoFAStep, setTwoFAStep] = useState("method"); // "method", "setup", "verify" const allRoles = ["admin", "fleet_ops", "distribution_ops", "config_center", "services", "front_office", "sales", "manager", "executive"]; const handleToggleRole = (username, role) => { const user = users.find((u) => u.username === username); if (!user) return; const newRoles = user.roles.includes(role) ? user.roles.filter((r) => r !== role) : [...user.roles, role]; fetch(`/api/admin/users/${username}/roles`, { method: "PUT", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, body: JSON.stringify({ roles: newRoles }), }) .then((r) => r.ok ? r.json() : Promise.reject("Failed")) .then(() => setUsers(users.map((u) => u.username === username ? { ...u, roles: newRoles } : u))) .catch(() => {}); }; const handleToggleEnabled = (username) => { const user = users.find((u) => u.username === username); if (!user) return; const newEnabled = !user.enabled; fetch(`/api/admin/users/${username}`, { method: "PATCH", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, body: JSON.stringify({ enabled: newEnabled }), }) .then((r) => r.ok ? r.json() : Promise.reject("Failed")) .then(() => setUsers(users.map((u) => u.username === username ? { ...u, enabled: newEnabled } : u))) .catch(() => {}); }; const needsLocalPassword = !ldapStatus?.enabled || newUser.ldap_bypass; const checkPasswordComplexity = (pw) => { if (!passwordRules || !pw) return []; const fails = []; if (pw.length < passwordRules.min_length) fails.push(`${passwordRules.min_length}+ characters`); if (passwordRules.require_uppercase && !/[A-Z]/.test(pw)) fails.push("uppercase letter"); if (passwordRules.require_lowercase && !/[a-z]/.test(pw)) fails.push("lowercase letter"); if (passwordRules.require_digit && !/[0-9]/.test(pw)) fails.push("digit"); if (passwordRules.require_special && !/[!@#$%^&*()_+\-=\[\]{}|;:',.<>?/~`]/.test(pw)) fails.push("special character"); return fails; }; const passwordIssues = needsLocalPassword ? checkPasswordComplexity(newUser.password) : []; const handleAddUser = async () => { setCreateUserError(""); if (!newUser.username || newUser.roles.length === 0) { setCreateUserError("Username and at least one role are required"); return; } if (needsLocalPassword && !newUser.password) { setCreateUserError("Password is required for local/bypass users"); return; } if (needsLocalPassword && passwordIssues.length > 0) { setCreateUserError("Password does not meet complexity requirements"); return; } try { const body = { username: newUser.username, display_name: newUser.display_name || newUser.username, email: newUser.email, department: newUser.department, roles: newUser.roles, ldap_bypass: newUser.ldap_bypass, }; if (needsLocalPassword) body.password = newUser.password; const res = await fetch("/api/admin/users", { method: "POST", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, body: JSON.stringify(body), }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.detail || "Failed to create user"); } // Refresh user list from API const listRes = await fetch("/api/admin/users", { headers: { "X-Session-Token": sessionToken } }); if (listRes.ok) { const data = await listRes.json(); const list = Array.isArray(data) ? data : (data.users || []); setUsers(list); } setNewUser({ username: "", password: "", display_name: "", email: "", department: "", roles: [], ldap_bypass: false }); setShowNewUserForm(false); } catch (err) { setCreateUserError(err.message); } }; const handleEnable2FA = async (user) => { setSelected2FAUser(user); setTwoFAMethod("totp"); setTwoFAStep("method"); setShow2FAModal(true); setTwoFAModalError(""); setTwoFAQRCode(null); setTwoFAVerifyCode(""); }; const handleSetup2FA = async () => { if (!selected2FAUser) return; setTwoFAModalError(""); try { const response = await fetch(`/api/admin/users/${selected2FAUser.username}/2fa/enable`, { method: "POST", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, body: JSON.stringify({ method: twoFAMethod }), }); if (response.ok) { const data = await response.json(); setTwoFAQRCode(data.qr_code); setTwoFAStep("verify"); } else { throw new Error("Failed to setup 2FA"); } } catch (err) { setTwoFAModalError(err.message || "Failed to setup 2FA"); } }; const handleVerify2FASetup = async () => { if (!selected2FAUser || !twoFAVerifyCode.trim()) { setTwoFAModalError("Please enter verification code"); return; } try { const response = await fetch(`/api/admin/users/${selected2FAUser.username}/2fa/verify-setup`, { method: "POST", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, body: JSON.stringify({ code: twoFAVerifyCode }), }); if (response.ok) { setUsers( users.map((u) => { if (u.username === selected2FAUser.username) { return { ...u, twofa_enabled: true, twofa_method: twoFAMethod }; } return u; }) ); setShow2FAModal(false); toast("2FA enabled successfully", "success"); } else { throw new Error("Verification failed"); } } catch (err) { setTwoFAModalError(err.message || "Verification failed"); } }; const [disable2FAConfirm, setDisable2FAConfirm] = useState(null); const handleDisable2FA = async (targetUser) => { try { const response = await fetch(`/api/admin/users/${targetUser.username}/2fa/disable`, { method: "POST", headers: { "Content-Type": "application/json", "X-Session-Token": sessionToken }, }); if (response.ok) { setUsers( users.map((u) => { if (u.username === targetUser.username) { return { ...u, twofa_enabled: false, twofa_method: null }; } return u; }) ); toast("2FA disabled successfully", "success"); } else { throw new Error("Failed to disable 2FA"); } } catch (err) { toast(err.message || "Failed to disable 2FA", "error"); } }; const showBaseUrl = llmConfig?.provider === "vllm" || llmConfig?.provider === "ollama"; const showApiKey = llmConfig?.provider === "anthropic" || llmConfig?.provider === "openai"; const showSmartRouting = llmConfig?.provider === "anthropic"; const modelSuggestions = { anthropic: ["claude-sonnet-4-5-20250929", "claude-haiku-4-5-20251001", "claude-opus-4-5-20251101"], openai: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo"], vllm: [], ollama: ["qwen3:14b", "llama3.1:8b", "mistral:7b"], }; return (
{adminSubTab === "llm" && llmConfig && (

LLM Configuration

{llmError &&
{llmError}
} {llmSuccess &&
{llmSuccess}
}
setLlmConfig({ ...llmConfig, model: e.target.value })} list="model-suggestions" className="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-white" /> {(modelSuggestions[llmConfig.provider] || []).map((m) =>
{showApiKey && (
setLlmConfig({ ...llmConfig, api_key: e.target.value })} placeholder={llmConfig.api_key_set ? "Leave blank to keep current" : "Enter API key"} className="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-white" /> {llmConfig.provider === "anthropic" && llmConfig.api_key_set && (

Key from environment or previous config

)}
)} {showBaseUrl && (
setLlmConfig({ ...llmConfig, base_url: e.target.value })} placeholder={llmConfig.provider === "ollama" ? "http://localhost:11434/v1" : "http://host:port/v1"} className="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-white" />
)}
setLlmConfig({ ...llmConfig, temperature: parseFloat(e.target.value) })} className="w-full" />
setLlmConfig({ ...llmConfig, max_tokens: parseInt(e.target.value) || 4096 })} min="1" max="32768" className="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-white" />
{showSmartRouting && ( )}