file transfer
This commit is contained in:
403
src/App.jsx
403
src/App.jsx
@@ -1,15 +1,15 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
Cpu,
|
Cpu,
|
||||||
HardDrive,
|
HardDrive,
|
||||||
Terminal,
|
Terminal,
|
||||||
Settings,
|
Settings,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
XCircle,
|
XCircle,
|
||||||
Zap,
|
Zap,
|
||||||
Flame,
|
Flame,
|
||||||
Server,
|
Server,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Play,
|
Play,
|
||||||
@@ -19,36 +19,12 @@ import {
|
|||||||
Clock,
|
Clock,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
Database,
|
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
Sliders
|
Sliders
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import sideBarLogo from './assets/side-bar-logo.png';
|
import sideBarLogo from './assets/side-bar-logo.png';
|
||||||
|
|
||||||
const API_BASE = import.meta.env.VITE_API_BASE_URL ||
|
import { API_BASE, apiFetch } from './api';
|
||||||
(window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
|
|
||||||
? 'http://localhost:8000'
|
|
||||||
: (window.location.protocol === 'https:'
|
|
||||||
? 'https://rmm-backend.seekright.com'
|
|
||||||
: 'http://rmm-backend.seekright.com'));
|
|
||||||
|
|
||||||
// Secure API fetch wrapper
|
|
||||||
const apiFetch = (url, options = {}) => {
|
|
||||||
const token = localStorage.getItem('rmm_token');
|
|
||||||
if (token) {
|
|
||||||
options.headers = {
|
|
||||||
...options.headers,
|
|
||||||
'Authorization': `Bearer ${token}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return fetch(url, options).then(response => {
|
|
||||||
if (response.status === 401) {
|
|
||||||
localStorage.removeItem('rmm_token');
|
|
||||||
window.dispatchEvent(new Event('rmm_unauthorized'));
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [token, setToken] = useState(localStorage.getItem('rmm_token') || '');
|
const [token, setToken] = useState(localStorage.getItem('rmm_token') || '');
|
||||||
@@ -61,17 +37,21 @@ function App() {
|
|||||||
const [telemetry, setTelemetry] = useState({});
|
const [telemetry, setTelemetry] = useState({});
|
||||||
const [logs, setLogs] = useState({});
|
const [logs, setLogs] = useState({});
|
||||||
const [whitelist, setWhitelist] = useState({});
|
const [whitelist, setWhitelist] = useState({});
|
||||||
|
|
||||||
|
// Remote video fetch state
|
||||||
|
const [fileTransfers, setFileTransfers] = useState({});
|
||||||
|
const [videoNameInputs, setVideoNameInputs] = useState({});
|
||||||
|
|
||||||
// Dispatcher form state
|
// Dispatcher form state
|
||||||
const [selectedClients, setSelectedClients] = useState([]);
|
const [selectedClients, setSelectedClients] = useState([]);
|
||||||
const [selectedCommand, setSelectedCommand] = useState('');
|
const [selectedCommand, setSelectedCommand] = useState('');
|
||||||
const [manualClient, setManualClient] = useState('');
|
const [manualClient, setManualClient] = useState('');
|
||||||
const [manualCommand, setManualCommand] = useState('');
|
const [manualCommand, setManualCommand] = useState('');
|
||||||
|
|
||||||
// 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 [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');
|
||||||
@@ -123,6 +103,12 @@ function App() {
|
|||||||
const logsData = await logsRes.json();
|
const logsData = await logsRes.json();
|
||||||
setLogs(logsData);
|
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) {
|
} catch (err) {
|
||||||
console.error('Network sync failure:', err);
|
console.error('Network sync failure:', err);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -172,6 +158,47 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRequestFile = async (clientId) => {
|
||||||
|
const filename = (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 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 = () => {
|
const handleLogout = () => {
|
||||||
localStorage.removeItem('rmm_token');
|
localStorage.removeItem('rmm_token');
|
||||||
setToken('');
|
setToken('');
|
||||||
@@ -327,14 +354,14 @@ function App() {
|
|||||||
{/* Decorative background glows */}
|
{/* 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 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="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="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="text-center">
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
<img
|
<img
|
||||||
src={sideBarLogo}
|
src={sideBarLogo}
|
||||||
alt="Seekright Logo"
|
alt="Seekright Logo"
|
||||||
className="h-12 sm:h-16 w-auto object-contain transition-all duration-300 hover:scale-105"
|
className="h-12 sm:h-16 w-auto object-contain transition-all duration-300 hover:scale-105"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -387,11 +414,10 @@ function App() {
|
|||||||
<div className="min-h-screen pb-16 flex flex-col font-sans antialiased text-slate-100 bg-[#050811]">
|
<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-5 py-4 rounded-2xl shadow-2xl flex items-center gap-3 border transition-all duration-300 transform translate-y-0 max-w-sm ${
|
<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/90 border-rose-500/30 text-rose-200 shadow-rose-950/20'
|
||||||
? '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'
|
: 'bg-emerald-950/90 border-emerald-500/30 text-emerald-200 shadow-emerald-950/20'
|
||||||
}`}>
|
}`}>
|
||||||
{toast.type === 'error' ? (
|
{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-rose-500/10"><XCircle className="w-5 h-5 text-rose-400" /></div>
|
||||||
) : (
|
) : (
|
||||||
@@ -405,35 +431,33 @@ function App() {
|
|||||||
<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)]">
|
<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="flex items-center justify-center shrink-0">
|
<div className="flex items-center justify-center shrink-0">
|
||||||
<img
|
<img
|
||||||
src={sideBarLogo}
|
src={sideBarLogo}
|
||||||
alt="Seekright Logo"
|
alt="Seekright Logo"
|
||||||
className="h-8 sm:h-10 w-auto object-contain transition-all"
|
className="h-8 sm:h-10 w-auto object-contain transition-all"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action Controls & Navigation tabs */}
|
{/* Action Controls & Navigation tabs */}
|
||||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3">
|
<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 flex-wrap 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={`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 ${
|
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 shadow-violet-500/10'
|
? '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" />
|
<Activity className="w-3.5 h-3.5" />
|
||||||
Fleet Status
|
Remote Fleet Systems
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('editor')}
|
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 ${
|
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 shadow-violet-500/10'
|
? '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" />
|
<Settings className="w-3.5 h-3.5" />
|
||||||
Whitelist Editor
|
Whitelist Editor
|
||||||
@@ -448,7 +472,7 @@ function App() {
|
|||||||
API Status: <b className="text-emerald-400 font-bold">Online</b>
|
API Status: <b className="text-emerald-400 font-bold">Online</b>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={syncFleetData}
|
onClick={syncFleetData}
|
||||||
title="Force Sync Fleet Vitals"
|
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"
|
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"
|
||||||
@@ -468,30 +492,28 @@ function App() {
|
|||||||
|
|
||||||
{/* Main Container */}
|
{/* 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">
|
<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' ? (
|
||||||
<>
|
<>
|
||||||
{/* Dashboard Vitals: Grid of Cards (Left/Center Column) */}
|
{/* Dashboard Vitals: Grid of Cards (Left/Center Column) */}
|
||||||
<div className="lg:col-span-2 space-y-6">
|
<div className="lg:col-span-2 space-y-6">
|
||||||
|
|
||||||
{/* Central Metrics Board — clickable filter tabs */}
|
{/* Central Metrics Board — clickable filter tabs */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
|
|
||||||
{/* ALL tab */}
|
{/* ALL tab */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setNodeFilter('all')}
|
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 ${
|
className={`glass-panel p-5 rounded-2xl flex items-center gap-4 relative overflow-hidden group text-left transition-all duration-200 ${nodeFilter === 'all'
|
||||||
nodeFilter === 'all'
|
|
||||||
? 'ring-2 ring-violet-500/60 shadow-[0_0_20px_rgba(139,92,246,0.15)]'
|
? '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)]'
|
: '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="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 ${
|
<div className={`w-12 h-12 rounded-xl flex items-center justify-center shrink-0 transition-all ${nodeFilter === 'all'
|
||||||
nodeFilter === 'all'
|
|
||||||
? 'bg-violet-500/25 border-2 border-violet-500/60 text-violet-300'
|
? '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'
|
: 'bg-violet-500/10 border border-violet-500/20 text-violet-400'
|
||||||
}`}>
|
}`}>
|
||||||
<Server className="w-6 h-6" />
|
<Server className="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -506,18 +528,16 @@ function App() {
|
|||||||
{/* ONLINE tab */}
|
{/* ONLINE tab */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setNodeFilter('online')}
|
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 ${
|
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'
|
||||||
nodeFilter === 'online'
|
|
||||||
? 'ring-2 ring-emerald-500/60 shadow-[0_0_20px_rgba(16,185,129,0.15)]'
|
? '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)]'
|
: '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="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 ${
|
<div className={`w-12 h-12 rounded-xl flex items-center justify-center shrink-0 transition-all ${nodeFilter === 'online'
|
||||||
nodeFilter === 'online'
|
|
||||||
? 'bg-emerald-500/25 border-2 border-emerald-500/60 text-emerald-300'
|
? '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'
|
: 'bg-emerald-500/10 border border-emerald-500/20 text-emerald-400'
|
||||||
}`}>
|
}`}>
|
||||||
<Activity className="w-6 h-6" />
|
<Activity className="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -532,18 +552,16 @@ function App() {
|
|||||||
{/* OFFLINE tab */}
|
{/* OFFLINE tab */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setNodeFilter('offline')}
|
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 ${
|
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'
|
||||||
nodeFilter === 'offline'
|
|
||||||
? 'ring-2 ring-rose-500/60 shadow-[0_0_20px_rgba(244,63,94,0.15)]'
|
? '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)]'
|
: '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="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 ${
|
<div className={`w-12 h-12 rounded-xl flex items-center justify-center shrink-0 transition-all ${nodeFilter === 'offline'
|
||||||
nodeFilter === 'offline'
|
|
||||||
? 'bg-rose-500/25 border-2 border-rose-500/60 text-rose-300'
|
? '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'
|
: 'bg-rose-500/10 border border-rose-500/20 text-rose-400'
|
||||||
}`}>
|
}`}>
|
||||||
<AlertTriangle className="w-6 h-6" />
|
<AlertTriangle className="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -564,11 +582,10 @@ function App() {
|
|||||||
<Sliders className="w-4 h-4 text-violet-400" />
|
<Sliders className="w-4 h-4 text-violet-400" />
|
||||||
Remote Fleet Systems Grid ({filteredClientEntries.length})
|
Remote Fleet Systems Grid ({filteredClientEntries.length})
|
||||||
{nodeFilter !== 'all' && (
|
{nodeFilter !== 'all' && (
|
||||||
<span className={`text-[9px] font-bold px-2 py-0.5 rounded-full uppercase tracking-wider ${
|
<span className={`text-[9px] font-bold px-2 py-0.5 rounded-full uppercase tracking-wider ${nodeFilter === 'online'
|
||||||
nodeFilter === 'online'
|
|
||||||
? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20'
|
? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20'
|
||||||
: 'bg-rose-500/10 text-rose-400 border border-rose-500/20'
|
: 'bg-rose-500/10 text-rose-400 border border-rose-500/20'
|
||||||
}`}>
|
}`}>
|
||||||
{nodeFilter === 'online' ? '🟢 Online' : '🔴 Offline'}
|
{nodeFilter === 'online' ? '🟢 Online' : '🔴 Offline'}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -597,7 +614,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{filteredClientEntries.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-500/60 mb-3" />
|
<AlertTriangle className="w-8 h-8 mx-auto text-yellow-500/60 mb-3" />
|
||||||
@@ -610,12 +627,13 @@ function App() {
|
|||||||
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;
|
const isExpanded = expandedClients[clientId] || false;
|
||||||
|
const ft = fileTransfers[clientId];
|
||||||
{/* Render Compact Card for Offline System */}
|
|
||||||
|
{/* Render Compact Card for Offline System */ }
|
||||||
if (!isOnline) {
|
if (!isOnline) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={clientId}
|
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"
|
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 flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||||
@@ -630,13 +648,13 @@ function App() {
|
|||||||
Offline
|
Offline
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between sm:justify-end gap-4">
|
<div className="flex items-center justify-between sm:justify-end gap-4">
|
||||||
<div className="text-left sm:text-right">
|
<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-[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>
|
<span className="text-[11px] font-semibold text-slate-400 font-mono">{lastSeenDate}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => toggleClientDetails(clientId)}
|
onClick={() => toggleClientDetails(clientId)}
|
||||||
@@ -659,10 +677,10 @@ function App() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
{/* Render Full Card for Active/Online System */}
|
{/* Render Full Card for Active/Online System */ }
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={clientId}
|
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"
|
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 */}
|
{/* Status top glow line */}
|
||||||
@@ -682,7 +700,7 @@ function App() {
|
|||||||
Active
|
Active
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-left sm:text-right">
|
<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-[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>
|
<span className="text-xs font-bold text-slate-300 font-mono">{lastSeenDate}</span>
|
||||||
@@ -705,17 +723,21 @@ function App() {
|
|||||||
<Cpu className={`w-4 h-4 ${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 font-mono ${isWarning ? 'text-rose-400' : 'text-white'}`}>{cpu}%</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>
|
||||||
<div className="w-full h-2 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
|
? 'bg-gradient-to-r from-rose-500 to-red-400'
|
||||||
? 'bg-gradient-to-r from-rose-500 to-red-400'
|
: cpu > 60
|
||||||
: cpu > 60
|
? 'bg-gradient-to-r from-amber-500 to-yellow-400'
|
||||||
? 'bg-gradient-to-r from-amber-500 to-yellow-400'
|
|
||||||
: 'bg-gradient-to-r from-violet-500 to-indigo-500'
|
: 'bg-gradient-to-r from-violet-500 to-indigo-500'
|
||||||
}`}
|
}`}
|
||||||
style={{ width: `${cpu}%` }}
|
style={{ width: `${cpu}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -745,14 +767,13 @@ function App() {
|
|||||||
<span className={`font-bold font-mono ${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 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
|
? 'bg-gradient-to-r from-rose-500 to-red-400'
|
||||||
? 'bg-gradient-to-r from-rose-500 to-red-400'
|
: ram > 70
|
||||||
: ram > 70
|
? 'bg-gradient-to-r from-amber-500 to-yellow-400'
|
||||||
? 'bg-gradient-to-r from-amber-500 to-yellow-400'
|
|
||||||
: 'bg-gradient-to-r from-violet-500 to-indigo-500'
|
: 'bg-gradient-to-r from-violet-500 to-indigo-500'
|
||||||
}`}
|
}`}
|
||||||
style={{ width: `${ram}%` }}
|
style={{ width: `${ram}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -779,7 +800,7 @@ function App() {
|
|||||||
const diskTotal = d.total_gb || 0;
|
const diskTotal = d.total_gb || 0;
|
||||||
const diskFree = d.free_gb || 0;
|
const diskFree = d.free_gb || 0;
|
||||||
const diskUsed = (diskTotal - diskFree).toFixed(2);
|
const diskUsed = (diskTotal - diskFree).toFixed(2);
|
||||||
|
|
||||||
// Get friendly label
|
// Get friendly label
|
||||||
const diskLabel = d.label || (d.mount === '/' ? 'Computer' : (d.mount.split(/[/\\]/).pop() || 'Volume'));
|
const diskLabel = d.label || (d.mount === '/' ? 'Computer' : (d.mount.split(/[/\\]/).pop() || 'Volume'));
|
||||||
|
|
||||||
@@ -808,7 +829,7 @@ function App() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right side: Free Space / Total */}
|
{/* Right side: Free Space / Total */}
|
||||||
<div className="text-right shrink-0">
|
<div className="text-right shrink-0">
|
||||||
<span className="text-[11px] font-bold text-slate-300 font-mono block">
|
<span className="text-[11px] font-bold text-slate-300 font-mono block">
|
||||||
@@ -819,18 +840,17 @@ function App() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Progress bar */}
|
{/* Progress bar */}
|
||||||
<div className="space-y-1.5">
|
<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="w-full h-1.5 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 ${d.percent > 85
|
||||||
d.percent > 85
|
? 'bg-gradient-to-r from-rose-500 to-red-500'
|
||||||
? 'bg-gradient-to-r from-rose-500 to-red-500'
|
: d.percent > 70
|
||||||
: d.percent > 70
|
? 'bg-gradient-to-r from-amber-500 to-yellow-500'
|
||||||
? 'bg-gradient-to-r from-amber-500 to-yellow-500'
|
|
||||||
: 'bg-gradient-to-r from-indigo-500 to-violet-500'
|
: 'bg-gradient-to-r from-indigo-500 to-violet-500'
|
||||||
}`}
|
}`}
|
||||||
style={{ width: `${d.percent}%` }}
|
style={{ width: `${d.percent}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -892,6 +912,92 @@ function App() {
|
|||||||
<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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
<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>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -941,7 +1047,7 @@ function App() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<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>
|
||||||
@@ -959,15 +1065,14 @@ function App() {
|
|||||||
setSelectedClients([...selectedClients, id]);
|
setSelectedClients([...selectedClients, id]);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
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/20 to-indigo-600/20 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-900/40 border-white/5 hover:border-white/10 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 dot */}
|
{/* 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}
|
||||||
@@ -1040,7 +1145,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : activeTab === 'editor' ? (
|
||||||
/* 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-5 md: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">
|
||||||
@@ -1054,7 +1159,7 @@ function App() {
|
|||||||
Permitted operations are checked against whitelist config and pushed to nodes without restarts.
|
Permitted operations are checked against whitelist config and pushed to nodes without restarts.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* JSON Validation Status Badge */}
|
{/* JSON Validation Status Badge */}
|
||||||
<div>
|
<div>
|
||||||
{jsonError ? (
|
{jsonError ? (
|
||||||
@@ -1099,7 +1204,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row justify-end gap-3 pt-2">
|
<div className="flex flex-col sm:flex-row justify-end gap-3 pt-2">
|
||||||
<button
|
<button
|
||||||
onClick={fetchWhitelist}
|
onClick={fetchWhitelist}
|
||||||
@@ -1110,11 +1215,10 @@ function App() {
|
|||||||
<button
|
<button
|
||||||
onClick={handleSaveWhitelist}
|
onClick={handleSaveWhitelist}
|
||||||
disabled={jsonError !== null}
|
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 ${
|
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
|
||||||
jsonError
|
? 'bg-slate-800 text-slate-500 border border-slate-700 cursor-not-allowed'
|
||||||
? '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]'
|
: '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" />
|
<Check className="w-3.5 h-3.5" />
|
||||||
Deploy Whitelist Configuration
|
Deploy Whitelist Configuration
|
||||||
@@ -1123,7 +1227,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : null}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Scrolling DevOps CLI output shell terminal */}
|
{/* Scrolling DevOps CLI output shell terminal */}
|
||||||
@@ -1143,7 +1247,7 @@ function App() {
|
|||||||
Live Fleet Activity & Webhook Output Shell
|
Live Fleet Activity & Webhook Output Shell
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3 w-full sm:w-auto justify-between sm:justify-end">
|
<div className="flex items-center gap-3 w-full sm:w-auto justify-between sm:justify-end">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<select
|
<select
|
||||||
@@ -1173,7 +1277,7 @@ function App() {
|
|||||||
) : (
|
) : (
|
||||||
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 break-words whitespace-pre-wrap">
|
<div key={index} className="text-slate-400 flex items-start gap-1 leading-relaxed break-words whitespace-pre-wrap">
|
||||||
@@ -1225,12 +1329,11 @@ function App() {
|
|||||||
</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-3 rounded-lg bg-black/80 font-mono text-[10px] leading-relaxed overflow-x-auto break-all whitespace-pre-wrap ${
|
<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'
|
||||||
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>
|
||||||
@@ -1252,17 +1355,17 @@ function App() {
|
|||||||
// Simple custom Check icon component since lucide-react Check may sometimes fail or need explicit import mapping
|
// Simple custom Check icon component since lucide-react Check may sometimes fail or need explicit import mapping
|
||||||
function Check({ className, ...props }) {
|
function Check({ className, ...props }) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="24"
|
width="24"
|
||||||
height="24"
|
height="24"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
className={className}
|
className={className}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<polyline points="20 6 9 17 4 12" />
|
<polyline points="20 6 9 17 4 12" />
|
||||||
|
|||||||
34
src/api.js
Normal file
34
src/api.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
let resolvedBase = '';
|
||||||
|
|
||||||
|
if (import.meta.env.VITE_API_BASE_URL) {
|
||||||
|
resolvedBase = import.meta.env.VITE_API_BASE_URL;
|
||||||
|
} else if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
||||||
|
resolvedBase = 'http://localhost:8000';
|
||||||
|
} else if (window.location.hostname.includes('devtunnels.ms')) {
|
||||||
|
// Automatically route devtunnels frontend (e.g. 4174) to devtunnels backend (8000)
|
||||||
|
const newHostname = window.location.hostname.replace(/-\d+/, '-8000');
|
||||||
|
resolvedBase = `https://${newHostname}`;
|
||||||
|
} else {
|
||||||
|
resolvedBase = window.location.protocol === 'https:'
|
||||||
|
? 'https://rmm-backend.seekright.com'
|
||||||
|
: 'http://rmm-backend.seekright.com';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const API_BASE = resolvedBase;
|
||||||
|
|
||||||
|
export const apiFetch = (url, options = {}) => {
|
||||||
|
const token = localStorage.getItem('rmm_token');
|
||||||
|
if (token) {
|
||||||
|
options.headers = {
|
||||||
|
...options.headers,
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return fetch(url, options).then(response => {
|
||||||
|
if (response.status === 401) {
|
||||||
|
localStorage.removeItem('rmm_token');
|
||||||
|
window.dispatchEvent(new Event('rmm_unauthorized'));
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -6,7 +6,7 @@ export default defineConfig({
|
|||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
port: 4173,
|
port: 6173,
|
||||||
allowedHosts: ["rmm.seekright.com"]
|
allowedHosts: ["rmm.seekright.com"]
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
Reference in New Issue
Block a user