feat: initialize frontend application with dashboard, command dispatcher, and configuration editor modules

This commit is contained in:
2026-06-16 12:21:28 +05:30
parent 3c189b5776
commit a79079868e

View File

@@ -33,10 +33,28 @@ const API_BASE = import.meta.env.VITE_API_BASE_URL ||
// Secure API fetch wrapper // Secure API fetch wrapper
const apiFetch = (url, options = {}) => { const apiFetch = (url, options = {}) => {
return fetch(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 [loginUsername, setLoginUsername] = useState('');
const [loginPassword, setLoginPassword] = useState('');
const [loginError, setLoginError] = useState('');
const [activeTab, setActiveTab] = useState('dashboard'); // 'dashboard' | 'editor' const [activeTab, setActiveTab] = useState('dashboard'); // 'dashboard' | 'editor'
const [clients, setClients] = useState({}); const [clients, setClients] = useState({});
const [telemetry, setTelemetry] = useState({}); const [telemetry, setTelemetry] = useState({});
@@ -112,13 +130,55 @@ function App() {
}; };
useEffect(() => { useEffect(() => {
const handleUnauthorized = () => {
setToken('');
};
window.addEventListener('rmm_unauthorized', handleUnauthorized);
return () => window.removeEventListener('rmm_unauthorized', handleUnauthorized);
}, []);
useEffect(() => {
if (!token) return;
fetchWhitelist(); fetchWhitelist();
syncFleetData(); syncFleetData();
// Set up rapid 3-second fleet synchronization loop // Set up rapid 3-second fleet synchronization loop
const syncInterval = setInterval(syncFleetData, 3000); const syncInterval = setInterval(syncFleetData, 3000);
return () => clearInterval(syncInterval); return () => clearInterval(syncInterval);
}, []); }, [token]);
const handleLogin = async (e) => {
e.preventDefault();
setLoginError('');
try {
const response = await fetch(`${API_BASE}/api/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: loginUsername, password: loginPassword })
});
const data = await response.json();
if (response.ok) {
localStorage.setItem('rmm_token', data.token);
setToken(data.token);
setLoginPassword('');
showToast('Login successful!', 'success');
} else {
setLoginError(data.detail || 'Login failed. Invalid username or password.');
}
} catch (err) {
setLoginError('Cannot connect to RMM backend server.');
}
};
const handleLogout = () => {
localStorage.removeItem('rmm_token');
setToken('');
setClients({});
setTelemetry({});
setLogs({});
showToast('Logged out successfully.', 'success');
};
// Scroll terminal logs to bottom automatically when new actions populate, // Scroll terminal logs to bottom automatically when new actions populate,
// but only if the user is already looking at the latest logs (near the bottom)! // but only if the user is already looking at the latest logs (near the bottom)!
@@ -260,6 +320,70 @@ function App() {
return matchesSearch && matchesFilter; return matchesSearch && matchesFilter;
}); });
if (!token) {
return (
<div className="min-h-screen flex items-center justify-center px-4 py-12 font-sans antialiased text-slate-100 bg-[#050811] relative overflow-hidden">
{/* Decorative background glows */}
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-violet-600/10 rounded-full blur-[120px]"></div>
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-indigo-600/10 rounded-full blur-[120px]"></div>
<div className="w-full max-w-md p-6 sm:p-8 md:p-10 glass-panel rounded-3xl border border-white/5 shadow-[0_8px_30px_rgba(0,0,0,0.6)] relative z-10 space-y-6">
<div className="text-center space-y-3">
<div className="w-14 h-14 mx-auto rounded-2xl bg-gradient-to-tr from-violet-600 to-indigo-600 flex items-center justify-center shadow-xl shadow-violet-500/20">
<Zap className="w-7 h-7 text-white animate-pulse" />
</div>
<div>
<h1 className="font-extrabold text-xl tracking-tight text-white">
SEEKRIGHT PULSE RMM
</h1>
<p className="text-xs text-slate-400 font-medium mt-1">Operator Authentication Required</p>
</div>
</div>
{loginError && (
<div className="p-3.5 bg-rose-950/40 border border-rose-500/30 rounded-2xl text-rose-300 font-semibold text-xs leading-relaxed flex gap-2 items-center">
<XCircle className="w-4 h-4 text-rose-400 shrink-0" />
<span>{loginError}</span>
</div>
)}
<form onSubmit={handleLogin} className="space-y-4">
<div>
<label className="block text-[10px] font-extrabold text-slate-400 uppercase tracking-wider mb-2">Username</label>
<input
type="text"
required
placeholder="root"
value={loginUsername}
onChange={(e) => setLoginUsername(e.target.value)}
className="w-full bg-slate-950 border border-white/10 rounded-xl p-3 text-xs text-slate-200 font-semibold focus:outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500/20 placeholder-slate-700 transition-all"
/>
</div>
<div>
<label className="block text-[10px] font-extrabold text-slate-400 uppercase tracking-wider mb-2">Password</label>
<input
type="password"
required
placeholder="••••••••"
value={loginPassword}
onChange={(e) => setLoginPassword(e.target.value)}
className="w-full bg-slate-950 border border-white/10 rounded-xl p-3 text-xs text-slate-200 font-semibold focus:outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500/20 placeholder-slate-700 transition-all"
/>
</div>
<button
type="submit"
className="w-full bg-gradient-to-r from-violet-600 to-indigo-600 hover:from-violet-500 hover:to-indigo-500 text-white font-bold py-3.5 px-4 rounded-xl text-xs flex items-center justify-center gap-2 shadow-lg shadow-violet-950/45 hover:shadow-violet-900/50 hover:translate-y-[-1px] active:translate-y-[1px] active:scale-[0.98] transition-all pt-3.5 mt-6"
>
Sign In to Fleet Control
</button>
</form>
</div>
</div>
);
}
return ( return (
<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 */}
@@ -337,6 +461,13 @@ function App() {
<RefreshCw className={`w-3.5 h-3.5 ${isSyncing ? 'animate-spin text-violet-400' : ''}`} /> <RefreshCw className={`w-3.5 h-3.5 ${isSyncing ? 'animate-spin text-violet-400' : ''}`} />
</button> </button>
</div> </div>
<button
onClick={handleLogout}
className="px-3.5 py-2 rounded-xl bg-rose-950/40 hover:bg-rose-900/30 text-rose-300 hover:text-rose-200 font-semibold text-xs transition-all border border-rose-500/10 hover:border-rose-500/20 shadow-md shadow-rose-950/15"
>
Logout
</button>
</div> </div>
</header> </header>