feat: initialize RMM dashboard frontend with telemetry, command dispatch, and configuration management capabilities
This commit is contained in:
1
.env.development
Normal file
1
.env.development
Normal file
@@ -0,0 +1 @@
|
|||||||
|
VITE_API_BASE_URL="http://localhost:8000"
|
||||||
1
.env.production
Normal file
1
.env.production
Normal file
@@ -0,0 +1 @@
|
|||||||
|
VITE_API_BASE_URL="https://rmm-backend.seekright.com"
|
||||||
582
package-lock.json
generated
582
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
611
src/App.jsx
611
src/App.jsx
@@ -12,12 +12,24 @@ import {
|
|||||||
Flame,
|
Flame,
|
||||||
Server,
|
Server,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Play
|
Play,
|
||||||
|
Search,
|
||||||
|
Code,
|
||||||
|
FileCode,
|
||||||
|
Clock,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
|
Database,
|
||||||
|
ArrowRight,
|
||||||
|
Sliders
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const API_BASE = window.location.protocol === 'https:'
|
const API_BASE = import.meta.env.VITE_API_BASE_URL ||
|
||||||
? 'https://rmm-backend.seekright.com'
|
(window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
|
||||||
: 'http://rmm-backend.seekright.com'; // Target FastAPI central server in production VM
|
? 'http://localhost:8000'
|
||||||
|
: (window.location.protocol === 'https:'
|
||||||
|
? 'https://rmm-backend.seekright.com'
|
||||||
|
: 'http://rmm-backend.seekright.com'));
|
||||||
|
|
||||||
// Secure API fetch wrapper
|
// Secure API fetch wrapper
|
||||||
const apiFetch = (url, options = {}) => {
|
const apiFetch = (url, options = {}) => {
|
||||||
@@ -39,10 +51,13 @@ function App() {
|
|||||||
|
|
||||||
// Whitelist config editor state
|
// Whitelist config editor state
|
||||||
const [editorText, setEditorText] = useState('{\n "nt": {},\n "posix": {}\n}');
|
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 [toast, setToast] = useState(null);
|
||||||
const [isSyncing, setIsSyncing] = useState(false);
|
const [isSyncing, setIsSyncing] = useState(false);
|
||||||
const [filterClient, setFilterClient] = useState('all');
|
const [filterClient, setFilterClient] = useState('all');
|
||||||
|
const [searchQuery, setSearchQuery] = useState(''); // Live search filter for clients
|
||||||
|
const [expandedClients, setExpandedClients] = useState({}); // Expanded telemetry details for offline nodes
|
||||||
const terminalRef = useRef(null);
|
const terminalRef = useRef(null);
|
||||||
|
|
||||||
const showToast = (message, type = 'success') => {
|
const showToast = (message, type = 'success') => {
|
||||||
@@ -120,6 +135,20 @@ function App() {
|
|||||||
}
|
}
|
||||||
}, [logs]);
|
}, [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
|
// Dispatch command to client execution queue
|
||||||
const handleDispatch = async (e) => {
|
const handleDispatch = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -165,6 +194,10 @@ function App() {
|
|||||||
|
|
||||||
// Dynamic Whitelist Save Config
|
// Dynamic Whitelist Save Config
|
||||||
const handleSaveWhitelist = async () => {
|
const handleSaveWhitelist = async () => {
|
||||||
|
if (jsonError) {
|
||||||
|
showToast('Cannot save configuration. Please resolve JSON errors first.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const parsedConfig = JSON.parse(editorText);
|
const parsedConfig = JSON.parse(editorText);
|
||||||
const response = await apiFetch(`${API_BASE}/api/save-commands`, {
|
const response = await apiFetch(`${API_BASE}/api/save-commands`, {
|
||||||
@@ -184,6 +217,13 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleClientDetails = (clientId) => {
|
||||||
|
setExpandedClients(prev => ({
|
||||||
|
...prev,
|
||||||
|
[clientId]: !prev[clientId]
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
// Compile individual client arrays into a single global chronological timeline for terminal display
|
// Compile individual client arrays into a single global chronological timeline for terminal display
|
||||||
const getSortedTimeline = () => {
|
const getSortedTimeline = () => {
|
||||||
const timeline = [];
|
const timeline = [];
|
||||||
@@ -209,81 +249,93 @@ function App() {
|
|||||||
const activeClients = Object.values(clients).filter(c => c.active).length;
|
const activeClients = Object.values(clients).filter(c => c.active).length;
|
||||||
const totalClients = Object.keys(clients).length;
|
const totalClients = Object.keys(clients).length;
|
||||||
|
|
||||||
|
// Filter clients based on search query
|
||||||
|
const filteredClientEntries = Object.entries(clients).filter(([clientId]) =>
|
||||||
|
clientId.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen pb-16 flex flex-col">
|
<div className="min-h-screen pb-16 flex flex-col font-sans antialiased text-slate-100 bg-[#050811]">
|
||||||
{/* Dynamic Toast Alerts */}
|
{/* Dynamic Toast Alerts */}
|
||||||
{toast && (
|
{toast && (
|
||||||
<div className={`fixed bottom-6 right-6 z-50 px-6 py-4 rounded-xl shadow-2xl flex items-center gap-3 border transition-all duration-300 transform translate-y-0 ${
|
<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'
|
toast.type === 'error'
|
||||||
? 'bg-rose-950/85 border-rose-500/30 text-rose-200'
|
? 'bg-rose-950/90 border-rose-500/30 text-rose-200 shadow-rose-950/20'
|
||||||
: 'bg-emerald-950/85 border-emerald-500/30 text-emerald-200'
|
: 'bg-emerald-950/90 border-emerald-500/30 text-emerald-200 shadow-emerald-950/20'
|
||||||
}`}>
|
}`}>
|
||||||
{toast.type === 'error' ? <XCircle className="w-5 h-5 text-rose-400" /> : <CheckCircle className="w-5 h-5 text-emerald-400" />}
|
{toast.type === 'error' ? (
|
||||||
<span className="font-semibold text-sm">{toast.message}</span>
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Modern Glass Header Section */}
|
{/* Modern Glass Header Section */}
|
||||||
<header className="sticky top-0 z-30 px-6 py-4 glass-panel border-b border-white/5 flex items-center justify-between">
|
<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 gap-3">
|
||||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-tr from-violet-600 to-indigo-600 flex items-center justify-center shadow-lg shadow-violet-900/30">
|
<div className="w-10 h-10 rounded-xl bg-gradient-to-tr from-violet-600 to-indigo-600 flex items-center justify-center shadow-lg shadow-violet-500/20 relative group overflow-hidden">
|
||||||
<Zap className="w-5 h-5 text-white" />
|
<Zap className="w-5 h-5 text-white animate-pulse" />
|
||||||
|
<span className="absolute inset-0 bg-white/20 translate-y-full group-hover:translate-y-0 transition-transform duration-300"></span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="font-bold text-lg tracking-tight text-white flex items-center gap-2">
|
<h1 className="font-bold text-lg tracking-tight text-white flex items-center gap-2">
|
||||||
SEEKRIGHT PULSE RMM
|
SEEKRIGHT PULSE RMM
|
||||||
<span className="text-[10px] uppercase font-mono px-2 py-0.5 rounded bg-violet-500/10 border border-violet-500/20 text-violet-300">CORE-v1.0</span>
|
<span className="text-[9px] uppercase font-mono px-2 py-0.5 rounded-full bg-violet-500/10 border border-violet-500/20 text-violet-300 font-semibold">CORE-v1.0</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xs text-slate-400 font-medium">Enterprise Global Fleet & Whitelist Agent Orchestrator</p>
|
<p className="text-[11px] text-slate-400 font-medium">Enterprise Global Fleet & Whitelist Agent Orchestrator</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action Controls & Navigation tabs */}
|
{/* Action Controls & Navigation tabs */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3">
|
||||||
<nav className="flex items-center gap-1.5 p-1 rounded-xl bg-slate-950/60 border border-white/5">
|
<nav className="flex items-center gap-1.5 p-1 rounded-xl bg-slate-950/60 border border-white/5">
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('dashboard')}
|
onClick={() => setActiveTab('dashboard')}
|
||||||
className={`px-4 py-2 rounded-lg font-semibold text-xs transition-all ${
|
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'
|
activeTab === 'dashboard'
|
||||||
? 'bg-gradient-to-r from-violet-600 to-indigo-600 text-white shadow-md'
|
? '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'
|
: 'text-slate-400 hover:text-slate-200 hover:bg-white/5'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Activity className="w-3.5 h-3.5 inline mr-1.5 -mt-0.5" />
|
<Activity className="w-3.5 h-3.5" />
|
||||||
Fleet Status
|
Fleet Status
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('editor')}
|
onClick={() => setActiveTab('editor')}
|
||||||
className={`px-4 py-2 rounded-lg font-semibold text-xs transition-all ${
|
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'
|
activeTab === 'editor'
|
||||||
? 'bg-gradient-to-r from-violet-600 to-indigo-600 text-white shadow-md'
|
? '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'
|
: 'text-slate-400 hover:text-slate-200 hover:bg-white/5'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Settings className="w-3.5 h-3.5 inline mr-1.5 -mt-0.5" />
|
<Settings className="w-3.5 h-3.5" />
|
||||||
Commands Whitelist
|
Whitelist Editor
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Sync indicator status badge */}
|
{/* Sync indicator status badge */}
|
||||||
<div className="flex items-center gap-2 px-3 py-1.5 rounded-xl bg-slate-900/50 border border-white/5">
|
<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">
|
||||||
<span className={`w-2 h-2 rounded-full ${isSyncing ? 'bg-violet-400 animate-spin' : 'bg-emerald-400 pulse-glow-emerald'}`}></span>
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs font-mono text-slate-300">
|
<span className={`w-2 h-2 rounded-full ${isSyncing ? 'bg-violet-400 animate-spin' : 'bg-emerald-400 glow-emerald'}`}></span>
|
||||||
API Status: <b className="text-emerald-400 font-bold">Online</b>
|
<span className="font-mono text-slate-300 font-medium">
|
||||||
</span>
|
API Status: <b className="text-emerald-400 font-bold">Online</b>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={syncFleetData}
|
onClick={syncFleetData}
|
||||||
title="Force Sync Fleet Vitals"
|
title="Force Sync Fleet Vitals"
|
||||||
className="p-1 rounded hover:bg-white/5 text-slate-400 hover:text-slate-200 transition-colors"
|
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 h-3 ${isSyncing ? 'animate-spin text-violet-400' : ''}`} />
|
<RefreshCw className={`w-3.5 h-3.5 ${isSyncing ? 'animate-spin text-violet-400' : ''}`} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Main Container */}
|
{/* Main Container */}
|
||||||
<main className="max-w-7xl w-full mx-auto px-6 mt-8 flex-grow grid grid-cols-1 lg:grid-cols-3 gap-8">
|
<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' ? (
|
{activeTab === 'dashboard' ? (
|
||||||
<>
|
<>
|
||||||
@@ -291,70 +343,150 @@ function App() {
|
|||||||
<div className="lg:col-span-2 space-y-6">
|
<div className="lg:col-span-2 space-y-6">
|
||||||
|
|
||||||
{/* Central Metrics Board */}
|
{/* Central Metrics Board */}
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
<div className="glass-panel p-4 rounded-2xl flex flex-col justify-between">
|
<div className="glass-panel p-5 rounded-2xl flex items-center gap-4 relative overflow-hidden group">
|
||||||
<span className="text-xs font-semibold text-slate-400">Total Systems</span>
|
<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>
|
||||||
<span className="text-2xl font-bold text-white mt-1">{totalClients}</span>
|
<div className="w-12 h-12 rounded-xl bg-violet-500/10 border border-violet-500/20 flex items-center justify-center text-violet-400 shrink-0">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="glass-panel p-4 rounded-2xl flex flex-col justify-between border-l-2 border-l-emerald-500">
|
|
||||||
<span className="text-xs font-semibold text-slate-400">🟢 Active Systems</span>
|
<div className="glass-panel p-5 rounded-2xl flex items-center gap-4 relative overflow-hidden border-l-2 border-l-emerald-500 group">
|
||||||
<span className="text-2xl font-bold text-emerald-400 mt-1">{activeClients}</span>
|
<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 bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center text-emerald-400 shrink-0">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="glass-panel p-4 rounded-2xl flex flex-col justify-between border-l-2 border-l-rose-500">
|
|
||||||
<span className="text-xs font-semibold text-slate-400">🔴 Offline Systems</span>
|
<div className="glass-panel p-5 rounded-2xl flex items-center gap-4 relative overflow-hidden border-l-2 border-l-rose-500 group">
|
||||||
<span className="text-2xl font-bold text-rose-400 mt-1">{totalClients - activeClients}</span>
|
<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 bg-rose-500/10 border border-rose-500/20 flex items-center justify-center text-rose-400 shrink-0">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Vitals Node Grid Cards */}
|
{/* Vitals Node Grid Cards */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h2 className="text-sm font-bold text-white tracking-wide uppercase flex items-center gap-2">
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||||
<Server className="w-4 h-4 text-violet-400" />
|
<h2 className="text-xs font-bold text-slate-300 tracking-wider uppercase flex items-center gap-2">
|
||||||
Remote Fleet Systems Grid
|
<Sliders className="w-4 h-4 text-violet-400" />
|
||||||
</h2>
|
Remote Fleet Systems Grid ({filteredClientEntries.length})
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
|
||||||
{Object.keys(clients).length === 0 ? (
|
{filteredClientEntries.length === 0 ? (
|
||||||
<div className="glass-panel p-12 rounded-2xl text-center text-slate-400">
|
<div className="glass-panel p-12 rounded-2xl text-center text-slate-400">
|
||||||
<AlertTriangle className="w-8 h-8 mx-auto text-yellow-400 mb-2 opacity-60" />
|
<AlertTriangle className="w-8 h-8 mx-auto text-yellow-500/60 mb-3" />
|
||||||
<p className="font-semibold text-sm">Searching for active RMM telemetry agents...</p>
|
<p className="font-semibold text-sm">No systems matching your query.</p>
|
||||||
<p className="text-xs text-slate-500 mt-1">Boot up an agent using deploy_agent.sh to register</p>
|
<p className="text-xs text-slate-500 mt-1">Make sure the agent services are actively running</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
Object.entries(clients).map(([clientId, info]) => {
|
filteredClientEntries.map(([clientId, info]) => {
|
||||||
const clientTelemetry = telemetry[clientId];
|
const clientTelemetry = telemetry[clientId];
|
||||||
const isOnline = info.active;
|
const isOnline = info.active;
|
||||||
const lastSeenDate = info.last_seen ? new Date(info.last_seen).toLocaleString() : 'Never';
|
const lastSeenDate = info.last_seen ? new Date(info.last_seen).toLocaleString() : 'Never';
|
||||||
|
const isExpanded = expandedClients[clientId] || false;
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
</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 (
|
return (
|
||||||
<div
|
<div
|
||||||
key={clientId}
|
key={clientId}
|
||||||
className="glass-panel p-6 rounded-2xl hover:border-violet-500/40 hover:-translate-y-1 hover:shadow-[0_8px_30px_rgb(99,102,241,0.12)] transition-all duration-300 relative overflow-hidden group bg-gradient-to-b from-slate-900/30 to-slate-950/50"
|
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 bar */}
|
{/* Status top glow line */}
|
||||||
<div className={`absolute top-0 left-0 right-0 h-1 ${isOnline ? 'bg-gradient-to-r from-emerald-500 to-teal-400' : 'bg-gradient-to-r from-rose-500 to-red-400'}`} />
|
<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 */}
|
{/* Title block */}
|
||||||
<div className="flex items-center justify-between mb-5">
|
<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="flex items-center gap-3">
|
||||||
<div className={`w-3.5 h-3.5 rounded-full flex items-center justify-center ${isOnline ? 'bg-emerald-500/10' : 'bg-rose-500/10'}`}>
|
<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 w-2">
|
<span className="relative flex h-2.5 w-2.5">
|
||||||
{isOnline && <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>}
|
<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 w-2 ${isOnline ? 'bg-emerald-500' : 'bg-rose-500'}`}></span>
|
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500 glow-emerald"></span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-extrabold text-white text-lg tracking-tight group-hover:text-violet-300 transition-colors">{clientId}</span>
|
<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 ${
|
<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">
|
||||||
isOnline
|
Active
|
||||||
? 'bg-emerald-500/10 border border-emerald-500/20 text-emerald-300'
|
|
||||||
: 'bg-rose-500/10 border border-rose-500/20 text-rose-300'
|
|
||||||
}`}>
|
|
||||||
{isOnline ? 'Active' : 'Offline'}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-right">
|
<div className="text-left sm:text-right">
|
||||||
<span className="text-[10px] text-slate-500 block">LAST CONTACT</span>
|
<span className="text-[9px] text-slate-500 block uppercase font-bold tracking-wider">LAST CHECK-IN</span>
|
||||||
<span className="text-[11px] font-bold text-slate-300 font-mono">{lastSeenDate}</span>
|
<span className="text-xs font-bold text-slate-300 font-mono">{lastSeenDate}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -362,21 +494,21 @@ function App() {
|
|||||||
{clientTelemetry ? (
|
{clientTelemetry ? (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
{/* CPU & RAM progress */}
|
{/* CPU & RAM progress */}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
{/* CPU Progress block */}
|
{/* CPU Progress block */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const cpu = clientTelemetry.cpu_percent || 0;
|
const cpu = clientTelemetry.cpu_percent || 0;
|
||||||
const isWarning = cpu > 80;
|
const isWarning = cpu > 80;
|
||||||
return (
|
return (
|
||||||
<div className="p-3.5 bg-slate-950/40 border border-white/5 rounded-2xl flex flex-col justify-between">
|
<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">
|
<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">
|
<span className="flex items-center gap-1.5">
|
||||||
<Cpu className={`w-3.5 h-3.5 ${isWarning ? 'text-rose-400 animate-pulse' : 'text-violet-400'}`} />
|
<Cpu className={`w-4 h-4 ${isWarning ? 'text-rose-400 animate-pulse' : 'text-violet-400'}`} />
|
||||||
CPU Load
|
CPU Load
|
||||||
</span>
|
</span>
|
||||||
<span className={`font-bold ${isWarning ? 'text-rose-400' : 'text-white'}`}>{cpu}%</span>
|
<span className={`font-bold font-mono ${isWarning ? 'text-rose-400' : 'text-white'}`}>{cpu}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full h-2.5 rounded-full bg-slate-900 overflow-hidden relative border border-white/5">
|
<div className="w-full h-2 rounded-full bg-slate-900 overflow-hidden relative border border-white/5">
|
||||||
<div
|
<div
|
||||||
className={`h-full rounded-full transition-all duration-500 ${
|
className={`h-full rounded-full transition-all duration-500 ${
|
||||||
isWarning
|
isWarning
|
||||||
@@ -389,8 +521,8 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{isWarning && (
|
{isWarning && (
|
||||||
<div className="text-[9px] text-rose-400 font-bold flex items-center gap-1 mt-1.5 animate-pulse">
|
<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 Utilization
|
<AlertTriangle className="w-2.5 h-2.5" /> High CPU Warning
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -405,15 +537,15 @@ function App() {
|
|||||||
const ram = clientTelemetry.memory_percent || 0;
|
const ram = clientTelemetry.memory_percent || 0;
|
||||||
const isWarning = ram > 85;
|
const isWarning = ram > 85;
|
||||||
return (
|
return (
|
||||||
<div className="p-3.5 bg-slate-950/40 border border-white/5 rounded-2xl flex flex-col justify-between">
|
<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">
|
<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">
|
<span className="flex items-center gap-1.5">
|
||||||
<Activity className={`w-3.5 h-3.5 ${isWarning ? 'text-rose-400 animate-pulse' : 'text-violet-400'}`} />
|
<Activity className={`w-4 h-4 ${isWarning ? 'text-rose-400 animate-pulse' : 'text-violet-400'}`} />
|
||||||
Virtual RAM
|
Virtual RAM
|
||||||
</span>
|
</span>
|
||||||
<span className={`font-bold ${isWarning ? 'text-rose-400' : 'text-white'}`}>{ram}%</span>
|
<span className={`font-bold font-mono ${isWarning ? 'text-rose-400' : 'text-white'}`}>{ram}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full h-2.5 rounded-full bg-slate-900 overflow-hidden relative border border-white/5">
|
<div className="w-full h-2 rounded-full bg-slate-900 overflow-hidden relative border border-white/5">
|
||||||
<div
|
<div
|
||||||
className={`h-full rounded-full transition-all duration-500 ${
|
className={`h-full rounded-full transition-all duration-500 ${
|
||||||
isWarning
|
isWarning
|
||||||
@@ -425,7 +557,7 @@ function App() {
|
|||||||
style={{ width: `${ram}%` }}
|
style={{ width: `${ram}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap justify-between text-[9px] text-slate-500 font-mono font-medium mt-2 border-t border-white/5 pt-1.5 font-bold gap-x-1">
|
<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>Used: <b className="text-slate-300">{used}G</b></span>
|
||||||
<span>Free: <b className="text-slate-300">{free}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>
|
<span>Total: <b className="text-slate-300">{total}G</b></span>
|
||||||
@@ -436,11 +568,11 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* GPU and Partition details */}
|
{/* GPU and Partition details */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-4 border-t border-white/5">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-5 pt-4 border-t border-white/5">
|
||||||
{/* Partition Storage */}
|
{/* Partition Storage */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2.5">
|
||||||
<span className="text-[10px] uppercase font-extrabold tracking-wider text-slate-400 flex items-center gap-1.5">
|
<span className="text-[10px] uppercase font-extrabold tracking-wider text-slate-400 flex items-center gap-1.5">
|
||||||
<HardDrive className="w-3.5 h-3.5 text-indigo-400" /> Disk Drive Space
|
<HardDrive className="w-4 h-4 text-indigo-400" /> Disk Drive Space
|
||||||
</span>
|
</span>
|
||||||
<div className="space-y-2.5">
|
<div className="space-y-2.5">
|
||||||
{clientTelemetry.disks && clientTelemetry.disks.length > 0 ? (
|
{clientTelemetry.disks && clientTelemetry.disks.length > 0 ? (
|
||||||
@@ -449,20 +581,20 @@ function App() {
|
|||||||
const diskFree = d.free_gb || 0;
|
const diskFree = d.free_gb || 0;
|
||||||
const diskUsed = (diskTotal - diskFree).toFixed(2);
|
const diskUsed = (diskTotal - diskFree).toFixed(2);
|
||||||
return (
|
return (
|
||||||
<div key={index} className="p-2.5 rounded-xl bg-slate-950/30 border border-white/5 space-y-1.5">
|
<div key={index} className="p-3 rounded-xl bg-slate-950/30 border border-white/5 space-y-2">
|
||||||
<div className="flex items-center justify-between text-xs">
|
<div className="flex items-center justify-between text-xs">
|
||||||
<span className="font-semibold text-slate-300">Disk [ {d.mount} ]</span>
|
<span className="font-bold text-slate-300">Disk [ {d.mount} ]</span>
|
||||||
<span className="font-mono text-slate-400 text-[10px]">
|
<span className="font-mono text-slate-400 text-[10px]">
|
||||||
Used: <b className="text-slate-300">{diskUsed}G</b> / {diskTotal}G
|
Used: <b className="text-slate-300">{diskUsed}G</b> / {diskTotal}G
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full h-1.5 rounded-full bg-slate-900 overflow-hidden">
|
<div className="w-full h-1.5 rounded-full bg-slate-900 overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className={`h-full rounded-full ${d.percent > 85 ? 'bg-rose-500' : 'bg-indigo-500'}`}
|
className={`h-full rounded-full ${d.percent > 85 ? 'bg-rose-505' : 'bg-indigo-500'}`}
|
||||||
style={{ width: `${d.percent}%` }}
|
style={{ width: `${d.percent}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-[8px] text-slate-500 font-mono">
|
<div className="flex justify-between text-[9px] text-slate-500 font-mono">
|
||||||
<span>Free: <b className="text-slate-400">{diskFree} GB</b></span>
|
<span>Free: <b className="text-slate-400">{diskFree} GB</b></span>
|
||||||
<span>{d.percent}% Used</span>
|
<span>{d.percent}% Used</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -476,35 +608,35 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Nvidia Core GPU Vitals */}
|
{/* Nvidia Core GPU Vitals */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2.5">
|
||||||
<span className="text-[10px] uppercase font-extrabold tracking-wider text-slate-400 flex items-center gap-1.5">
|
<span className="text-[10px] uppercase font-extrabold tracking-wider text-slate-400 flex items-center gap-1.5">
|
||||||
<Flame className="w-3.5 h-3.5 text-rose-400 animate-pulse" /> Nvidia GPU Accelerator
|
<Flame className="w-4 h-4 text-rose-400 animate-pulse" /> Nvidia GPU Accelerator
|
||||||
</span>
|
</span>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-2.5">
|
||||||
{clientTelemetry.gpus && clientTelemetry.gpus.length > 0 ? (
|
{clientTelemetry.gpus && clientTelemetry.gpus.length > 0 ? (
|
||||||
clientTelemetry.gpus.map((gpu, index) => (
|
clientTelemetry.gpus.map((gpu, index) => (
|
||||||
<div key={index} className="p-2.5 rounded-xl bg-slate-950/40 border border-white/5 space-y-2">
|
<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">
|
<div className="flex justify-between items-center text-xs border-b border-white/5 pb-1.5">
|
||||||
<span className="font-bold text-slate-200 truncate max-w-[130px]">🎮 {gpu.name}</span>
|
<span className="font-extrabold text-slate-200 truncate max-w-[150px]">🎮 {gpu.name}</span>
|
||||||
<span className="text-violet-400 font-extrabold font-mono text-[11px]">Load: {gpu.utilization}</span>
|
<span className="text-violet-400 font-bold font-mono text-xs">Load: {gpu.utilization}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-x-2 gap-y-1 text-[10px] text-slate-400 font-mono">
|
<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"><span>Temp:</span> <b className="text-slate-200 font-semibold">{gpu.temp}</b></div>
|
<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"><span>Fans:</span> <b className="text-slate-200 font-semibold">{gpu.fan_speed}</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 mt-0.5">
|
<div className="col-span-2 flex justify-between border-t border-white/5 pt-1.5 mt-1">
|
||||||
<span>VRAM Used:</span>
|
<span>VRAM:</span>
|
||||||
<b className="text-slate-200 font-semibold">{gpu.memory_used} / {gpu.memory_total || 'N/A'}</b>
|
<b className="text-slate-200">{gpu.memory_used} / {gpu.memory_total || 'N/A'}</b>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 flex justify-between">
|
<div className="col-span-2 flex justify-between">
|
||||||
<span>Power Draw:</span>
|
<span>Power:</span>
|
||||||
<b className="text-slate-200 font-semibold">{gpu.power_draw} / {gpu.power_limit || 'N/A'}</b>
|
<b className="text-slate-200">{gpu.power_draw} / {gpu.power_limit || 'N/A'}</b>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="text-xs text-slate-500 py-3 text-center font-medium bg-slate-950/15 border border-white/5 rounded-xl italic">
|
<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 reported.
|
No NVIDIA GPU hardware detected.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -513,7 +645,7 @@ function App() {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-6 bg-slate-950/20 rounded-xl border border-white/5 my-4">
|
<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>
|
<p className="text-xs text-slate-400 font-medium italic">Telemetry check-in pending... Waiting for dynamic vitals transmission</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -526,46 +658,48 @@ function App() {
|
|||||||
|
|
||||||
{/* Maintenance Command Dispatch deck (Right sidebar) */}
|
{/* Maintenance Command Dispatch deck (Right sidebar) */}
|
||||||
<div className="lg:col-span-1">
|
<div className="lg:col-span-1">
|
||||||
<div className="glass-panel p-6 rounded-2xl sticky top-24 border border-white/5 space-y-6">
|
<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">
|
<div className="border-b border-white/5 pb-3">
|
||||||
<h2 className="text-sm font-bold text-white uppercase tracking-wider flex items-center gap-2">
|
<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" />
|
<Play className="w-4 h-4 text-violet-400 fill-violet-400" />
|
||||||
Maintenance Dispatcher
|
Maintenance Dispatcher
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-xs text-slate-400 mt-0.5">Send a whitelisted command down to a node</p>
|
<p className="text-[11px] text-slate-400 mt-0.5">Send a whitelisted command down to a node</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleDispatch} className="space-y-4">
|
<form onSubmit={handleDispatch} className="space-y-5">
|
||||||
{/* Select target node check list */}
|
{/* Select target node check list */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2 flex justify-between">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<span>Select Fleet Targets</span>
|
<label className="text-xs font-bold text-slate-400 uppercase tracking-wider">
|
||||||
<div className="flex gap-2.5">
|
Select Targets
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSelectedClients(Object.keys(clients))}
|
onClick={() => setSelectedClients(Object.keys(clients))}
|
||||||
className="text-[10px] text-violet-400 font-extrabold hover:text-violet-300 transition-colors uppercase tracking-wider"
|
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
|
All
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSelectedClients(Object.entries(clients).filter(([_, info]) => info.active).map(([id]) => id))}
|
onClick={() => setSelectedClients(Object.entries(clients).filter(([_, info]) => info.active).map(([id]) => id))}
|
||||||
className="text-[10px] text-emerald-400 font-extrabold hover:text-emerald-300 transition-colors uppercase tracking-wider"
|
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
|
Active
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSelectedClients([])}
|
onClick={() => setSelectedClients([])}
|
||||||
className="text-[10px] text-slate-500 font-extrabold hover:text-slate-400 transition-colors uppercase tracking-wider"
|
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"
|
||||||
>
|
>
|
||||||
None
|
Clear
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2 max-h-48 overflow-y-auto p-1 bg-slate-950/40 border border-white/5 rounded-xl">
|
<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 ? (
|
{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>
|
<span className="col-span-2 text-xs text-slate-500 italic text-center py-6 block font-medium">No registered nodes found.</span>
|
||||||
) : (
|
) : (
|
||||||
@@ -584,21 +718,21 @@ function App() {
|
|||||||
}}
|
}}
|
||||||
className={`p-2.5 rounded-xl border text-left transition-all duration-200 relative flex flex-col justify-between overflow-hidden ${
|
className={`p-2.5 rounded-xl border text-left transition-all duration-200 relative flex flex-col justify-between overflow-hidden ${
|
||||||
isSelected
|
isSelected
|
||||||
? 'bg-gradient-to-br from-violet-600/15 to-indigo-600/15 border-violet-500/80 shadow-md shadow-violet-900/15 text-white'
|
? '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-950/60 border-white/5 hover:border-white/15 text-slate-400 hover:text-slate-200'
|
: 'bg-slate-900/40 border-white/5 hover:border-white/10 text-slate-400 hover:text-slate-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* Glow element */}
|
{/* 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" />}
|
{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">
|
<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'}`}>
|
<span className={`text-xs font-extrabold truncate ${isSelected ? 'text-violet-200' : 'text-slate-300'}`}>
|
||||||
{id}
|
{id}
|
||||||
</span>
|
</span>
|
||||||
<span className={`w-1.5 h-1.5 rounded-full shrink-0 ${info.active ? 'bg-emerald-400' : 'bg-rose-400'}`} />
|
<span className={`w-1.5 h-1.5 rounded-full shrink-0 ${info.active ? 'bg-emerald-400 glow-emerald' : 'bg-rose-400'}`} />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[9px] font-mono text-slate-500 mt-1 font-semibold uppercase tracking-wider">
|
<span className="text-[9px] font-mono text-slate-500 mt-1 font-semibold uppercase tracking-wider">
|
||||||
{info.active ? '🟢 Online' : '🔴 Offline'}
|
{info.active ? 'Online' : 'Offline'}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
@@ -609,25 +743,25 @@ function App() {
|
|||||||
|
|
||||||
{/* Manual Target target ID */}
|
{/* Manual Target target ID */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1.5">Or Type Target Name manually (comma-separated for batch)</label>
|
<label className="block text-[11px] font-semibold text-slate-400 uppercase tracking-wider mb-1.5">Or Type Custom Node Target</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="e.g. kawsik, client_1, client_2"
|
placeholder="e.g. client_1, client_2"
|
||||||
value={manualClient}
|
value={manualClient}
|
||||||
onChange={(e) => setManualClient(e.target.value)}
|
onChange={(e) => setManualClient(e.target.value)}
|
||||||
className="w-full bg-slate-950 border border-white/10 rounded-xl p-3 text-sm text-slate-300 font-semibold focus:outline-none focus:border-violet-500 placeholder-slate-600 transition-colors"
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Select command key */}
|
{/* Select command key */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1.5">Pick Whitelisted Operation</label>
|
<label className="block text-[11px] font-semibold text-slate-400 uppercase tracking-wider mb-1.5">Select Whitelisted Operation Key</label>
|
||||||
<select
|
<select
|
||||||
value={selectedCommand}
|
value={selectedCommand}
|
||||||
onChange={(e) => setSelectedCommand(e.target.value)}
|
onChange={(e) => setSelectedCommand(e.target.value)}
|
||||||
className="w-full bg-slate-950 border border-white/10 rounded-xl p-3 text-sm text-slate-300 font-semibold focus:outline-none focus:border-violet-500 transition-colors"
|
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="">-- Choose Whitelisted Command --</option>
|
<option value="">-- Select Permitted Operation --</option>
|
||||||
{Array.from(new Set([
|
{Array.from(new Set([
|
||||||
...Object.keys(whitelist.nt || {}),
|
...Object.keys(whitelist.nt || {}),
|
||||||
...Object.keys(whitelist.posix || {})
|
...Object.keys(whitelist.posix || {})
|
||||||
@@ -641,23 +775,23 @@ function App() {
|
|||||||
|
|
||||||
{/* Manual command command key */}
|
{/* Manual command command key */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1.5">Or Type Operation Key manually</label>
|
<label className="block text-[11px] font-semibold text-slate-400 uppercase tracking-wider mb-1.5">Or Type Custom Operation Key</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="e.g. repair_mysql"
|
placeholder="e.g. check_drives"
|
||||||
value={manualCommand}
|
value={manualCommand}
|
||||||
onChange={(e) => setManualCommand(e.target.value)}
|
onChange={(e) => setManualCommand(e.target.value)}
|
||||||
className="w-full bg-slate-950 border border-white/10 rounded-xl p-3 text-sm text-slate-300 font-semibold focus:outline-none focus:border-violet-500 placeholder-slate-600 transition-colors"
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Fire execution button */}
|
{/* Fire execution button */}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
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 px-4 rounded-xl text-sm 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] transition-all"
|
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-4 h-4 fill-white" />
|
<Zap className="w-3.5 h-3.5 fill-white" />
|
||||||
Dispatch Operation to Node
|
Dispatch Command Batch
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -666,37 +800,81 @@ function App() {
|
|||||||
) : (
|
) : (
|
||||||
/* Visual Whitelist JSON Configuration Editor */
|
/* Visual Whitelist JSON Configuration Editor */
|
||||||
<div className="col-span-3">
|
<div className="col-span-3">
|
||||||
<div className="glass-panel p-6 rounded-2xl border border-white/5 space-y-6">
|
<div className="glass-panel p-5 md:p-6 rounded-2xl border border-white/5 space-y-6">
|
||||||
<div className="border-b border-white/5 pb-3">
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-white/5 pb-4">
|
||||||
<h2 className="text-sm font-bold text-white uppercase tracking-wider flex items-center gap-2">
|
<div>
|
||||||
<Settings className="w-4 h-4 text-violet-400" />
|
<h2 className="text-sm font-bold text-white uppercase tracking-wider flex items-center gap-2">
|
||||||
Visual Command Whitelist Editor
|
<Settings className="w-4 h-4 text-violet-400" />
|
||||||
</h2>
|
Visual Commands whitelist Editor
|
||||||
<p className="text-xs text-slate-400 mt-0.5">
|
</h2>
|
||||||
Directly configure permitted cluster arrays. Permitted operations are pushed dynamically to remote agents without any central restart required!
|
<p className="text-[11px] text-slate-400 mt-0.5">
|
||||||
</p>
|
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>
|
</div>
|
||||||
|
|
||||||
|
{/* IDE Editor UI wrapper */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<textarea
|
<div className="rounded-xl overflow-hidden border border-white/5 bg-[#02040a] shadow-inner relative">
|
||||||
className="w-full h-[400px] bg-slate-950 border border-white/10 rounded-xl p-4 font-mono text-sm text-emerald-400 leading-relaxed focus:outline-none focus:border-violet-500 transition-colors focus:ring-1 focus:ring-violet-500/25"
|
{/* Editor Tab Bar */}
|
||||||
spellCheck="false"
|
<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">
|
||||||
value={editorText}
|
<div className="flex items-center gap-2">
|
||||||
onChange={(e) => setEditorText(e.target.value)}
|
<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 justify-end gap-3">
|
<div className="flex flex-col sm:flex-row justify-end gap-3 pt-2">
|
||||||
<button
|
<button
|
||||||
onClick={fetchWhitelist}
|
onClick={fetchWhitelist}
|
||||||
className="px-5 py-2.5 rounded-xl border border-white/10 hover:bg-white/5 font-semibold text-xs text-slate-300 transition-colors"
|
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
|
Discard Changes
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleSaveWhitelist}
|
onClick={handleSaveWhitelist}
|
||||||
className="px-6 py-2.5 rounded-xl bg-gradient-to-r from-violet-600 to-indigo-600 hover:from-violet-500 hover:to-indigo-500 text-white font-bold text-xs shadow-lg shadow-violet-950/40 hover:shadow-violet-900/50 hover:translate-y-[-1px] active:translate-y-[1px] transition-all"
|
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]'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
Deploy updated JSON Configuration
|
<Check className="w-3.5 h-3.5" />
|
||||||
|
Deploy Whitelist Configuration
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -706,51 +884,60 @@ function App() {
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Scrolling DevOps CLI output shell terminal */}
|
{/* Scrolling DevOps CLI output shell terminal */}
|
||||||
<footer className="max-w-7xl w-full mx-auto px-6 mt-8">
|
<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">
|
<div className="bg-slate-950/95 border border-white/5 rounded-2xl overflow-hidden shadow-2xl">
|
||||||
<div className="bg-slate-900/80 px-4 py-2 border-b border-white/5 flex items-center justify-between">
|
{/* 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">
|
<div className="flex items-center gap-2">
|
||||||
|
{/* Window Controls */}
|
||||||
<div className="flex gap-1.5">
|
<div className="flex gap-1.5">
|
||||||
<span className="w-2.5 h-2.5 rounded-full bg-rose-500/60 block"></span>
|
<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/60 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/60 block"></span>
|
<span className="w-2.5 h-2.5 rounded-full bg-emerald-500/70 block"></span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[11px] uppercase tracking-wider font-bold text-slate-400 font-mono ml-2 flex items-center gap-1.5">
|
<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-3.5 h-3.5 text-violet-400" />
|
<Terminal className="w-4 h-4 text-violet-400" />
|
||||||
Live Fleet Activity Logs & Webhook outputs
|
Live Fleet Activity & Webhook Output Shell
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<select
|
<div className="flex items-center gap-3 w-full sm:w-auto justify-between sm:justify-end">
|
||||||
value={filterClient}
|
<div className="relative">
|
||||||
onChange={(e) => setFilterClient(e.target.value)}
|
<select
|
||||||
className="bg-slate-950 border border-white/10 rounded-lg px-2.5 py-1 text-[11px] text-slate-300 font-semibold focus:outline-none focus:border-violet-500 transition-colors"
|
value={filterClient}
|
||||||
>
|
onChange={(e) => setFilterClient(e.target.value)}
|
||||||
<option value="all">🔍 All Fleet Logs</option>
|
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"
|
||||||
{Object.keys(clients).map((cid) => (
|
>
|
||||||
<option key={cid} value={cid}>
|
<option value="all">🔍 Show All Nodes</option>
|
||||||
{cid} logs
|
{Object.keys(clients).map((cid) => (
|
||||||
</option>
|
<option key={cid} value={cid}>
|
||||||
))}
|
{cid} logs
|
||||||
</select>
|
</option>
|
||||||
<span className="text-[10px] text-slate-500 font-mono font-medium">Terminal Shell v1.0</span>
|
))}
|
||||||
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ref={terminalRef} className="p-4 h-64 overflow-y-auto font-mono text-xs space-y-2.5 terminal-scroll bg-black/90">
|
{/* 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 ? (
|
{sortedTimelineLogs.length === 0 ? (
|
||||||
<span className="text-slate-500 block italic py-2">Waiting for check-in events to stream... No historical logs recorded yet.</span>
|
<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) => {
|
sortedTimelineLogs.map((log, index) => {
|
||||||
const formattedTime = new Date(log.timestamp).toLocaleTimeString();
|
const formattedTime = new Date(log.timestamp).toLocaleTimeString();
|
||||||
|
|
||||||
if (log.type === 'telemetry') {
|
if (log.type === 'telemetry') {
|
||||||
return (
|
return (
|
||||||
<div key={index} className="text-slate-400 flex items-start gap-1 leading-relaxed">
|
<div key={index} className="text-slate-400 flex items-start gap-1 leading-relaxed break-words whitespace-pre-wrap">
|
||||||
<span className="text-slate-600 flex-shrink-0">[{formattedTime}]</span>
|
<span className="text-slate-600 shrink-0 select-none">[{formattedTime}]</span>
|
||||||
<span className="text-sky-400 flex-shrink-0 font-bold">INFO:</span>
|
<span className="text-sky-400 shrink-0 font-bold select-none">INFO:</span>
|
||||||
<span>
|
<span>
|
||||||
Client <b className="text-white">{log.client_id}</b> check-in telemetry pushed. (CPU: {log.detail.cpu_percent}%, RAM: {log.detail.memory_percent}%)
|
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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -758,11 +945,11 @@ function App() {
|
|||||||
|
|
||||||
if (log.type === 'command_scheduled') {
|
if (log.type === 'command_scheduled') {
|
||||||
return (
|
return (
|
||||||
<div key={index} className="text-yellow-200 flex items-start gap-1 leading-relaxed">
|
<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 flex-shrink-0">[{formattedTime}]</span>
|
<span className="text-slate-600 shrink-0 select-none">[{formattedTime}]</span>
|
||||||
<span className="text-yellow-500 flex-shrink-0 font-bold">DISPATCH:</span>
|
<span className="text-amber-400 shrink-0 font-bold select-none">DISPATCH:</span>
|
||||||
<span>
|
<span>
|
||||||
Operation <b className="text-yellow-400 font-semibold">"{log.detail.command}"</b> successfully queued for node <b className="text-white">{log.client_id}</b>.
|
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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -770,11 +957,11 @@ function App() {
|
|||||||
|
|
||||||
if (log.type === 'command_polled') {
|
if (log.type === 'command_polled') {
|
||||||
return (
|
return (
|
||||||
<div key={index} className="text-orange-200/90 flex items-start gap-1 leading-relaxed">
|
<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 flex-shrink-0">[{formattedTime}]</span>
|
<span className="text-slate-600 shrink-0 select-none">[{formattedTime}]</span>
|
||||||
<span className="text-orange-400 flex-shrink-0 font-bold">POLL:</span>
|
<span className="text-orange-400 shrink-0 font-bold select-none">POLL:</span>
|
||||||
<span>
|
<span>
|
||||||
Agent <b className="text-white">{log.client_id}</b> retrieved pending command <b className="text-orange-300 font-semibold">"{log.detail.command}"</b>. Starting execution...
|
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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -783,22 +970,24 @@ function App() {
|
|||||||
if (log.type === 'command_result') {
|
if (log.type === 'command_result') {
|
||||||
const isSuccess = log.detail.returncode === 0;
|
const isSuccess = log.detail.returncode === 0;
|
||||||
return (
|
return (
|
||||||
<div key={index} className="p-2.5 rounded-lg border bg-slate-950/70 border-white/5 space-y-1 my-1">
|
<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 items-center justify-between">
|
<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 items-center gap-1.5">
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
<span className="text-slate-600 font-mono">[{formattedTime}]</span>
|
<span className="text-slate-600 select-none">[{formattedTime}]</span>
|
||||||
<span className={`font-bold ${isSuccess ? 'text-emerald-400' : 'text-rose-400'}`}>
|
<span className={`font-bold text-xs ${isSuccess ? 'text-emerald-400' : 'text-rose-400'}`}>
|
||||||
{isSuccess ? 'OUTCOME SUCCESS:' : 'OUTCOME FAILURE:'}
|
{isSuccess ? '✓ SUCCESS' : '✗ FAILURE'}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-slate-200">
|
<span className="text-slate-300">
|
||||||
Command <b className="text-white">"{log.detail.command}"</b> on <b className="text-white">{log.client_id}</b> finished with code <b className="font-mono">{log.detail.returncode}</b>.
|
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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Subprocess console stdout/stderr */}
|
{/* Subprocess console stdout/stderr */}
|
||||||
{(log.detail.stdout || log.detail.stderr) && (
|
{(log.detail.stdout || log.detail.stderr) && (
|
||||||
<pre className={`p-2 rounded bg-black/60 font-mono text-[11px] leading-normal overflow-x-auto ${isSuccess ? 'text-slate-300 border-l border-emerald-500/45' : 'text-rose-300 border-l border-rose-500/45'}`}>
|
<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.stdout && <code className="block">{log.detail.stdout}</code>}
|
||||||
{log.detail.stderr && <code className="block text-rose-400 font-semibold">{log.detail.stderr}</code>}
|
{log.detail.stderr && <code className="block text-rose-400 font-semibold">{log.detail.stderr}</code>}
|
||||||
</pre>
|
</pre>
|
||||||
@@ -810,7 +999,6 @@ function App() {
|
|||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
{/* Removed internal dummy anchor to avoid viewport jumping */}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
@@ -818,4 +1006,25 @@ function App() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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;
|
export default App;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@@ -7,12 +7,15 @@
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
font-family: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||||
background-color: #080c14;
|
background-color: #050811;
|
||||||
background-image: radial-gradient(circle at 12% 18%, rgba(139, 92, 246, 0.05) 0%, transparent 45%),
|
background-image:
|
||||||
radial-gradient(circle at 88% 82%, rgba(59, 130, 246, 0.05) 0%, transparent 45%);
|
radial-gradient(circle at 10% 20%, rgba(124, 58, 237, 0.06) 0%, transparent 40%),
|
||||||
|
radial-gradient(circle at 90% 80%, rgba(59, 130, 246, 0.06) 0%, transparent 40%),
|
||||||
|
radial-gradient(circle at 50% 50%, rgba(16, 185, 129, 0.02) 0%, transparent 50%);
|
||||||
color: #f3f4f6;
|
color: #f3f4f6;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
@@ -21,44 +24,85 @@ code {
|
|||||||
|
|
||||||
/* Custom scrollbars */
|
/* Custom scrollbars */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 6px;
|
||||||
height: 8px;
|
height: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: rgba(0, 0, 0, 0.2);
|
background: rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.08);
|
||||||
border-radius: 4px;
|
border-radius: 9999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: rgba(255, 255, 255, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Glassmorphism Panel styles */
|
/* Premium Glassmorphism Panel styles */
|
||||||
.glass-panel {
|
.glass-panel {
|
||||||
background: rgba(13, 18, 30, 0.75);
|
background: rgba(10, 15, 28, 0.7);
|
||||||
backdrop-filter: blur(20px);
|
backdrop-filter: blur(16px);
|
||||||
-webkit-backdrop-filter: blur(20px);
|
-webkit-backdrop-filter: blur(16px);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
border: 1px solid rgba(255, 255, 255, 0.04);
|
||||||
box-shadow: 0 10px 40px 0 rgba(0, 0, 0, 0.45);
|
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Smooth active pulses */
|
.glass-panel:hover {
|
||||||
.pulse-glow-emerald {
|
border-color: rgba(255, 255, 255, 0.08);
|
||||||
box-shadow: 0 0 12px #10b981;
|
box-shadow: 0 12px 40px 0 rgba(0, 0, 0, 0.45);
|
||||||
animation: pulse-emerald 2s infinite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse-emerald {
|
/* Glowing Animations for Statuses */
|
||||||
0% { transform: scale(0.92); opacity: 1; }
|
.glow-emerald {
|
||||||
50% { transform: scale(1.15); opacity: 0.6; }
|
box-shadow: 0 0 12px rgba(16, 185, 129, 0.4);
|
||||||
100% { transform: scale(0.92); opacity: 1; }
|
animation: pulse-glow-emerald 2.5s infinite alternate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.glow-rose {
|
||||||
|
box-shadow: 0 0 12px rgba(244, 63, 94, 0.3);
|
||||||
|
animation: pulse-glow-rose 2.5s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-violet {
|
||||||
|
box-shadow: 0 0 12px rgba(139, 92, 246, 0.4);
|
||||||
|
animation: pulse-glow-violet 2.5s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-glow-emerald {
|
||||||
|
0% { transform: scale(0.95); opacity: 0.8; box-shadow: 0 0 4px rgba(16, 185, 129, 0.2); }
|
||||||
|
100% { transform: scale(1.05); opacity: 1; box-shadow: 0 0 14px rgba(16, 185, 129, 0.6); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-glow-rose {
|
||||||
|
0% { transform: scale(0.95); opacity: 0.7; box-shadow: 0 0 4px rgba(244, 63, 94, 0.15); }
|
||||||
|
100% { transform: scale(1.05); opacity: 1; box-shadow: 0 0 12px rgba(244, 63, 94, 0.45); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-glow-violet {
|
||||||
|
0% { transform: scale(0.95); opacity: 0.8; box-shadow: 0 0 4px rgba(139, 92, 246, 0.2); }
|
||||||
|
100% { transform: scale(1.05); opacity: 1; box-shadow: 0 0 14px rgba(139, 92, 246, 0.6); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth Scrolling */
|
||||||
.terminal-scroll {
|
.terminal-scroll {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Animated border glow on focus */
|
||||||
|
.focus-glow-violet:focus {
|
||||||
|
border-color: rgba(139, 92, 246, 0.5);
|
||||||
|
box-shadow: 0 0 12px rgba(139, 92, 246, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Textarea customization */
|
||||||
|
.code-textarea {
|
||||||
|
background: #02040a;
|
||||||
|
color: #34d399; /* emerald-400 */
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user