file transfer

This commit is contained in:
2026-07-16 14:30:38 +05:30
parent 0a2833d371
commit dc604a4c30
3 changed files with 288 additions and 151 deletions

View File

@@ -19,36 +19,12 @@ import {
Clock,
ChevronDown,
ChevronUp,
Database,
ArrowRight,
Sliders
} from 'lucide-react';
import sideBarLogo from './assets/side-bar-logo.png';
const API_BASE = import.meta.env.VITE_API_BASE_URL ||
(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;
});
};
import { API_BASE, apiFetch } from './api';
function App() {
const [token, setToken] = useState(localStorage.getItem('rmm_token') || '');
@@ -62,6 +38,10 @@ function App() {
const [logs, setLogs] = useState({});
const [whitelist, setWhitelist] = useState({});
// Remote video fetch state
const [fileTransfers, setFileTransfers] = useState({});
const [videoNameInputs, setVideoNameInputs] = useState({});
// Dispatcher form state
const [selectedClients, setSelectedClients] = useState([]);
const [selectedCommand, setSelectedCommand] = useState('');
@@ -123,6 +103,12 @@ function App() {
const logsData = await logsRes.json();
setLogs(logsData);
}
// Sync remote video fetch transfer states
const ftRes = await apiFetch(`${API_BASE}/api/file-transfers`);
if (ftRes.ok) {
setFileTransfers(await ftRes.json());
}
} catch (err) {
console.error('Network sync failure:', err);
} finally {
@@ -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 = () => {
localStorage.removeItem('rmm_token');
setToken('');
@@ -387,8 +414,7 @@ function App() {
<div className="min-h-screen pb-16 flex flex-col font-sans antialiased text-slate-100 bg-[#050811]">
{/* Dynamic Toast Alerts */}
{toast && (
<div className={`fixed bottom-6 right-6 z-50 px-5 py-4 rounded-2xl shadow-2xl flex items-center gap-3 border transition-all duration-300 transform translate-y-0 max-w-sm ${
toast.type === 'error'
<div className={`fixed bottom-6 right-6 z-50 px-5 py-4 rounded-2xl shadow-2xl flex items-center gap-3 border transition-all duration-300 transform translate-y-0 max-w-sm ${toast.type === 'error'
? 'bg-rose-950/90 border-rose-500/30 text-rose-200 shadow-rose-950/20'
: 'bg-emerald-950/90 border-emerald-500/30 text-emerald-200 shadow-emerald-950/20'
}`}>
@@ -415,22 +441,20 @@ function App() {
{/* Action Controls & Navigation tabs */}
<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
onClick={() => setActiveTab('dashboard')}
className={`flex-1 sm:flex-initial px-4 py-2 rounded-lg font-semibold text-xs transition-all flex items-center justify-center gap-1.5 ${
activeTab === 'dashboard'
className={`flex-1 sm:flex-initial px-4 py-2 rounded-lg font-semibold text-xs transition-all flex items-center justify-center gap-1.5 ${activeTab === 'dashboard'
? 'bg-gradient-to-r from-violet-600 to-indigo-600 text-white shadow-md shadow-violet-500/10'
: 'text-slate-400 hover:text-slate-200 hover:bg-white/5'
}`}
>
<Activity className="w-3.5 h-3.5" />
Fleet Status
Remote Fleet Systems
</button>
<button
onClick={() => setActiveTab('editor')}
className={`flex-1 sm:flex-initial px-4 py-2 rounded-lg font-semibold text-xs transition-all flex items-center justify-center gap-1.5 ${
activeTab === 'editor'
className={`flex-1 sm:flex-initial px-4 py-2 rounded-lg font-semibold text-xs transition-all flex items-center justify-center gap-1.5 ${activeTab === 'editor'
? 'bg-gradient-to-r from-violet-600 to-indigo-600 text-white shadow-md shadow-violet-500/10'
: 'text-slate-400 hover:text-slate-200 hover:bg-white/5'
}`}
@@ -480,15 +504,13 @@ function App() {
{/* ALL tab */}
<button
onClick={() => setNodeFilter('all')}
className={`glass-panel p-5 rounded-2xl flex items-center gap-4 relative overflow-hidden group text-left transition-all duration-200 ${
nodeFilter === 'all'
className={`glass-panel p-5 rounded-2xl flex items-center gap-4 relative overflow-hidden group text-left transition-all duration-200 ${nodeFilter === 'all'
? 'ring-2 ring-violet-500/60 shadow-[0_0_20px_rgba(139,92,246,0.15)]'
: 'hover:ring-1 hover:ring-violet-500/20 hover:shadow-[0_0_12px_rgba(139,92,246,0.08)]'
}`}
>
<div className="absolute top-0 right-0 w-24 h-24 bg-violet-600/5 rounded-full blur-2xl group-hover:bg-violet-600/10 transition-all"></div>
<div className={`w-12 h-12 rounded-xl flex items-center justify-center shrink-0 transition-all ${
nodeFilter === 'all'
<div className={`w-12 h-12 rounded-xl flex items-center justify-center shrink-0 transition-all ${nodeFilter === 'all'
? 'bg-violet-500/25 border-2 border-violet-500/60 text-violet-300'
: 'bg-violet-500/10 border border-violet-500/20 text-violet-400'
}`}>
@@ -506,15 +528,13 @@ function App() {
{/* ONLINE tab */}
<button
onClick={() => setNodeFilter('online')}
className={`glass-panel p-5 rounded-2xl flex items-center gap-4 relative overflow-hidden border-l-2 border-l-emerald-500 group text-left transition-all duration-200 ${
nodeFilter === 'online'
className={`glass-panel p-5 rounded-2xl flex items-center gap-4 relative overflow-hidden border-l-2 border-l-emerald-500 group text-left transition-all duration-200 ${nodeFilter === 'online'
? 'ring-2 ring-emerald-500/60 shadow-[0_0_20px_rgba(16,185,129,0.15)]'
: 'hover:ring-1 hover:ring-emerald-500/20 hover:shadow-[0_0_12px_rgba(16,185,129,0.08)]'
}`}
>
<div className="absolute top-0 right-0 w-24 h-24 bg-emerald-500/5 rounded-full blur-2xl group-hover:bg-emerald-500/10 transition-all"></div>
<div className={`w-12 h-12 rounded-xl flex items-center justify-center shrink-0 transition-all ${
nodeFilter === 'online'
<div className={`w-12 h-12 rounded-xl flex items-center justify-center shrink-0 transition-all ${nodeFilter === 'online'
? 'bg-emerald-500/25 border-2 border-emerald-500/60 text-emerald-300'
: 'bg-emerald-500/10 border border-emerald-500/20 text-emerald-400'
}`}>
@@ -532,15 +552,13 @@ function App() {
{/* OFFLINE tab */}
<button
onClick={() => setNodeFilter('offline')}
className={`glass-panel p-5 rounded-2xl flex items-center gap-4 relative overflow-hidden border-l-2 border-l-rose-500 group text-left transition-all duration-200 ${
nodeFilter === 'offline'
className={`glass-panel p-5 rounded-2xl flex items-center gap-4 relative overflow-hidden border-l-2 border-l-rose-500 group text-left transition-all duration-200 ${nodeFilter === 'offline'
? 'ring-2 ring-rose-500/60 shadow-[0_0_20px_rgba(244,63,94,0.15)]'
: 'hover:ring-1 hover:ring-rose-500/20 hover:shadow-[0_0_12px_rgba(244,63,94,0.08)]'
}`}
>
<div className="absolute top-0 right-0 w-24 h-24 bg-rose-500/5 rounded-full blur-2xl group-hover:bg-rose-500/10 transition-all"></div>
<div className={`w-12 h-12 rounded-xl flex items-center justify-center shrink-0 transition-all ${
nodeFilter === 'offline'
<div className={`w-12 h-12 rounded-xl flex items-center justify-center shrink-0 transition-all ${nodeFilter === 'offline'
? 'bg-rose-500/25 border-2 border-rose-500/60 text-rose-300'
: 'bg-rose-500/10 border border-rose-500/20 text-rose-400'
}`}>
@@ -564,8 +582,7 @@ function App() {
<Sliders className="w-4 h-4 text-violet-400" />
Remote Fleet Systems Grid ({filteredClientEntries.length})
{nodeFilter !== 'all' && (
<span className={`text-[9px] font-bold px-2 py-0.5 rounded-full uppercase tracking-wider ${
nodeFilter === 'online'
<span className={`text-[9px] font-bold px-2 py-0.5 rounded-full uppercase tracking-wider ${nodeFilter === 'online'
? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20'
: 'bg-rose-500/10 text-rose-400 border border-rose-500/20'
}`}>
@@ -610,8 +627,9 @@ function App() {
const isOnline = info.active;
const lastSeenDate = info.last_seen ? new Date(info.last_seen).toLocaleString() : 'Never';
const isExpanded = expandedClients[clientId] || false;
const ft = fileTransfers[clientId];
{/* Render Compact Card for Offline System */}
{/* Render Compact Card for Offline System */ }
if (!isOnline) {
return (
<div
@@ -659,7 +677,7 @@ function App() {
);
}
{/* Render Full Card for Active/Online System */}
{/* Render Full Card for Active/Online System */ }
return (
<div
key={clientId}
@@ -705,12 +723,16 @@ function App() {
<Cpu className={`w-4 h-4 ${isWarning ? 'text-rose-400 animate-pulse' : 'text-violet-400'}`} />
CPU Load
</span>
<span className={`font-bold font-mono ${isWarning ? 'text-rose-400' : 'text-white'}`}>{cpu}%</span>
<span className={`font-bold font-mono ${isWarning ? 'text-rose-400' : 'text-white'}`}>
{cpu}%
{clientTelemetry.cpu_temp !== undefined && clientTelemetry.cpu_temp !== null && (
<span className="text-slate-500 ml-1">@ {clientTelemetry.cpu_temp}°C</span>
)}
</span>
</div>
<div className="w-full h-2 rounded-full bg-slate-900 overflow-hidden relative border border-white/5">
<div
className={`h-full rounded-full transition-all duration-500 ${
isWarning
className={`h-full rounded-full transition-all duration-500 ${isWarning
? 'bg-gradient-to-r from-rose-500 to-red-400'
: cpu > 60
? 'bg-gradient-to-r from-amber-500 to-yellow-400'
@@ -746,8 +768,7 @@ function App() {
</div>
<div className="w-full h-2 rounded-full bg-slate-900 overflow-hidden relative border border-white/5">
<div
className={`h-full rounded-full transition-all duration-500 ${
isWarning
className={`h-full rounded-full transition-all duration-500 ${isWarning
? 'bg-gradient-to-r from-rose-500 to-red-400'
: ram > 70
? 'bg-gradient-to-r from-amber-500 to-yellow-400'
@@ -824,8 +845,7 @@ function App() {
<div className="space-y-1.5">
<div className="w-full h-1.5 rounded-full bg-slate-900 overflow-hidden relative border border-white/5">
<div
className={`h-full rounded-full transition-all duration-500 ${
d.percent > 85
className={`h-full rounded-full transition-all duration-500 ${d.percent > 85
? 'bg-gradient-to-r from-rose-500 to-red-500'
: d.percent > 70
? 'bg-gradient-to-r from-amber-500 to-yellow-500'
@@ -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>
</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>
);
})
@@ -959,8 +1065,7 @@ function App() {
setSelectedClients([...selectedClients, id]);
}
}}
className={`p-2.5 rounded-xl border text-left transition-all duration-200 relative flex flex-col justify-between overflow-hidden ${
isSelected
className={`p-2.5 rounded-xl border text-left transition-all duration-200 relative flex flex-col justify-between overflow-hidden ${isSelected
? 'bg-gradient-to-br from-violet-600/20 to-indigo-600/20 border-violet-500/80 shadow-md shadow-violet-900/15 text-white'
: 'bg-slate-900/40 border-white/5 hover:border-white/10 text-slate-400 hover:text-slate-200'
}`}
@@ -1040,7 +1145,7 @@ function App() {
</div>
</div>
</>
) : (
) : activeTab === 'editor' ? (
/* Visual Whitelist JSON Configuration Editor */
<div className="col-span-3">
<div className="glass-panel p-5 md:p-6 rounded-2xl border border-white/5 space-y-6">
@@ -1110,8 +1215,7 @@ function App() {
<button
onClick={handleSaveWhitelist}
disabled={jsonError !== null}
className={`w-full sm:w-auto px-6 py-2.5 rounded-xl text-white font-bold text-xs flex items-center justify-center gap-1.5 transition-all ${
jsonError
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]'
}`}
@@ -1123,7 +1227,7 @@ function App() {
</div>
</div>
</div>
)}
) : null}
</main>
{/* Scrolling DevOps CLI output shell terminal */}
@@ -1228,8 +1332,7 @@ function App() {
{/* Subprocess console stdout/stderr */}
{(log.detail.stdout || log.detail.stderr) && (
<pre className={`p-3 rounded-lg bg-black/80 font-mono text-[10px] leading-relaxed overflow-x-auto break-all whitespace-pre-wrap ${
isSuccess ? 'text-slate-300 border-l-2 border-emerald-500/50' : 'text-rose-300 border-l-2 border-rose-500/50'
<pre className={`p-3 rounded-lg bg-black/80 font-mono text-[10px] leading-relaxed overflow-x-auto break-all whitespace-pre-wrap ${isSuccess ? 'text-slate-300 border-l-2 border-emerald-500/50' : 'text-rose-300 border-l-2 border-rose-500/50'
}`}>
{log.detail.stdout && <code className="block">{log.detail.stdout}</code>}
{log.detail.stderr && <code className="block text-rose-400 font-semibold">{log.detail.stderr}</code>}

34
src/api.js Normal file
View 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;
});
};

View File

@@ -6,7 +6,7 @@ export default defineConfig({
plugins: [react()],
server: {
host: '0.0.0.0',
port: 4173,
port: 6173,
allowedHosts: ["rmm.seekright.com"]
},
})