feat: initialize frontend application with dashboard, command dispatcher, and configuration editor modules
This commit is contained in:
135
src/App.jsx
135
src/App.jsx
@@ -33,10 +33,28 @@ const API_BASE = import.meta.env.VITE_API_BASE_URL ||
|
||||
|
||||
// Secure API fetch wrapper
|
||||
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() {
|
||||
const [token, setToken] = useState(localStorage.getItem('rmm_token') || '');
|
||||
const [loginUsername, setLoginUsername] = useState('');
|
||||
const [loginPassword, setLoginPassword] = useState('');
|
||||
const [loginError, setLoginError] = useState('');
|
||||
|
||||
const [activeTab, setActiveTab] = useState('dashboard'); // 'dashboard' | 'editor'
|
||||
const [clients, setClients] = useState({});
|
||||
const [telemetry, setTelemetry] = useState({});
|
||||
@@ -112,13 +130,55 @@ function App() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleUnauthorized = () => {
|
||||
setToken('');
|
||||
};
|
||||
window.addEventListener('rmm_unauthorized', handleUnauthorized);
|
||||
return () => window.removeEventListener('rmm_unauthorized', handleUnauthorized);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) return;
|
||||
|
||||
fetchWhitelist();
|
||||
syncFleetData();
|
||||
|
||||
// Set up rapid 3-second fleet synchronization loop
|
||||
const syncInterval = setInterval(syncFleetData, 3000);
|
||||
return () => clearInterval(syncInterval);
|
||||
}, []);
|
||||
}, [token]);
|
||||
|
||||
const handleLogin = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoginError('');
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: loginUsername, password: loginPassword })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
localStorage.setItem('rmm_token', data.token);
|
||||
setToken(data.token);
|
||||
setLoginPassword('');
|
||||
showToast('Login successful!', 'success');
|
||||
} else {
|
||||
setLoginError(data.detail || 'Login failed. Invalid username or password.');
|
||||
}
|
||||
} catch (err) {
|
||||
setLoginError('Cannot connect to RMM backend server.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('rmm_token');
|
||||
setToken('');
|
||||
setClients({});
|
||||
setTelemetry({});
|
||||
setLogs({});
|
||||
showToast('Logged out successfully.', 'success');
|
||||
};
|
||||
|
||||
// Scroll terminal logs to bottom automatically when new actions populate,
|
||||
// but only if the user is already looking at the latest logs (near the bottom)!
|
||||
@@ -260,6 +320,70 @@ function App() {
|
||||
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 (
|
||||
<div className="min-h-screen pb-16 flex flex-col font-sans antialiased text-slate-100 bg-[#050811]">
|
||||
{/* Dynamic Toast Alerts */}
|
||||
@@ -337,6 +461,13 @@ function App() {
|
||||
<RefreshCw className={`w-3.5 h-3.5 ${isSyncing ? 'animate-spin text-violet-400' : ''}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="px-3.5 py-2 rounded-xl bg-rose-950/40 hover:bg-rose-900/30 text-rose-300 hover:text-rose-200 font-semibold text-xs transition-all border border-rose-500/10 hover:border-rose-500/20 shadow-md shadow-rose-950/15"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user