Files
rmm-frontend/src/App.jsx
Kaushik 91ad75178a feat: agent version badges, SHIFT path config and video search per system
- revert vite port to 4173 to match the Synology reverse-proxy upstream
- show agent version chip on each system card (or "legacy")
- SHIFT folder path editor per node, saved to backend
- search videos in a node's SHIFT folder by date/name, fetch from results
2026-07-16 17:32:07 +05:30

1529 lines
84 KiB
JavaScript

import React, { useState, useEffect, useRef } from 'react';
import {
Activity,
Cpu,
HardDrive,
Terminal,
Settings,
RefreshCw,
CheckCircle,
XCircle,
Zap,
Flame,
Server,
AlertTriangle,
Play,
Search,
Code,
FileCode,
Clock,
ChevronDown,
ChevronUp,
ArrowRight,
Sliders
} from 'lucide-react';
import sideBarLogo from './assets/side-bar-logo.png';
import { API_BASE, apiFetch } from './api';
function App() {
const [token, setToken] = useState(localStorage.getItem('rmm_token') || '');
const [loginUsername, setLoginUsername] = useState('');
const [loginPassword, setLoginPassword] = useState('');
const [loginError, setLoginError] = useState('');
const [activeTab, setActiveTab] = useState('dashboard'); // 'dashboard' | 'editor'
const [clients, setClients] = useState({});
const [telemetry, setTelemetry] = useState({});
const [logs, setLogs] = useState({});
const [whitelist, setWhitelist] = useState({});
// Remote video fetch state
const [fileTransfers, setFileTransfers] = useState({});
const [videoNameInputs, setVideoNameInputs] = useState({});
const [shiftPathInputs, setShiftPathInputs] = useState({});
const [searchInputs, setSearchInputs] = useState({});
// Dispatcher form state
const [selectedClients, setSelectedClients] = useState([]);
const [selectedCommand, setSelectedCommand] = useState('');
const [manualClient, setManualClient] = useState('');
const [manualCommand, setManualCommand] = useState('');
// Whitelist config editor state
const [editorText, setEditorText] = useState('{\n "nt": {},\n "posix": {}\n}');
const [jsonError, setJsonError] = useState(null); // Real-time JSON validation error
const [toast, setToast] = useState(null);
const [isSyncing, setIsSyncing] = useState(false);
const [filterClient, setFilterClient] = useState('all');
const [nodeFilter, setNodeFilter] = useState('all'); // 'all' | 'online' | 'offline'
const [searchQuery, setSearchQuery] = useState(''); // Live search filter for clients
const [expandedClients, setExpandedClients] = useState({}); // Expanded telemetry details for offline nodes
const terminalRef = useRef(null);
const showToast = (message, type = 'success') => {
setToast({ message, type });
setTimeout(() => setToast(null), 4000);
};
// 1. Initial configuration bootstrap
const fetchWhitelist = async () => {
try {
const response = await apiFetch(`${API_BASE}/api/get-raw-commands`);
if (response.ok) {
const data = await response.json();
setWhitelist(data);
setEditorText(JSON.stringify(data, null, 2));
}
} catch (err) {
console.error('Failed to load raw commands whitelist:', err);
}
};
// 2. Continuous telemetry & log sync polling
const syncFleetData = async () => {
setIsSyncing(true);
try {
// Sync active clients registry
const clientsRes = await apiFetch(`${API_BASE}/api/clients`);
if (clientsRes.ok) {
const clientsData = await clientsRes.json();
setClients(clientsData);
}
// Sync active telemetry details
const telRes = await apiFetch(`${API_BASE}/api/telemetry`);
if (telRes.ok) {
const telData = await telRes.json();
setTelemetry(telData);
}
// Sync circular history logs
const logsRes = await apiFetch(`${API_BASE}/api/logs`);
if (logsRes.ok) {
const logsData = await logsRes.json();
setLogs(logsData);
}
// Sync remote video fetch transfer states
const ftRes = await apiFetch(`${API_BASE}/api/file-transfers`);
if (ftRes.ok) {
setFileTransfers(await ftRes.json());
}
} catch (err) {
console.error('Network sync failure:', err);
} finally {
setIsSyncing(false);
}
};
useEffect(() => {
const handleUnauthorized = () => {
setToken('');
};
window.addEventListener('rmm_unauthorized', handleUnauthorized);
return () => window.removeEventListener('rmm_unauthorized', handleUnauthorized);
}, []);
useEffect(() => {
if (!token) return;
fetchWhitelist();
syncFleetData();
// Set up rapid 3-second fleet synchronization loop
const syncInterval = setInterval(syncFleetData, 3000);
return () => clearInterval(syncInterval);
}, [token]);
const handleLogin = async (e) => {
e.preventDefault();
setLoginError('');
try {
const response = await fetch(`${API_BASE}/api/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: loginUsername, password: loginPassword })
});
const data = await response.json();
if (response.ok) {
localStorage.setItem('rmm_token', data.token);
setToken(data.token);
setLoginPassword('');
showToast('Login successful!', 'success');
} else {
setLoginError(data.detail || 'Login failed. Invalid username or password.');
}
} catch (err) {
setLoginError('Cannot connect to RMM backend server.');
}
};
const handleRequestFile = async (clientId, overrideName) => {
const filename = (overrideName || videoNameInputs[clientId] || '').trim();
if (!filename) {
showToast('Enter a video file name first.', 'error');
return;
}
try {
const response = await apiFetch(
`${API_BASE}/api/request-file?client_id=${encodeURIComponent(clientId)}&filename=${encodeURIComponent(filename)}`,
{ method: 'POST' }
);
if (response.ok) {
showToast(`Requested '${filename}' from ${clientId}`);
syncFleetData();
} else {
const data = await response.json().catch(() => ({}));
showToast(data.detail || 'Failed to request file.', 'error');
}
} catch (err) {
showToast('Cannot connect to RMM backend server.', 'error');
}
};
const handleSaveShiftPath = async (clientId) => {
const path = (shiftPathInputs[clientId] ?? clients[clientId]?.shift_path ?? '').trim();
try {
const response = await apiFetch(
`${API_BASE}/api/set-shift-path?client_id=${encodeURIComponent(clientId)}&shift_path=${encodeURIComponent(path)}`,
{ method: 'POST' }
);
if (response.ok) {
showToast(`SHIFT path saved for ${clientId}`);
syncFleetData();
} else {
const data = await response.json().catch(() => ({}));
showToast(data.detail || 'Failed to save SHIFT path.', 'error');
}
} catch (err) {
showToast('Cannot connect to RMM backend server.', 'error');
}
};
const handleSearchVideos = async (clientId) => {
const query = (searchInputs[clientId] || '').trim();
if (!query) {
showToast('Enter a search term first (e.g. a date like 20260716).', 'error');
return;
}
try {
const response = await apiFetch(
`${API_BASE}/api/request-search?client_id=${encodeURIComponent(clientId)}&query=${encodeURIComponent(query)}`,
{ method: 'POST' }
);
if (response.ok) {
showToast(`Searching '${query}' on ${clientId}...`);
syncFleetData();
} else {
const data = await response.json().catch(() => ({}));
showToast(data.detail || 'Failed to start search.', 'error');
}
} catch (err) {
showToast('Cannot connect to RMM backend server.', 'error');
}
};
const handleCancelUpload = async (clientId, filename) => {
try {
const response = await apiFetch(
`${API_BASE}/api/cancel-upload?client_id=${encodeURIComponent(clientId)}&filename=${encodeURIComponent(filename)}`,
{ method: 'POST' }
);
if (response.ok) {
showToast(`Cancelled upload for '${filename}'`);
syncFleetData();
} else {
const data = await response.json().catch(() => ({}));
showToast(data.detail || 'Failed to cancel upload.', 'error');
}
} catch (err) {
showToast('Cannot connect to server.', 'error');
}
};
const handleLogout = () => {
localStorage.removeItem('rmm_token');
setToken('');
setClients({});
setTelemetry({});
setLogs({});
showToast('Logged out successfully.', 'success');
};
// Scroll terminal logs to bottom automatically when new actions populate,
// but only if the user is already looking at the latest logs (near the bottom)!
// If they scrolled up to inspect an error, we freeze the screen scroll.
useEffect(() => {
if (terminalRef.current) {
const { scrollTop, scrollHeight, clientHeight } = terminalRef.current;
// Define a 120px threshold close to the bottom
const isNearBottom = scrollHeight - scrollTop - clientHeight < 120;
const isFirstLoad = scrollTop === 0;
if (isNearBottom || isFirstLoad) {
terminalRef.current.scrollTop = scrollHeight;
}
}
}, [logs]);
// Real-time JSON validation
useEffect(() => {
try {
if (editorText.trim() === '') {
setJsonError('JSON configuration cannot be empty.');
return;
}
JSON.parse(editorText);
setJsonError(null);
} catch (err) {
setJsonError(err.message);
}
}, [editorText]);
// Dispatch command to client execution queue
const handleDispatch = async (e) => {
e.preventDefault();
// Resolve final list of targets
let finalClientsString = '';
if (manualClient.trim()) {
finalClientsString = manualClient.trim();
} else if (selectedClients.length > 0) {
finalClientsString = selectedClients.join(',');
}
const finalCommand = (manualCommand.trim() || selectedCommand).trim();
if (!finalClientsString) {
showToast('Please select target nodes or specify a manual name!', 'error');
return;
}
if (!finalCommand) {
showToast('Please specify a Whitelisted Operation Key!', 'error');
return;
}
try {
const response = await apiFetch(
`${API_BASE}/api/schedule-command?client_id=${encodeURIComponent(finalClientsString)}&command=${encodeURIComponent(finalCommand)}`,
{ method: 'POST' }
);
const data = await response.json();
if (response.ok) {
showToast(`Operation batch successfully dispatched to: ${finalClientsString}!`, 'success');
setManualCommand('');
setManualClient('');
setSelectedClients([]);
syncFleetData();
} else {
showToast(data.detail || 'Dispatch scheduling failed.', 'error');
}
} catch (err) {
showToast('Network error contacting central RMM server API.', 'error');
}
};
// Dynamic Whitelist Save Config
const handleSaveWhitelist = async () => {
if (jsonError) {
showToast('Cannot save configuration. Please resolve JSON errors first.', 'error');
return;
}
try {
const parsedConfig = JSON.parse(editorText);
const response = await apiFetch(`${API_BASE}/api/save-commands`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(parsedConfig)
});
if (response.ok) {
showToast('Dynamic Commands Whitelist successfully deployed to cluster!', 'success');
fetchWhitelist(); // Reload whitelists
} else {
const errorData = await response.json();
showToast(`Failed to deploy: ${errorData.detail}`, 'error');
}
} catch (err) {
showToast('Invalid JSON structure! Please audit your config syntax.', 'error');
}
};
const toggleClientDetails = (clientId) => {
setExpandedClients(prev => ({
...prev,
[clientId]: !prev[clientId]
}));
};
// Compile individual client arrays into a single global chronological timeline for terminal display
const getSortedTimeline = () => {
const timeline = [];
Object.entries(logs).forEach(([cid, entryList]) => {
// Apply system filter
if (filterClient !== 'all' && cid !== filterClient) {
return;
}
if (Array.isArray(entryList)) {
entryList.forEach((logItem) => {
timeline.push({
...logItem,
client_id: cid
});
});
}
});
// Sort ascending by ISO timestamp
return timeline.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
};
const sortedTimelineLogs = getSortedTimeline();
const activeClients = Object.values(clients).filter(c => c.active).length;
const totalClients = Object.keys(clients).length;
// Filter clients based on search query AND node status tab
const filteredClientEntries = Object.entries(clients).filter(([clientId, info]) => {
const matchesSearch = clientId.toLowerCase().includes(searchQuery.toLowerCase());
const matchesFilter =
nodeFilter === 'all' ||
(nodeFilter === 'online' && info.active) ||
(nodeFilter === 'offline' && !info.active);
return matchesSearch && matchesFilter;
});
if (!token) {
return (
<div className="min-h-screen flex items-center justify-center px-4 py-12 font-sans antialiased text-slate-100 bg-[#050811] relative overflow-hidden">
{/* Decorative background glows */}
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-violet-600/10 rounded-full blur-[120px]"></div>
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-indigo-600/10 rounded-full blur-[120px]"></div>
<div className="w-full max-w-md p-6 sm:p-8 md:p-10 glass-panel rounded-3xl border border-white/5 shadow-[0_8px_30px_rgba(0,0,0,0.6)] relative z-10 space-y-6">
<div className="text-center">
<div className="flex items-center justify-center">
<img
src={sideBarLogo}
alt="Seekright Logo"
className="h-12 sm:h-16 w-auto object-contain transition-all duration-300 hover:scale-105"
/>
</div>
</div>
{loginError && (
<div className="p-3.5 bg-rose-950/40 border border-rose-500/30 rounded-2xl text-rose-300 font-semibold text-xs leading-relaxed flex gap-2 items-center">
<XCircle className="w-4 h-4 text-rose-400 shrink-0" />
<span>{loginError}</span>
</div>
)}
<form onSubmit={handleLogin} className="space-y-4">
<div>
<label className="block text-[10px] font-extrabold text-slate-400 uppercase tracking-wider mb-2">Username</label>
<input
type="text"
required
placeholder="root"
value={loginUsername}
onChange={(e) => setLoginUsername(e.target.value)}
className="w-full bg-slate-950 border border-white/10 rounded-xl p-3 text-xs text-slate-200 font-semibold focus:outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500/20 placeholder-slate-700 transition-all"
/>
</div>
<div>
<label className="block text-[10px] font-extrabold text-slate-400 uppercase tracking-wider mb-2">Password</label>
<input
type="password"
required
placeholder="••••••••"
value={loginPassword}
onChange={(e) => setLoginPassword(e.target.value)}
className="w-full bg-slate-950 border border-white/10 rounded-xl p-3 text-xs text-slate-200 font-semibold focus:outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500/20 placeholder-slate-700 transition-all"
/>
</div>
<button
type="submit"
className="w-full bg-gradient-to-r from-violet-600 to-indigo-600 hover:from-violet-500 hover:to-indigo-500 text-white font-bold py-3.5 px-4 rounded-xl text-xs flex items-center justify-center gap-2 shadow-lg shadow-violet-950/45 hover:shadow-violet-900/50 hover:translate-y-[-1px] active:translate-y-[1px] active:scale-[0.98] transition-all pt-3.5 mt-6"
>
Sign In to Fleet Control
</button>
</form>
</div>
</div>
);
}
return (
<div className="min-h-screen pb-16 flex flex-col font-sans antialiased text-slate-100 bg-[#050811]">
{/* Dynamic Toast Alerts */}
{toast && (
<div className={`fixed bottom-6 right-6 z-50 px-5 py-4 rounded-2xl shadow-2xl flex items-center gap-3 border transition-all duration-300 transform translate-y-0 max-w-sm ${toast.type === 'error'
? 'bg-rose-950/90 border-rose-500/30 text-rose-200 shadow-rose-950/20'
: 'bg-emerald-950/90 border-emerald-500/30 text-emerald-200 shadow-emerald-950/20'
}`}>
{toast.type === 'error' ? (
<div className="p-1 rounded-full bg-rose-500/10"><XCircle className="w-5 h-5 text-rose-400" /></div>
) : (
<div className="p-1 rounded-full bg-emerald-500/10"><CheckCircle className="w-5 h-5 text-emerald-400" /></div>
)}
<span className="font-semibold text-sm tracking-wide">{toast.message}</span>
</div>
)}
{/* Modern Glass Header Section */}
<header className="sticky top-0 z-30 w-full px-4 py-4 md:px-6 glass-panel border-b border-white/5 flex flex-col md:flex-row gap-4 items-stretch md:items-center justify-between shadow-[0_4px_30px_rgba(0,0,0,0.4)]">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center shrink-0">
<img
src={sideBarLogo}
alt="Seekright Logo"
className="h-8 sm:h-10 w-auto object-contain transition-all"
/>
</div>
</div>
{/* Action Controls & Navigation tabs */}
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3">
<nav className="flex flex-wrap items-center gap-1.5 p-1 rounded-xl bg-slate-950/60 border border-white/5">
<button
onClick={() => setActiveTab('dashboard')}
className={`flex-1 sm:flex-initial px-4 py-2 rounded-lg font-semibold text-xs transition-all flex items-center justify-center gap-1.5 ${activeTab === 'dashboard'
? 'bg-gradient-to-r from-violet-600 to-indigo-600 text-white shadow-md shadow-violet-500/10'
: 'text-slate-400 hover:text-slate-200 hover:bg-white/5'
}`}
>
<Activity className="w-3.5 h-3.5" />
Remote Fleet Systems
</button>
<button
onClick={() => setActiveTab('editor')}
className={`flex-1 sm:flex-initial px-4 py-2 rounded-lg font-semibold text-xs transition-all flex items-center justify-center gap-1.5 ${activeTab === 'editor'
? 'bg-gradient-to-r from-violet-600 to-indigo-600 text-white shadow-md shadow-violet-500/10'
: 'text-slate-400 hover:text-slate-200 hover:bg-white/5'
}`}
>
<Settings className="w-3.5 h-3.5" />
Whitelist Editor
</button>
</nav>
{/* Sync indicator status badge */}
<div className="flex items-center justify-between sm:justify-start gap-2.5 px-3 py-2 rounded-xl bg-slate-950/40 border border-white/5 text-xs">
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${isSyncing ? 'bg-violet-400 animate-spin' : 'bg-emerald-400 glow-emerald'}`}></span>
<span className="font-mono text-slate-300 font-medium">
API Status: <b className="text-emerald-400 font-bold">Online</b>
</span>
</div>
<button
onClick={syncFleetData}
title="Force Sync Fleet Vitals"
className="p-1.5 rounded-lg bg-white/5 hover:bg-white/10 text-slate-400 hover:text-slate-200 transition-all border border-white/5"
>
<RefreshCw className={`w-3.5 h-3.5 ${isSyncing ? 'animate-spin text-violet-400' : ''}`} />
</button>
</div>
<button
onClick={handleLogout}
className="px-3.5 py-2 rounded-xl bg-rose-950/40 hover:bg-rose-900/30 text-rose-300 hover:text-rose-200 font-semibold text-xs transition-all border border-rose-500/10 hover:border-rose-500/20 shadow-md shadow-rose-950/15"
>
Logout
</button>
</div>
</header>
{/* Main Container */}
<main className="max-w-7xl w-full mx-auto px-4 md:px-6 mt-8 flex-grow grid grid-cols-1 lg:grid-cols-3 gap-6">
{activeTab === 'dashboard' ? (
<>
{/* Dashboard Vitals: Grid of Cards (Left/Center Column) */}
<div className="lg:col-span-2 space-y-6">
{/* Central Metrics Board — clickable filter tabs */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{/* ALL tab */}
<button
onClick={() => setNodeFilter('all')}
className={`glass-panel p-5 rounded-2xl flex items-center gap-4 relative overflow-hidden group text-left transition-all duration-200 ${nodeFilter === 'all'
? 'ring-2 ring-violet-500/60 shadow-[0_0_20px_rgba(139,92,246,0.15)]'
: 'hover:ring-1 hover:ring-violet-500/20 hover:shadow-[0_0_12px_rgba(139,92,246,0.08)]'
}`}
>
<div className="absolute top-0 right-0 w-24 h-24 bg-violet-600/5 rounded-full blur-2xl group-hover:bg-violet-600/10 transition-all"></div>
<div className={`w-12 h-12 rounded-xl flex items-center justify-center shrink-0 transition-all ${nodeFilter === 'all'
? 'bg-violet-500/25 border-2 border-violet-500/60 text-violet-300'
: 'bg-violet-500/10 border border-violet-500/20 text-violet-400'
}`}>
<Server className="w-6 h-6" />
</div>
<div>
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider block">Total Fleet Nodes</span>
<span className="text-3xl font-extrabold text-white mt-0.5 block">{totalClients}</span>
{nodeFilter === 'all' && (
<span className="text-[9px] text-violet-400 font-bold uppercase tracking-wider mt-0.5 block"> Active Filter</span>
)}
</div>
</button>
{/* ONLINE tab */}
<button
onClick={() => setNodeFilter('online')}
className={`glass-panel p-5 rounded-2xl flex items-center gap-4 relative overflow-hidden border-l-2 border-l-emerald-500 group text-left transition-all duration-200 ${nodeFilter === 'online'
? 'ring-2 ring-emerald-500/60 shadow-[0_0_20px_rgba(16,185,129,0.15)]'
: 'hover:ring-1 hover:ring-emerald-500/20 hover:shadow-[0_0_12px_rgba(16,185,129,0.08)]'
}`}
>
<div className="absolute top-0 right-0 w-24 h-24 bg-emerald-500/5 rounded-full blur-2xl group-hover:bg-emerald-500/10 transition-all"></div>
<div className={`w-12 h-12 rounded-xl flex items-center justify-center shrink-0 transition-all ${nodeFilter === 'online'
? 'bg-emerald-500/25 border-2 border-emerald-500/60 text-emerald-300'
: 'bg-emerald-500/10 border border-emerald-500/20 text-emerald-400'
}`}>
<Activity className="w-6 h-6" />
</div>
<div>
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider block">🟢 Online Nodes</span>
<span className="text-3xl font-extrabold text-emerald-400 mt-0.5 block">{activeClients}</span>
{nodeFilter === 'online' && (
<span className="text-[9px] text-emerald-400 font-bold uppercase tracking-wider mt-0.5 block"> Active Filter</span>
)}
</div>
</button>
{/* OFFLINE tab */}
<button
onClick={() => setNodeFilter('offline')}
className={`glass-panel p-5 rounded-2xl flex items-center gap-4 relative overflow-hidden border-l-2 border-l-rose-500 group text-left transition-all duration-200 ${nodeFilter === 'offline'
? 'ring-2 ring-rose-500/60 shadow-[0_0_20px_rgba(244,63,94,0.15)]'
: 'hover:ring-1 hover:ring-rose-500/20 hover:shadow-[0_0_12px_rgba(244,63,94,0.08)]'
}`}
>
<div className="absolute top-0 right-0 w-24 h-24 bg-rose-500/5 rounded-full blur-2xl group-hover:bg-rose-500/10 transition-all"></div>
<div className={`w-12 h-12 rounded-xl flex items-center justify-center shrink-0 transition-all ${nodeFilter === 'offline'
? 'bg-rose-500/25 border-2 border-rose-500/60 text-rose-300'
: 'bg-rose-500/10 border border-rose-500/20 text-rose-400'
}`}>
<AlertTriangle className="w-6 h-6" />
</div>
<div>
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider block">🔴 Offline Nodes</span>
<span className="text-3xl font-extrabold text-rose-400 mt-0.5 block">{totalClients - activeClients}</span>
{nodeFilter === 'offline' && (
<span className="text-[9px] text-rose-400 font-bold uppercase tracking-wider mt-0.5 block"> Active Filter</span>
)}
</div>
</button>
</div>
{/* Vitals Node Grid Cards */}
<div className="space-y-4">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<h2 className="text-xs font-bold text-slate-300 tracking-wider uppercase flex items-center gap-2">
<Sliders className="w-4 h-4 text-violet-400" />
Remote Fleet Systems Grid ({filteredClientEntries.length})
{nodeFilter !== 'all' && (
<span className={`text-[9px] font-bold px-2 py-0.5 rounded-full uppercase tracking-wider ${nodeFilter === 'online'
? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20'
: 'bg-rose-500/10 text-rose-400 border border-rose-500/20'
}`}>
{nodeFilter === 'online' ? '🟢 Online' : '🔴 Offline'}
</span>
)}
</h2>
<div className="flex items-center gap-2 w-full sm:w-auto">
{/* Clear filter shortcut */}
{nodeFilter !== 'all' && (
<button
onClick={() => setNodeFilter('all')}
className="text-[10px] font-bold text-slate-400 hover:text-slate-200 uppercase tracking-wider bg-white/5 hover:bg-white/10 px-2.5 py-1.5 rounded-lg border border-white/5 transition-all whitespace-nowrap"
>
Clear Filter
</button>
)}
{/* Node Search Bar */}
<div className="relative w-full sm:w-64">
<Search className="w-4 h-4 text-slate-500 absolute left-3 top-2.5" />
<input
type="text"
placeholder="Search systems..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-slate-950/60 border border-white/5 rounded-xl pl-9 pr-4 py-1.5 text-xs text-slate-300 placeholder-slate-500 focus:outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500/20 transition-all"
/>
</div>
</div>
</div>
{filteredClientEntries.length === 0 ? (
<div className="glass-panel p-12 rounded-2xl text-center text-slate-400">
<AlertTriangle className="w-8 h-8 mx-auto text-yellow-500/60 mb-3" />
<p className="font-semibold text-sm">No systems matching your query.</p>
<p className="text-xs text-slate-500 mt-1">Make sure the agent services are actively running</p>
</div>
) : (
filteredClientEntries.map(([clientId, info]) => {
const clientTelemetry = telemetry[clientId];
const isOnline = info.active;
const lastSeenDate = info.last_seen ? new Date(info.last_seen).toLocaleString() : 'Never';
const isExpanded = expandedClients[clientId] || false;
const ft = fileTransfers[clientId];
{/* Render Compact Card for Offline System */ }
if (!isOnline) {
return (
<div
key={clientId}
className="glass-panel p-4 rounded-xl border border-white/5 hover:border-rose-500/20 hover:shadow-[0_4px_20px_rgba(244,63,94,0.04)] transition-all duration-300 bg-gradient-to-r from-slate-950/40 to-slate-900/10"
>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div className="flex items-center gap-3">
<div className="w-3.5 h-3.5 rounded-full flex items-center justify-center bg-rose-500/10">
<span className="relative flex h-2.5 w-2.5">
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-rose-500 glow-rose"></span>
</span>
</div>
<span className="font-bold text-slate-400 text-base">{clientId}</span>
<span className="text-[9px] uppercase tracking-wider font-extrabold px-2 py-0.5 rounded bg-rose-500/10 border border-rose-500/20 text-rose-300/80">
Offline
</span>
<span className="text-[9px] tracking-wider font-extrabold px-2 py-0.5 rounded bg-white/5 border border-white/10 text-slate-500 font-mono">
{info.agent_version ? `v${info.agent_version}` : 'legacy'}
</span>
</div>
<div className="flex items-center justify-between sm:justify-end gap-4">
<div className="text-left sm:text-right">
<span className="text-[9px] text-slate-500 block uppercase font-bold tracking-wider">Last Contact</span>
<span className="text-[11px] font-semibold text-slate-400 font-mono">{lastSeenDate}</span>
</div>
<button
type="button"
onClick={() => toggleClientDetails(clientId)}
className="p-1.5 rounded-lg bg-white/5 hover:bg-white/10 text-slate-400 hover:text-slate-200 transition-colors border border-white/5 flex items-center gap-1 text-[10px] font-semibold uppercase tracking-wider"
>
{isExpanded ? <ChevronUp className="w-3.5 h-3.5" /> : <ChevronDown className="w-3.5 h-3.5" />}
Info
</button>
</div>
</div>
{/* Expanded Empty Telemetry Status */}
{isExpanded && (
<div className="mt-3 p-4 bg-black/40 rounded-lg border border-white/5 text-center text-xs text-slate-500 italic">
<AlertTriangle className="w-4 h-4 mx-auto text-rose-500/40 mb-1" />
Telemetry check-in pending. System is offline and not responding to pings.
</div>
)}
</div>
);
}
{/* Render Full Card for Active/Online System */ }
return (
<div
key={clientId}
className="glass-panel p-5 md:p-6 rounded-2xl hover:border-violet-500/40 hover:-translate-y-0.5 hover:shadow-[0_8px_30px_rgba(139,92,246,0.06)] transition-all duration-300 relative overflow-hidden group bg-gradient-to-b from-slate-900/40 to-slate-950/60"
>
{/* Status top glow line */}
<div className="absolute top-0 left-0 right-0 h-[3px] bg-gradient-to-r from-emerald-500 via-teal-400 to-emerald-400" />
{/* Title block */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-5">
<div className="flex items-center gap-3">
<div className="w-3.5 h-3.5 rounded-full flex items-center justify-center bg-emerald-500/10">
<span className="relative flex h-2.5 w-2.5">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500 glow-emerald"></span>
</span>
</div>
<span className="font-extrabold text-white text-lg tracking-tight group-hover:text-violet-300 transition-colors">{clientId}</span>
<span className="text-[9px] uppercase tracking-wider font-extrabold px-2 py-0.5 rounded bg-emerald-500/10 border border-emerald-500/20 text-emerald-300">
Active
</span>
<span className="text-[9px] tracking-wider font-extrabold px-2 py-0.5 rounded bg-violet-500/10 border border-violet-500/20 text-violet-300 font-mono">
{info.agent_version ? `v${info.agent_version}` : 'legacy'}
</span>
</div>
<div className="text-left sm:text-right">
<span className="text-[9px] text-slate-500 block uppercase font-bold tracking-wider">LAST CHECK-IN</span>
<span className="text-xs font-bold text-slate-300 font-mono">{lastSeenDate}</span>
</div>
</div>
{/* Telemetry charts row */}
{clientTelemetry ? (
<div className="space-y-5">
{/* CPU & RAM progress */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* CPU Progress block */}
{(() => {
const cpu = clientTelemetry.cpu_percent || 0;
const isWarning = cpu > 80;
return (
<div className="p-4 bg-slate-950/40 border border-white/5 rounded-xl flex flex-col justify-between">
<div className="flex justify-between items-center text-xs font-bold text-slate-400 mb-2.5">
<span className="flex items-center gap-1.5">
<Cpu className={`w-4 h-4 ${isWarning ? 'text-rose-400 animate-pulse' : 'text-violet-400'}`} />
CPU Load
</span>
<span className={`font-bold font-mono ${isWarning ? 'text-rose-400' : 'text-white'}`}>
{cpu}%
{clientTelemetry.cpu_temp !== undefined && clientTelemetry.cpu_temp !== null && (
<span className="text-slate-500 ml-1">@ {clientTelemetry.cpu_temp}°C</span>
)}
</span>
</div>
<div className="w-full h-2 rounded-full bg-slate-900 overflow-hidden relative border border-white/5">
<div
className={`h-full rounded-full transition-all duration-500 ${isWarning
? 'bg-gradient-to-r from-rose-500 to-red-400'
: cpu > 60
? 'bg-gradient-to-r from-amber-500 to-yellow-400'
: 'bg-gradient-to-r from-violet-500 to-indigo-500'
}`}
style={{ width: `${cpu}%` }}
/>
</div>
{isWarning && (
<div className="text-[9px] text-rose-400 font-bold flex items-center gap-1 mt-2 animate-pulse">
<AlertTriangle className="w-2.5 h-2.5" /> High CPU Warning
</div>
)}
</div>
);
})()}
{/* Memory Progress block */}
{(() => {
const total = clientTelemetry.memory_total_gb || 0;
const free = clientTelemetry.memory_free_gb || 0;
const used = (total - free).toFixed(2);
const ram = clientTelemetry.memory_percent || 0;
const isWarning = ram > 85;
return (
<div className="p-4 bg-slate-950/40 border border-white/5 rounded-xl flex flex-col justify-between">
<div className="flex justify-between items-center text-xs font-bold text-slate-400 mb-2.5">
<span className="flex items-center gap-1.5">
<Activity className={`w-4 h-4 ${isWarning ? 'text-rose-400 animate-pulse' : 'text-violet-400'}`} />
Virtual RAM
</span>
<span className={`font-bold font-mono ${isWarning ? 'text-rose-400' : 'text-white'}`}>{ram}%</span>
</div>
<div className="w-full h-2 rounded-full bg-slate-900 overflow-hidden relative border border-white/5">
<div
className={`h-full rounded-full transition-all duration-500 ${isWarning
? 'bg-gradient-to-r from-rose-500 to-red-400'
: ram > 70
? 'bg-gradient-to-r from-amber-500 to-yellow-400'
: 'bg-gradient-to-r from-violet-500 to-indigo-500'
}`}
style={{ width: `${ram}%` }}
/>
</div>
<div className="flex justify-between text-[9px] text-slate-500 font-mono mt-2.5 border-t border-white/5 pt-2">
<span>Used: <b className="text-slate-300">{used}G</b></span>
<span>Free: <b className="text-slate-300">{free}G</b></span>
<span>Total: <b className="text-slate-300">{total}G</b></span>
</div>
</div>
);
})()}
</div>
{/* GPU and Partition details */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-5 pt-4 border-t border-white/5">
{/* Partition Storage */}
<div className="space-y-2.5">
<span className="text-[10px] uppercase font-extrabold tracking-wider text-slate-400 flex items-center gap-1.5">
<HardDrive className="w-4 h-4 text-indigo-400" /> Disk Drive Space
</span>
<div className="space-y-2.5">
{clientTelemetry.disks && clientTelemetry.disks.length > 0 ? (
clientTelemetry.disks.map((d, index) => {
const diskTotal = d.total_gb || 0;
const diskFree = d.free_gb || 0;
const diskUsed = (diskTotal - diskFree).toFixed(2);
// Get friendly label
const diskLabel = d.label || (d.mount === '/' ? 'Computer' : (d.mount.split(/[/\\]/).pop() || 'Volume'));
return (
<div key={index} className="p-4 rounded-xl bg-slate-950/40 border border-white/5 hover:border-violet-500/15 transition-all duration-200 space-y-3">
<div className="flex items-start justify-between gap-3">
{/* Left side: Icon, Label, Device badge, Mount path */}
<div className="flex items-center gap-3 min-w-0">
<div className="p-2 rounded-lg bg-slate-900 border border-white/5 text-indigo-400 shrink-0">
<HardDrive className="w-4 h-4 text-indigo-400" />
</div>
<div className="min-w-0">
<span className="font-extrabold text-white text-xs block truncate">
{diskLabel}
</span>
<span className="text-[9px] text-slate-400 font-mono flex flex-wrap items-center gap-1 mt-0.5">
{d.device && (
<span className="px-1.5 py-0.5 rounded bg-slate-900 border border-white/5 font-semibold text-[8px] text-slate-300">
{d.device}
</span>
)}
{d.device && <span className="text-slate-600">on</span>}
<span className="text-slate-400 truncate max-w-[120px]" title={d.mount}>
{d.mount}
</span>
</span>
</div>
</div>
{/* Right side: Free Space / Total */}
<div className="text-right shrink-0">
<span className="text-[11px] font-bold text-slate-300 font-mono block">
{diskFree} GB free
</span>
<span className="text-[9px] text-slate-500 font-mono block mt-0.5">
of {diskTotal} GB
</span>
</div>
</div>
{/* Progress bar */}
<div className="space-y-1.5">
<div className="w-full h-1.5 rounded-full bg-slate-900 overflow-hidden relative border border-white/5">
<div
className={`h-full rounded-full transition-all duration-500 ${d.percent > 85
? 'bg-gradient-to-r from-rose-500 to-red-500'
: d.percent > 70
? 'bg-gradient-to-r from-amber-500 to-yellow-500'
: 'bg-gradient-to-r from-indigo-500 to-violet-500'
}`}
style={{ width: `${d.percent}%` }}
/>
</div>
<div className="flex justify-between text-[9px] text-slate-500 font-mono">
<span>{diskUsed} GB Used</span>
<span className={d.percent > 85 ? "text-rose-400 font-bold" : ""}>
{d.percent}% Used
</span>
</div>
</div>
</div>
);
})
) : (
<span className="text-xs text-slate-500 block italic">No storage drives reported.</span>
)}
</div>
</div>
{/* Nvidia Core GPU Vitals */}
<div className="space-y-2.5">
<span className="text-[10px] uppercase font-extrabold tracking-wider text-slate-400 flex items-center gap-1.5">
<Flame className="w-4 h-4 text-rose-400 animate-pulse" /> Nvidia GPU Accelerator
</span>
<div className="space-y-2.5">
{clientTelemetry.gpus && clientTelemetry.gpus.length > 0 ? (
clientTelemetry.gpus.map((gpu, index) => (
<div key={index} className="p-3 rounded-xl bg-slate-950/40 border border-white/5 space-y-2">
<div className="flex justify-between items-center text-xs border-b border-white/5 pb-1.5">
<span className="font-extrabold text-slate-200 truncate max-w-[150px]">🎮 {gpu.name}</span>
<span className="text-violet-400 font-bold font-mono text-xs">Load: {gpu.utilization}</span>
</div>
<div className="grid grid-cols-2 gap-x-3 gap-y-1.5 text-[10px] text-slate-400 font-mono">
<div className="flex justify-between border-r border-white/5 pr-1.5"><span>Temp:</span> <b className="text-slate-200">{gpu.temp}</b></div>
<div className="flex justify-between pl-1.5"><span>Fans:</span> <b className="text-slate-200">{gpu.fan_speed}</b></div>
<div className="col-span-2 flex justify-between border-t border-white/5 pt-1.5 mt-1">
<span>VRAM:</span>
<b className="text-slate-200">{gpu.memory_used} / {gpu.memory_total || 'N/A'}</b>
</div>
<div className="col-span-2 flex justify-between">
<span>Power:</span>
<b className="text-slate-200">{gpu.power_draw} / {gpu.power_limit || 'N/A'}</b>
</div>
</div>
</div>
))
) : (
<div className="text-xs text-slate-500 py-6 text-center font-medium bg-slate-950/15 border border-white/5 rounded-xl italic">
No NVIDIA GPU hardware detected.
</div>
)}
</div>
</div>
</div>
</div>
) : (
<div className="text-center py-8 bg-slate-950/20 rounded-2xl border border-white/5 my-4">
<p className="text-xs text-slate-400 font-medium italic">Telemetry check-in pending... Waiting for dynamic vitals transmission</p>
</div>
)}
{/* Remote Video Fetch panel */}
<div className="mt-5 pt-4 border-t border-white/5">
<span className="text-[10px] uppercase font-extrabold tracking-wider text-slate-400 flex items-center gap-1.5 mb-2.5">
<Play className="w-4 h-4 text-violet-400" /> Fetch Video From Node
</span>
{/* SHIFT folder location for this node */}
<div className="flex flex-col sm:flex-row gap-2 mb-2.5">
<div className="flex-1 relative">
<HardDrive className="w-3.5 h-3.5 text-slate-500 absolute left-3 top-2.5" />
<input
type="text"
placeholder="SHIFT folder path, e.g. /mnt/<disk-uuid>/SR/SHIFT"
value={shiftPathInputs[clientId] ?? info.shift_path ?? ''}
onChange={(e) => setShiftPathInputs(prev => ({ ...prev, [clientId]: e.target.value }))}
onKeyDown={(e) => { if (e.key === 'Enter') handleSaveShiftPath(clientId); }}
className="w-full bg-slate-950/60 border border-white/5 rounded-xl pl-9 pr-3 py-2 text-xs text-slate-300 font-mono placeholder-slate-600 focus:outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500/20 transition-all"
/>
</div>
<button
type="button"
onClick={() => handleSaveShiftPath(clientId)}
className="px-4 py-2 rounded-xl text-slate-200 font-bold text-xs flex items-center justify-center gap-1.5 bg-white/5 border border-white/10 hover:bg-white/10 transition-all"
>
<CheckCircle className="w-3.5 h-3.5" />
Save Path
</button>
</div>
{/* Search videos inside the SHIFT folder */}
<div className="flex flex-col sm:flex-row gap-2 mb-2.5">
<div className="flex-1 relative">
<Search className="w-3.5 h-3.5 text-slate-500 absolute left-3 top-2.5" />
<input
type="text"
placeholder="Search SHIFT folder, e.g. 20260716"
value={searchInputs[clientId] || ''}
onChange={(e) => setSearchInputs(prev => ({ ...prev, [clientId]: e.target.value }))}
onKeyDown={(e) => { if (e.key === 'Enter') handleSearchVideos(clientId); }}
className="w-full bg-slate-950/60 border border-white/5 rounded-xl pl-9 pr-3 py-2 text-xs text-slate-300 font-mono placeholder-slate-600 focus:outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500/20 transition-all"
/>
</div>
<button
type="button"
onClick={() => handleSearchVideos(clientId)}
className="px-4 py-2 rounded-xl text-white font-bold text-xs flex items-center justify-center gap-1.5 bg-gradient-to-r from-slate-700 to-slate-600 hover:from-slate-600 hover:to-slate-500 transition-all"
>
<Search className="w-3.5 h-3.5" />
Search
</button>
</div>
{parseFloat(info.agent_version) < 3.3 && (
<p className="text-[10px] text-amber-400/70 mb-2.5 italic">
Search needs agent v3.3-shift run update_agent on this node first.
</p>
)}
{/* Search results */}
{info.file_search && (
<div className="mb-2.5 p-3 rounded-xl bg-slate-950/40 border border-white/5">
<div className="flex items-center justify-between gap-2 mb-1.5">
<span className="text-[10px] font-bold text-slate-400 truncate">
Results for <span className="text-slate-200 font-mono">'{info.file_search.query}'</span>
{info.file_search.searched_path && (
<span className="text-slate-600"> in {info.file_search.searched_path}</span>
)}
</span>
<span className={`text-[9px] uppercase tracking-wider font-extrabold px-2 py-0.5 rounded border shrink-0 flex items-center gap-1 ${
info.file_search.status === 'done'
? 'bg-emerald-500/10 border-emerald-500/20 text-emerald-300'
: info.file_search.status === 'error'
? 'bg-rose-500/10 border-rose-500/20 text-rose-300'
: 'bg-violet-500/10 border-violet-500/20 text-violet-300'
}`}>
{info.file_search.status === 'searching' && <RefreshCw className="w-2.5 h-2.5 animate-spin" />}
{info.file_search.status === 'done'
? `${(info.file_search.results || []).length} found`
: info.file_search.status}
</span>
</div>
{info.file_search.error && (
<p className="text-[10px] text-rose-400 font-mono">{info.file_search.error}</p>
)}
{info.file_search.status === 'done' && (info.file_search.results || []).length > 0 && (
<div className="max-h-48 overflow-y-auto space-y-1 mt-1">
{info.file_search.results.map((r) => (
<div key={r.path} className="flex items-center justify-between gap-2 px-2 py-1.5 rounded-lg bg-slate-900/60 border border-white/5">
<div className="min-w-0">
<p className="text-[11px] font-mono font-bold text-slate-200 truncate">{r.name}</p>
<p className="text-[9px] text-slate-500 font-mono">
{r.size_mb !== undefined ? `${r.size_mb} MB` : ''}{r.modified ? ` · ${r.modified}` : ''}
</p>
</div>
<button
type="button"
onClick={() => handleRequestFile(clientId, r.name)}
className="shrink-0 px-3 py-1 rounded-md text-[10px] font-bold uppercase tracking-wider text-violet-300 bg-violet-500/10 border border-violet-500/20 hover:bg-violet-500/20 transition-all flex items-center gap-1"
>
<ArrowRight className="w-3 h-3" /> Fetch
</button>
</div>
))}
</div>
)}
</div>
)}
<div className="flex flex-col sm:flex-row gap-2">
<input
type="text"
placeholder="e.g. 20260707000026_000084.MP4"
value={videoNameInputs[clientId] || ''}
onChange={(e) => setVideoNameInputs(prev => ({ ...prev, [clientId]: e.target.value }))}
onKeyDown={(e) => { if (e.key === 'Enter') handleRequestFile(clientId); }}
className="flex-1 bg-slate-950/60 border border-white/5 rounded-xl px-3 py-2 text-xs text-slate-300 font-mono placeholder-slate-600 focus:outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500/20 transition-all"
/>
<button
type="button"
onClick={() => handleRequestFile(clientId)}
className="px-5 py-2 rounded-xl text-white font-bold text-xs flex items-center justify-center gap-1.5 bg-gradient-to-r from-violet-600 to-indigo-600 hover:from-violet-500 hover:to-indigo-500 shadow-lg shadow-violet-950/40 transition-all"
>
<ArrowRight className="w-3.5 h-3.5" />
Fetch
</button>
</div>
{ft && (
<div className="mt-3 p-3 rounded-xl bg-slate-950/40 border border-white/5">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
<span className="text-xs font-mono font-bold text-slate-200 truncate">{ft.filename}</span>
<span className={`text-[9px] uppercase tracking-wider font-extrabold px-2 py-0.5 rounded border shrink-0 flex items-center gap-1 ${
ft.status === 'ready'
? 'bg-emerald-500/10 border-emerald-500/20 text-emerald-300'
: ft.status === 'not_found' || ft.status === 'error'
? 'bg-rose-500/10 border-rose-500/20 text-rose-300'
: 'bg-violet-500/10 border-violet-500/20 text-violet-300'
}`}>
{(ft.status === 'requested' || ft.status === 'searching' || ft.status === 'uploading') && (
<RefreshCw className="w-2.5 h-2.5 animate-spin" />
)}
{ft.status === 'ready' && <CheckCircle className="w-2.5 h-2.5" />}
{(ft.status === 'not_found' || ft.status === 'error') && <XCircle className="w-2.5 h-2.5" />}
{ft.status}
</span>
</div>
{ft.message && (
<p className="text-[10px] text-slate-500 mt-1.5 font-mono break-all">{ft.message}</p>
)}
{ft.progress_percent !== undefined && ft.progress_percent !== null && (
<div className="mt-2 h-1.5 w-full bg-slate-800 rounded-full overflow-hidden">
<div
className="h-full bg-violet-500 transition-all duration-300"
style={{ width: `${Math.min(Math.max(ft.progress_percent, 0), 100)}%` }}
/>
</div>
)}
{(ft.status === 'requested' || ft.status === 'searching' || ft.status === 'uploading') && (
<div className="mt-2.5 flex justify-end">
<button
onClick={() => handleCancelUpload(clientId, ft.filename)}
className="px-3 py-1 rounded-md text-[10px] font-bold uppercase tracking-wider text-rose-300 bg-rose-500/10 border border-rose-500/20 hover:bg-rose-500/20 transition-all flex items-center gap-1.5"
>
<XCircle className="w-3 h-3" /> Cancel
</button>
</div>
)}
{ft.status === 'ready' && (
<div className="flex gap-2 mt-2.5">
<a
href={`${API_BASE}/api/download-file?client_id=${encodeURIComponent(clientId)}&filename=${encodeURIComponent(ft.filename)}&token=${encodeURIComponent(token)}&inline=true`}
target="_blank"
rel="noreferrer"
className="px-4 py-1.5 rounded-lg text-[10px] font-bold uppercase tracking-wider text-emerald-300 bg-emerald-500/10 border border-emerald-500/20 hover:bg-emerald-500/20 transition-all flex items-center gap-1.5"
>
<Play className="w-3 h-3" /> Play
</a>
<a
href={`${API_BASE}/api/download-file?client_id=${encodeURIComponent(clientId)}&filename=${encodeURIComponent(ft.filename)}&token=${encodeURIComponent(token)}`}
className="px-4 py-1.5 rounded-lg text-[10px] font-bold uppercase tracking-wider text-slate-300 bg-white/5 border border-white/10 hover:bg-white/10 transition-all flex items-center gap-1.5"
>
<HardDrive className="w-3 h-3" /> Download
</a>
</div>
)}
</div>
)}
</div>
</div>
);
})
)}
</div>
</div>
{/* Maintenance Command Dispatch deck (Right sidebar) */}
<div className="lg:col-span-1">
<div className="glass-panel p-5 md:p-6 rounded-2xl sticky top-24 border border-white/5 space-y-6">
<div className="border-b border-white/5 pb-3">
<h2 className="text-xs font-bold text-slate-300 uppercase tracking-wider flex items-center gap-2">
<Play className="w-4 h-4 text-violet-400 fill-violet-400" />
Maintenance Dispatcher
</h2>
<p className="text-[11px] text-slate-400 mt-0.5">Send a whitelisted command down to a node</p>
</div>
<form onSubmit={handleDispatch} className="space-y-5">
{/* Select target node check list */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-xs font-bold text-slate-400 uppercase tracking-wider">
Select Targets
</label>
<div className="flex gap-2">
<button
type="button"
onClick={() => setSelectedClients(Object.keys(clients))}
className="text-[10px] text-violet-400 font-bold hover:text-violet-300 transition-colors uppercase tracking-wider bg-violet-500/5 px-2 py-0.5 rounded border border-violet-500/10"
>
All
</button>
<button
type="button"
onClick={() => setSelectedClients(Object.entries(clients).filter(([_, info]) => info.active).map(([id]) => id))}
className="text-[10px] text-emerald-400 font-bold hover:text-emerald-300 transition-colors uppercase tracking-wider bg-emerald-500/5 px-2 py-0.5 rounded border border-emerald-500/10"
>
Active
</button>
<button
type="button"
onClick={() => setSelectedClients([])}
className="text-[10px] text-slate-500 font-bold hover:text-slate-400 transition-colors uppercase tracking-wider bg-white/5 px-2 py-0.5 rounded border border-white/5"
>
Clear
</button>
</div>
</div>
<div className="grid grid-cols-2 gap-2 max-h-48 overflow-y-auto p-2 bg-slate-950 border border-white/5 rounded-xl">
{Object.entries(clients).length === 0 ? (
<span className="col-span-2 text-xs text-slate-500 italic text-center py-6 block font-medium">No registered nodes found.</span>
) : (
Object.entries(clients).map(([id, info]) => {
const isSelected = selectedClients.includes(id);
return (
<button
key={id}
type="button"
onClick={() => {
if (isSelected) {
setSelectedClients(selectedClients.filter(c => c !== id));
} else {
setSelectedClients([...selectedClients, id]);
}
}}
className={`p-2.5 rounded-xl border text-left transition-all duration-200 relative flex flex-col justify-between overflow-hidden ${isSelected
? 'bg-gradient-to-br from-violet-600/20 to-indigo-600/20 border-violet-500/80 shadow-md shadow-violet-900/15 text-white'
: 'bg-slate-900/40 border-white/5 hover:border-white/10 text-slate-400 hover:text-slate-200'
}`}
>
{/* Glow dot */}
{isSelected && <span className="absolute top-0 right-0 w-2 h-2 bg-violet-400 rounded-full blur-[1px] -mr-0.5 -mt-0.5" />}
<div className="flex items-center justify-between w-full gap-2">
<span className={`text-xs font-extrabold truncate ${isSelected ? 'text-violet-200' : 'text-slate-300'}`}>
{id}
</span>
<span className={`w-1.5 h-1.5 rounded-full shrink-0 ${info.active ? 'bg-emerald-400 glow-emerald' : 'bg-rose-400'}`} />
</div>
<span className="text-[9px] font-mono text-slate-500 mt-1 font-semibold uppercase tracking-wider">
{info.active ? 'Online' : 'Offline'}
</span>
</button>
);
})
)}
</div>
</div>
{/* Manual Target target ID */}
<div>
<label className="block text-[11px] font-semibold text-slate-400 uppercase tracking-wider mb-1.5">Or Type Custom Node Target</label>
<input
type="text"
placeholder="e.g. client_1, client_2"
value={manualClient}
onChange={(e) => setManualClient(e.target.value)}
className="w-full bg-slate-950 border border-white/10 rounded-xl p-3 text-xs text-slate-200 font-semibold focus:outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500/20 placeholder-slate-600 transition-all focus-glow-violet"
/>
</div>
{/* Select command key */}
<div>
<label className="block text-[11px] font-semibold text-slate-400 uppercase tracking-wider mb-1.5">Select Whitelisted Operation Key</label>
<select
value={selectedCommand}
onChange={(e) => setSelectedCommand(e.target.value)}
className="w-full bg-slate-950 border border-white/10 rounded-xl p-3 text-xs text-slate-200 font-semibold focus:outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500/20 transition-all"
>
<option value="">-- Select Permitted Operation --</option>
{Array.from(new Set([
...Object.keys(whitelist.nt || {}),
...Object.keys(whitelist.posix || {})
])).map((cmdKey) => (
<option key={cmdKey} value={cmdKey}>
{cmdKey}
</option>
))}
</select>
</div>
{/* Manual command command key */}
<div>
<label className="block text-[11px] font-semibold text-slate-400 uppercase tracking-wider mb-1.5">Or Type Custom Operation Key</label>
<input
type="text"
placeholder="e.g. check_drives"
value={manualCommand}
onChange={(e) => setManualCommand(e.target.value)}
className="w-full bg-slate-950 border border-white/10 rounded-xl p-3 text-xs text-slate-200 font-semibold focus:outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500/20 placeholder-slate-600 transition-all focus-glow-violet"
/>
</div>
{/* Fire execution button */}
<button
type="submit"
className="w-full bg-gradient-to-r from-violet-600 to-indigo-600 hover:from-violet-500 hover:to-indigo-500 text-white font-bold py-3.5 px-4 rounded-xl text-xs flex items-center justify-center gap-2 shadow-lg shadow-violet-950/45 hover:shadow-violet-900/50 hover:translate-y-[-1px] active:translate-y-[1px] active:scale-[0.98] transition-all"
>
<Zap className="w-3.5 h-3.5 fill-white" />
Dispatch Command Batch
</button>
</form>
</div>
</div>
</>
) : activeTab === 'editor' ? (
/* Visual Whitelist JSON Configuration Editor */
<div className="col-span-3">
<div className="glass-panel p-5 md:p-6 rounded-2xl border border-white/5 space-y-6">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-white/5 pb-4">
<div>
<h2 className="text-sm font-bold text-white uppercase tracking-wider flex items-center gap-2">
<Settings className="w-4 h-4 text-violet-400" />
Visual Commands whitelist Editor
</h2>
<p className="text-[11px] text-slate-400 mt-0.5">
Permitted operations are checked against whitelist config and pushed to nodes without restarts.
</p>
</div>
{/* JSON Validation Status Badge */}
<div>
{jsonError ? (
<span className="inline-flex items-center gap-1 px-3 py-1.5 rounded-lg bg-rose-500/10 border border-rose-500/20 text-rose-300 text-xs font-semibold">
<XCircle className="w-4 h-4 text-rose-400 shrink-0" />
Invalid JSON Config
</span>
) : (
<span className="inline-flex items-center gap-1 px-3 py-1.5 rounded-lg bg-emerald-500/10 border border-emerald-500/20 text-emerald-300 text-xs font-semibold">
<CheckCircle className="w-4 h-4 text-emerald-400 shrink-0" />
Valid JSON Config
</span>
)}
</div>
</div>
{/* IDE Editor UI wrapper */}
<div className="space-y-4">
<div className="rounded-xl overflow-hidden border border-white/5 bg-[#02040a] shadow-inner relative">
{/* Editor Tab Bar */}
<div className="bg-[#0b0f19] px-4 py-2 border-b border-white/5 flex items-center justify-between text-xs text-slate-400 font-mono">
<div className="flex items-center gap-2">
<FileCode className="w-4 h-4 text-violet-400" />
<span className="font-semibold text-slate-300">commands.json</span>
</div>
<span className="text-[10px] text-slate-500 font-semibold uppercase tracking-wider bg-slate-900 px-2 py-0.5 rounded border border-white/5">JSON</span>
</div>
<textarea
className="w-full h-[450px] bg-transparent p-4 font-mono text-xs text-emerald-400 leading-relaxed focus:outline-none resize-y"
spellCheck="false"
value={editorText}
onChange={(e) => setEditorText(e.target.value)}
/>
</div>
{jsonError && (
<div className="p-3.5 bg-rose-950/40 border border-rose-500/30 rounded-xl text-rose-300 font-mono text-xs leading-relaxed flex gap-2 items-start">
<AlertTriangle className="w-4 h-4 text-rose-400 shrink-0 mt-0.5" />
<div>
<span className="font-bold text-rose-200">JSON Parse Error:</span> {jsonError}
</div>
</div>
)}
<div className="flex flex-col sm:flex-row justify-end gap-3 pt-2">
<button
onClick={fetchWhitelist}
className="w-full sm:w-auto px-5 py-2.5 rounded-xl border border-white/10 hover:bg-white/5 font-semibold text-xs text-slate-300 transition-colors"
>
Discard Changes
</button>
<button
onClick={handleSaveWhitelist}
disabled={jsonError !== null}
className={`w-full sm:w-auto px-6 py-2.5 rounded-xl text-white font-bold text-xs flex items-center justify-center gap-1.5 transition-all ${jsonError
? 'bg-slate-800 text-slate-500 border border-slate-700 cursor-not-allowed'
: 'bg-gradient-to-r from-violet-600 to-indigo-600 hover:from-violet-500 hover:to-indigo-500 shadow-lg shadow-violet-950/40 hover:shadow-violet-900/50 hover:translate-y-[-1px] active:translate-y-[1px]'
}`}
>
<Check className="w-3.5 h-3.5" />
Deploy Whitelist Configuration
</button>
</div>
</div>
</div>
</div>
) : null}
</main>
{/* Scrolling DevOps CLI output shell terminal */}
<footer className="max-w-7xl w-full mx-auto px-4 md:px-6 mt-8">
<div className="bg-slate-950/95 border border-white/5 rounded-2xl overflow-hidden shadow-2xl">
{/* Terminal Title Bar */}
<div className="bg-[#0b0f19] px-4 py-3 border-b border-white/5 flex flex-col sm:flex-row gap-3 sm:items-center justify-between">
<div className="flex items-center gap-2">
{/* Window Controls */}
<div className="flex gap-1.5">
<span className="w-2.5 h-2.5 rounded-full bg-rose-500/70 block"></span>
<span className="w-2.5 h-2.5 rounded-full bg-yellow-500/70 block"></span>
<span className="w-2.5 h-2.5 rounded-full bg-emerald-500/70 block"></span>
</div>
<span className="text-[10px] uppercase tracking-wider font-extrabold text-slate-400 font-mono ml-2.5 flex items-center gap-2">
<Terminal className="w-4 h-4 text-violet-400" />
Live Fleet Activity & Webhook Output Shell
</span>
</div>
<div className="flex items-center gap-3 w-full sm:w-auto justify-between sm:justify-end">
<div className="relative">
<select
value={filterClient}
onChange={(e) => setFilterClient(e.target.value)}
className="bg-slate-900 border border-white/10 rounded-lg pl-3 pr-8 py-1.5 text-xs text-slate-300 font-semibold focus:outline-none focus:border-violet-500 transition-colors appearance-none cursor-pointer"
>
<option value="all">🔍 Show All Nodes</option>
{Object.keys(clients).map((cid) => (
<option key={cid} value={cid}>
{cid} logs
</option>
))}
</select>
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-slate-400">
<ChevronDown className="w-3.5 h-3.5" />
</div>
</div>
<span className="text-[10px] text-slate-500 font-mono font-medium hidden sm:inline">Shell v1.0</span>
</div>
</div>
{/* Terminal Console output */}
<div ref={terminalRef} className="p-4 h-72 overflow-y-auto font-mono text-[11px] space-y-3 terminal-scroll bg-black/95 select-text">
{sortedTimelineLogs.length === 0 ? (
<span className="text-slate-600 block italic py-2">Waiting for fleet check-ins and dispatches to stream... No historical logs recorded yet.</span>
) : (
sortedTimelineLogs.map((log, index) => {
const formattedTime = new Date(log.timestamp).toLocaleTimeString();
if (log.type === 'telemetry') {
return (
<div key={index} className="text-slate-400 flex items-start gap-1 leading-relaxed break-words whitespace-pre-wrap">
<span className="text-slate-600 shrink-0 select-none">[{formattedTime}]</span>
<span className="text-sky-400 shrink-0 font-bold select-none">INFO:</span>
<span>
Client <b className="text-white font-semibold">{log.client_id}</b> check-in telemetry pushed. (CPU: <b className="text-sky-300">{log.detail.cpu_percent}%</b>, RAM: <b className="text-sky-300">{log.detail.memory_percent}%</b>)
</span>
</div>
);
}
if (log.type === 'command_scheduled') {
return (
<div key={index} className="text-amber-200/90 flex items-start gap-1 leading-relaxed break-words whitespace-pre-wrap">
<span className="text-slate-600 shrink-0 select-none">[{formattedTime}]</span>
<span className="text-amber-400 shrink-0 font-bold select-none">DISPATCH:</span>
<span>
Operation <b className="text-amber-300 font-bold">"{log.detail.command}"</b> successfully queued for node <b className="text-white font-semibold">{log.client_id}</b>.
</span>
</div>
);
}
if (log.type === 'command_polled') {
return (
<div key={index} className="text-orange-200/95 flex items-start gap-1 leading-relaxed break-words whitespace-pre-wrap">
<span className="text-slate-600 shrink-0 select-none">[{formattedTime}]</span>
<span className="text-orange-400 shrink-0 font-bold select-none">POLL:</span>
<span>
Agent <b className="text-white font-semibold">{log.client_id}</b> retrieved pending command <b className="text-orange-300 font-bold">"{log.detail.command}"</b>. Executing process...
</span>
</div>
);
}
if (log.type === 'command_result') {
const isSuccess = log.detail.returncode === 0;
return (
<div key={index} className="p-3.5 rounded-xl border bg-slate-950/80 border-white/5 space-y-2.5 my-1.5">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 border-b border-white/5 pb-2">
<div className="flex flex-wrap items-center gap-1.5">
<span className="text-slate-600 select-none">[{formattedTime}]</span>
<span className={`font-bold text-xs ${isSuccess ? 'text-emerald-400' : 'text-rose-400'}`}>
{isSuccess ? '✓ SUCCESS' : '✗ FAILURE'}
</span>
<span className="text-slate-300">
Command <b className="text-white font-semibold">"{log.detail.command}"</b> on <b className="text-white font-semibold">{log.client_id}</b> finished with code <b className="font-mono text-slate-100">{log.detail.returncode}</b>.
</span>
</div>
</div>
{/* Subprocess console stdout/stderr */}
{(log.detail.stdout || log.detail.stderr) && (
<pre className={`p-3 rounded-lg bg-black/80 font-mono text-[10px] leading-relaxed overflow-x-auto break-all whitespace-pre-wrap ${isSuccess ? 'text-slate-300 border-l-2 border-emerald-500/50' : 'text-rose-300 border-l-2 border-rose-500/50'
}`}>
{log.detail.stdout && <code className="block">{log.detail.stdout}</code>}
{log.detail.stderr && <code className="block text-rose-400 font-semibold">{log.detail.stderr}</code>}
</pre>
)}
</div>
);
}
return null;
})
)}
</div>
</div>
</footer>
</div>
);
}
// Simple custom Check icon component since lucide-react Check may sometimes fail or need explicit import mapping
function Check({ className, ...props }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
{...props}
>
<polyline points="20 6 9 17 4 12" />
</svg>
);
}
export default App;