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
|
// 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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user