refactor(ui): semantic token theming + cleaner SaaS palette

Replace the brittle 266-rule `:root.light` `!important` override block with a
Tailwind v4 `@theme inline` semantic token system (surface/header/card/inner/
field/line/fg/fg-muted/fg-faint + success/info/primary/warning/danger/rose/
violet/sky/orange/blue, each with vivid/soft/line). Migrate all 14 components
and App.tsx off hardcoded slate/hex utilities onto the tokens, so dark/light
is now a pure CSS-variable swap with no per-utility overrides.

- index.css ~984 -> ~150 lines; CSS bundle 145 -> 98 kB
- calmer, desaturated accents; removed gratuitous glows and constant pulsing
- branding, playful copy and intentionally-dark code blocks preserved

Also wires `requireAdmin` onto settings, bookings-delete, database, checkmk,
semaphore and caddy routes.
This commit is contained in:
Brückner
2026-06-17 15:27:32 +02:00
parent 8e24487172
commit f1d46e7f56
17 changed files with 995 additions and 1768 deletions

View File

@ -36,7 +36,7 @@
```
+-----------------------------------------------------------------------------+
| GHOSTGRID PLATFORM |
| GHOSTGRID PLATFORM |
+-----------------------------------------------------------------------------+
| +---------------------------------------------------------------------+ |
| | PRESENTATION LAYER | |
@ -49,7 +49,7 @@
| | authFetch > Bearer <JWT> |
| +---------------------------------------------------------------------+ |
| | APPLICATION LAYER (server.ts) | |
| | Single Express process — serves API + frontend | |
| | Single Express process — serves API + frontend | |
| | +-----------+ +-----------+ +-----------+ +-----------+ +--------+ | |
| | | Auth | | Devices | | Labs | | Bookings | | Logs | | |
| | | (JWT/MSAL)| | CRUD | | CRUD | | CRUD | | | | |

View File

@ -399,7 +399,7 @@ async function startServer() {
// -------------------------------------------------------------
// RESTFUL API: Settings (admin only)
// -------------------------------------------------------------
app.get('/api/settings', requireAuth, (_req, res) => {
app.get('/api/settings', requireAuth, requireAdmin, (_req, res) => {
try {
res.json(maskSettings(getAllSettings()));
} catch (err: any) {
@ -407,7 +407,7 @@ async function startServer() {
}
});
app.put('/api/settings', requireAuth, (req, res) => {
app.put('/api/settings', requireAuth, requireAdmin, (req, res) => {
try {
const allowed = ['azure_enabled', 'azure_client_id', 'azure_tenant_id', 'azure_client_secret',
'azure_redirect_uri', 'azure_allowed_group', 'checkmk_enabled', 'checkmk_api_url', 'checkmk_api_user',
@ -844,7 +844,7 @@ async function startServer() {
}
});
app.delete('/api/bookings/:id', requireAuth, (req, res) => {
app.delete('/api/bookings/:id', requireAuth, requireAdmin, (req, res) => {
try {
const id = req.params.id;
const booking = db.prepare('SELECT * FROM bookings WHERE id = ?').get(id) as any;
@ -961,7 +961,7 @@ async function startServer() {
// -------------------------------------------------------------
// DATABASE API
// -------------------------------------------------------------
app.get('/api/database/info', requireAuth, (_req, res) => {
app.get('/api/database/info', requireAuth, requireAdmin, (_req, res) => {
try {
const stats = fs.statSync(DB_FILE);
const tables = ['users', 'devices', 'labs', 'bookings', 'logs', 'links', 'settings', 'caddy'];
@ -980,7 +980,7 @@ async function startServer() {
}
});
app.get('/api/database/backup', requireAuth, async (_req, res) => {
app.get('/api/database/backup', requireAuth, requireAdmin, async (_req, res) => {
const tempPath = `${DB_FILE}.backup-${Date.now()}`;
try {
await db.backup(tempPath);
@ -994,7 +994,7 @@ async function startServer() {
}
});
app.post('/api/database/import', requireAuth,
app.post('/api/database/import', requireAuth, requireAdmin,
express.raw({ type: 'application/octet-stream', limit: '50mb' }),
(req, res) => {
const tempPath = `${DB_FILE}.import-${Date.now()}`;
@ -1154,7 +1154,7 @@ async function startServer() {
}
scheduleSync();
app.post('/api/checkmk/sync', requireAuth, async (_req, res) => {
app.post('/api/checkmk/sync', requireAuth, requireAdmin, async (_req, res) => {
try {
await syncCheckMkStatuses();
res.json({ ok: true });
@ -1266,7 +1266,7 @@ async function startServer() {
scheduleSemaphoreCheck();
// Proxy Semaphore template list so the UI can populate dropdowns
app.get('/api/semaphore/templates', requireAuth, async (_req, res) => {
app.get('/api/semaphore/templates', requireAuth, requireAdmin, async (_req, res) => {
const apiUrl = getSetting('semaphore_api_url');
const token = getSetting('semaphore_api_token');
const projectId = getSetting('semaphore_project_id');
@ -1320,7 +1320,7 @@ async function startServer() {
// -------------------------------------------------------------
// CADDY API
// -------------------------------------------------------------
app.get('/api/caddy/status', requireAuth, async (_req, res) => {
app.get('/api/caddy/status', requireAuth, requireAdmin, async (_req, res) => {
try {
const adminUrl = getSetting('caddy_admin_url') || 'http://127.0.0.1:2019';
const r = await fetch(`${adminUrl}/config/`, { signal: AbortSignal.timeout(2000) });
@ -1330,7 +1330,7 @@ async function startServer() {
}
});
app.get('/api/caddy/routes', requireAuth, (_req, res) => {
app.get('/api/caddy/routes', requireAuth, requireAdmin, (_req, res) => {
try {
res.json(getCaddyRoutes());
} catch (err: any) {
@ -1338,7 +1338,7 @@ async function startServer() {
}
});
app.post('/api/caddy/routes', requireAuth, async (req, res) => {
app.post('/api/caddy/routes', requireAuth, requireAdmin, async (req, res) => {
try {
if (!IS_PRODUCTION) return res.status(403).json({ error: 'Caddy is managed by another instance.' });
const { hostname, upstream, tls, compress, redirect } = req.body as {
@ -1356,7 +1356,7 @@ async function startServer() {
}
});
app.put('/api/caddy/routes/:id', requireAuth, async (req, res) => {
app.put('/api/caddy/routes/:id', requireAuth, requireAdmin, async (req, res) => {
try {
if (!IS_PRODUCTION) return res.status(403).json({ error: 'Caddy is managed by another instance.' });
const id = Number(req.params.id);
@ -1374,7 +1374,7 @@ async function startServer() {
}
});
app.delete('/api/caddy/routes/:id', requireAuth, (req, res) => {
app.delete('/api/caddy/routes/:id', requireAuth, requireAdmin, (req, res) => {
try {
if (!IS_PRODUCTION) return res.status(403).json({ error: 'Caddy is managed by another instance.' });
const id = Number(req.params.id);

View File

@ -411,12 +411,12 @@ export default function App() {
// Startup check not done yet
if (!authChecked) {
return (
<div className="min-h-screen bg-[#0B0F19] text-slate-100 font-sans flex flex-col items-center justify-center p-4">
<div className="min-h-screen bg-surface text-fg font-sans flex flex-col items-center justify-center p-4">
<div className="text-center space-y-4">
<div className="p-4 bg-slate-950/80 border border-slate-800 rounded-2xl shadow-[0_0_40px_rgba(6,182,212,0.15)] inline-flex">
<GhostGridLogo className="w-16 h-16 animate-pulse" />
<div className="p-4 bg-card border border-line rounded-2xl shadow-lg inline-flex">
<GhostGridLogo className="w-16 h-16" />
</div>
<p className="text-xs text-slate-400 font-mono">booting...</p>
<p className="text-xs text-fg-muted font-mono">booting...</p>
</div>
</div>
);
@ -433,16 +433,16 @@ export default function App() {
// Loading data after login
if (loading) {
return (
<div className="min-h-screen bg-[#0B0F19] text-slate-100 font-sans flex flex-col items-center justify-center p-4">
<div className="min-h-screen bg-surface text-fg font-sans flex flex-col items-center justify-center p-4">
<div className="text-center space-y-6 max-w-sm">
<div className="p-4 bg-slate-950/80 border border-slate-850 rounded-2xl shadow-[0_0_40px_rgba(6,182,212,0.15)] inline-flex">
<GhostGridLogo className="w-20 h-20 animate-pulse" />
<div className="p-4 bg-card border border-line rounded-2xl shadow-lg inline-flex">
<GhostGridLogo className="w-20 h-20" />
</div>
<div className="space-y-2">
<h2 className="text-base font-bold tracking-tight text-white">GhostGrid Virtualization</h2>
<p className="text-xs text-slate-400 leading-normal">Mounting the SQLite file, walking the topology graph and pulling fresh box states. tail -f the spinner...</p>
<div className="inline-flex items-center gap-1 bg-cyan-950/40 border border-cyan-900/50 rounded-full px-2.5 py-0.5 text-[9px] font-mono text-cyan-400 font-semibold mt-1">
<span className="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-ping"></span>
<h2 className="text-base font-bold tracking-tight text-fg">GhostGrid Virtualization</h2>
<p className="text-xs text-fg-muted leading-normal">Mounting the SQLite file, walking the topology graph and pulling fresh box states. tail -f the spinner...</p>
<div className="inline-flex items-center gap-1 bg-info-soft border border-info-line rounded-full px-2.5 py-0.5 text-[9px] font-mono text-info font-semibold mt-1">
<span className="w-1.5 h-1.5 rounded-full bg-info animate-ping"></span>
SQLITE DATABASE HYDRATION ONGOING
</div>
</div>
@ -452,7 +452,7 @@ export default function App() {
}
return (
<div className="min-h-screen bg-[#0B0F19] text-slate-100 font-sans flex flex-col selection:bg-emerald-500/30 selection:text-emerald-300" id="main-root">
<div className="min-h-screen bg-surface text-fg font-sans flex flex-col selection:bg-emerald-500/30 selection:text-emerald-300" id="main-root">
<Header
currentUser={currentUser}
@ -468,32 +468,32 @@ export default function App() {
<div className="flex-1 flex flex-col md:flex-row">
<aside
className={`w-full bg-[#0F172A] border-r border-[#1E293B] p-4 flex flex-col justify-between py-6 shrink-0 transition-all duration-200 ${navCollapsed ? 'md:w-20' : 'md:w-64'}`}
className={`w-full bg-header border-r border-line p-4 flex flex-col justify-between py-6 shrink-0 transition-all duration-200 ${navCollapsed ? 'md:w-20' : 'md:w-64'}`}
id="nav-sidebar"
>
<div className="space-y-5">
{/* Collapse toggle */}
<div className={`flex items-center px-1 ${navCollapsed ? 'md:justify-center justify-between' : 'justify-between'}`}>
<span className={`text-[10px] uppercase font-mono font-bold tracking-wider text-slate-500 ${navCollapsed ? 'md:hidden' : 'block'}`}>Main Menu</span>
<span className={`text-[10px] uppercase font-mono font-bold tracking-wider text-fg-faint ${navCollapsed ? 'md:hidden' : 'block'}`}>Main Menu</span>
<button
onClick={() => setNavCollapsed(c => !c)}
title={navCollapsed ? 'Expand menu' : 'Collapse menu'}
className="p-1.5 rounded-md text-slate-400 hover:text-emerald-400 hover:bg-slate-900 transition-all hover:cursor-pointer"
className="p-1.5 rounded-md text-fg-muted hover:text-success hover:bg-inner transition-all hover:cursor-pointer"
>
{navCollapsed ? <PanelLeftOpen className="w-4 h-4" /> : <PanelLeftClose className="w-4 h-4" />}
</button>
</div>
<nav className="space-y-4">
{navigationGroups.map((group, gi) => (
{navigationGroups.filter(g => g.label !== 'System' || currentUser.role.toLowerCase() === 'admin').map((group, gi) => (
<div key={gi} className="space-y-1">
{group.label && (
<span className={`text-[9px] uppercase font-mono font-bold tracking-wider text-slate-600 px-3 ${navCollapsed ? 'md:hidden block' : 'block'}`}>
<span className={`text-[9px] uppercase font-mono font-bold tracking-wider text-fg-faint px-3 ${navCollapsed ? 'md:hidden block' : 'block'}`}>
{group.label}
</span>
)}
{/* Thin divider stands in for the group label when collapsed */}
{group.label && navCollapsed && <div className="hidden md:block h-px bg-slate-850 mx-2" />}
{group.label && navCollapsed && <div className="hidden md:block h-px bg-line mx-2" />}
{group.items.map((item) => {
const isActive = activeTab === item.id;
return (
@ -506,8 +506,8 @@ export default function App() {
title={navCollapsed ? item.label : undefined}
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-xs font-semibold font-sans tracking-wide transition-all ${navCollapsed ? 'md:justify-center' : ''} ${
isActive
? 'bg-gradient-to-r from-emerald-500/10 to-teal-500/5 border-l-4 border-emerald-500 text-white shadow-[inset_1px_0_10px_rgba(16,185,129,0.03)]'
: 'text-slate-400 hover:text-white hover:bg-slate-900'
? 'bg-success-soft border-l-2 border-success text-fg'
: 'text-fg-muted hover:text-fg hover:bg-inner'
}`}
>
{item.icon}
@ -520,15 +520,15 @@ export default function App() {
</nav>
</div>
<div className={`bg-slate-950 p-4 border border-slate-900 rounded-xl space-y-2 mt-8 ${navCollapsed ? 'hidden' : 'hidden md:block'}`}>
<h4 className="text-[10px] text-emerald-400 font-mono font-bold">Overall Status</h4>
<div className="text-[11px] text-slate-400 leading-relaxed font-sans space-y-0.5">
<div>Active: <span className="text-emerald-400 font-semibold font-mono">{bookings.filter(b => b.status === 'active').length}</span></div>
<div>Upcoming: <span className="text-indigo-400 font-semibold font-mono">{bookings.filter(b => b.status === 'upcoming').length}</span></div>
<div>Online: <span className="text-white font-semibold font-mono">{devices.filter(d => d.status === 'online').length}<span className="text-slate-500">/{devices.length}</span></span> devices</div>
<div>Labs: <span className="text-white font-semibold font-mono">{labs.length}</span> configured</div>
<div className={`bg-inner p-4 border border-line rounded-xl space-y-2 mt-8 ${navCollapsed ? 'hidden' : 'hidden md:block'}`}>
<h4 className="text-[10px] text-success font-mono font-bold">Overall Status</h4>
<div className="text-[11px] text-fg-muted leading-relaxed font-sans space-y-0.5">
<div>Active: <span className="text-success font-semibold font-mono">{bookings.filter(b => b.status === 'active').length}</span></div>
<div>Upcoming: <span className="text-primary font-semibold font-mono">{bookings.filter(b => b.status === 'upcoming').length}</span></div>
<div>Online: <span className="text-fg font-semibold font-mono">{devices.filter(d => d.status === 'online').length}<span className="text-fg-faint">/{devices.length}</span></span> devices</div>
<div>Labs: <span className="text-fg font-semibold font-mono">{labs.length}</span> configured</div>
</div>
<div className="h-1 bg-gradient-to-r from-emerald-500 to-teal-600 rounded-full w-full animate-pulse mt-2" /></div>
<div className="h-1 bg-gradient-to-r from-emerald-500 to-teal-600 rounded-full w-full mt-2" /></div>
</aside>
<main className="flex-1 p-6 md:p-8 overflow-y-auto" id="main-content-display">
@ -538,6 +538,7 @@ export default function App() {
bookings={bookings}
labs={labs}
devices={devices}
users={users}
links={links}
onCancelBooking={handleCancelBooking}
onDeleteBooking={handleDeleteBooking}
@ -612,7 +613,7 @@ export default function App() {
onAddLog={handleAddLogManually}
/>
)}
{activeTab === 'settings' && (
{activeTab === 'settings' && currentUser.role.toLowerCase() === 'admin' && (
<Settings currentUser={currentUser} />
)}
</main>

View File

@ -237,10 +237,8 @@ export default function BookingCalendar({
};
const handleQuickBookDevice = (device: Device) => {
// Find or pick a lab that contains this device; fall back to device ID as labId marker
const hostLab = labs.find(l => l.deviceIds.includes(device.id));
onAddBooking({
labId: hostLab?.id ?? `device:${device.id}`,
labId: `device:${device.id}`,
userId: currentUser.id,
startDateTime: toLocalISO(quickWindow.start),
endDateTime: toLocalISO(quickWindow.end),
@ -258,23 +256,23 @@ export default function BookingCalendar({
{/* ── Quick Booking Modal ── */}
{showQuickPanel && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
<div className="w-full max-w-lg bg-[#0F172A] border border-slate-700 rounded-2xl shadow-2xl overflow-hidden">
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-overlay backdrop-blur-sm">
<div className="w-full max-w-lg bg-card border border-line-strong rounded-2xl shadow-2xl overflow-hidden">
{/* Modal Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-slate-800">
<div className="flex items-center justify-between px-5 py-4 border-b border-line">
<div className="flex items-center gap-2">
<Zap className="w-4 h-4 text-emerald-400 fill-emerald-500/30" />
<h3 className="font-bold text-sm text-white font-sans">Quick Booking</h3>
<Zap className="w-4 h-4 text-success fill-success/30" />
<h3 className="font-bold text-sm text-fg font-sans">Quick Booking</h3>
</div>
<button onClick={() => setShowQuickPanel(false)} className="text-slate-400 hover:text-white transition-colors">
<button onClick={() => setShowQuickPanel(false)} className="text-fg-muted hover:text-fg transition-colors">
<X className="w-5 h-5" />
</button>
</div>
{/* Duration Selector */}
<div className="px-5 pt-4 space-y-1">
<p className="text-[11px] text-slate-400 font-sans">Duration starting now:</p>
<p className="text-[11px] text-fg-muted font-sans">Duration starting now:</p>
<div className="flex gap-2">
{[1, 2, 4, 8].map(h => (
<button
@ -283,14 +281,14 @@ export default function BookingCalendar({
className={`flex-1 py-2 rounded-lg text-xs font-semibold font-sans border transition-all ${
quickDuration === h
? 'bg-emerald-600 border-emerald-500 text-white'
: 'bg-slate-900 border-slate-800 text-slate-300 hover:border-emerald-700 hover:text-emerald-400'
: 'bg-inner border-line text-fg-muted hover:border-success hover:text-success'
}`}
>
{h}h
</button>
))}
</div>
<p className="text-[10px] text-slate-500 font-mono">
<p className="text-[10px] text-fg-faint font-mono">
{new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} &gt;{' '}
{new Date(Date.now() + quickDuration * 3600_000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</p>
@ -301,7 +299,7 @@ export default function BookingCalendar({
<button
onClick={() => setQuickTab('labs')}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
quickTab === 'labs' ? 'bg-emerald-900/50 text-emerald-400 border border-emerald-800/60' : 'text-slate-400 hover:text-white'
quickTab === 'labs' ? 'bg-success-soft text-success border border-success-line' : 'text-fg-muted hover:text-fg'
}`}
>
<Layers className="w-3.5 h-3.5" /> Lab Topologies ({availableLabs.length} available)
@ -309,7 +307,7 @@ export default function BookingCalendar({
<button
onClick={() => setQuickTab('devices')}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
quickTab === 'devices' ? 'bg-emerald-900/50 text-emerald-400 border border-emerald-800/60' : 'text-slate-400 hover:text-white'
quickTab === 'devices' ? 'bg-success-soft text-success border border-success-line' : 'text-fg-muted hover:text-fg'
}`}
>
<Server className="w-3.5 h-3.5" /> Individual Devices ({availableDevices.length} free)
@ -320,19 +318,19 @@ export default function BookingCalendar({
<div className="px-5 pb-5 pt-3 max-h-72 overflow-y-auto space-y-2">
{quickTab === 'labs' ? (
availableLabs.length === 0 ? (
<p className="text-xs text-slate-400 text-center py-6 italic">Nothing free for {quickDuration}h right now. All slots leased.</p>
<p className="text-xs text-fg-muted text-center py-6 italic">Nothing free for {quickDuration}h right now. All slots leased.</p>
) : (
availableLabs.map(lab => {
const labDevices = lab.deviceIds.map(id => devices.find(d => d.id === id)).filter(Boolean) as Device[];
const offlineCount = labDevices.filter(d => !isOnline(d)).length;
return (
<div key={lab.id} className="flex items-center justify-between gap-3 p-3 bg-slate-900/60 border border-slate-800 rounded-lg hover:border-emerald-800/50 transition-all">
<div key={lab.id} className="flex items-center justify-between gap-3 p-3 bg-inner border border-line rounded-lg hover:border-success-line transition-all">
<div className="min-w-0">
<p className="text-xs font-bold text-white truncate">{lab.name}</p>
<p className="text-[10px] text-slate-400 font-mono">{lab.location} · {labDevices.length} device{labDevices.length !== 1 ? 's' : ''}</p>
<p className="text-[9px] text-slate-500 truncate mt-0.5">{labDevices.map(d => d.hostname).join(', ')}</p>
<p className="text-xs font-bold text-fg truncate">{lab.name}</p>
<p className="text-[10px] text-fg-muted font-mono">{lab.location} · {labDevices.length} device{labDevices.length !== 1 ? 's' : ''}</p>
<p className="text-[9px] text-fg-faint truncate mt-0.5">{labDevices.map(d => d.hostname).join(', ')}</p>
{offlineCount > 0 && (
<p className="flex items-center gap-0.5 text-[9px] text-amber-400 font-mono mt-0.5">
<p className="flex items-center gap-0.5 text-[9px] text-warning font-mono mt-0.5">
<AlertTriangle className="w-2.5 h-2.5 shrink-0" />{offlineCount} device{offlineCount !== 1 ? 's' : ''} not reachable
</p>
)}
@ -354,20 +352,20 @@ export default function BookingCalendar({
const free = !isDeviceBooked(device.id, quickWindow.startMs, quickWindow.endMs);
return (
<div key={device.id} className={`flex items-center justify-between gap-3 p-3 border rounded-lg transition-all ${
free ? 'bg-slate-900/60 border-slate-800 hover:border-emerald-800/50' : 'bg-slate-950/40 border-slate-900 opacity-60'
free ? 'bg-inner border-line hover:border-success-line' : 'bg-surface border-line opacity-60'
}`}>
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className={`w-1.5 h-1.5 rounded-full ${online ? 'bg-emerald-400' : status === 'offline' ? 'bg-rose-400' : 'bg-slate-500'}`} />
<p className="text-xs font-bold text-white font-mono">{device.hostname}</p>
<span className={`w-1.5 h-1.5 rounded-full ${online ? 'bg-success' : status === 'offline' ? 'bg-rose' : 'bg-fg-faint'}`} />
<p className="text-xs font-bold text-fg font-mono">{device.hostname}</p>
{!online && free && (
<span className="flex items-center gap-0.5 text-[9px] text-amber-400 font-mono" title="Not reachable in CheckMK">
<span className="flex items-center gap-0.5 text-[9px] text-warning font-mono" title="Not reachable in CheckMK">
<AlertTriangle className="w-2.5 h-2.5" />{status}
</span>
)}
</div>
<p className="text-[10px] text-slate-400 font-mono">{device.type} · {device.ip}</p>
<p className="text-[9px] text-slate-500">{device.location}</p>
<p className="text-[10px] text-fg-muted font-mono">{device.type} · {device.ip}</p>
<p className="text-[9px] text-fg-faint">{device.location}</p>
</div>
{free ? (
<button
@ -377,7 +375,7 @@ export default function BookingCalendar({
Book
</button>
) : (
<span className="shrink-0 text-[10px] text-rose-400 font-mono font-semibold">Busy</span>
<span className="shrink-0 text-[10px] text-rose font-mono font-semibold">Busy</span>
)}
</div>
);
@ -389,32 +387,32 @@ export default function BookingCalendar({
)}
{/* ── LEFT: Visual Schedule Grid ── */}
<div className="lg:col-span-8 bg-[#1E293B] border border-slate-800 rounded-xl p-5 flex flex-col h-full font-sans" id="timeline-card">
<div className="lg:col-span-8 bg-card border border-line rounded-xl p-5 flex flex-col h-full font-sans" id="timeline-card">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 mb-4">
<div>
<h2 className="text-base font-bold text-white flex items-center gap-2">
<Calendar className="text-emerald-400 w-5 h-5" />
<h2 className="text-base font-bold text-fg flex items-center gap-2">
<Calendar className="text-success w-5 h-5" />
Bookings
</h2>
<p className="text-xs text-slate-400">Who has which box, and until when. mutex for hardware, basically.</p>
<p className="text-xs text-fg-muted">Who has which box, and until when. mutex for hardware, basically.</p>
</div>
{/* Day navigation */}
<div className="flex items-center gap-1 bg-slate-950 p-1 rounded-lg border border-slate-805 shrink-0">
<div className="flex items-center gap-1 bg-inner p-1 rounded-lg border border-line shrink-0">
<button
onClick={() => setDayOffset(dayOffset - 1)}
disabled={dayOffset <= -30}
className="p-1 px-1.5 bg-slate-900 border border-slate-800 text-slate-350 hover:text-white rounded disabled:opacity-30 transition-opacity"
className="p-1 px-1.5 bg-card border border-line text-fg-muted hover:text-fg rounded disabled:opacity-30 transition-opacity"
>
<ChevronLeft className="w-3.5 h-3.5" />
</button>
<div className="text-xs font-semibold px-2.5 text-center text-slate-200 min-w-[130px] font-mono select-none">
<div className="text-xs font-semibold px-2.5 text-center text-fg min-w-[130px] font-mono select-none">
{dayOffset === 0 ? `${fmtDate(0)} (Today)` : dayOffset < 0 ? `${fmtDate(dayOffset)} (Past)` : fmtDate(dayOffset)}
</div>
<button
onClick={() => setDayOffset(dayOffset + 1)}
className="p-1 px-1.5 bg-slate-900 border border-slate-800 text-slate-350 hover:text-white rounded transition-opacity"
className="p-1 px-1.5 bg-card border border-line text-fg-muted hover:text-fg rounded transition-opacity"
>
<ChevronRight className="w-3.5 h-3.5" />
</button>
@ -422,32 +420,32 @@ export default function BookingCalendar({
</div>
{/* Matrix Grid */}
<div className="flex-1 overflow-x-auto rounded-lg border border-slate-850 p-1 bg-slate-950/40">
<div className="flex-1 overflow-x-auto rounded-lg border border-line p-1 bg-inner">
<div style={{ minWidth: '860px' }}>
{/* Header row */}
<div
className="border-b border-slate-800 pb-1"
className="border-b border-line pb-1"
style={{ display: 'grid', gridTemplateColumns: '140px repeat(12, minmax(0, 1fr))' }}
>
<div className="text-left pl-3 text-[10px] text-slate-400 font-sans font-bold self-center">Device</div>
<div className="text-left pl-3 text-[10px] text-fg-muted font-sans font-bold self-center">Device</div>
{TIME_SLOTS.map((slot, i) => (
<div key={i} className="text-center py-1 border-l border-slate-855">
<span className="text-[9px] font-mono text-slate-300 leading-none">{slot.label}</span>
<div key={i} className="text-center py-1 border-l border-line">
<span className="text-[9px] font-mono text-fg-muted leading-none">{slot.label}</span>
</div>
))}
</div>
{/* Device rows */}
<div className="divide-y divide-slate-850 max-h-[460px] overflow-y-auto">
<div className="divide-y divide-line max-h-[460px] overflow-y-auto">
{devices.map((device) => (
<div
key={device.id}
className="items-center group hover:bg-slate-900/35"
className="items-center group hover:bg-card"
style={{ display: 'grid', gridTemplateColumns: '140px repeat(12, minmax(0, 1fr))' }}
>
<div className="pl-3 py-2 text-left">
<p className="font-mono font-bold text-[11px] text-white group-hover:text-emerald-400 transition-colors leading-none truncate">{device.hostname}</p>
<p className="text-[9px] font-mono text-slate-500 mt-0.5 leading-none">{device.type}</p>
<p className="font-mono font-bold text-[11px] text-fg group-hover:text-success transition-colors leading-none truncate">{device.hostname}</p>
<p className="text-[9px] font-mono text-fg-faint mt-0.5 leading-none">{device.type}</p>
</div>
{TIME_SLOTS.map((slot, sIdx) => {
const cur = getBookingForDeviceInSlot(device, dayOffset, slot.start, slot.end);
@ -456,8 +454,8 @@ export default function BookingCalendar({
if (!cur) {
return (
<div key={sIdx} className="px-0.5 py-1.5 h-10 flex items-center border-l border-slate-850">
<div className="w-full h-full rounded border border-dashed border-slate-800/40 hover:border-slate-700/60 transition-all" />
<div key={sIdx} className="px-0.5 py-1.5 h-10 flex items-center border-l border-line">
<div className="w-full h-full rounded border border-dashed border-line hover:border-line-strong transition-all" />
</div>
);
}
@ -472,13 +470,13 @@ export default function BookingCalendar({
: isLast ? 'rounded-r'
: '';
const borderCls = isMe
? `bg-emerald-500/30 border-emerald-500/60 hover:bg-emerald-500/45 ${isFirst ? 'border-l' : ''} border-y ${isLast ? 'border-r' : ''}`
: `bg-indigo-500/25 border-indigo-500/50 hover:bg-indigo-500/35 ${isFirst ? 'border-l' : ''} border-y ${isLast ? 'border-r' : ''}`;
? `bg-success/30 border-success/60 hover:bg-success/45 ${isFirst ? 'border-l' : ''} border-y ${isLast ? 'border-r' : ''}`
: `bg-primary/25 border-primary/50 hover:bg-primary/35 ${isFirst ? 'border-l' : ''} border-y ${isLast ? 'border-r' : ''}`;
return (
<div
key={sIdx}
className={`py-1.5 h-10 flex items-center ${isFirst ? 'pl-0.5' : ''} ${isLast ? 'pr-0.5' : ''} border-l border-slate-850`}
className={`py-1.5 h-10 flex items-center ${isFirst ? 'pl-0.5' : ''} ${isLast ? 'pr-0.5' : ''} border-l border-line`}
>
<div
onClick={() => onSelectBookingDetails(cur)}
@ -492,7 +490,7 @@ export default function BookingCalendar({
? initials(name)
: name.split(' ')[0];
return (
<span className="text-[8px] font-semibold leading-tight truncate px-1 text-white/90">
<span className="text-[8px] font-semibold leading-tight truncate px-1 text-fg">
{label}
</span>
);
@ -508,11 +506,11 @@ export default function BookingCalendar({
</div>
{/* Legend */}
<div className="mt-4 pt-4 border-t border-slate-855 flex items-center justify-between text-[11px] font-sans text-slate-400">
<div className="mt-4 pt-4 border-t border-line flex items-center justify-between text-[11px] font-sans text-fg-muted">
<div className="flex gap-4">
<span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded bg-emerald-500/30 border border-emerald-500/60" /> My Booking</span>
<span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded bg-indigo-500/25 border border-indigo-500/50" /> Colleague's Allocation</span>
<span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded border border-dashed border-slate-800/40" /> Available</span>
<span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded bg-success/30 border border-success/60" /> My Booking</span>
<span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded bg-primary/25 border border-primary/50" /> Colleague's Allocation</span>
<span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded border border-dashed border-line" /> Available</span>
</div>
<p className="italic">Double-bookings get rejected at commit time. no race conditions on your watch.</p>
</div>
@ -522,13 +520,13 @@ export default function BookingCalendar({
<div className="lg:col-span-4 space-y-6" id="booking-actions-card">
{/* Quick Booking Trigger */}
<div className="bg-[#1D2535] border border-emerald-900/30 rounded-xl p-5 shadow-sm relative overflow-hidden">
<div className="bg-success-soft border border-success-line rounded-xl p-5 shadow-sm relative overflow-hidden">
<div className="absolute top-0 right-0 left-0 h-1 bg-gradient-to-r from-emerald-500 to-teal-500" />
<h3 className="font-bold text-sm text-slate-100 flex items-center gap-1.5 mb-1.5 font-sans">
<Zap className="w-4 h-4 text-emerald-400 fill-emerald-500/30" />
<h3 className="font-bold text-sm text-fg flex items-center gap-1.5 mb-1.5 font-sans">
<Zap className="w-4 h-4 text-success fill-success/30" />
Quick Booking
</h3>
<p className="text-[11px] text-slate-400 leading-relaxed font-sans mb-4">
<p className="text-[11px] text-fg-muted leading-relaxed font-sans mb-4">
Pick a duration, then grab a free lab or a single box from the live list. ttl-based hardware leasing, basically.
</p>
@ -537,7 +535,7 @@ export default function BookingCalendar({
<button
key={h}
onClick={() => { setQuickDuration(h); setShowQuickPanel(true); }}
className="py-2.5 bg-slate-900 border border-slate-800 hover:border-emerald-500 hover:bg-slate-900 text-slate-200 hover:text-emerald-400 font-sans font-semibold text-xs rounded-lg transition-all"
className="py-2.5 bg-card border border-line hover:border-success text-fg-muted hover:text-success font-sans font-semibold text-xs rounded-lg transition-all"
>
{h}h
</button>
@ -546,37 +544,37 @@ export default function BookingCalendar({
<button
onClick={() => setShowQuickPanel(true)}
className="w-full py-2 bg-emerald-600/20 hover:bg-emerald-600/30 border border-emerald-800/50 text-emerald-400 font-semibold text-xs rounded-lg flex items-center justify-center gap-1.5 transition-all"
className="w-full py-2 bg-success/15 hover:bg-success/25 border border-success-line text-success font-semibold text-xs rounded-lg flex items-center justify-center gap-1.5 transition-all"
>
<Clock className="w-3.5 h-3.5" />
Show Available Now
</button>
<div className="mt-3 flex items-center gap-3 text-[10px] text-slate-500 font-mono">
<span className="flex items-center gap-1"><span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />{availableLabs.length} labs free</span>
<span className="flex items-center gap-1"><span className="w-1.5 h-1.5 rounded-full bg-cyan-400" />{availableDevices.length} devices free</span>
<div className="mt-3 flex items-center gap-3 text-[10px] text-fg-faint font-mono">
<span className="flex items-center gap-1"><span className="w-1.5 h-1.5 rounded-full bg-success" />{availableLabs.length} labs free</span>
<span className="flex items-center gap-1"><span className="w-1.5 h-1.5 rounded-full bg-info" />{availableDevices.length} devices free</span>
</div>
</div>
{/* Standard Booking Form */}
<div className="bg-[#1E293B] border border-slate-800 rounded-xl p-5 shadow-sm font-sans">
<h3 className="font-bold text-sm text-slate-100 flex items-center gap-2 mb-3">
<Calendar className="w-4.5 h-4.5 text-indigo-400" />
<div className="bg-card border border-line rounded-xl p-5 shadow-sm font-sans">
<h3 className="font-bold text-sm text-fg flex items-center gap-2 mb-3">
<Calendar className="w-4.5 h-4.5 text-primary" />
Reserve Slot
</h3>
<form onSubmit={handleCreateBooking} className="space-y-4 text-xs">
{/* Resource type toggle: whole lab topology or a single device */}
<div>
<label className="block text-slate-300 font-semibold mb-1">Reserve</label>
<label className="block text-fg-muted font-semibold mb-1">Reserve</label>
<div className="grid grid-cols-2 gap-1.5">
<button
type="button"
onClick={() => setResourceType('lab')}
className={`flex items-center justify-center gap-1.5 py-2 rounded-lg text-xs font-semibold border transition-all ${
resourceType === 'lab'
? 'bg-indigo-500/15 border-indigo-500 text-indigo-300'
: 'bg-slate-950 border-slate-800 text-slate-400 hover:text-white'
? 'bg-primary-soft border-primary text-primary'
: 'bg-inner border-line text-fg-muted hover:text-fg'
}`}
>
<Layers className="w-3.5 h-3.5" /> Topology
@ -586,8 +584,8 @@ export default function BookingCalendar({
onClick={() => setResourceType('device')}
className={`flex items-center justify-center gap-1.5 py-2 rounded-lg text-xs font-semibold border transition-all ${
resourceType === 'device'
? 'bg-indigo-500/15 border-indigo-500 text-indigo-300'
: 'bg-slate-950 border-slate-800 text-slate-400 hover:text-white'
? 'bg-primary-soft border-primary text-primary'
: 'bg-inner border-line text-fg-muted hover:text-fg'
}`}
>
<Server className="w-3.5 h-3.5" /> Single Device
@ -597,11 +595,11 @@ export default function BookingCalendar({
{resourceType === 'lab' ? (
<div>
<label className="block text-slate-300 font-semibold mb-1">Topology</label>
<label className="block text-fg-muted font-semibold mb-1">Topology</label>
<select
value={selectedLabId}
onChange={(e) => setSelectedLabId(e.target.value)}
className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 focus:outline-none focus:border-indigo-500"
className="w-full bg-field text-fg border border-line-strong rounded p-2 focus:outline-none focus:border-primary"
>
{bookableLabs.filter(l => l.scope === 'global').length > 0 && (
<optgroup label="Global Topologies">
@ -621,11 +619,11 @@ export default function BookingCalendar({
</div>
) : (
<div>
<label className="block text-slate-300 font-semibold mb-1">Device</label>
<label className="block text-fg-muted font-semibold mb-1">Device</label>
<select
value={selectedDeviceId}
onChange={(e) => setSelectedDeviceId(e.target.value)}
className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 focus:outline-none focus:border-indigo-500 font-mono"
className="w-full bg-field text-fg border border-line-strong rounded p-2 focus:outline-none focus:border-primary font-mono"
>
{devices.map((d) => (
<option key={d.id} value={d.id}>
@ -638,7 +636,7 @@ export default function BookingCalendar({
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-slate-300 font-semibold mb-1">Start Date</label>
<label className="block text-fg-muted font-semibold mb-1">Start Date</label>
<input
type="date"
value={startDate}
@ -651,38 +649,38 @@ export default function BookingCalendar({
setDayOffset(Math.round((sel - today) / 86_400_000));
if (e.target.value > endDate) setEndDate(e.target.value);
}}
className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 focus:outline-none focus:border-indigo-500 font-mono text-xs"
className="w-full bg-field text-fg border border-line-strong rounded p-2 focus:outline-none focus:border-primary font-mono text-xs"
/>
</div>
<div>
<label className="block text-slate-300 font-semibold mb-1">End Date</label>
<label className="block text-fg-muted font-semibold mb-1">End Date</label>
<input
type="date"
value={endDate}
min={startDate}
onChange={(e) => setEndDate(e.target.value)}
className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 focus:outline-none focus:border-indigo-500 font-mono text-xs"
className="w-full bg-field text-fg border border-line-strong rounded p-2 focus:outline-none focus:border-primary font-mono text-xs"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-slate-300 font-semibold mb-1">Start</label>
<label className="block text-fg-muted font-semibold mb-1">Start</label>
<select
value={startTime}
onChange={(e) => setStartTime(e.target.value)}
className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 focus:outline-none focus:border-indigo-500 font-mono"
className="w-full bg-field text-fg border border-line-strong rounded p-2 focus:outline-none focus:border-primary font-mono"
>
{TIME_OPTIONS.slice(0, -1).map(t => <option key={t} value={t}>{t}</option>)}
</select>
</div>
<div>
<label className="block text-slate-300 font-semibold mb-1">End</label>
<label className="block text-fg-muted font-semibold mb-1">End</label>
<select
value={endTime}
onChange={(e) => setEndTime(e.target.value)}
className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 focus:outline-none focus:border-indigo-500 font-mono"
className="w-full bg-field text-fg border border-line-strong rounded p-2 focus:outline-none focus:border-primary font-mono"
>
{TIME_OPTIONS.slice(1).map(t => <option key={t} value={t}>{t}</option>)}
</select>
@ -690,13 +688,13 @@ export default function BookingCalendar({
</div>
<div>
<label className="block text-slate-300 font-semibold mb-1">Notes / Objective</label>
<label className="block text-fg-muted font-semibold mb-1">Notes / Objective</label>
<textarea
required
rows={3}
value={bookingNotes}
onChange={(e) => setBookingNotes(e.target.value)}
className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 focus:outline-none focus:border-indigo-500"
className="w-full bg-field text-fg border border-line-strong rounded p-2 focus:outline-none focus:border-primary"
/>
</div>
@ -707,16 +705,16 @@ export default function BookingCalendar({
if (conflict.hasConflict) {
return (
<div className="bg-rose-950/40 p-2.5 rounded border border-rose-900/60 flex gap-2 text-rose-300 text-[11px] leading-normal">
<AlertCircle className="w-4 h-4 text-rose-400 shrink-0" />
<div className="bg-rose-soft p-2.5 rounded border border-rose-line flex gap-2 text-rose text-[11px] leading-normal">
<AlertCircle className="w-4 h-4 text-rose shrink-0" />
<span>{conflict.message}</span>
</div>
);
}
if (cmkEnabled && offline.length > 0) {
return (
<div className="bg-amber-950/40 p-2.5 rounded border border-amber-800/60 flex gap-2 text-amber-300 text-[11px] leading-normal">
<AlertTriangle className="w-4 h-4 text-amber-400 shrink-0" />
<div className="bg-warning-soft p-2.5 rounded border border-warning-line flex gap-2 text-warning text-[11px] leading-normal">
<AlertTriangle className="w-4 h-4 text-warning shrink-0" />
<span>
Warning {offline.map(d => `${d.hostname} (${effectiveStatus(d)})`).join(', ')} {offline.length === 1 ? 'is' : 'are'} not reachable in CheckMK. Booking will still be created.
</span>
@ -724,8 +722,8 @@ export default function BookingCalendar({
);
}
return (
<div className="bg-emerald-950/20 p-2.5 rounded border border-emerald-900/40 flex gap-2 text-emerald-300 text-[11px] leading-normal">
<CheckCircle2 className="w-4 h-4 text-emerald-400 shrink-0" />
<div className="bg-success-soft p-2.5 rounded border border-success-line flex gap-2 text-success text-[11px] leading-normal">
<CheckCircle2 className="w-4 h-4 text-success shrink-0" />
<span>Timeframe is available.</span>
</div>
);
@ -739,7 +737,7 @@ export default function BookingCalendar({
<button
type="submit"
disabled={disabled}
className="w-full py-2 bg-emerald-600 hover:bg-emerald-500 text-slate-950 font-bold rounded text-xs transition-colors hover:cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-emerald-600"
className="w-full py-2 bg-emerald-600 hover:bg-emerald-500 text-white font-bold rounded text-xs transition-colors hover:cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-emerald-600"
>
Confirm Reservation
</button>
@ -750,7 +748,7 @@ export default function BookingCalendar({
</div>
{/* ── Reservation Table ── */}
<div className="lg:col-span-12 bg-[#1E293B] border border-slate-800 rounded-xl p-5 sm:p-6 font-sans shadow-sm" id="reservation-registry-card">
<div className="lg:col-span-12 bg-card border border-line rounded-xl p-5 sm:p-6 font-sans shadow-sm" id="reservation-registry-card">
<button
type="button"
onClick={() => setShowReservations(s => !s)}
@ -758,26 +756,26 @@ export default function BookingCalendar({
aria-expanded={showReservations}
>
<div>
<h3 className="text-sm font-bold text-slate-100 flex items-center gap-2">
<ChevronDown className={`w-4 h-4 text-slate-400 transition-transform ${showReservations ? '' : '-rotate-90'}`} />
<Database className="w-4 h-4 text-emerald-400" />
<h3 className="text-sm font-bold text-fg flex items-center gap-2">
<ChevronDown className={`w-4 h-4 text-fg-muted transition-transform ${showReservations ? '' : '-rotate-90'}`} />
<Database className="w-4 h-4 text-success" />
Reservations
</h3>
<p className="text-xs text-slate-400 pl-6">SELECT * FROM bookings. every lease ever, straight from SQLite.</p>
<p className="text-xs text-fg-muted pl-6">SELECT * FROM bookings. every lease ever, straight from SQLite.</p>
</div>
<span className="text-[10px] bg-slate-950 px-2.5 py-1 rounded font-mono font-bold text-slate-300 border border-slate-800">
<span className="text-[10px] bg-inner px-2.5 py-1 rounded font-mono font-bold text-fg-muted border border-line">
DATABASE SELECT: {bookings.length} RECORDS
</span>
</button>
{!showReservations ? null : bookings.length === 0 ? (
<p className="mt-4 text-slate-500 text-xs text-center py-6 italic border border-dashed border-slate-800 rounded-lg">
<p className="mt-4 text-fg-faint text-xs text-center py-6 italic border border-dashed border-line rounded-lg">
No active reservation structures currently exist inside the database.
</p>
) : (
<div className="mt-4 overflow-x-auto rounded-lg border border-slate-800 bg-slate-900/10">
<table className="w-full text-xs text-left text-slate-300 divide-y divide-slate-800">
<thead className="bg-[#0f172a]/60 text-slate-400 font-mono text-[10px] uppercase tracking-wider">
<div className="mt-4 overflow-x-auto rounded-lg border border-line bg-inner">
<table className="w-full text-xs text-left text-fg-muted divide-y divide-line">
<thead className="bg-inner text-fg-muted font-mono text-[10px] uppercase tracking-wider">
<tr>
<th className="px-4 py-3">ID</th>
<th className="px-4 py-3">Topology / Resource</th>
@ -787,7 +785,7 @@ export default function BookingCalendar({
<th className="px-4 py-3 text-right font-sans">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800 bg-slate-950/10 font-sans">
<tbody className="divide-y divide-line bg-card font-sans">
{bookings.map((b) => {
const lab = labs.find(l => l.id === b.labId);
const isDeviceBooking = b.labId?.startsWith('device:');
@ -797,35 +795,35 @@ export default function BookingCalendar({
const tStart = new Date(b.startDateTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
const tEnd = new Date(b.endDateTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
return (
<tr key={b.id} className="hover:bg-slate-900/40 transition">
<td className="px-4 py-3.5 font-mono font-bold text-emerald-500/80">#{b.id.slice(-8)}</td>
<tr key={b.id} className="hover:bg-inner transition">
<td className="px-4 py-3.5 font-mono font-bold text-success">#{b.id.slice(-8)}</td>
<td className="px-4 py-3.5">
<span className="text-slate-100 font-semibold block">{isDeviceBooking ? (device?.hostname ?? deviceId) : (lab?.name ?? 'Unknown')}</span>
<span className="text-[10px] text-slate-400 font-mono">{isDeviceBooking ? 'Device booking' : (lab?.location ?? 'Staging')}</span>
<span className="text-fg font-semibold block">{isDeviceBooking ? (device?.hostname ?? deviceId) : (lab?.name ?? 'Unknown')}</span>
<span className="text-[10px] text-fg-muted font-mono">{isDeviceBooking ? 'Device booking' : (lab?.location ?? 'Staging')}</span>
</td>
<td className="px-4 py-3.5 font-mono">
<span className="block text-slate-200">{day}</span>
<span className="text-[10px] text-slate-400">{tStart} - {tEnd}</span>
<span className="block text-fg">{day}</span>
<span className="text-[10px] text-fg-muted">{tStart} - {tEnd}</span>
</td>
<td className="px-4 py-3.5">
<span className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[10px] font-semibold border capitalize ${
b.status === 'active' ? 'bg-emerald-950/60 border-emerald-500/30 text-emerald-400' :
b.status === 'upcoming' ? 'bg-indigo-950/60 border-indigo-500/30 text-indigo-400' :
b.status === 'completed' ? 'bg-slate-900 border-slate-800 text-slate-400' :
'bg-rose-950/60 border-rose-500/20 text-rose-400 font-bold'
b.status === 'active' ? 'bg-success-soft border-success-line text-success' :
b.status === 'upcoming' ? 'bg-primary-soft border-primary-line text-primary' :
b.status === 'completed' ? 'bg-inner border-line text-fg-muted' :
'bg-rose-soft border-rose-line text-rose font-bold'
}`}>{b.status}</span>
</td>
<td className="px-4 py-3.5 text-slate-400 max-w-[150px] truncate">{b.notes || '-'}</td>
<td className="px-4 py-3.5 text-fg-muted max-w-[150px] truncate">{b.notes || '-'}</td>
<td className="px-4 py-3.5 text-right space-x-1 whitespace-nowrap">
<button
onClick={() => onSelectBookingDetails(b)}
className="px-2.5 py-1.5 bg-slate-900 border border-slate-800 hover:border-slate-700 text-cyan-400 hover:text-cyan-300 rounded text-[11px] font-semibold cursor-pointer"
className="px-2.5 py-1.5 bg-inner border border-line hover:border-line-strong text-info hover:opacity-80 rounded text-[11px] font-semibold cursor-pointer"
>
Details
</button>
<button
onClick={() => { if (confirm(`Delete reservation #${b.id.slice(-8)}?`)) onDeleteBooking(b.id); }}
className="px-2.5 py-1.5 text-[11px] bg-rose-950/20 hover:bg-rose-900/30 text-rose-400 hover:text-rose-300 rounded transition cursor-pointer"
className="px-2.5 py-1.5 text-[11px] bg-rose-soft hover:opacity-80 text-rose rounded transition cursor-pointer"
>
Delete
</button>

View File

@ -118,26 +118,26 @@ export default function BookingDetailsModal({
};
return (
<div className="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-50 flex items-center justify-center p-4" id="booking-details-modal">
<div className="bg-[#1E293B] border border-slate-705 w-full max-w-4xl rounded-2xl shadow-2xl overflow-hidden animate-in fade-in zoom-in-95 duration-150 flex flex-col max-h-[90vh]">
<div className="fixed inset-0 bg-overlay backdrop-blur-sm z-50 flex items-center justify-center p-4" id="booking-details-modal">
<div className="bg-card border border-line w-full max-w-4xl rounded-2xl shadow-2xl overflow-hidden animate-in fade-in zoom-in-95 duration-150 flex flex-col max-h-[90vh]">
{/* Modal Header */}
<div className="bg-slate-900 px-6 py-4 border-b border-slate-800 flex items-center justify-between font-sans">
<div className="bg-inner px-6 py-4 border-b border-line flex items-center justify-between font-sans">
<div className="flex items-center gap-3">
<div className="p-1.5 bg-emerald-500/10 border border-emerald-500/30 rounded-lg text-emerald-400">
<div className="p-1.5 bg-success-soft border border-success-line rounded-lg text-success">
<HardDrive className="w-5 h-5" />
</div>
<div>
<h3 className="text-sm font-bold text-white flex items-center gap-2">
<h3 className="text-sm font-bold text-fg flex items-center gap-2">
<span>Reservation Details</span>
<span className="text-slate-500 font-mono font-normal">#{booking.id}</span>
<span className="text-fg-faint font-mono font-normal">#{booking.id}</span>
</h3>
<p className="text-[11px] text-slate-400">Inspect allocation status and diagnostic automation APIs</p>
<p className="text-[11px] text-fg-muted">Inspect allocation status and diagnostic automation APIs</p>
</div>
</div>
<button
onClick={onClose}
className="text-slate-400 hover:text-white p-1 hover:bg-slate-800 rounded transition-colors"
className="text-fg-muted hover:text-fg p-1 hover:bg-inner rounded transition-colors"
>
<X className="w-5 h-5" />
</button>
@ -150,10 +150,10 @@ export default function BookingDetailsModal({
<div className="grid grid-cols-1 md:grid-cols-12 gap-6">
{/* Left Box: Meta stats block */}
<div className="md:col-span-5 bg-slate-950/60 rounded-xl p-4 border border-slate-850 space-y-4">
<div className="md:col-span-5 bg-inner rounded-xl p-4 border border-line space-y-4">
<div>
<span className="text-[10px] uppercase font-mono tracking-wider font-bold text-slate-500">Scheduled Blueprint</span>
<h4 className="text-base font-bold text-white mt-0.5">{lab?.name || 'Standalone Test Lab'}</h4>
<span className="text-[10px] uppercase font-mono tracking-wider font-bold text-fg-faint">Scheduled Blueprint</span>
<h4 className="text-base font-bold text-fg mt-0.5">{lab?.name || 'Standalone Test Lab'}</h4>
<div className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[10px] font-mono capitalize mt-2 font-semibold border"
style={{
backgroundColor:
@ -180,35 +180,35 @@ export default function BookingDetailsModal({
{/* Time blocks */}
<div className="space-y-2.5 font-sans">
<div className="flex gap-2.5 text-xs text-slate-300">
<Calendar className="w-4.5 h-4.5 text-emerald-400 shrink-0 mt-0.5" />
<div className="flex gap-2.5 text-xs text-fg-muted">
<Calendar className="w-4.5 h-4.5 text-success shrink-0 mt-0.5" />
<div>
<span className="font-semibold block text-slate-400 text-[10px] uppercase tracking-wider">Start Time</span>
<span className="font-mono text-slate-200">{startFormatted}</span>
<span className="font-semibold block text-fg-muted text-[10px] uppercase tracking-wider">Start Time</span>
<span className="font-mono text-fg">{startFormatted}</span>
</div>
</div>
<div className="flex gap-2.5 text-xs text-slate-300">
<Clock className="w-4.5 h-4.5 text-indigo-400 shrink-0 mt-0.5" />
<div className="flex gap-2.5 text-xs text-fg-muted">
<Clock className="w-4.5 h-4.5 text-primary shrink-0 mt-0.5" />
<div>
<span className="font-semibold block text-slate-400 text-[10px] uppercase tracking-wider">Terminations On</span>
<span className="font-mono text-slate-200">{endFormatted}</span>
<span className="font-semibold block text-fg-muted text-[10px] uppercase tracking-wider">Terminations On</span>
<span className="font-mono text-fg">{endFormatted}</span>
</div>
</div>
<div className="flex gap-2.5 text-xs text-slate-300">
<UserIcon className="w-4.5 h-4.5 text-slate-500 shrink-0 mt-0.5" />
<div className="flex gap-2.5 text-xs text-fg-muted">
<UserIcon className="w-4.5 h-4.5 text-fg-faint shrink-0 mt-0.5" />
<div>
<span className="font-semibold block text-slate-400 text-[10px] uppercase tracking-wider">Reserved Operator</span>
<span className="text-slate-200">{creator.name}</span>
<span className="font-semibold block text-fg-muted text-[10px] uppercase tracking-wider">Reserved Operator</span>
<span className="text-fg">{creator.name}</span>
</div>
</div>
</div>
{/* Operator Notes */}
<div className="pt-3 border-t border-slate-850/80 font-sans text-xs">
<span className="font-semibold block text-slate-400 text-[10px] uppercase tracking-wider mb-1">Testing Objective Notes</span>
<p className="text-slate-300 leading-relaxed italic bg-slate-900 border border-slate-850 p-2.5 rounded">
<div className="pt-3 border-t border-line font-sans text-xs">
<span className="font-semibold block text-fg-muted text-[10px] uppercase tracking-wider mb-1">Testing Objective Notes</span>
<p className="text-fg-muted leading-relaxed italic bg-inner border border-line p-2.5 rounded">
"{booking.notes || 'No objectives specified.'}"
</p>
</div>
@ -216,27 +216,27 @@ export default function BookingDetailsModal({
</div>
{/* Right Box: Allocated Device checklist */}
<div className="md:col-span-7 bg-slate-900/35 border border-slate-800 rounded-xl p-4 flex flex-col justify-between">
<div className="md:col-span-7 bg-inner border border-line rounded-xl p-4 flex flex-col justify-between">
<div>
<div className="flex justify-between items-center mb-3">
<span className="text-[10px] uppercase font-mono tracking-wider font-bold text-emerald-400">Allocated Nodes Pool ({mappedDevices.length})</span>
<span className="text-[10px] text-slate-500 font-mono">Location: {lab?.location}</span>
<span className="text-[10px] uppercase font-mono tracking-wider font-bold text-success">Allocated Nodes Pool ({mappedDevices.length})</span>
<span className="text-[10px] text-fg-faint font-mono">Location: {lab?.location}</span>
</div>
{mappedDevices.length === 0 ? (
<p className="text-xs text-slate-400 italic py-6 text-center">No hardware nodes directly mapped to this scenario.</p>
<p className="text-xs text-fg-muted italic py-6 text-center">No hardware nodes directly mapped to this scenario.</p>
) : (
<div className="space-y-2 max-h-[220px] overflow-y-auto pr-1">
{mappedDevices.map((device) => (
<div key={device.id} className="p-3 bg-slate-950/65 border border-slate-850 hover:border-slate-800 rounded-lg flex items-center justify-between font-sans">
<div key={device.id} className="p-3 bg-card border border-line hover:border-line-strong rounded-lg flex items-center justify-between font-sans">
<div className="flex items-center gap-2.5">
<span className={`w-2 h-2 rounded-full ${device.status === 'online' ? 'bg-emerald-400' : 'bg-rose-400'}`} />
<span className={`w-2 h-2 rounded-full ${device.status === 'online' ? 'bg-success' : 'bg-rose'}`} />
<div>
<p className="text-xs font-mono font-bold text-white leading-none">{device.hostname}</p>
<p className="text-[9px] text-slate-400 mt-1 font-mono leading-none">{device.type} {device.location}</p>
<p className="text-xs font-mono font-bold text-fg leading-none">{device.hostname}</p>
<p className="text-[9px] text-fg-muted mt-1 font-mono leading-none">{device.type} {device.location}</p>
</div>
</div>
<span className="text-xs font-mono font-bold text-emerald-400">{device.ip}</span>
<span className="text-xs font-mono font-bold text-success">{device.ip}</span>
</div>
))}
</div>
@ -244,8 +244,8 @@ export default function BookingDetailsModal({
</div>
{/* Notice */}
<div className="bg-slate-900 p-3 rounded-lg border border-slate-800 flex gap-2.5 text-[11px] leading-normal text-slate-400 mt-4 font-sans">
<HelpCircle className="w-4 h-4 text-emerald-400 shrink-0 mt-0.5" />
<div className="bg-inner p-3 rounded-lg border border-line flex gap-2.5 text-[11px] leading-normal text-fg-muted mt-4 font-sans">
<HelpCircle className="w-4 h-4 text-success shrink-0 mt-0.5" />
<span>During the active slot, GhostGrid automatically binds these switches exclusively to your namespace and drops custom console-lines to open CLI pipelines.</span>
</div>
</div>
@ -254,27 +254,27 @@ export default function BookingDetailsModal({
{/* Ansible Semaphore automation status */}
{(lab?.semaphoreSetupTemplateId || lab?.semaphoreTeardownTemplateId) && (
<div className="border border-orange-900/40 rounded-xl bg-orange-950/10 p-4 font-sans">
<div className="border border-orange-line rounded-xl bg-orange-soft p-4 font-sans">
<div className="flex items-center gap-2 mb-3">
<Terminal className="w-4 h-4 text-orange-400" />
<span className="text-[10px] uppercase tracking-wider font-bold text-orange-400">Ansible Automation</span>
<Terminal className="w-4 h-4 text-orange" />
<span className="text-[10px] uppercase tracking-wider font-bold text-orange">Ansible Automation</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{lab.semaphoreSetupTemplateId && (
<div className="bg-slate-950/60 border border-slate-800 rounded-lg p-3 space-y-2">
<p className="text-[10px] text-slate-400 uppercase tracking-wide font-semibold">Setup</p>
<div className="bg-inner border border-line rounded-lg p-3 space-y-2">
<p className="text-[10px] text-fg-muted uppercase tracking-wide font-semibold">Setup</p>
{setupTriggered ? (
<div className="flex items-center gap-1.5 text-emerald-400 text-xs font-mono">
<div className="flex items-center gap-1.5 text-success text-xs font-mono">
<CheckCircle className="w-3.5 h-3.5" />
{setupJobId ? `Job #${setupJobId}` : 'Triggered'}
</div>
) : (
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500 font-mono">Pending</span>
<span className="text-xs text-fg-faint font-mono">Pending</span>
<button
onClick={() => manualTrigger('setup')}
disabled={triggering || booking.status === 'cancelled'}
className="flex items-center gap-1 px-2 py-1 bg-orange-900/40 hover:bg-orange-800/40 border border-orange-800/50 text-orange-400 text-[10px] font-semibold rounded transition-all disabled:opacity-40 disabled:cursor-not-allowed"
className="flex items-center gap-1 px-2 py-1 bg-orange-soft hover:opacity-80 border border-orange-line text-orange text-[10px] font-semibold rounded transition-all disabled:opacity-40 disabled:cursor-not-allowed"
>
<Play className="w-2.5 h-2.5" /> Trigger now
</button>
@ -283,20 +283,20 @@ export default function BookingDetailsModal({
</div>
)}
{lab.semaphoreTeardownTemplateId && (
<div className="bg-slate-950/60 border border-slate-800 rounded-lg p-3 space-y-2">
<p className="text-[10px] text-slate-400 uppercase tracking-wide font-semibold">Teardown</p>
<div className="bg-inner border border-line rounded-lg p-3 space-y-2">
<p className="text-[10px] text-fg-muted uppercase tracking-wide font-semibold">Teardown</p>
{teardownTriggered ? (
<div className="flex items-center gap-1.5 text-emerald-400 text-xs font-mono">
<div className="flex items-center gap-1.5 text-success text-xs font-mono">
<CheckCircle className="w-3.5 h-3.5" />
{teardownJobId ? `Job #${teardownJobId}` : 'Triggered'}
</div>
) : (
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500 font-mono">Pending</span>
<span className="text-xs text-fg-faint font-mono">Pending</span>
<button
onClick={() => manualTrigger('teardown')}
disabled={triggering || booking.status === 'cancelled'}
className="flex items-center gap-1 px-2 py-1 bg-orange-900/40 hover:bg-orange-800/40 border border-orange-800/50 text-orange-400 text-[10px] font-semibold rounded transition-all disabled:opacity-40 disabled:cursor-not-allowed"
className="flex items-center gap-1 px-2 py-1 bg-orange-soft hover:opacity-80 border border-orange-line text-orange text-[10px] font-semibold rounded transition-all disabled:opacity-40 disabled:cursor-not-allowed"
>
<Play className="w-2.5 h-2.5" /> Trigger now
</button>
@ -306,7 +306,7 @@ export default function BookingDetailsModal({
)}
</div>
{triggerStatus && (
<p className={`mt-2 text-[11px] font-mono ${triggerStatus.startsWith('Error') ? 'text-red-400' : 'text-emerald-400'}`}>
<p className={`mt-2 text-[11px] font-mono ${triggerStatus.startsWith('Error') ? 'text-danger' : 'text-success'}`}>
{triggerStatus}
</p>
)}
@ -339,22 +339,24 @@ export default function BookingDetailsModal({
</div>
{/* Modal Footer */}
<div className="bg-slate-900 px-6 py-4 border-t border-slate-800 flex justify-between items-center font-sans gap-3 flex-wrap">
<div className="bg-inner px-6 py-4 border-t border-line flex justify-between items-center font-sans gap-3 flex-wrap">
<div className="flex gap-2">
{/* Delete button option */}
<button
onClick={() => {
if (confirm(`WARING: Do you want to permanently DELETE reservation blueprint #${booking.id}? This physical dataset will be purged forever from SQLite databases.`)) {
onDelete(booking.id);
onClose();
}
}}
className="px-3 py-1.5 bg-rose-950/40 border border-rose-900/60 text-rose-400 hover:bg-rose-900 hover:text-white rounded-lg text-xs font-semibold flex items-center gap-1.5 transition-all hover:cursor-pointer"
>
<Trash2 className="w-3.5 h-3.5" />
<span>Purge Entry (SQLite DELETE)</span>
</button>
{/* Delete button option — admin only */}
{currentUser.role.toLowerCase() === 'admin' && (
<button
onClick={() => {
if (confirm(`WARING: Do you want to permanently DELETE reservation blueprint #${booking.id}? This physical dataset will be purged forever from SQLite databases.`)) {
onDelete(booking.id);
onClose();
}
}}
className="px-3 py-1.5 bg-rose-soft border border-rose-line text-rose hover:opacity-80 rounded-lg text-xs font-semibold flex items-center gap-1.5 transition-all hover:cursor-pointer"
>
<Trash2 className="w-3.5 h-3.5" />
<span>Purge Entry (SQLite DELETE)</span>
</button>
)}
{/* Cancel Status Toggle */}
{booking.status !== 'cancelled' && booking.status !== 'completed' && (
@ -365,7 +367,7 @@ export default function BookingDetailsModal({
onClose();
}
}}
className="px-3 py-1.5 bg-amber-950/40 border border-amber-700/50 text-amber-400 hover:bg-amber-900/60 hover:border-amber-500 hover:text-amber-300 rounded-lg text-xs font-semibold flex items-center gap-1.5 transition-all hover:cursor-pointer"
className="px-3 py-1.5 bg-warning-soft border border-warning-line text-warning hover:opacity-80 rounded-lg text-xs font-semibold flex items-center gap-1.5 transition-all hover:cursor-pointer"
>
<Ban className="w-3.5 h-3.5" />
<span>Cancel Reservation</span>
@ -376,7 +378,7 @@ export default function BookingDetailsModal({
<button
onClick={onClose}
className="px-4 py-1.5 bg-emerald-600 hover:bg-emerald-550 text-slate-950 hover:text-slate-1000 font-bold rounded-lg text-xs transition-colors hover:cursor-pointer"
className="px-4 py-1.5 bg-emerald-600 hover:bg-emerald-500 text-white font-bold rounded-lg text-xs transition-colors hover:cursor-pointer"
>
Acknowledge Specs
</button>

View File

@ -15,6 +15,7 @@ interface DashboardProps {
bookings: Booking[];
labs: LabTemplate[];
devices: Device[];
users: User[];
links: QuickLink[];
onCancelBooking: (id: string) => void;
onDeleteBooking: (id: string) => void;
@ -26,8 +27,8 @@ interface DashboardProps {
}
const LINK_ACCENT: Record<string, string> = {
emerald: 'text-emerald-400', cyan: 'text-cyan-400', indigo: 'text-indigo-400',
amber: 'text-amber-400', rose: 'text-rose-400', violet: 'text-violet-400',
emerald: 'text-success', cyan: 'text-info', indigo: 'text-primary',
amber: 'text-warning', rose: 'text-rose', violet: 'text-violet',
};
export default function Dashboard({
@ -35,6 +36,7 @@ export default function Dashboard({
bookings,
labs,
devices,
users,
links,
onCancelBooking,
onDeleteBooking,
@ -53,14 +55,15 @@ export default function Dashboard({
const ONE_HOUR_MS = 60 * 60 * 1000;
const personalBookings = bookings.filter(b => b.userId === currentUser.id && b.status !== 'cancelled');
// "Active" = currently running, plus a 1h grace window after the end so
// freshly-finished sessions linger briefly instead of jumping to "Expired".
const activeBookings = personalBookings.filter(b => {
// "Active" = currently running across ALL users, plus a 1h grace window after
// the end so freshly-finished sessions linger briefly instead of jumping to "Expired".
const activeBookings = bookings.filter(b => {
if (b.status === 'cancelled') return false;
const start = new Date(b.startDateTime).getTime();
const end = new Date(b.endDateTime).getTime();
return start <= now.getTime() && end > now.getTime() - ONE_HOUR_MS;
});
const upcomingBookings = personalBookings.filter(b => b.status === 'upcoming' && new Date(b.startDateTime) > now);
const upcomingBookings = bookings.filter(b => b.status === 'upcoming' && new Date(b.startDateTime) > now);
// Quick state checklist for the user to mark items as done as they test their lab!
// (kept deliberately nerdy - morale is a critical infrastructure dependency)
@ -92,26 +95,26 @@ export default function Dashboard({
<div className="space-y-6" id="dashboard-cockpit-root">
{/* Banner */}
<div className="bg-[#1E293B] border border-slate-800 rounded-2xl p-6 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="bg-card border border-line rounded-2xl p-6 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="space-y-1.5">
<h2 className="text-xl font-bold tracking-tight text-white font-sans">
Welcome back, <span className="text-emerald-400">{currentUser.name}</span>
<h2 className="text-xl font-bold tracking-tight text-fg font-sans">
Welcome back, <span className="text-success">{currentUser.name}</span>
</h2>
<p className="text-xs text-slate-400 font-sans">
<p className="text-xs text-fg-muted font-sans">
Your lab management hub. Reserve hardware, track active sessions, and keep runbooks close.
</p>
</div>
<div className="flex items-center gap-3 shrink-0">
<button
onClick={onNavigateToCalendar}
className="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-slate-950 font-sans font-bold text-xs rounded-lg transition-colors flex items-center gap-1.5 hover:cursor-pointer"
className="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-white font-sans font-bold text-xs rounded-lg transition-colors flex items-center gap-1.5 hover:cursor-pointer"
>
<Zap className="w-4 h-4 text-slate-950 fill-slate-950" />
<Zap className="w-4 h-4" />
Book Your Lab
</button>
<button
onClick={onNavigateToDevices}
className="px-4 py-2 bg-slate-900 hover:bg-slate-800 text-slate-200 border border-slate-800 hover:border-slate-700 rounded-lg text-xs font-semibold transition-all flex items-center gap-1 hover:cursor-pointer"
className="px-4 py-2 bg-inner hover:bg-card text-fg border border-line hover:border-line-strong rounded-lg text-xs font-semibold transition-all flex items-center gap-1 hover:cursor-pointer"
>
Browse Inventory
</button>
@ -125,24 +128,25 @@ export default function Dashboard({
<div className="lg:col-span-8 space-y-6">
{/* Active Sessions */}
<div className="bg-[#1E293B] border border-slate-800 rounded-xl p-5 shadow-sm">
<h3 className="font-bold text-sm text-slate-100 flex items-center gap-2 mb-4 font-sans justify-between">
<div className="bg-card border border-line rounded-xl p-5 shadow-sm">
<h3 className="font-bold text-sm text-fg flex items-center gap-2 mb-4 font-sans justify-between">
<span className="flex items-center gap-2">
<Clock className="w-4 h-4 text-emerald-400" />
<Clock className="w-4 h-4 text-success" />
Active Reservations
</span>
<span className="w-2 h-2 rounded-full bg-emerald-500 shrink-0" />
<span className="w-2 h-2 rounded-full bg-success shrink-0" />
</h3>
{activeBookings.length === 0 ? (
<div className="text-center py-8 bg-slate-900/35 rounded-lg border border-slate-800 font-sans">
<PlayCircle className="w-7 h-7 text-slate-600 mx-auto mb-2 opacity-50" />
<p className="text-xs text-slate-400">No active sessions.</p>
<div className="text-center py-8 bg-inner rounded-lg border border-line font-sans">
<PlayCircle className="w-7 h-7 text-fg-faint mx-auto mb-2 opacity-50" />
<p className="text-xs text-fg-muted">No active sessions.</p>
</div>
) : (
<div className="space-y-4 font-sans">
{activeBookings.map((booking) => {
const lab = labs.find(l => l.id === booking.labId);
const booker = users.find(u => u.id === booking.userId);
const startDate = new Date(booking.startDateTime);
const endDate = new Date(booking.endDateTime);
const sameDay = startDate.toDateString() === endDate.toDateString();
@ -153,43 +157,48 @@ export default function Dashboard({
? endDate.toLocaleTimeString('en-US', timeFmt)
: `${endDate.toLocaleDateString('en-US', dayFmt)}, ${endDate.toLocaleTimeString('en-US', timeFmt)}`;
return (
<div key={booking.id} className="p-4 bg-slate-950/60 border border-emerald-500/30 rounded-xl relative overflow-hidden">
<div className="absolute top-0 right-0 bottom-0 w-1 bg-emerald-500" />
<div key={booking.id} className="p-4 bg-inner border border-success-line rounded-xl relative overflow-hidden">
<div className="absolute top-0 right-0 bottom-0 w-1 bg-success" />
<div className="flex justify-between items-start mb-2 gap-2">
<div>
<h4 className="text-sm font-bold text-white font-sans">{lab?.name}</h4>
<span className="text-[10px] text-slate-400 flex items-center gap-1 font-sans mt-0.5">
<MapPin className="w-3.5 h-3.5 text-slate-500" />
<h4 className="text-sm font-bold text-fg font-sans">{lab?.name}</h4>
<span className="text-[10px] text-fg-muted flex items-center gap-1 font-sans mt-0.5">
<MapPin className="w-3.5 h-3.5 text-fg-faint" />
{lab?.location}
</span>
{booker && (
<span className="text-[10px] text-fg-faint font-sans mt-0.5 block">{booker.name}</span>
)}
</div>
{/* Countdown Pill */}
<span className="px-2.5 py-0.5 bg-emerald-950 border border-emerald-900 text-emerald-400 font-mono font-bold text-[10px] rounded-full">
<span className="px-2.5 py-0.5 bg-success-soft border border-success-line text-success font-mono font-bold text-[10px] rounded-full">
{getRemainingTimeText(booking.endDateTime)}
</span>
</div>
<div className="pt-3 border-t border-slate-800/60 flex justify-between items-center text-[10px]">
<span className="font-mono text-slate-400">
<div className="pt-3 border-t border-line flex justify-between items-center text-[10px]">
<span className="font-mono text-fg-muted">
{startF} {endF}
</span>
<div className="flex gap-2">
<button
onClick={() => onSelectBookingDetails(booking)}
className="px-3 py-1.5 bg-emerald-950/80 border border-emerald-500/30 text-emerald-400 hover:text-emerald-300 rounded-lg text-xs font-semibold transition-all hover:cursor-pointer"
className="px-3 py-1.5 bg-success-soft border border-success-line text-success hover:opacity-80 rounded-lg text-xs font-semibold transition-all hover:cursor-pointer"
>
Details
</button>
<button
onClick={() => {
if (confirm('Release this reservation early?')) {
onCancelBooking(booking.id);
}
}}
className="px-3 py-1.5 bg-rose-950/40 border border-rose-700/50 text-rose-400 hover:bg-rose-900/60 hover:border-rose-500 hover:text-rose-300 rounded-lg text-xs font-semibold transition-all hover:cursor-pointer"
>
Release
</button>
{booking.userId === currentUser.id && (
<button
onClick={() => {
if (confirm('Release this reservation early?')) {
onCancelBooking(booking.id);
}
}}
className="px-3 py-1.5 bg-rose-soft border border-rose-line text-rose hover:opacity-80 rounded-lg text-xs font-semibold transition-all hover:cursor-pointer"
>
Release
</button>
)}
</div>
</div>
</div>
@ -200,65 +209,73 @@ export default function Dashboard({
</div>
{/* Upcoming Sessions */}
<div className="bg-[#1E293B] border border-slate-800 rounded-xl p-5 shadow-sm font-sans">
<h3 className="font-bold text-sm text-slate-100 flex items-center gap-2 mb-3.5">
<Calendar className="w-4 h-4 text-slate-400" />
<div className="bg-card border border-line rounded-xl p-5 shadow-sm font-sans">
<h3 className="font-bold text-sm text-fg flex items-center gap-2 mb-3.5">
<Calendar className="w-4 h-4 text-fg-muted" />
Upcoming ({upcomingBookings.length})
</h3>
{upcomingBookings.length === 0 ? (
<p className="text-xs text-slate-400 py-4 text-center">No upcoming reservations.</p>
<p className="text-xs text-fg-muted py-4 text-center">No upcoming reservations.</p>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{upcomingBookings.map((booking) => {
const lab = labs.find(l => l.id === booking.labId);
const booker = users.find(u => u.id === booking.userId);
const dayStr = new Date(booking.startDateTime).toLocaleDateString('en-US', { weekday: 'short', day: 'numeric', month: 'short' });
const startF = new Date(booking.startDateTime).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
const endF = new Date(booking.endDateTime).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
return (
<div key={booking.id} className="p-3 bg-slate-900/30 border border-slate-800 hover:border-slate-700 rounded-lg flex flex-col justify-between">
<div key={booking.id} className="p-3 bg-inner border border-line hover:border-line-strong rounded-lg flex flex-col justify-between">
<div>
<div className="flex justify-between items-start mb-1">
<span className="font-mono font-bold text-[10px] text-indigo-400 bg-indigo-950/50 border border-indigo-900/50 px-2 py-0.5 rounded">
<span className="font-mono font-bold text-[10px] text-primary bg-primary-soft border border-primary-line px-2 py-0.5 rounded">
{dayStr}
</span>
<span className="text-[10px] font-mono text-slate-500">
<span className="text-[10px] font-mono text-fg-faint">
{startF} {endF}
</span>
</div>
<h4 className="text-xs font-bold text-slate-200 mt-1 font-sans">{lab?.name}</h4>
<p className="text-[10px] text-slate-400 line-clamp-1 mt-0.5 leading-normal">
<h4 className="text-xs font-bold text-fg mt-1 font-sans">{lab?.name}</h4>
{booker && (
<span className="text-[10px] text-fg-faint font-sans block mt-0.5">{booker.name}</span>
)}
<p className="text-[10px] text-fg-muted line-clamp-1 mt-0.5 leading-normal">
{booking.notes}
</p>
</div>
<div className="pt-2 mt-2 border-t border-slate-800 flex justify-end gap-1.5">
<div className="pt-2 mt-2 border-t border-line flex justify-end gap-1.5">
<button
onClick={() => onSelectBookingDetails(booking)}
className="px-2.5 py-1 text-xs text-emerald-400 hover:text-emerald-300 bg-emerald-950/40 border border-emerald-900/30 rounded-lg font-semibold transition hover:cursor-pointer"
className="px-2.5 py-1 text-xs text-success hover:opacity-80 bg-success-soft border border-success-line rounded-lg font-semibold transition hover:cursor-pointer"
>
Details
</button>
<button
onClick={() => {
if (confirm('Cancel this upcoming reservation?')) {
onCancelBooking(booking.id);
}
}}
className="px-2.5 py-1 text-xs text-slate-400 hover:text-slate-200 hover:bg-slate-800 rounded-lg border border-slate-700/50 hover:cursor-pointer transition"
>
Cancel
</button>
<button
onClick={() => {
if (confirm('Permanently delete this reservation?')) {
onDeleteBooking(booking.id);
}
}}
className="px-2.5 py-1 text-xs text-rose-400 hover:text-rose-300 hover:bg-rose-950/30 rounded-lg border border-rose-900/30 hover:cursor-pointer transition"
>
Purge
</button>
{booking.userId === currentUser.id && (
<button
onClick={() => {
if (confirm('Cancel this upcoming reservation?')) {
onCancelBooking(booking.id);
}
}}
className="px-2.5 py-1 text-xs text-fg-muted hover:text-fg hover:bg-card rounded-lg border border-line hover:cursor-pointer transition"
>
Cancel
</button>
)}
{currentUser.role.toLowerCase() === 'admin' && (
<button
onClick={() => {
if (confirm('Permanently delete this reservation?')) {
onDeleteBooking(booking.id);
}
}}
className="px-2.5 py-1 text-xs text-rose hover:opacity-80 hover:bg-rose-soft rounded-lg border border-rose-line hover:cursor-pointer transition"
>
Purge
</button>
)}
</div>
</div>
);
@ -273,9 +290,9 @@ export default function Dashboard({
<div className="lg:col-span-4 space-y-6">
{/* Lab Checklist */}
<div className="bg-[#1E293B] border border-slate-800 rounded-xl p-5 shadow-sm font-sans">
<h3 className="font-bold text-sm text-slate-100 flex items-center gap-2 mb-3.5">
<ListTodo className="w-4 h-4 text-slate-400" />
<div className="bg-card border border-line rounded-xl p-5 shadow-sm font-sans">
<h3 className="font-bold text-sm text-fg flex items-center gap-2 mb-3.5">
<ListTodo className="w-4 h-4 text-fg-muted" />
Lab Checklist
</h3>
@ -284,15 +301,15 @@ export default function Dashboard({
<div
key={item.id}
onClick={() => toggleTodo(item.id)}
className="flex items-start gap-2.5 p-2 bg-slate-900/40 hover:bg-slate-900 rounded-lg cursor-pointer transition-all border border-slate-800/60"
className="flex items-start gap-2.5 p-2 bg-inner hover:bg-card rounded-lg cursor-pointer transition-all border border-line"
>
<input
type="checkbox"
checked={item.checked}
onChange={() => {}}
className="mt-0.5 rounded border-slate-700 text-emerald-500 w-3.5 h-3.5 shrink-0"
className="mt-0.5 rounded border-line-strong text-success w-3.5 h-3.5 shrink-0"
/>
<span className={`text-xs leading-tight ${item.checked ? 'text-slate-500 line-through' : 'text-slate-300'}`}>
<span className={`text-xs leading-tight ${item.checked ? 'text-fg-faint line-through' : 'text-fg-muted'}`}>
{item.text}
</span>
</div>
@ -301,27 +318,27 @@ export default function Dashboard({
</div>
{/* Quick Links */}
<div className="bg-[#1E293B] border border-slate-800 rounded-xl p-5 shadow-sm font-sans">
<h3 className="font-bold text-sm text-slate-100 flex items-center gap-2 mb-3.5 justify-between">
<div className="bg-card border border-line rounded-xl p-5 shadow-sm font-sans">
<h3 className="font-bold text-sm text-fg flex items-center gap-2 mb-3.5 justify-between">
<span className="flex items-center gap-2">
<LinkIcon className="w-4 h-4 text-slate-400" />
<LinkIcon className="w-4 h-4 text-fg-muted" />
Quick Links
</span>
<button
onClick={onNavigateToLinks}
className="text-[10px] text-slate-400 hover:text-slate-200 font-semibold flex items-center gap-0.5 hover:cursor-pointer"
className="text-[10px] text-fg-muted hover:text-fg font-semibold flex items-center gap-0.5 hover:cursor-pointer"
>
Manage <ArrowRight className="w-3 h-3" />
</button>
</h3>
{links.length === 0 ? (
<div className="text-center py-6 bg-slate-900/35 rounded-lg border border-slate-800">
<Globe className="w-7 h-7 text-slate-600 mx-auto mb-2 opacity-50" />
<p className="text-xs text-slate-400">No shared links yet.</p>
<div className="text-center py-6 bg-inner rounded-lg border border-line">
<Globe className="w-7 h-7 text-fg-faint mx-auto mb-2 opacity-50" />
<p className="text-xs text-fg-muted">No shared links yet.</p>
<button
onClick={onNavigateToLinks}
className="text-xs text-slate-400 font-semibold underline mt-1.5 hover:text-slate-200 hover:cursor-pointer"
className="text-xs text-fg-muted font-semibold underline mt-1.5 hover:text-fg hover:cursor-pointer"
>
Add links
</button>
@ -338,23 +355,23 @@ export default function Dashboard({
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="group flex items-center gap-2.5 p-2 bg-slate-900/40 hover:bg-slate-900 rounded-lg border border-slate-800/60 hover:border-slate-700 transition-all"
className="group flex items-center gap-2.5 p-2 bg-inner hover:bg-card rounded-lg border border-line hover:border-line-strong transition-all"
>
<span className={`w-7 h-7 rounded-md bg-slate-950 border border-slate-800 flex items-center justify-center shrink-0 ${accent}`}>
<span className={`w-7 h-7 rounded-md bg-surface border border-line flex items-center justify-center shrink-0 ${accent}`}>
<Globe className="w-3.5 h-3.5" />
</span>
<span className="min-w-0 flex-1">
<span className="block text-xs font-semibold text-slate-200 group-hover:text-white truncate">{link.title}</span>
<span className="block text-xs font-semibold text-fg group-hover:text-fg truncate">{link.title}</span>
<span className={`block text-[10px] font-mono truncate ${accent}`}>{host}</span>
</span>
<ExternalLink className="w-3.5 h-3.5 text-slate-600 group-hover:text-slate-400 shrink-0" />
<ExternalLink className="w-3.5 h-3.5 text-fg-faint group-hover:text-fg-muted shrink-0" />
</a>
);
})}
{links.length > 6 && (
<button
onClick={onNavigateToLinks}
className="w-full text-center text-[10px] text-slate-500 hover:text-slate-300 pt-1.5 font-semibold hover:cursor-pointer"
className="w-full text-center text-[10px] text-fg-faint hover:text-fg-muted pt-1.5 font-semibold hover:cursor-pointer"
>
+{links.length - 6} more links
</button>

View File

@ -70,9 +70,9 @@ export default function DeviceInventory({
: null;
const statusMeta = (s: 'online' | 'offline' | 'unknown') => {
if (s === 'online') return { label: 'online', badge: 'bg-emerald-950/60 border-emerald-900/80 text-emerald-400', dot: 'bg-emerald-400' };
if (s === 'offline') return { label: 'offline', badge: 'bg-rose-950/60 border-rose-900/80 text-rose-400', dot: 'bg-rose-400' };
return { label: 'unknown', badge: 'bg-slate-800/60 border-slate-700 text-slate-400', dot: 'bg-slate-500' };
if (s === 'online') return { label: 'online', badge: 'bg-success-soft border-success-line text-success', dot: 'bg-success' };
if (s === 'offline') return { label: 'offline', badge: 'bg-rose-soft border-rose-line text-rose', dot: 'bg-rose' };
return { label: 'unknown', badge: 'bg-inner border-line text-fg-muted', dot: 'bg-fg-faint' };
};
// Filtered devices list
@ -160,31 +160,31 @@ export default function DeviceInventory({
// Safe internal renderer for markdown headings and code blocks to support the emergency sheet visually
const renderEmergencySheetHtml = (text: string) => {
if (!text) return <p className="text-slate-400 italic text-xs font-sans">No emergency manual entry registered for this device node.</p>;
if (!text) return <p className="text-fg-muted italic text-xs font-sans">No emergency manual entry registered for this device node.</p>;
const lines = text.split('\n');
return lines.map((line, idx) => {
// Headers
if (line.startsWith('### ')) {
return <h4 key={idx} className="text-sm font-bold text-slate-100 mt-4 mb-2 border-b border-slate-800 pb-1 font-sans">{line.replace('### ', '')}</h4>;
return <h4 key={idx} className="text-sm font-bold text-fg mt-4 mb-2 border-b border-line pb-1 font-sans">{line.replace('### ', '')}</h4>;
}
if (line.startsWith('#### ')) {
return <h5 key={idx} className="text-xs font-bold text-emerald-400 mt-3 mb-1 font-sans">{line.replace('#### ', '')}</h5>;
return <h5 key={idx} className="text-xs font-bold text-success mt-3 mb-1 font-sans">{line.replace('#### ', '')}</h5>;
}
if (line.startsWith('**') && line.endsWith('**')) {
return <p key={idx} className="text-xs font-semibold text-slate-200 mt-2 font-sans">{line.replace(/\*\*/g, '')}</p>;
return <p key={idx} className="text-xs font-semibold text-fg mt-2 font-sans">{line.replace(/\*\*/g, '')}</p>;
}
// Bullet lists
if (line.startsWith('* ') || line.startsWith('- ')) {
return <div key={idx} className="flex gap-2 text-xs text-slate-350 ml-4 font-sans align-top mb-1">
<span className="text-emerald-500"></span>
return <div key={idx} className="flex gap-2 text-xs text-fg-muted ml-4 font-sans align-top mb-1">
<span className="text-success"></span>
<span>{line.replace(/^[\*\-]\s+/, '')}</span>
</div>;
}
// Numeric lists
if (/^\d+\s*\.\s/.test(line)) {
return <div key={idx} className="flex gap-2 text-xs text-slate-350 ml-4 font-sans align-top mb-1">
<span className="text-emerald-400 font-bold">{line.match(/^\d+/)?.[0]}.</span>
return <div key={idx} className="flex gap-2 text-xs text-fg-muted ml-4 font-sans align-top mb-1">
<span className="text-success font-bold">{line.match(/^\d+/)?.[0]}.</span>
<span>{line.replace(/^\d+\s*\.\s+/, '')}</span>
</div>;
}
@ -195,20 +195,20 @@ export default function DeviceInventory({
if (line.trim() === '```bash' || line.trim() === '```') {
return null;
}
// Inline formatting fallback
if (line.includes('**')) {
return (
<p key={idx} className="text-xs text-slate-300 my-1 font-sans">
<p key={idx} className="text-xs text-fg-muted my-1 font-sans">
{line.split('**').map((tok, ti) => {
return ti % 2 === 1 ? <strong key={ti} className="text-slate-100">{tok}</strong> : tok;
return ti % 2 === 1 ? <strong key={ti} className="text-fg">{tok}</strong> : tok;
})}
</p>
);
}
if (line.includes('`')) {
return (
<p key={idx} className="text-xs text-slate-300 my-1 font-sans">
<p key={idx} className="text-xs text-fg-muted my-1 font-sans">
{line.split('`').map((tok, ti) => {
return ti % 2 === 1 ? <code key={ti} className="bg-slate-950 px-1 py-0.5 rounded text-[10px] text-emerald-300 font-mono">{tok}</code> : tok;
})}
@ -216,7 +216,7 @@ export default function DeviceInventory({
);
}
return line.trim() === '' ? <div key={idx} className="h-2" /> : <p key={idx} className="text-xs text-slate-300 my-0.5 font-sans">{line}</p>;
return line.trim() === '' ? <div key={idx} className="h-2" /> : <p key={idx} className="text-xs text-fg-muted my-0.5 font-sans">{line}</p>;
});
};
@ -224,40 +224,40 @@ export default function DeviceInventory({
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6" id="device-inventory-root">
{/* LEFT COLUMN: Device List & Controls */}
<div className="lg:col-span-7 bg-[#1E293B] border border-slate-800 rounded-xl p-5 flex flex-col h-full" id="inventory-list-container">
<div className="lg:col-span-7 bg-card border border-line rounded-xl p-5 flex flex-col h-full" id="inventory-list-container">
{/* Title */}
<div className="flex justify-between items-center mb-4">
<div>
<h2 className="text-base font-bold text-white flex items-center gap-2 font-sans">
<Server className="w-5 h-5 text-emerald-400" />
<h2 className="text-base font-bold text-fg flex items-center gap-2 font-sans">
<Server className="w-5 h-5 text-success" />
Inventory
</h2>
<p className="text-xs text-slate-400 font-sans">Every switch, firewall and AP we own. the source of truth, until someone forgets to update it.</p>
<p className="text-xs text-fg-muted font-sans">Every switch, firewall and AP we own. the source of truth, until someone forgets to update it.</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={handleOpenAdd}
className="flex items-center gap-1.5 px-3 py-1.5 bg-emerald-600 hover:bg-emerald-500 text-slate-950 font-sans font-semibold rounded-lg text-xs transition-colors hover:cursor-pointer"
className="flex items-center gap-1.5 px-3 py-1.5 bg-emerald-600 hover:bg-emerald-500 text-white font-sans font-semibold rounded-lg text-xs transition-colors hover:cursor-pointer"
id="btn-add-device"
>
<Plus className="w-4 h-4 text-slate-950 stroke-[3]" />
<Plus className="w-4 h-4 stroke-[3]" />
Add Device
</button>
</div>
</div>
{/* Filter Toolbar */}
<div className="flex flex-col sm:flex-row gap-3 mb-4 p-3 bg-slate-900/60 border border-slate-800 rounded-lg">
<div className="flex flex-col sm:flex-row gap-3 mb-4 p-3 bg-inner border border-line rounded-lg">
<div className="relative flex-1">
<span className="absolute left-3 top-2.5 text-slate-400">
<span className="absolute left-3 top-2.5 text-fg-faint">
<Search className="w-4 h-4" />
</span>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded-lg pl-9 pr-4 py-1.5 text-xs focus:outline-none focus:border-emerald-500 transition-colors placeholder:text-slate-500"
className="w-full bg-field text-fg border border-line-strong rounded-lg pl-9 pr-4 py-1.5 text-xs focus:outline-none focus:border-success transition-colors placeholder:text-fg-faint"
/>
</div>
<div className="flex gap-1.5 shrink-0 font-sans text-xs">
@ -267,8 +267,8 @@ export default function DeviceInventory({
onClick={() => setTypeFilter(type)}
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
typeFilter === type
? 'bg-emerald-500/15 border border-emerald-500 text-emerald-400'
: 'bg-slate-950 text-slate-400 border border-slate-850 hover:bg-slate-850 hover:text-white'
? 'bg-success-soft border border-success text-success'
: 'bg-inner text-fg-muted border border-line hover:bg-card hover:text-fg'
}`}
>
{type === 'all' ? 'All' : type}
@ -280,7 +280,7 @@ export default function DeviceInventory({
{/* Device Listing Card Table */}
<div className="flex-1 overflow-y-auto space-y-2.5 max-h-[600px] pr-1">
{filteredDevices.length === 0 ? (
<div className="text-center py-12 text-slate-500 text-xs font-sans">
<div className="text-center py-12 text-fg-faint text-xs font-sans">
grep came back empty. no boxes match that filter.
</div>
) : (
@ -292,29 +292,29 @@ export default function DeviceInventory({
onClick={() => setSelectedDeviceId(device.id)}
className={`p-4 border rounded-xl cursor-pointer transition-all flex justify-between items-center ${
isSelected
? 'bg-slate-900 border-emerald-500/80 shadow-[0_4px_12px_rgba(16,185,129,0.06)]'
: 'bg-slate-900/40 border-slate-800 hover:border-slate-700 hover:bg-slate-900/60'
? 'bg-card border-success'
: 'bg-inner border-line hover:border-line-strong hover:bg-card'
}`}
>
<div className="flex items-start gap-3.5">
{/* Device Icon Circle */}
<div className={`p-2 rounded-lg border text-base ${
device.type === 'Firewall' ? 'bg-rose-950/20 border-rose-900/60 text-rose-400' :
device.type === 'Access-Point' ? 'bg-amber-950/20 border-amber-900/60 text-amber-400' :
device.type === 'Controller' ? 'bg-cyan-950/20 border-cyan-900/60 text-cyan-400' :
'bg-teal-950/20 border-teal-900/60 text-teal-400'
device.type === 'Firewall' ? 'bg-rose-soft border-rose-line text-rose' :
device.type === 'Access-Point' ? 'bg-warning-soft border-warning-line text-warning' :
device.type === 'Controller' ? 'bg-info-soft border-info-line text-info' :
'bg-success-soft border-success-line text-success'
}`}>
<Server className="w-5 h-5" />
</div>
<div>
<div className="flex items-center gap-2">
<span className="font-mono font-bold text-white text-sm">{device.hostname}</span>
<span className="text-[10px] px-2 py-0.5 font-sans rounded-full font-medium bg-slate-800 border border-slate-700 text-slate-400">{device.type}</span>
<span className="font-mono font-bold text-fg text-sm">{device.hostname}</span>
<span className="text-[10px] px-2 py-0.5 font-sans rounded-full font-medium bg-inner border border-line text-fg-muted">{device.type}</span>
</div>
<div className="flex flex-col gap-0.5 mt-1 font-sans">
<span className="text-xs font-mono text-emerald-400">{device.ip}</span>
<span className="text-[10px] text-slate-400 flex items-center gap-1">
<MapPin className="w-3 h-3 text-slate-500" />
<span className="text-xs font-mono text-success">{device.ip}</span>
<span className="text-[10px] text-fg-muted flex items-center gap-1">
<MapPin className="w-3 h-3 text-fg-faint" />
{device.location}
</span>
</div>
@ -334,13 +334,13 @@ export default function DeviceInventory({
); })()}
{/* Action Panel */}
<div className="flex items-center gap-1.5 border-l border-slate-800/80 pl-3">
<div className="flex items-center gap-1.5 border-l border-line pl-3">
{cmkHostUrl(device) && (
<a
href={cmkHostUrl(device)!}
target="_blank"
rel="noopener noreferrer"
className="p-1 px-1.5 rounded hover:bg-slate-800 text-slate-400 hover:text-cyan-400 transition-colors"
className="p-1 px-1.5 rounded hover:bg-inner text-fg-muted hover:text-info transition-colors"
title="Open host in CheckMK"
>
<ExternalLink className="w-3.5 h-3.5" />
@ -348,7 +348,7 @@ export default function DeviceInventory({
)}
<button
onClick={() => handleOpenEdit(device)}
className="p-1 px-1.5 rounded hover:bg-slate-800 text-slate-400 hover:text-indigo-400 transition-colors"
className="p-1 px-1.5 rounded hover:bg-inner text-fg-muted hover:text-primary transition-colors"
title="Edit specifications"
>
<Edit2 className="w-3.5 h-3.5" />
@ -359,7 +359,7 @@ export default function DeviceInventory({
onDeleteDevice(device.id);
}
}}
className="p-1 px-1.5 rounded hover:bg-slate-800 text-slate-400 hover:text-rose-400 transition-colors"
className="p-1 px-1.5 rounded hover:bg-inner text-fg-muted hover:text-rose transition-colors"
title="Delete device"
>
<Trash className="w-3.5 h-3.5" />
@ -379,34 +379,34 @@ export default function DeviceInventory({
{selectedDevice ? (
<>
{/* Header Spec Block */}
<div className="bg-[#1E293B] border border-slate-800 rounded-xl p-5 shadow-sm">
<span className="text-[10px] font-mono uppercase bg-slate-900 border border-slate-800 px-2.5 py-0.5 rounded text-amber-400 font-semibold">
<div className="bg-card border border-line rounded-xl p-5 shadow-sm">
<span className="text-[10px] font-mono uppercase bg-inner border border-line px-2.5 py-0.5 rounded text-warning font-semibold">
SPECS ID: {selectedDevice.id.toUpperCase()}
</span>
<h3 className="text-lg font-bold text-slate-100 mt-2 font-mono flex items-center justify-between">
<h3 className="text-lg font-bold text-fg mt-2 font-mono flex items-center justify-between">
<span>{selectedDevice.hostname}</span>
<span className="text-xs font-sans text-slate-400 font-normal">Active Link State</span>
<span className="text-xs font-sans text-fg-muted font-normal">Active Link State</span>
</h3>
<p className="text-xs text-slate-400 font-mono mt-0.5 bg-slate-950 p-2.5 rounded border border-slate-850 mt-2 leading-relaxed">
Hostname: <span className="text-slate-200">{selectedDevice.hostname}</span><br />
IP Address: <span className="text-emerald-400 font-bold">{selectedDevice.ip}</span><br />
Location: <span className="text-slate-200">{selectedDevice.location}</span><br />
Node Class: <span className="text-slate-200">{selectedDevice.type}</span>
<p className="text-xs text-fg-muted font-mono mt-0.5 bg-surface p-2.5 rounded border border-line mt-2 leading-relaxed">
Hostname: <span className="text-fg">{selectedDevice.hostname}</span><br />
IP Address: <span className="text-success font-bold">{selectedDevice.ip}</span><br />
Location: <span className="text-fg">{selectedDevice.location}</span><br />
Node Class: <span className="text-fg">{selectedDevice.type}</span>
</p>
<div className="mt-4 font-sans">
<h4 className="text-xs font-semibold text-slate-300">Description & Technical Notes:</h4>
<div className="mt-1 bg-slate-900/50 rounded p-2.5 border border-slate-800/80 text-xs text-slate-300 leading-relaxed">
<h4 className="text-xs font-semibold text-fg-muted">Description & Technical Notes:</h4>
<div className="mt-1 bg-inner rounded p-2.5 border border-line text-xs text-fg-muted leading-relaxed">
{selectedDevice.notes || 'No description notes registered.'}
</div>
</div>
{/* CheckMK Monitoring Panel only when CheckMK is enabled */}
{cmkEnabled && (
<div className="mt-4 pt-4 border-t border-slate-800 space-y-2.5">
<div className="mt-4 pt-4 border-t border-line space-y-2.5">
<div className="flex items-center justify-between">
<span className="text-xs text-slate-400 font-sans font-medium flex items-center gap-1.5">
<Gauge className="w-3.5 h-3.5 text-cyan-400" />
<span className="text-xs text-fg-muted font-sans font-medium flex items-center gap-1.5">
<Gauge className="w-3.5 h-3.5 text-info" />
CheckMK Monitoring
</span>
{(() => { const m = statusMeta(effectiveStatus(selectedDevice)); return (
@ -421,14 +421,14 @@ export default function DeviceInventory({
href={cmkHostUrl(selectedDevice)!}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-1.5 px-3 py-1.5 bg-slate-900 border border-slate-700 text-slate-200 hover:text-cyan-400 hover:border-cyan-500 rounded text-xs transition-colors font-mono"
className="flex items-center justify-center gap-1.5 px-3 py-1.5 bg-inner border border-line text-fg hover:text-info hover:border-info rounded text-xs transition-colors font-mono"
>
<ExternalLink className="w-3.5 h-3.5" />
Open host in CheckMK
</a>
)}
{selectedDevice.lastCheckedAt && (
<p className="text-[10px] text-slate-500 font-mono">
<p className="text-[10px] text-fg-faint font-mono">
Last checked: {new Date(selectedDevice.lastCheckedAt).toLocaleString()}
</p>
)}
@ -437,33 +437,33 @@ export default function DeviceInventory({
</div>
{/* Emergency rescue guidelines sheet */}
<div className="bg-[#1D2432] border border-amber-900/40 rounded-xl p-5 shadow-sm overflow-hidden relative">
<div className="bg-warning-soft border border-warning-line rounded-xl p-5 shadow-sm overflow-hidden relative">
<div className="absolute top-0 right-0 left-0 h-1 bg-gradient-to-r from-amber-500 to-rose-500" />
<div className="flex items-center justify-between border-b border-amber-900/30 pb-3 mb-4">
<div className="flex items-center justify-between border-b border-warning-line pb-3 mb-4">
<div className="flex items-center gap-2">
<BookOpen className="w-5 h-5 text-amber-500" />
<h3 className="font-bold text-sm text-slate-100 font-sans">
<BookOpen className="w-5 h-5 text-warning" />
<h3 className="font-bold text-sm text-fg font-sans">
Emergency Sheet & Disaster Recovery
</h3>
</div>
<span className="text-[9px] font-mono font-bold bg-amber-950 text-amber-300 px-2 py-0.5 rounded border border-amber-900/50">
<span className="text-[9px] font-mono font-bold bg-warning-soft text-warning px-2 py-0.5 rounded border border-warning-line">
RESCUE SHEET
</span>
</div>
{/* Markdown Content box */}
<div className="max-h-[350px] overflow-y-auto bg-slate-950/80 p-4 rounded-lg border border-slate-900 leading-relaxed font-sans scrollbar-thin">
<div className="max-h-[350px] overflow-y-auto bg-surface p-4 rounded-lg border border-line leading-relaxed font-sans scrollbar-thin">
{renderEmergencySheetHtml(selectedDevice.emergencySheet)}
</div>
<div className="mt-4 flex items-center gap-2 text-[10px] text-slate-400 bg-slate-900/60 p-2.5 rounded border border-slate-800">
<Info className="w-4 h-4 text-amber-400 shrink-0" />
<div className="mt-4 flex items-center gap-2 text-[10px] text-fg-muted bg-inner p-2.5 rounded border border-line">
<Info className="w-4 h-4 text-warning shrink-0" />
<span>Console baud rates, break-glass logins and the reset-button dance - for when SSH is a distant memory.</span>
</div>
</div>
</>
) : (
<div className="bg-slate-900 border border-slate-800 rounded-xl p-10 text-center text-slate-500 text-xs font-sans">
<div className="bg-card border border-line rounded-xl p-10 text-center text-fg-faint text-xs font-sans">
Pick a box from the list to see its specs and break-glass playbook.
</div>
)}
@ -471,16 +471,16 @@ Pick a box from the list to see its specs and break-glass playbook.
{/* FORM MODAL: Add / Edit Equipment */}
{isEditing && (
<div className="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div className="bg-[#1E293B] border border-slate-700 w-full max-w-lg rounded-xl shadow-2xl overflow-hidden animate-in fade-in zoom-in-95 duration-150">
<div className="bg-slate-900 px-5 py-4 border-b border-slate-700 flex items-center justify-between font-sans">
<h3 className="text-sm font-bold text-white flex items-center gap-2">
<Server className="w-4 h-4 text-emerald-400" />
<div className="fixed inset-0 bg-overlay backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div className="bg-card border border-line-strong w-full max-w-lg rounded-xl shadow-2xl overflow-hidden animate-in fade-in zoom-in-95 duration-150">
<div className="bg-inner px-5 py-4 border-b border-line flex items-center justify-between font-sans">
<h3 className="text-sm font-bold text-fg flex items-center gap-2">
<Server className="w-4 h-4 text-success" />
{formMode === 'add' ? 'Register New Hardware Node' : 'Modify Hardware Specifications'}
</h3>
<button
onClick={() => setIsEditing(false)}
className="text-slate-400 hover:text-white"
className="text-fg-muted hover:text-fg"
>
<X className="w-4 h-4" />
</button>
@ -489,40 +489,40 @@ Pick a box from the list to see its specs and break-glass playbook.
<form onSubmit={handleSave} className="p-5 space-y-4 font-sans text-xs">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-slate-300 font-semibold mb-1">Hostname</label>
<label className="block text-fg-muted font-semibold mb-1">Hostname</label>
<input
type="text"
required
value={formData.hostname}
onChange={(e) => setFormData({ ...formData, hostname: e.target.value })}
className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 focus:outline-none focus:border-emerald-500 font-mono"
className="w-full bg-field text-fg border border-line-strong rounded p-2 focus:outline-none focus:border-success font-mono"
/>
</div>
<div>
<label className="block text-slate-300 font-semibold mb-1">IP Address</label>
<label className="block text-fg-muted font-semibold mb-1">IP Address</label>
<input
type="text"
required
value={formData.ip}
onChange={(e) => setFormData({ ...formData, ip: e.target.value })}
className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 focus:outline-none focus:border-emerald-500 font-mono"
className="w-full bg-field text-fg border border-line-strong rounded p-2 focus:outline-none focus:border-success font-mono"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-slate-300 font-semibold mb-1">Physical Location</label>
<label className="block text-fg-muted font-semibold mb-1">Physical Location</label>
<input
type="text"
required
value={formData.location}
onChange={(e) => setFormData({ ...formData, location: e.target.value })}
className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 focus:outline-none focus:border-emerald-500"
className="w-full bg-field text-fg border border-line-strong rounded p-2 focus:outline-none focus:border-success"
/>
</div>
<div>
<label className="block text-slate-300 font-semibold mb-1">Device Class</label>
<label className="block text-fg-muted font-semibold mb-1">Device Class</label>
<select
value={isCustomType ? '__custom__' : formData.type}
onChange={(e) => {
@ -534,7 +534,7 @@ Pick a box from the list to see its specs and break-glass playbook.
setFormData({ ...formData, type: e.target.value as DeviceType });
}
}}
className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 focus:outline-none focus:border-emerald-500"
className="w-full bg-field text-fg border border-line-strong rounded p-2 focus:outline-none focus:border-success"
>
<option value="Switch">Switch (L2/L3 Routing-Distribution)</option>
<option value="Firewall">Firewall / Security Appliance</option>
@ -549,46 +549,46 @@ Pick a box from the list to see its specs and break-glass playbook.
autoFocus
value={formData.type}
onChange={(e) => setFormData({ ...formData, type: e.target.value as DeviceType })}
className="w-full mt-2 bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 focus:outline-none focus:border-emerald-500"
className="w-full mt-2 bg-field text-fg border border-line-strong rounded p-2 focus:outline-none focus:border-success"
/>
)}
</div>
</div>
<div>
<label className="block text-slate-300 font-semibold mb-1">Technical Notes / Patching Mappings</label>
<label className="block text-fg-muted font-semibold mb-1">Technical Notes / Patching Mappings</label>
<textarea
rows={2}
value={formData.notes}
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 focus:outline-none focus:border-emerald-500"
className="w-full bg-field text-fg border border-line-strong rounded p-2 focus:outline-none focus:border-success"
/>
</div>
<div>
<label className="block text-slate-300 font-semibold mb-1">Disaster Recovery Guide (Markdown formatting supported)</label>
<label className="block text-fg-muted font-semibold mb-1">Disaster Recovery Guide (Markdown formatting supported)</label>
<textarea
rows={6}
value={formData.emergencySheet}
onChange={(e) => setFormData({ ...formData, emergencySheet: e.target.value })}
className="w-full bg-slate-950 text-slate-250 border border-slate-800 rounded p-2 focus:outline-none focus:border-emerald-500 font-mono text-[11px] leading-tight"
className="w-full bg-field text-fg border border-line-strong rounded p-2 focus:outline-none focus:border-success font-mono text-[11px] leading-tight"
placeholder="### EMERGENCY DETAILS..."
/>
</div>
<div className="pt-3 border-t border-slate-800 flex justify-end gap-2.5">
<div className="pt-3 border-t border-line flex justify-end gap-2.5">
<button
type="button"
onClick={() => setIsEditing(false)}
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded font-semibold text-xs"
className="px-4 py-2 bg-inner hover:bg-card text-fg-muted rounded font-semibold text-xs"
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-slate-950 font-bold rounded text-xs flex items-center gap-1.5"
className="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-white font-bold rounded text-xs flex items-center gap-1.5"
>
<Save className="w-3.5 h-3.5 text-slate-950" />
<Save className="w-3.5 h-3.5" />
Save
</button>
</div>

View File

@ -31,17 +31,17 @@ export default function Header({
const userBookings = bookings.filter(b => b.userId === currentUser.id && b.emailSent);
return (
<header className="sticky top-0 z-50 bg-[#0F172A] border-b border-[#1E293B] text-slate-100 backdrop-blur-md bg-opacity-95 px-6 py-4 flex items-center justify-between" id="app-header">
<header className="sticky top-0 z-50 bg-header border-b border-line text-fg backdrop-blur-md bg-opacity-95 px-6 py-4 flex items-center justify-between" id="app-header">
{/* Brand Logo & Title */}
<div className="flex items-center gap-3">
<div className="p-1 bg-slate-950/90 border border-slate-800 rounded-xl shadow-[0_0_15px_rgba(6,182,212,0.15)] flex items-center justify-center text-white shrink-0 hover:border-cyan-550/50 transition-all duration-300" id="brand-logo">
<GhostGridLogo className="w-10 h-10 animate-pulse" />
<div className="p-1 bg-inner border border-line rounded-xl flex items-center justify-center text-fg shrink-0 hover:border-line-strong transition-all duration-300" id="brand-logo">
<GhostGridLogo className="w-10 h-10" />
</div>
<div>
<h1 className="text-xl font-sans tracking-tight font-bold flex items-center gap-2 text-white">
<h1 className="text-xl font-sans tracking-tight font-bold flex items-center gap-2 text-fg">
GhostGrid
</h1>
<p className="text-[9px] font-mono text-cyan-400 tracking-widest uppercase">it's not dns. there's no way it's dns. it was dns.</p>
<p className="text-[9px] font-mono text-info tracking-widest uppercase">it's not dns. there's no way it's dns. it was dns.</p>
<div className="flex items-center gap-1 mt-1">
<span className="airit-badge inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[8px] font-sans font-semibold">
<span className="w-1.5 h-1.5 rounded-sm bg-white/70 inline-block" />
@ -57,15 +57,15 @@ export default function Header({
{/* Theme Toggle */}
<button
onClick={onThemeToggle}
className="p-2.5 rounded-lg border border-slate-800 bg-slate-900 text-slate-300 hover:bg-slate-800/80 hover:text-white transition-all flex items-center justify-center cursor-pointer"
className="p-2.5 rounded-lg border border-line bg-inner text-fg-muted hover:bg-card hover:text-fg transition-all flex items-center justify-center cursor-pointer"
title={theme === 'dark' ? 'Switch to Light Mode' : 'Switch to Dark Mode'}
>
{theme === 'dark' ? <Sun className="w-5 h-5 text-amber-400" /> : <Moon className="w-5 h-4.5 text-indigo-450" />}
{theme === 'dark' ? <Sun className="w-5 h-5 text-warning" /> : <Moon className="w-5 h-4.5 text-primary" />}
</button>
{/* System Indicator */}
<div className="hidden md:flex items-center gap-2 px-3 py-1 bg-slate-800/60 rounded-full border border-slate-700/50 text-xs font-mono text-slate-300">
<span className={`w-2 h-2 rounded-full animate-pulse ${isProduction ? 'bg-emerald-500' : 'bg-amber-400'}`} />
<div className="hidden md:flex items-center gap-2 px-3 py-1 bg-inner rounded-full border border-line text-xs font-mono text-fg-muted">
<span className={`w-2 h-2 rounded-full animate-pulse ${isProduction ? 'bg-success' : 'bg-warning'}`} />
<span>System: {isProduction ? 'Production' : 'Development'}</span>
</div>
@ -74,36 +74,36 @@ export default function Header({
<button
onClick={() => { setShowMailInbox(!showMailInbox); setShowBellDropdown(false); }}
className={`p-2.5 rounded-lg border transition-all relative ${
showMailInbox ? 'bg-slate-800 border-emerald-500 text-emerald-400' : 'bg-slate-900 border-slate-800 text-slate-300 hover:bg-slate-800/80 hover:text-white'
showMailInbox ? 'bg-card border-success text-success' : 'bg-inner border-line text-fg-muted hover:bg-card hover:text-fg'
}`}
title="E-Mail Inbox (Booking Confirmations)"
>
<Mail className="w-5 h-5" />
{userBookings.length > 0 && (
<span className="absolute -top-1 -right-1 bg-emerald-500 text-slate-950 font-sans text-[10px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
<span className="absolute -top-1 -right-1 bg-success text-white font-sans text-[10px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
{userBookings.length}
</span>
)}
</button>
{showMailInbox && (
<div className="absolute right-0 mt-3 w-96 bg-[#1E293B] border border-slate-700 rounded-xl shadow-2xl overflow-hidden z-50 animate-in fade-in slide-in-from-top-2 duration-200">
<div className="bg-slate-900 px-4 py-3 border-b border-slate-700 flex items-center justify-between">
<div className="absolute right-0 mt-3 w-96 bg-card border border-line-strong rounded-xl shadow-2xl overflow-hidden z-50 animate-in fade-in slide-in-from-top-2 duration-200">
<div className="bg-inner px-4 py-3 border-b border-line flex items-center justify-between">
<div>
<h3 className="font-semibold text-sm text-white flex items-center gap-2">
<Mail className="w-4 h-4 text-emerald-400" />
<h3 className="font-semibold text-sm text-fg flex items-center gap-2">
<Mail className="w-4 h-4 text-success" />
Mail Inbox: {currentUser.email}
</h3>
<p className="text-[10px] text-slate-400 font-sans">Automatic booking confirmations & dynamic alerts</p>
<p className="text-[10px] text-fg-muted font-sans">Automatic booking confirmations & dynamic alerts</p>
</div>
<button onClick={() => setShowMailInbox(false)} className="text-slate-400 hover:text-white text-xs font-sans">Close</button>
<button onClick={() => setShowMailInbox(false)} className="text-fg-muted hover:text-fg text-xs font-sans">Close</button>
</div>
<div className="max-h-[360px] overflow-y-auto divide-y divide-slate-800 p-2 space-y-1">
<div className="max-h-[360px] overflow-y-auto divide-y divide-line p-2 space-y-1">
{userBookings.length === 0 ? (
<div className="text-center py-8 text-slate-400 text-sm font-sans">
<Mail className="w-8 h-8 text-slate-600 mx-auto mb-2 opacity-50" />
<div className="text-center py-8 text-fg-muted text-sm font-sans">
<Mail className="w-8 h-8 text-fg-faint mx-auto mb-2 opacity-50" />
No emails in inbox.
<p className="text-xs text-slate-500 mt-1">Book a lab to receive automated SMTP confirmations.</p>
<p className="text-xs text-fg-faint mt-1">Book a lab to receive automated SMTP confirmations.</p>
</div>
) : (
userBookings.map((booking) => {
@ -111,22 +111,22 @@ export default function Header({
const formattedStart = new Date(booking.startDateTime).toLocaleString('en-US', { dateStyle: 'short', timeStyle: 'short' });
const formattedEnd = new Date(booking.endDateTime).toLocaleString('en-US', { dateStyle: 'short', timeStyle: 'short' });
return (
<div key={booking.id} className="p-3 bg-slate-900/40 rounded-lg hover:bg-slate-900/80 transition-colors">
<div key={booking.id} className="p-3 bg-inner rounded-lg hover:bg-card transition-colors">
<div className="flex justify-between items-start mb-1 gap-1">
<span className="text-[11px] font-mono text-emerald-400 font-semibold bg-emerald-950/80 px-2 py-0.5 rounded border border-emerald-900/50">SMTP INCOMING</span>
<span className="text-[10px] font-mono text-slate-400">Just now</span>
<span className="text-[11px] font-mono text-success font-semibold bg-success-soft px-2 py-0.5 rounded border border-success-line">SMTP INCOMING</span>
<span className="text-[10px] font-mono text-fg-muted">Just now</span>
</div>
<h4 className="text-xs font-semibold text-white font-sans">Confirmation: Lab Booking for {lab?.name || 'Unknown'}</h4>
<div className="mt-2 text-[11px] text-slate-300 leading-relaxed space-y-1.5 font-sans border-l-2 border-emerald-500/50 pl-2">
<h4 className="text-xs font-semibold text-fg font-sans">Confirmation: Lab Booking for {lab?.name || 'Unknown'}</h4>
<div className="mt-2 text-[11px] text-fg-muted leading-relaxed space-y-1.5 font-sans border-l-2 border-success pl-2">
<p>Hello <strong>{currentUser.name}</strong>,</p>
<p>Your booking request for lab <strong>{lab?.name}</strong> has been registered successfully.</p>
<div className="bg-slate-850 p-1.5 rounded font-mono text-[9px] text-slate-300 border border-slate-700/50">
<div className="bg-surface p-1.5 rounded font-mono text-[9px] text-fg-muted border border-line">
<strong>Lab Location:</strong> {lab?.location}<br />
<strong>Start Time:</strong> {formattedStart}<br />
<strong>End Time:</strong> {formattedEnd}<br />
<strong>Notes:</strong> {booking.notes || 'None'}
</div>
<p className="text-[10px] text-slate-500 italic">GhostGrid Automation Mailbot</p>
<p className="text-[10px] text-fg-faint italic">GhostGrid Automation Mailbot</p>
</div>
</div>
);
@ -142,39 +142,39 @@ export default function Header({
<button
onClick={() => { setShowBellDropdown(!showBellDropdown); setShowMailInbox(false); }}
className={`p-2.5 rounded-lg border transition-all relative ${
showBellDropdown ? 'bg-slate-800 border-amber-500 text-amber-400' : 'bg-slate-905 border-slate-800 text-slate-300 hover:bg-slate-800/80 hover:text-white'
showBellDropdown ? 'bg-card border-warning text-warning' : 'bg-inner border-line text-fg-muted hover:bg-card hover:text-fg'
}`}
title="Interface & System Alerts"
>
<Bell className="w-5 h-5" />
{notifications.length > 0 && (
<span className="absolute -top-1 -right-1 bg-amber-500 text-slate-950 font-sans text-[10px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
<span className="absolute -top-1 -right-1 bg-warning text-white font-sans text-[10px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
{notifications.length}
</span>
)}
</button>
{showBellDropdown && (
<div className="absolute right-0 mt-3 w-80 bg-[#1E293B] border border-slate-700 rounded-xl shadow-2xl overflow-hidden z-50 animate-in fade-in slide-in-from-top-2 duration-200">
<div className="bg-slate-900 px-4 py-3 border-b border-slate-700 flex items-center justify-between">
<div className="absolute right-0 mt-3 w-80 bg-card border border-line-strong rounded-xl shadow-2xl overflow-hidden z-50 animate-in fade-in slide-in-from-top-2 duration-200">
<div className="bg-inner px-4 py-3 border-b border-line flex items-center justify-between">
<div>
<h3 className="font-semibold text-sm text-white flex items-center gap-2 font-sans">
<Bell className="w-4 h-4 text-amber-400" />
<h3 className="font-semibold text-sm text-fg flex items-center gap-2 font-sans">
<Bell className="w-4 h-4 text-warning" />
Notifications ({notifications.length})
</h3>
<p className="text-[10px] text-slate-400 font-sans">Booking lifecycles & countdowns</p>
<p className="text-[10px] text-fg-muted font-sans">Booking lifecycles & countdowns</p>
</div>
{notifications.length > 0 && (
<button onClick={onClearNotifications} className="text-amber-400 hover:text-amber-300 text-xs font-semibold font-sans">Clear All</button>
<button onClick={onClearNotifications} className="text-warning hover:opacity-80 text-xs font-semibold font-sans">Clear All</button>
)}
</div>
<div className="max-h-[300px] overflow-y-auto divide-y divide-slate-800 p-2">
<div className="max-h-[300px] overflow-y-auto divide-y divide-line p-2">
{notifications.length === 0 ? (
<div className="text-center py-6 text-slate-400 text-xs font-sans">No active system alerts.</div>
<div className="text-center py-6 text-fg-muted text-xs font-sans">No active system alerts.</div>
) : (
notifications.map((notif, index) => (
<div key={index} className="p-2.5 text-xs text-slate-200 flex gap-2 hover:bg-slate-905/40 rounded transition-colors mb-1 font-sans">
<AlertTriangle className="w-4 h-4 text-amber-400 shrink-0 mt-0.5" />
<div key={index} className="p-2.5 text-xs text-fg flex gap-2 hover:bg-inner rounded transition-colors mb-1 font-sans">
<AlertTriangle className="w-4 h-4 text-warning shrink-0 mt-0.5" />
<p>{notif}</p>
</div>
))
@ -185,15 +185,15 @@ export default function Header({
</div>
{/* User Info + Logout */}
<div className="flex items-center gap-2 pl-2 pr-1 py-1.5 bg-slate-900 border border-slate-800 rounded-lg text-slate-200">
<div className="flex items-center gap-2 pl-2 pr-1 py-1.5 bg-inner border border-line rounded-lg text-fg">
<div className="hidden sm:block">
<div className="text-xs font-semibold leading-3 text-white max-w-[120px] truncate">{currentUser.name}</div>
<div className="text-[9px] text-cyan-400 font-mono tracking-wider mt-0.5 max-w-[125px] truncate">{currentUser.email}</div>
<div className="text-xs font-semibold leading-3 text-fg max-w-[120px] truncate">{currentUser.name}</div>
<div className="text-[9px] text-info font-mono tracking-wider mt-0.5 max-w-[125px] truncate">{currentUser.email}</div>
</div>
<button
onClick={onLogout}
title="Sign out"
className="ml-1 p-1.5 rounded-md text-slate-400 hover:text-red-400 hover:bg-red-950/40 transition-all"
className="ml-1 p-1.5 rounded-md text-fg-muted hover:text-danger hover:bg-danger-soft transition-all"
>
<LogOut className="w-4 h-4" />
</button>

View File

@ -178,18 +178,18 @@ export default function LabTemplates({
onClick={() => setSelectedLab(lab)}
className={`p-4 rounded-xl border transition-all cursor-pointer relative ${
isSelected
? 'bg-slate-900 border-emerald-500'
: 'bg-slate-900/40 border-slate-800 hover:border-slate-700 hover:bg-slate-900/60'
? 'bg-card border-success'
: 'bg-inner border-line hover:border-line-strong hover:bg-card'
}`}
>
<div className="flex justify-between items-start">
<h3 className="font-bold text-sm text-white">{lab.name}</h3>
<h3 className="font-bold text-sm text-fg">{lab.name}</h3>
<div className="flex gap-1.5" onClick={e => e.stopPropagation()}>
{editable && (
<>
<button
onClick={() => handleOpenEdit(lab)}
className="text-slate-400 hover:text-indigo-400 p-0.5"
className="text-fg-muted hover:text-primary p-0.5"
title="Edit template configuration"
>
<Edit3 className="w-3.5 h-3.5" />
@ -200,7 +200,7 @@ export default function LabTemplates({
onDeleteLab(lab.id);
}
}}
className="text-slate-400 hover:text-rose-400 p-0.5"
className="text-fg-muted hover:text-rose p-0.5"
title="Delete template"
>
<Trash className="w-3.5 h-3.5" />
@ -210,37 +210,37 @@ export default function LabTemplates({
</div>
</div>
<p className="text-xs text-slate-400 mt-1 line-clamp-2 leading-relaxed">
<p className="text-xs text-fg-muted mt-1 line-clamp-2 leading-relaxed">
{lab.description}
</p>
<div className="mt-3 pt-3 border-t border-slate-800/80 grid grid-cols-2 gap-1 text-[10px] text-slate-350">
<div className="mt-3 pt-3 border-t border-line grid grid-cols-2 gap-1 text-[10px] text-fg-muted">
<div className="flex items-center gap-1">
<UserIcon className="w-3.5 h-3.5 text-slate-500 shrink-0" />
<UserIcon className="w-3.5 h-3.5 text-fg-faint shrink-0" />
<span className="truncate">{lab.contactPerson}</span>
</div>
<div className="flex items-center gap-1">
<MapPin className="w-3.5 h-3.5 text-slate-500 shrink-0" />
<MapPin className="w-3.5 h-3.5 text-fg-faint shrink-0" />
<span className="truncate">{lab.location}</span>
</div>
</div>
<div className="mt-2.5 flex items-center justify-between">
<div className="flex items-center gap-1.5">
<span className="text-[10px] font-mono text-emerald-400 bg-emerald-950/50 px-2 py-0.5 rounded border border-emerald-900/50">
<span className="text-[10px] font-mono text-success bg-success-soft px-2 py-0.5 rounded border border-success-line">
{lab.deviceIds.length} connected devices
</span>
{lab.scope === 'personal' ? (
<span className="text-[10px] font-mono text-indigo-400 bg-indigo-950/50 px-2 py-0.5 rounded border border-indigo-900/50 flex items-center gap-1">
<span className="text-[10px] font-mono text-primary bg-primary-soft px-2 py-0.5 rounded border border-primary-line flex items-center gap-1">
<Lock className="w-2.5 h-2.5" /> Personal
</span>
) : (
<span className="text-[10px] font-mono text-slate-400 bg-slate-950/50 px-2 py-0.5 rounded border border-slate-800/50 flex items-center gap-1">
<span className="text-[10px] font-mono text-fg-muted bg-inner px-2 py-0.5 rounded border border-line flex items-center gap-1">
<Globe className="w-2.5 h-2.5" /> Global
</span>
)}
</div>
<ChevronRight className={`w-4 h-4 text-slate-500 transition-transform ${isSelected ? 'translate-x-1 text-emerald-400' : ''}`} />
<ChevronRight className={`w-4 h-4 text-fg-faint transition-transform ${isSelected ? 'translate-x-1 text-success' : ''}`} />
</div>
</div>
);
@ -250,21 +250,21 @@ export default function LabTemplates({
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6" id="lab-templates-container">
{/* LEFT COLUMN: Lab List */}
<div className="lg:col-span-4 bg-[#1E293B] border border-slate-800 rounded-xl p-5 flex flex-col h-full font-sans" id="labs-list-section">
<div className="lg:col-span-4 bg-card border border-line rounded-xl p-5 flex flex-col h-full font-sans" id="labs-list-section">
<div className="flex justify-between items-center mb-4">
<div>
<h2 className="text-base font-bold text-white flex items-center gap-2">
<Layers className="w-5 h-5 text-emerald-400" />
<h2 className="text-base font-bold text-fg flex items-center gap-2">
<Layers className="w-5 h-5 text-success" />
Topology
</h2>
<p className="text-xs text-slate-400">Predefined architectural scenarios & wiring profiles.</p>
<p className="text-xs text-fg-muted">Predefined architectural scenarios & wiring profiles.</p>
</div>
<button
onClick={handleOpenAdd}
className="p-1.5 bg-emerald-600 hover:bg-emerald-500 text-slate-950 font-bold rounded-lg text-xs flex items-center gap-1.5 transition-colors"
className="p-1.5 bg-emerald-600 hover:bg-emerald-500 text-white font-bold rounded-lg text-xs flex items-center gap-1.5 transition-colors"
title="Create new lab template"
>
<Plus className="w-4 h-4 text-slate-950" />
<Plus className="w-4 h-4" />
New
</button>
</div>
@ -273,24 +273,24 @@ export default function LabTemplates({
<div className="space-y-3 flex-1 overflow-y-auto max-h-[550px] pr-1">
{myPersonalLabs.length > 0 && (
<>
<p className="text-[10px] font-mono uppercase tracking-widest text-indigo-400 px-1">My Topologies</p>
<p className="text-[10px] font-mono uppercase tracking-widest text-primary px-1">My Topologies</p>
{myPersonalLabs.map(renderLabCard)}
</>
)}
{globalLabs.length > 0 && (
<>
<p className="text-[10px] font-mono uppercase tracking-widest text-slate-500 px-1 mt-2">Global Topologies</p>
<p className="text-[10px] font-mono uppercase tracking-widest text-fg-faint px-1 mt-2">Global Topologies</p>
{globalLabs.map(renderLabCard)}
</>
)}
{othersPersonal.length > 0 && (
<>
<p className="text-[10px] font-mono uppercase tracking-widest text-slate-500 px-1 mt-2">Others' Personal</p>
<p className="text-[10px] font-mono uppercase tracking-widest text-fg-faint px-1 mt-2">Others' Personal</p>
{othersPersonal.map(renderLabCard)}
</>
)}
{labs.length === 0 && (
<p className="text-xs text-slate-500 text-center py-8">No topology templates yet.</p>
<p className="text-xs text-fg-faint text-center py-8">No topology templates yet.</p>
)}
</div>
</div>
@ -300,33 +300,33 @@ export default function LabTemplates({
{selectedLab ? (
<>
{/* Template Card Meta */}
<div className="bg-[#1E293B] border border-slate-800 rounded-xl p-5 shadow-sm font-sans">
<div className="bg-card border border-line rounded-xl p-5 shadow-sm font-sans">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2 mb-4">
<div>
<span className="text-[9px] font-mono uppercase tracking-widest text-slate-400 bg-slate-900 border border-slate-800 px-2.5 py-0.5 rounded">
<span className="text-[9px] font-mono uppercase tracking-widest text-fg-muted bg-inner border border-line px-2.5 py-0.5 rounded">
TEMPLATE ID: {selectedLab.id.toUpperCase()}
</span>
<h3 className="text-lg font-bold text-white mt-1.5">{selectedLab.name}</h3>
<h3 className="text-lg font-bold text-fg mt-1.5">{selectedLab.name}</h3>
</div>
<div className="flex items-center gap-2.5">
<div className="bg-slate-900 p-2 rounded-lg border border-slate-805 text-[10px] flex items-center gap-1">
<UserIcon className="w-3.5 h-3.5 text-slate-400" />
<div className="bg-inner p-2 rounded-lg border border-line text-[10px] flex items-center gap-1">
<UserIcon className="w-3.5 h-3.5 text-fg-muted" />
<div>
<p className="text-slate-400 leading-none">Primary Contact</p>
<p className="text-white font-semibold mt-0.5">{selectedLab.contactPerson}</p>
<p className="text-fg-muted leading-none">Primary Contact</p>
<p className="text-fg font-semibold mt-0.5">{selectedLab.contactPerson}</p>
</div>
</div>
<div className="bg-slate-900 p-2 rounded-lg border border-slate-805 text-[10px] flex items-center gap-1">
<MapPin className="w-3.5 h-3.5 text-slate-400" />
<div className="bg-inner p-2 rounded-lg border border-line text-[10px] flex items-center gap-1">
<MapPin className="w-3.5 h-3.5 text-fg-muted" />
<div>
<p className="text-slate-400 leading-none">Testing Location</p>
<p className="text-white font-semibold mt-0.5">{selectedLab.location}</p>
<p className="text-fg-muted leading-none">Testing Location</p>
<p className="text-fg font-semibold mt-0.5">{selectedLab.location}</p>
</div>
</div>
</div>
</div>
<p className="text-xs text-slate-300 leading-relaxed bg-slate-900/30 p-3 rounded-lg border border-slate-800">
<p className="text-xs text-fg-muted leading-relaxed bg-inner p-3 rounded-lg border border-line">
{selectedLab.description}
</p>
</div>
@ -339,28 +339,28 @@ export default function LabTemplates({
/>
{/* Sub-Devices components list */}
<div className="bg-[#1E293B] border border-slate-800 rounded-xl p-5 shadow-sm font-sans">
<h4 className="text-xs font-bold text-slate-300 uppercase tracking-widest font-mono mb-3">Associated Physical Hardware Map</h4>
<div className="bg-card border border-line rounded-xl p-5 shadow-sm font-sans">
<h4 className="text-xs font-bold text-fg-muted uppercase tracking-widest font-mono mb-3">Associated Physical Hardware Map</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{labDevices.map((device) => (
<div
<div
key={device.id}
onClick={() => onOpenDeviceDetails(device)}
className="p-3 bg-slate-900/40 border border-slate-800 hover:border-slate-700 hover:bg-slate-900 transition-colors rounded-lg cursor-pointer flex justify-between items-center"
className="p-3 bg-inner border border-line hover:border-line-strong hover:bg-card transition-colors rounded-lg cursor-pointer flex justify-between items-center"
>
<div className="flex items-center gap-2.5 font-sans">
<div className={`p-1.5 rounded text-indigo-400 bg-indigo-950/30 border border-indigo-900/50`}>
<div className={`p-1.5 rounded text-primary bg-primary-soft border border-primary-line`}>
<Server className="w-4 h-4" />
</div>
<div>
<p className="text-xs font-mono font-bold text-white leading-none">{device.hostname}</p>
<p className="text-[9px] font-mono text-emerald-400 mt-1">{device.ip}</p>
<p className="text-xs font-mono font-bold text-fg leading-none">{device.hostname}</p>
<p className="text-[9px] font-mono text-success mt-1">{device.ip}</p>
</div>
</div>
<div className="flex items-center gap-2 font-mono">
<span className={`w-2 h-2 rounded-full ${device.status === 'online' ? 'bg-emerald-500' : 'bg-rose-500'}`} />
<span className="text-[10px] text-slate-400 capitalize">{device.status}</span>
<span className={`w-2 h-2 rounded-full ${device.status === 'online' ? 'bg-success' : 'bg-rose'}`} />
<span className="text-[10px] text-fg-muted capitalize">{device.status}</span>
</div>
</div>
))}
@ -368,7 +368,7 @@ export default function LabTemplates({
</div>
</>
) : (
<div className="bg-slate-900 border border-slate-800 rounded-xl p-16 text-center text-slate-500 text-xs font-sans">
<div className="bg-card border border-line rounded-xl p-16 text-center text-fg-faint text-xs font-sans">
Select a lab scenario template from the left directory column to inspect active port topology connections.
</div>
)}
@ -376,17 +376,17 @@ export default function LabTemplates({
{/* FORM MODAL: Create or Edit Lab Template */}
{isEditing && (
<div className="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div className="bg-[#1E293B] border border-slate-700 w-full max-w-2xl rounded-xl shadow-2xl overflow-hidden animate-in fade-in zoom-in-95 duration-150">
<div className="bg-slate-900 px-5 py-4 border-b border-slate-700 flex items-center justify-between font-sans overflow-x-auto">
<h3 className="text-sm font-bold text-white flex items-center gap-2">
<Layers className="w-5 h-5 text-emerald-400" />
<div className="fixed inset-0 bg-overlay backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div className="bg-card border border-line-strong w-full max-w-2xl rounded-xl shadow-2xl overflow-hidden animate-in fade-in zoom-in-95 duration-150">
<div className="bg-inner px-5 py-4 border-b border-line flex items-center justify-between font-sans overflow-x-auto">
<h3 className="text-sm font-bold text-fg flex items-center gap-2">
<Layers className="w-5 h-5 text-success" />
{formMode === 'add' ? 'Design New Lab Template' : 'Modify Lab Template Configuration'}
</h3>
<button
onClick={() => setIsEditing(false)}
className="text-slate-400 hover:text-white"
className="text-fg-muted hover:text-fg"
>
<X className="w-4 h-4" />
</button>
@ -396,23 +396,23 @@ export default function LabTemplates({
{/* Name & Location */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-slate-300 font-semibold mb-1">Topology Name</label>
<label className="block text-fg-muted font-semibold mb-1">Topology Name</label>
<input
type="text"
required
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 focus:outline-none focus:border-emerald-500"
className="w-full bg-field text-fg border border-line-strong rounded p-2 focus:outline-none focus:border-success"
/>
</div>
<div>
<label className="block text-slate-300 font-semibold mb-1">Physical Location</label>
<label className="block text-fg-muted font-semibold mb-1">Physical Location</label>
<input
type="text"
required
value={formData.location}
onChange={(e) => setFormData({ ...formData, location: e.target.value })}
className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 focus:outline-none focus:border-emerald-500"
className="w-full bg-field text-fg border border-line-strong rounded p-2 focus:outline-none focus:border-success"
/>
</div>
</div>
@ -420,38 +420,38 @@ export default function LabTemplates({
{/* Description & Contact person */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-slate-300 font-semibold mb-1">Caretaker / Owner</label>
<label className="block text-fg-muted font-semibold mb-1">Caretaker / Owner</label>
<input
type="text"
required
value={formData.contactPerson}
onChange={(e) => setFormData({ ...formData, contactPerson: e.target.value })}
className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 focus:outline-none focus:border-emerald-500"
className="w-full bg-field text-fg border border-line-strong rounded p-2 focus:outline-none focus:border-success"
/>
</div>
<div>
<label className="block text-slate-300 font-semibold mb-1">Description</label>
<label className="block text-fg-muted font-semibold mb-1">Description</label>
<input
type="text"
required
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 focus:outline-none focus:border-emerald-500"
className="w-full bg-field text-fg border border-line-strong rounded p-2 focus:outline-none focus:border-success"
/>
</div>
</div>
{/* Scope toggle */}
<div className="border-t border-slate-800 pt-3">
<label className="block text-slate-300 font-semibold mb-1.5">Visibility</label>
<div className="border-t border-line pt-3">
<label className="block text-fg-muted font-semibold mb-1.5">Visibility</label>
<div className="grid grid-cols-2 gap-1.5">
<button
type="button"
onClick={() => setFormData({ ...formData, scope: 'global' })}
className={`py-2 rounded-lg text-xs font-semibold border transition-all flex items-center justify-center gap-1.5 ${
formData.scope === 'global'
? 'bg-slate-800 border-slate-500 text-slate-200'
: 'bg-slate-950 border-slate-800 text-slate-400 hover:text-white'
? 'bg-card border-line-strong text-fg'
: 'bg-inner border-line text-fg-muted hover:text-fg'
}`}
>
<Globe className="w-3.5 h-3.5" /> Global — visible to all
@ -461,8 +461,8 @@ export default function LabTemplates({
onClick={() => setFormData({ ...formData, scope: 'personal' })}
className={`py-2 rounded-lg text-xs font-semibold border transition-all flex items-center justify-center gap-1.5 ${
formData.scope === 'personal'
? 'bg-indigo-900/30 border-indigo-500 text-indigo-300'
: 'bg-slate-950 border-slate-800 text-slate-400 hover:text-white'
? 'bg-primary-soft border-primary text-primary'
: 'bg-inner border-line text-fg-muted hover:text-fg'
}`}
>
<Lock className="w-3.5 h-3.5" /> Personal — only you
@ -471,10 +471,10 @@ export default function LabTemplates({
</div>
{/* Hardware checklist */}
<div className="border-t border-slate-800 pt-3">
<label className="block text-slate-300 font-bold mb-1.5">1. Assign Dedicated Hardware Nodes</label>
<p className="text-[10px] text-slate-400 mb-2">Select the devices from the active hardware inventory associated with this testing group.</p>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2 bg-slate-950/60 p-3 rounded-lg border border-slate-855">
<div className="border-t border-line pt-3">
<label className="block text-fg-muted font-bold mb-1.5">1. Assign Dedicated Hardware Nodes</label>
<p className="text-[10px] text-fg-muted mb-2">Select the devices from the active hardware inventory associated with this testing group.</p>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2 bg-inner p-3 rounded-lg border border-line">
{devices.map((dev) => {
const isChecked = formData.deviceIds.includes(dev.id);
return (
@ -483,16 +483,16 @@ export default function LabTemplates({
key={dev.id}
onClick={() => handleToggleDevice(dev.id)}
className={`p-2 rounded text-left border flex items-center justify-between transition-colors ${
isChecked
? 'bg-emerald-900/20 border-emerald-500 text-slate-100'
: 'bg-slate-900 border-slate-800 hover:border-slate-700 text-slate-300'
isChecked
? 'bg-success-soft border-success text-fg'
: 'bg-card border-line hover:border-line-strong text-fg-muted'
}`}
>
<div className="truncate pr-1">
<p className="font-bold text-[10px] font-mono leading-none">{dev.hostname}</p>
<p className="text-[9px] font-mono text-slate-400 mt-1">{dev.ip}</p>
<p className="text-[9px] font-mono text-fg-muted mt-1">{dev.ip}</p>
</div>
{isChecked && <Check className="w-3.5 h-3.5 text-emerald-400 shrink-0" />}
{isChecked && <Check className="w-3.5 h-3.5 text-success shrink-0" />}
</button>
);
})}
@ -500,18 +500,18 @@ export default function LabTemplates({
</div>
{/* Physical/Logical topology builder link creator */}
<div className="border-t border-slate-800 pt-3">
<label className="block text-slate-300 font-bold mb-1.5">2. Define Ports & Link Connections</label>
<p className="text-[10px] text-slate-400 mb-2">Model logical and physical patch cabling among the selected devices (e.g. Trunk aggregation profiles, LACP uplinks).</p>
<div className="border-t border-line pt-3">
<label className="block text-fg-muted font-bold mb-1.5">2. Define Ports & Link Connections</label>
<p className="text-[10px] text-fg-muted mb-2">Model logical and physical patch cabling among the selected devices (e.g. Trunk aggregation profiles, LACP uplinks).</p>
{/* Connection Inputs */}
<div className="grid grid-cols-1 sm:grid-cols-4 gap-2.5 p-2 bg-slate-1000 border border-slate-800 rounded-lg items-end mb-3">
<div className="grid grid-cols-1 sm:grid-cols-4 gap-2.5 p-2 bg-inner border border-line rounded-lg items-end mb-3">
<div>
<label className="block text-[10px] text-slate-400 mb-1">Source Node</label>
<label className="block text-[10px] text-fg-muted mb-1">Source Node</label>
<select
value={linkFrom}
onChange={(e) => setLinkFrom(e.target.value)}
className="w-full bg-slate-950 text-slate-250 border border-slate-805 rounded p-1 font-mono text-[11px]"
className="w-full bg-field text-fg border border-line-strong rounded p-1 font-mono text-[11px]"
>
<option value="">-- Choose --</option>
{formData.deviceIds.map((id) => {
@ -521,11 +521,11 @@ export default function LabTemplates({
</select>
</div>
<div>
<label className="block text-[10px] text-slate-400 mb-1">Target Node</label>
<label className="block text-[10px] text-fg-muted mb-1">Target Node</label>
<select
value={linkTo}
onChange={(e) => setLinkTo(e.target.value)}
className="w-full bg-slate-950 text-slate-250 border border-slate-805 rounded p-1 font-mono text-[11px]"
className="w-full bg-field text-fg border border-line-strong rounded p-1 font-mono text-[11px]"
>
<option value="">-- Choose --</option>
{formData.deviceIds.map((id) => {
@ -535,10 +535,10 @@ export default function LabTemplates({
</select>
</div>
<div>
<label className="block text-[10px] text-slate-400 mb-1">Link Identifier Description (Label)</label>
<label className="block text-[10px] text-fg-muted mb-1">Link Identifier Description (Label)</label>
<input
type="text"
className="w-full bg-slate-950 text-slate-250 border border-slate-805 p-1 rounded font-mono text-[11px]"
className="w-full bg-field text-fg border border-line-strong p-1 rounded font-mono text-[11px]"
value={linkType}
onChange={(e) => setLinkType(e.target.value)}
/>
@ -561,8 +561,8 @@ export default function LabTemplates({
const toDev = devices.find(d => d.id === link.toDevice)?.hostname || link.toDevice;
const isEditingThis = editingLinkIdx === idx;
return (
<div key={idx} className="flex items-center gap-2 bg-slate-950 px-3 py-1.5 rounded border border-slate-855 font-mono text-[10px] hover:border-slate-700">
<span className="text-slate-300 shrink-0"><strong>{fromDev}</strong> ────</span>
<div key={idx} className="flex items-center gap-2 bg-inner px-3 py-1.5 rounded border border-line font-mono text-[10px] hover:border-line-strong">
<span className="text-fg-muted shrink-0"><strong>{fromDev}</strong> ────</span>
{isEditingThis ? (
<input
autoFocus
@ -583,17 +583,17 @@ export default function LabTemplates({
}
if (e.key === 'Escape') setEditingLinkIdx(null);
}}
className="flex-1 min-w-0 bg-slate-800 text-slate-100 border border-indigo-500 rounded px-1.5 py-0.5 focus:outline-none"
className="flex-1 min-w-0 bg-field text-fg border border-primary rounded px-1.5 py-0.5 focus:outline-none"
/>
) : (
<span className="flex-1 min-w-0 text-indigo-300 truncate">{link.type}</span>
<span className="flex-1 min-w-0 text-primary truncate">{link.type}</span>
)}
<span className="text-slate-300 shrink-0"> <strong>{toDev}</strong></span>
<span className="text-fg-muted shrink-0"> <strong>{toDev}</strong></span>
<div className="flex gap-1.5 shrink-0 ml-auto font-sans">
<button
type="button"
onClick={() => { setEditingLinkIdx(idx); setEditingLinkLabel(link.type); }}
className="text-slate-400 hover:text-indigo-400 transition-colors"
className="text-fg-muted hover:text-primary transition-colors"
title="Edit label"
>
<Pencil className="w-3 h-3" />
@ -601,7 +601,7 @@ export default function LabTemplates({
<button
type="button"
onClick={() => handleRemoveLink(idx)}
className="text-rose-500 hover:text-rose-400 font-bold"
className="text-rose hover:opacity-80 font-bold"
>
<X className="w-3 h-3" />
</button>
@ -611,53 +611,53 @@ export default function LabTemplates({
})}
</div>
) : (
<p className="text-[10px] text-slate-500 italic">No interface connections formulated yet.</p>
<p className="text-[10px] text-fg-faint italic">No interface connections formulated yet.</p>
)}
</div>
{/* Ansible Semaphore Automation */}
{semaphoreEnabled && <div className="border-t border-slate-800 pt-3">
<label className="block text-slate-300 font-bold mb-1.5 flex items-center gap-1.5">
<Terminal className="w-3.5 h-3.5 text-orange-400" />
{semaphoreEnabled && <div className="border-t border-line pt-3">
<label className="block text-fg-muted font-bold mb-1.5 flex items-center gap-1.5">
<Terminal className="w-3.5 h-3.5 text-orange" />
3. Ansible Automation (optional)
</label>
<p className="text-[10px] text-slate-400 mb-2">Semaphore task template IDs to trigger at booking start and end. Leave empty to skip automation for this lab.</p>
<p className="text-[10px] text-fg-muted mb-2">Semaphore task template IDs to trigger at booking start and end. Leave empty to skip automation for this lab.</p>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-[10px] text-slate-400 mb-1">Setup Template ID</label>
<label className="block text-[10px] text-fg-muted mb-1">Setup Template ID</label>
<input
type="text"
inputMode="numeric"
value={formData.semaphoreSetupTemplateId}
onChange={(e) => setFormData({ ...formData, semaphoreSetupTemplateId: e.target.value })}
className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 font-mono text-xs focus:outline-none focus:border-orange-500/60"
className="w-full bg-field text-fg border border-line-strong rounded p-2 font-mono text-xs focus:outline-none focus:border-orange"
/>
</div>
<div>
<label className="block text-[10px] text-slate-400 mb-1">Teardown Template ID</label>
<label className="block text-[10px] text-fg-muted mb-1">Teardown Template ID</label>
<input
type="text"
inputMode="numeric"
value={formData.semaphoreTeardownTemplateId}
onChange={(e) => setFormData({ ...formData, semaphoreTeardownTemplateId: e.target.value })}
className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 font-mono text-xs focus:outline-none focus:border-orange-500/60"
className="w-full bg-field text-fg border border-line-strong rounded p-2 font-mono text-xs focus:outline-none focus:border-orange"
/>
</div>
</div>
</div>}
{/* Form submit handlers */}
<div className="pt-3 border-t border-slate-800 flex justify-end gap-2.5">
<div className="pt-3 border-t border-line flex justify-end gap-2.5">
<button
type="button"
onClick={() => setIsEditing(false)}
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded font-semibold text-xs animate-none"
className="px-4 py-2 bg-inner hover:bg-card text-fg-muted rounded font-semibold text-xs animate-none"
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-slate-950 font-bold rounded text-xs animate-none"
className="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-white font-bold rounded text-xs animate-none"
>
Save
</button>

View File

@ -20,12 +20,12 @@ interface LinkDashboardProps {
// Accent palette - keys are stored in the DB so they survive reloads.
const ACCENTS: Record<string, { ring: string; text: string; bg: string; dot: string; bar: string }> = {
emerald: { ring: 'hover:border-emerald-500/60', text: 'text-emerald-400', bg: 'bg-emerald-950/40', dot: 'bg-emerald-500', bar: 'bg-emerald-500' },
cyan: { ring: 'hover:border-cyan-500/60', text: 'text-cyan-400', bg: 'bg-cyan-950/40', dot: 'bg-cyan-500', bar: 'bg-cyan-500' },
indigo: { ring: 'hover:border-indigo-500/60', text: 'text-indigo-400', bg: 'bg-indigo-950/40', dot: 'bg-indigo-500', bar: 'bg-indigo-500' },
amber: { ring: 'hover:border-amber-500/60', text: 'text-amber-400', bg: 'bg-amber-950/40', dot: 'bg-amber-500', bar: 'bg-amber-500' },
rose: { ring: 'hover:border-rose-500/60', text: 'text-rose-400', bg: 'bg-rose-950/40', dot: 'bg-rose-500', bar: 'bg-rose-500' },
violet: { ring: 'hover:border-violet-500/60', text: 'text-violet-400', bg: 'bg-violet-950/40', dot: 'bg-violet-500', bar: 'bg-violet-500' },
emerald: { ring: 'hover:border-success', text: 'text-success', bg: 'bg-success-soft', dot: 'bg-success', bar: 'bg-success' },
cyan: { ring: 'hover:border-info', text: 'text-info', bg: 'bg-info-soft', dot: 'bg-info', bar: 'bg-info' },
indigo: { ring: 'hover:border-primary', text: 'text-primary', bg: 'bg-primary-soft', dot: 'bg-primary', bar: 'bg-primary' },
amber: { ring: 'hover:border-warning', text: 'text-warning', bg: 'bg-warning-soft', dot: 'bg-warning', bar: 'bg-warning' },
rose: { ring: 'hover:border-rose', text: 'text-rose', bg: 'bg-rose-soft', dot: 'bg-rose', bar: 'bg-rose' },
violet: { ring: 'hover:border-violet', text: 'text-violet', bg: 'bg-violet-soft', dot: 'bg-violet', bar: 'bg-violet' },
};
const ACCENT_KEYS = Object.keys(ACCENTS);
const accent = (key: string) => ACCENTS[key] ?? ACCENTS.emerald;
@ -138,23 +138,23 @@ export default function LinkDashboard({ links, currentUser, onAddLink, onUpdateL
<div className="space-y-6 font-sans" id="link-dashboard-root">
{/* Header banner */}
<div className="bg-gradient-to-br from-[#1E293B] to-[#111827] border border-slate-800 rounded-2xl p-6 shadow-sm relative overflow-hidden">
<div className="absolute top-0 right-0 p-8 text-slate-800 pointer-events-none select-none font-mono text-9xl font-extrabold opacity-10">
<div className="bg-card border border-line rounded-2xl p-6 shadow-sm relative overflow-hidden">
<div className="absolute top-0 right-0 p-8 text-fg-faint pointer-events-none select-none font-mono text-9xl font-extrabold opacity-10">
LINKS
</div>
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 relative">
<div className="space-y-1.5">
<h2 className="text-2xl font-bold tracking-tight text-white flex items-center gap-2.5">
<LinkIcon className="w-6 h-6 text-emerald-400" />
<h2 className="text-2xl font-bold tracking-tight text-fg flex items-center gap-2.5">
<LinkIcon className="w-6 h-6 text-success" />
Tooling & Quick Links
</h2>
<p className="text-xs text-slate-300 max-w-xl leading-relaxed">
<p className="text-xs text-fg-muted max-w-xl leading-relaxed">
The team's bookmark bar that doesn't live in one person's browser. CheckMK, Semaphore, Grafana, that one wiki page nobody can ever find again. Stored in SQLite, shared with everyone.
</p>
</div>
<button
onClick={openAdd}
className="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-slate-950 font-bold text-xs rounded-lg transition-colors flex items-center gap-1.5 shrink-0 hover:cursor-pointer"
className="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs rounded-lg transition-colors flex items-center gap-1.5 shrink-0 hover:cursor-pointer"
id="btn-add-link">
<Plus className="w-4 h-4" />
Add Link
@ -163,20 +163,20 @@ export default function LinkDashboard({ links, currentUser, onAddLink, onUpdateL
</div>
{/* Toolbar: search + category filter */}
<div className="flex flex-col sm:flex-row gap-3 p-3 bg-[#1E293B] border border-slate-800 rounded-xl">
<div className="flex flex-col sm:flex-row gap-3 p-3 bg-card border border-line rounded-xl">
<div className="relative flex-1">
<span className="absolute left-3 top-2.5 text-slate-550"><Search className="w-4 h-4" /></span>
<span className="absolute left-3 top-2.5 text-fg-faint"><Search className="w-4 h-4" /></span>
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded-lg pl-9 pr-4 py-1.5 text-xs focus:outline-none focus:border-emerald-600"
className="w-full bg-field text-fg border border-line-strong rounded-lg pl-9 pr-4 py-1.5 text-xs focus:outline-none focus:border-success"
/>
</div>
<div className="flex gap-1 flex-wrap shrink-0">
<button
onClick={() => setActiveCategory('all')}
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${activeCategory === 'all' ? 'bg-emerald-500/15 border border-emerald-500 text-emerald-400' : 'bg-slate-950 text-slate-400 border border-slate-850 hover:bg-slate-850 hover:text-white'}`}
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${activeCategory === 'all' ? 'bg-success-soft border border-success text-success' : 'bg-inner text-fg-muted border border-line hover:bg-card hover:text-fg'}`}
>
All
</button>
@ -184,7 +184,7 @@ export default function LinkDashboard({ links, currentUser, onAddLink, onUpdateL
<button
key={cat}
onClick={() => setActiveCategory(cat)}
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${activeCategory === cat ? 'bg-emerald-500/15 border border-emerald-500 text-emerald-400' : 'bg-slate-950 text-slate-400 border border-slate-850 hover:bg-slate-850 hover:text-white'}`}
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${activeCategory === cat ? 'bg-success-soft border border-success text-success' : 'bg-inner text-fg-muted border border-line hover:bg-card hover:text-fg'}`}
>
{cat}
</button>
@ -194,30 +194,30 @@ export default function LinkDashboard({ links, currentUser, onAddLink, onUpdateL
{/* Empty state */}
{links.length === 0 ? (
<div className="text-center py-16 bg-[#1E293B] border border-dashed border-slate-700 rounded-2xl">
<Globe className="w-10 h-10 text-slate-600 mx-auto mb-3 opacity-60" />
<h3 className="text-sm font-bold text-slate-200">404: links not found</h3>
<p className="text-xs text-slate-400 mt-1 max-w-sm mx-auto">
<div className="text-center py-16 bg-card border border-dashed border-line-strong rounded-2xl">
<Globe className="w-10 h-10 text-fg-faint mx-auto mb-3 opacity-60" />
<h3 className="text-sm font-bold text-fg">404: links not found</h3>
<p className="text-xs text-fg-muted mt-1 max-w-sm mx-auto">
Drop in the daily-driver tools - monitoring, automation, ticketing - so nobody has to ask "what's the URL for CheckMK again?" for the 40th time.
</p>
<button
onClick={openAdd}
className="mt-4 px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-slate-950 font-bold text-xs rounded-lg inline-flex items-center gap-1.5 hover:cursor-pointer"
className="mt-4 px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs rounded-lg inline-flex items-center gap-1.5 hover:cursor-pointer"
>
<Plus className="w-4 h-4" /> Add your first link
</button>
</div>
) : filtered.length === 0 ? (
<p className="text-center py-16 text-slate-500 text-xs">No links match your search.</p>
<p className="text-center py-16 text-fg-faint text-xs">No links match your search.</p>
) : (
<div className="space-y-8">
{grouped.map(([category, items]) => (
<section key={category}>
<div className="flex items-center gap-2 mb-3">
<FolderOpen className="w-4 h-4 text-slate-500" />
<h3 className="text-xs font-bold uppercase tracking-wider text-slate-400 font-mono">{category}</h3>
<span className="text-[10px] text-slate-600 font-mono">({items.length})</span>
<div className="flex-1 h-px bg-slate-850 ml-2" />
<FolderOpen className="w-4 h-4 text-fg-faint" />
<h3 className="text-xs font-bold uppercase tracking-wider text-fg-muted font-mono">{category}</h3>
<span className="text-[10px] text-fg-faint font-mono">({items.length})</span>
<div className="flex-1 h-px bg-line ml-2" />
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-4">
@ -226,12 +226,12 @@ export default function LinkDashboard({ links, currentUser, onAddLink, onUpdateL
return (
<div
key={link.id}
className={`group relative bg-[#1E293B] border border-slate-800 rounded-xl p-4 transition-all ${a.ring} hover:shadow-lg`}
className={`group relative bg-card border border-line rounded-xl p-4 transition-all ${a.ring} hover:shadow-lg`}
>
<span className={`absolute top-0 left-0 bottom-0 w-1 rounded-l-xl ${a.bar} opacity-70`} />
<div className="flex items-start gap-3">
<div className={`w-10 h-10 rounded-lg ${a.bg} border border-slate-800 flex items-center justify-center shrink-0 overflow-hidden`}>
<div className={`w-10 h-10 rounded-lg ${a.bg} border border-line flex items-center justify-center shrink-0 overflow-hidden`}>
<Globe className={`w-5 h-5 ${a.text}`} />
</div>
@ -240,7 +240,7 @@ export default function LinkDashboard({ links, currentUser, onAddLink, onUpdateL
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-bold text-white hover:underline flex items-center gap-1.5 truncate"
className="text-sm font-bold text-fg hover:underline flex items-center gap-1.5 truncate"
title={link.title}
>
<span className="truncate">{link.title}</span>
@ -261,13 +261,13 @@ export default function LinkDashboard({ links, currentUser, onAddLink, onUpdateL
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); commitDescEdit(link); }
if (e.key === 'Escape') { setEditingDescId(null); }
}}
className="w-full mt-3 bg-slate-950 text-slate-200 text-[11px] border border-emerald-600 rounded px-2 py-1 resize-none focus:outline-none leading-relaxed"
className="w-full mt-3 bg-field text-fg text-[11px] border border-success rounded px-2 py-1 resize-none focus:outline-none leading-relaxed"
/>
) : (
<p
onClick={() => startDescEdit(link)}
title="Click to edit description"
className={`text-[11px] leading-relaxed mt-3 line-clamp-2 cursor-text ${link.description ? 'text-slate-400 hover:text-slate-200' : 'text-slate-600 italic hover:text-slate-400'} transition-colors`}
className={`text-[11px] leading-relaxed mt-3 line-clamp-2 cursor-text ${link.description ? 'text-fg-muted hover:text-fg' : 'text-fg-faint italic hover:text-fg-muted'} transition-colors`}
>
{link.description || 'Add a description…'}
</p>
@ -278,7 +278,7 @@ export default function LinkDashboard({ links, currentUser, onAddLink, onUpdateL
<button
onClick={() => openEdit(link)}
title="Edit link"
className="p-1.5 rounded-md bg-slate-900/90 border border-slate-800 text-slate-400 hover:text-emerald-400 hover:border-emerald-600 transition-all hover:cursor-pointer"
className="p-1.5 rounded-md bg-card border border-line text-fg-muted hover:text-success hover:border-success transition-all hover:cursor-pointer"
>
<Pencil className="w-3.5 h-3.5" />
</button>
@ -287,7 +287,7 @@ export default function LinkDashboard({ links, currentUser, onAddLink, onUpdateL
if (confirm(`Remove "${link.title}" from the shared link dashboard?`)) onDeleteLink(link.id);
}}
title="Delete link"
className="p-1.5 rounded-md bg-slate-900/90 border border-slate-800 text-slate-400 hover:text-rose-400 hover:border-rose-600 transition-all hover:cursor-pointer"
className="p-1.5 rounded-md bg-card border border-line text-fg-muted hover:text-rose hover:border-rose transition-all hover:cursor-pointer"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
@ -303,47 +303,47 @@ export default function LinkDashboard({ links, currentUser, onAddLink, onUpdateL
{/* Add / Edit modal */}
{showForm && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in duration-150" onClick={closeForm}>
<div className="fixed inset-0 z-50 flex items-center justify-center bg-overlay backdrop-blur-sm p-4 animate-in fade-in duration-150" onClick={closeForm}>
<div
className="w-full max-w-md bg-[#1E293B] border border-slate-700 rounded-2xl shadow-2xl overflow-hidden animate-in zoom-in-95 duration-150"
className="w-full max-w-md bg-card border border-line-strong rounded-2xl shadow-2xl overflow-hidden animate-in zoom-in-95 duration-150"
onClick={(e) => e.stopPropagation()}
>
<div className="bg-slate-900 px-5 py-3.5 border-b border-slate-800 flex items-center justify-between">
<h3 className="font-bold text-sm text-white flex items-center gap-2">
<Star className="w-4 h-4 text-emerald-400" />
<div className="bg-inner px-5 py-3.5 border-b border-line flex items-center justify-between">
<h3 className="font-bold text-sm text-fg flex items-center gap-2">
<Star className="w-4 h-4 text-success" />
{editingId ? 'Edit Link' : 'New Quick Link'}
</h3>
<button onClick={closeForm} className="text-slate-400 hover:text-white"><X className="w-4 h-4" /></button>
<button onClick={closeForm} className="text-fg-muted hover:text-fg"><X className="w-4 h-4" /></button>
</div>
<form onSubmit={handleSubmit} className="p-5 space-y-4 text-xs">
<div>
<label className="block text-slate-300 font-semibold mb-1">Title *</label>
<label className="block text-fg-muted font-semibold mb-1">Title *</label>
<input
required autoFocus
value={draft.title}
onChange={(e) => setDraft({ ...draft, title: e.target.value })}
className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 focus:outline-none focus:border-emerald-600"
className="w-full bg-field text-fg border border-line-strong rounded p-2 focus:outline-none focus:border-success"
/>
</div>
<div>
<label className="block text-slate-300 font-semibold mb-1">URL *</label>
<label className="block text-fg-muted font-semibold mb-1">URL *</label>
<input
required
value={draft.url}
onChange={(e) => setDraft({ ...draft, url: e.target.value })}
className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 focus:outline-none focus:border-emerald-600 font-mono"
className="w-full bg-field text-fg border border-line-strong rounded p-2 focus:outline-none focus:border-success font-mono"
/>
</div>
<div>
<label className="block text-slate-300 font-semibold mb-1">Category</label>
<label className="block text-fg-muted font-semibold mb-1">Category</label>
<input
list="link-categories"
value={draft.category}
onChange={(e) => setDraft({ ...draft, category: e.target.value })}
className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 focus:outline-none focus:border-emerald-600"
className="w-full bg-field text-fg border border-line-strong rounded p-2 focus:outline-none focus:border-success"
/>
<datalist id="link-categories">
{categories.map(c => <option key={c} value={c} />)}
@ -351,24 +351,24 @@ export default function LinkDashboard({ links, currentUser, onAddLink, onUpdateL
</div>
<div>
<label className="block text-slate-300 font-semibold mb-1">Description</label>
<label className="block text-fg-muted font-semibold mb-1">Description</label>
<textarea
rows={2}
value={draft.description}
onChange={(e) => setDraft({ ...draft, description: e.target.value })}
className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 focus:outline-none focus:border-emerald-600 resize-none"
className="w-full bg-field text-fg border border-line-strong rounded p-2 focus:outline-none focus:border-success resize-none"
/>
</div>
<div>
<label className="block text-slate-300 font-semibold mb-1.5">Accent</label>
<label className="block text-fg-muted font-semibold mb-1.5">Accent</label>
<div className="flex gap-2">
{ACCENT_KEYS.map(key => (
<button
type="button"
key={key}
onClick={() => setDraft({ ...draft, color: key })}
className={`w-7 h-7 rounded-full ${accent(key).dot} transition-all hover:cursor-pointer ${draft.color === key ? 'ring-2 ring-offset-2 ring-offset-[#1E293B] ring-white scale-110' : 'opacity-70 hover:opacity-100'}`}
className={`w-7 h-7 rounded-full ${accent(key).dot} transition-all hover:cursor-pointer ${draft.color === key ? 'ring-2 ring-offset-2 ring-offset-card ring-fg scale-110' : 'opacity-70 hover:opacity-100'}`}
title={key}
/>
))}
@ -376,10 +376,10 @@ export default function LinkDashboard({ links, currentUser, onAddLink, onUpdateL
</div>
<div className="flex gap-2 pt-1">
<button type="button" onClick={closeForm} className="flex-1 py-2 bg-slate-900 border border-slate-800 text-slate-300 hover:text-white rounded font-semibold transition-colors hover:cursor-pointer">
<button type="button" onClick={closeForm} className="flex-1 py-2 bg-inner border border-line text-fg-muted hover:text-fg rounded font-semibold transition-colors hover:cursor-pointer">
Cancel
</button>
<button type="submit" className="flex-1 py-2 bg-emerald-600 hover:bg-emerald-500 text-slate-950 font-bold rounded flex items-center justify-center gap-1.5 transition-colors hover:cursor-pointer">
<button type="submit" className="flex-1 py-2 bg-emerald-600 hover:bg-emerald-500 text-white font-bold rounded flex items-center justify-center gap-1.5 transition-colors hover:cursor-pointer">
<Save className="w-3.5 h-3.5" />
{editingId ? 'Save Changes' : 'Add Link'}
</button>

View File

@ -66,14 +66,14 @@ export default function Logbook({ logs, devices, users, currentUser, onAddLog }:
const getLogTypeBadge = (type: string) => {
switch (type) {
case 'maintenance':
return 'bg-amber-950 border border-amber-900/60 text-amber-400';
return 'bg-warning-soft border border-warning-line text-warning';
case 'booking':
return 'bg-emerald-950 border border-emerald-900/60 text-emerald-400';
return 'bg-success-soft border border-success-line text-success';
case 'status':
return 'bg-cyan-950 border border-cyan-900/60 text-cyan-400';
return 'bg-info-soft border border-info-line text-info';
case 'system':
default:
return 'bg-slate-900 border border-slate-800 text-slate-350';
return 'bg-inner border border-line text-fg-muted';
}
};
@ -90,36 +90,36 @@ export default function Logbook({ logs, devices, users, currentUser, onAddLog }:
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 font-sans" id="logbook-dashboard">
{/* LEFT COLUMN: Chronological Log List */}
<div className="lg:col-span-8 bg-[#1E293B] border border-slate-800 rounded-xl p-5 flex flex-col h-full" id="logbook-ledger-card">
<div className="lg:col-span-8 bg-card border border-line rounded-xl p-5 flex flex-col h-full" id="logbook-ledger-card">
<div className="flex justify-between items-center mb-4">
<div>
<h2 className="text-base font-bold text-white flex items-center gap-2">
<History className="w-5 h-5 text-emerald-400" />
<h2 className="text-base font-bold text-fg flex items-center gap-2">
<History className="w-5 h-5 text-success" />
Audit Log & Maintenance Journal
</h2>
<p className="text-xs text-slate-400">Append-only history of who touched what. git blame, but for the lab.</p>
<p className="text-xs text-fg-muted">Append-only history of who touched what. git blame, but for the lab.</p>
</div>
<button
onClick={() => setShowAddLog(!showAddLog)}
className="px-3 py-1.5 bg-slate-900 border border-slate-800 text-slate-200 hover:text-emerald-400 hover:border-emerald-500 rounded-lg text-xs font-semibold flex items-center gap-1.5 transition-all font-sans hover:cursor-pointer"
className="px-3 py-1.5 bg-inner border border-line text-fg hover:text-success hover:border-success rounded-lg text-xs font-semibold flex items-center gap-1.5 transition-all font-sans hover:cursor-pointer"
id="btn-toggle-add-log"
>
<Plus className="w-4 h-4 text-emerald-400" />
<Plus className="w-4 h-4 text-success" />
File Maintenance Report
</button>
</div>
{/* Search and Filters toolbar */}
<div className="flex flex-col sm:flex-row gap-3 mb-4 p-3 bg-slate-900/60 border border-slate-850 rounded-lg">
<div className="flex flex-col sm:flex-row gap-3 mb-4 p-3 bg-inner border border-line rounded-lg">
<div className="relative flex-1">
<span className="absolute left-3 top-2.5 text-slate-550">
<span className="absolute left-3 top-2.5 text-fg-faint">
<Search className="w-4 h-4" />
</span>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full bg-slate-950 text-slate-101 border border-slate-800 rounded-lg pl-9 pr-4 py-1.5 text-xs focus:outline-none"
className="w-full bg-field text-fg border border-line-strong rounded-lg pl-9 pr-4 py-1.5 text-xs focus:outline-none"
/>
</div>
<div className="flex gap-1 shrink-0 text-xs font-medium flex-wrap">
@ -135,8 +135,8 @@ export default function Logbook({ logs, devices, users, currentUser, onAddLog }:
onClick={() => setTypeFilter(key)}
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
typeFilter === key
? 'bg-emerald-500/15 border border-emerald-500 text-emerald-400'
: 'bg-slate-950 text-slate-400 border border-slate-850 hover:bg-slate-850 hover:text-white'
? 'bg-success-soft border border-success text-success'
: 'bg-inner text-fg-muted border border-line hover:bg-card hover:text-fg'
}`}
>
{label}
@ -148,7 +148,7 @@ export default function Logbook({ logs, devices, users, currentUser, onAddLog }:
{/* Audit Log Sheet */}
<div className="flex-1 overflow-y-auto max-h-[550px] space-y-2 pr-1">
{filteredLogs.length === 0 ? (
<p className="text-center py-16 text-slate-500 text-xs">
<p className="text-center py-16 text-fg-faint text-xs">
No audit records match the selected filtering rules.
</p>
) : (
@ -161,29 +161,29 @@ export default function Logbook({ logs, devices, users, currentUser, onAddLog }:
});
return (
<div key={log.id} className="p-3 bg-slate-900/40 border border-slate-855 rounded-xl hover:border-slate-800 hover:bg-slate-900/70 transition-all flex items-start gap-3.5">
<div key={log.id} className="p-3 bg-inner border border-line rounded-xl hover:border-line-strong hover:bg-card transition-all flex items-start gap-3.5">
<div className="flex flex-col items-center gap-1.5 shrink-0 pt-0.5 min-w-[80px]">
<span className={`text-[9px] font-semibold uppercase tracking-wider px-2 py-0.5 rounded font-sans scale-90 text-center block w-full ${getLogTypeBadge(log.type)}`}>
{getLogTypeLabel(log.type)}
</span>
<span className="text-[9px] font-mono text-slate-500 leading-none">
<span className="text-[9px] font-mono text-fg-faint leading-none">
{new Date(log.timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
<div className="flex-1">
<p className="text-xs text-slate-200 leading-relaxed font-sans">{log.message}</p>
<div className="mt-1.5 flex flex-wrap items-center gap-3 text-[10px] font-mono text-slate-500 pt-1.5 border-t border-slate-900/40">
<p className="text-xs text-fg leading-relaxed font-sans">{log.message}</p>
<div className="mt-1.5 flex flex-wrap items-center gap-3 text-[10px] font-mono text-fg-faint pt-1.5 border-t border-line">
<span>Calendar Time: {timestampFormatted}</span>
{user && (
<span className="flex items-center gap-1 text-slate-400">
<UserIcon className="w-3 h-3 text-slate-500" />
<span className="flex items-center gap-1 text-fg-muted">
<UserIcon className="w-3 h-3 text-fg-faint" />
Operator: {user.name}
</span>
)}
{dev && (
<span className="flex items-center gap-1 text-emerald-450 font-semibold">
<Server className="w-3 h-3 text-slate-500" />
<span className="flex items-center gap-1 text-success font-semibold">
<Server className="w-3 h-3 text-fg-faint" />
Node: {dev.hostname}
</span>
)}
@ -199,15 +199,15 @@ export default function Logbook({ logs, devices, users, currentUser, onAddLog }:
{/* RIGHT COLUMN: Interactive Maintenance Addition Form */}
<div className="lg:col-span-4" id="logbook-forms-side">
{showAddLog ? (
<div className="bg-[#1E293B] border border-slate-800 rounded-xl p-5 shadow-sm space-y-4 animate-in fade-in slide-in-from-right-3 duration-200 font-sans">
<div className="flex items-center justify-between pb-2 border-b border-slate-800">
<h3 className="font-bold text-sm text-slate-100 flex items-center gap-1.5">
<Hammer className="w-4 h-4 text-amber-500" />
<div className="bg-card border border-line rounded-xl p-5 shadow-sm space-y-4 animate-in fade-in slide-in-from-right-3 duration-200 font-sans">
<div className="flex items-center justify-between pb-2 border-b border-line">
<h3 className="font-bold text-sm text-fg flex items-center gap-1.5">
<Hammer className="w-4 h-4 text-warning" />
Journal Maintenance Work
</h3>
<button
onClick={() => setShowAddLog(false)}
className="text-slate-400 hover:text-white"
className="text-fg-muted hover:text-fg"
>
Cancel
</button>
@ -215,11 +215,11 @@ export default function Logbook({ logs, devices, users, currentUser, onAddLog }:
<form onSubmit={handleSubmitLog} className="space-y-4 text-xs">
<div>
<label className="block text-slate-300 font-semibold mb-1">Target Network Host (Optional)</label>
<label className="block text-fg-muted font-semibold mb-1">Target Network Host (Optional)</label>
<select
value={targetDeviceId}
onChange={(e) => setTargetDeviceId(e.target.value)}
className="w-full bg-slate-950 text-slate-200 border border-slate-800 rounded p-2 focus:outline-none"
className="w-full bg-field text-fg border border-line-strong rounded p-2 focus:outline-none"
>
<option value="">-- Complete Lab Cluster / General Event --</option>
{devices.map((d) => (
@ -231,24 +231,24 @@ export default function Logbook({ logs, devices, users, currentUser, onAddLog }:
</div>
<div>
<label className="block text-slate-300 font-semibold mb-1">Documented Actions / Findings</label>
<label className="block text-fg-muted font-semibold mb-1">Documented Actions / Findings</label>
<textarea
required
rows={4}
value={logMessage}
onChange={(e) => setLogMessage(e.target.value)}
className="w-full bg-slate-950 text-slate-200 border border-slate-800 rounded p-2 focus:outline-none"
className="w-full bg-field text-fg border border-line-strong rounded p-2 focus:outline-none"
/>
</div>
<div className="bg-slate-900 border border-slate-800 p-2.5 rounded text-[11px] text-slate-450 leading-normal flex gap-2">
<Info className="w-4 h-4 text-emerald-400 shrink-0 mt-0.5" />
<div className="bg-inner border border-line p-2.5 rounded text-[11px] text-fg-muted leading-normal flex gap-2">
<Info className="w-4 h-4 text-success shrink-0 mt-0.5" />
<span>This writes straight to the shared ledger - everyone on the team sees it, so spell it like the next on-call has to read it at 3am.</span>
</div>
<button
type="submit"
className="w-full py-2 bg-emerald-600 hover:bg-emerald-500 text-slate-950 font-bold rounded text-xs flex items-center justify-center gap-1.5 transition-colors"
className="w-full py-2 bg-emerald-600 hover:bg-emerald-500 text-white font-bold rounded text-xs flex items-center justify-center gap-1.5 transition-colors"
>
<Save className="w-3.5 h-3.5" />
Publish to Shared Log Book
@ -256,15 +256,15 @@ export default function Logbook({ logs, devices, users, currentUser, onAddLog }:
</form>
</div>
) : (
<div className="bg-slate-900 border border-slate-800 rounded-xl p-5 shadow-sm text-xs text-slate-400 font-sans leading-relaxed">
<h3 className="font-bold text-white mb-2 text-sm flex items-center gap-2">
<ChevronRight className="w-4 h-4 text-emerald-400 shrink-0" />
<div className="bg-card border border-line rounded-xl p-5 shadow-sm text-xs text-fg-muted font-sans leading-relaxed">
<h3 className="font-bold text-fg mb-2 text-sm flex items-center gap-2">
<ChevronRight className="w-4 h-4 text-success shrink-0" />
Shared Audit & Fault Logging
</h3>
<p>
Labs drift. If you hit an STP block, a bridging loop or that one port that mysteriously err-disables itself, write down how you dug out. future-you (and the next on-call) will thank you.
</p>
<div className="mt-4 p-3 bg-amber-950/20 border border-amber-900/40 rounded-lg text-amber-300 font-mono text-[10px]">
<div className="mt-4 p-3 bg-warning-soft border border-warning-line rounded-lg text-warning font-mono text-[10px]">
Tip: hit "File Maintenance Report" up top to log a patch, a swap or a magic-smoke incident.
</div>
</div>

View File

@ -52,21 +52,21 @@ export default function LoginPage({ onLogin, onNavigateToRegister, authError }:
};
return (
<div className="min-h-screen bg-[#0B0F19] text-slate-100 font-sans flex items-center justify-center p-4">
<div className="min-h-screen bg-surface text-fg font-sans flex items-center justify-center p-4">
<div className="w-full max-w-md space-y-8">
{/* Logo & Brand */}
<div className="text-center space-y-3">
<div className="inline-flex p-3 bg-slate-950/80 border border-slate-800 rounded-2xl shadow-[0_0_40px_rgba(6,182,212,0.15)]">
<div className="inline-flex p-3 bg-card border border-line rounded-2xl shadow-lg">
<GhostGridLogo className="w-14 h-14" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight text-white">GhostGrid</h1>
<p className="text-[10px] font-mono text-cyan-400 tracking-widest uppercase mt-0.5">
<h1 className="text-2xl font-bold tracking-tight text-fg">GhostGrid</h1>
<p className="text-[10px] font-mono text-info tracking-widest uppercase mt-0.5">
BUILD AND CONTROL INVISIBLE INFRASTRUCTURE
</p>
<div className="flex items-center justify-center gap-1.5 mt-2">
<span className="text-[9px] text-slate-500 font-sans">A product by</span>
<span className="text-[9px] text-fg-faint font-sans">A product by</span>
<span className="airit-badge inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[8px] font-sans font-semibold">
<span className="w-1.5 h-1.5 rounded-sm bg-white/70 inline-block" />
AirITSystems
@ -76,22 +76,22 @@ export default function LoginPage({ onLogin, onNavigateToRegister, authError }:
</div>
{/* Login Card */}
<div className="bg-[#0F172A] border border-[#1E293B] rounded-2xl p-8 shadow-2xl space-y-6">
<div className="bg-card border border-line rounded-2xl p-8 shadow-2xl space-y-6">
<div>
<h2 className="text-lg font-semibold text-white">Sign in</h2>
<p className="text-xs text-slate-400 mt-1">Enter your credentials to access the platform.</p>
<h2 className="text-lg font-semibold text-fg">Sign in</h2>
<p className="text-xs text-fg-muted mt-1">Enter your credentials to access the platform.</p>
</div>
{error && (
<div className="flex items-center gap-2 bg-red-950/60 border border-red-800/60 rounded-lg px-3 py-2.5 text-xs text-red-300">
<AlertCircle className="w-4 h-4 shrink-0 text-red-400" />
<div className="flex items-center gap-2 bg-danger-soft border border-danger-line rounded-lg px-3 py-2.5 text-xs text-danger">
<AlertCircle className="w-4 h-4 shrink-0 text-danger" />
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-1.5">
<label className="block text-xs font-semibold text-slate-300" htmlFor="email">
<label className="block text-xs font-semibold text-fg-muted" htmlFor="email">
Email address
</label>
<input
@ -101,12 +101,12 @@ export default function LoginPage({ onLogin, onNavigateToRegister, authError }:
required
value={email}
onChange={e => setEmail(e.target.value)}
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2.5 text-sm text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50 transition-all"
className="w-full bg-field border border-line-strong rounded-lg px-3 py-2.5 text-sm text-fg placeholder-fg-faint focus:outline-none focus:ring-2 focus:ring-info/50 focus:border-info transition-all"
/>
</div>
<div className="space-y-1.5">
<label className="block text-xs font-semibold text-slate-300" htmlFor="password">
<label className="block text-xs font-semibold text-fg-muted" htmlFor="password">
Password
</label>
<div className="relative">
@ -117,12 +117,12 @@ export default function LoginPage({ onLogin, onNavigateToRegister, authError }:
required
value={password}
onChange={e => setPassword(e.target.value)}
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2.5 pr-10 text-sm text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50 transition-all"
className="w-full bg-field border border-line-strong rounded-lg px-3 py-2.5 pr-10 text-sm text-fg placeholder-fg-faint focus:outline-none focus:ring-2 focus:ring-info/50 focus:border-info transition-all"
/>
<button
type="button"
onClick={() => setShowPassword(v => !v)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-200 transition-colors"
className="absolute right-3 top-1/2 -translate-y-1/2 text-fg-muted hover:text-fg transition-colors"
tabIndex={-1}
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
@ -133,7 +133,7 @@ export default function LoginPage({ onLogin, onNavigateToRegister, authError }:
<button
type="submit"
disabled={loading}
className="w-full flex items-center justify-center gap-2 bg-cyan-600 hover:bg-cyan-500 disabled:bg-slate-700 disabled:text-slate-500 text-white font-semibold text-sm py-2.5 rounded-lg transition-all shadow-lg shadow-cyan-900/30"
className="w-full flex items-center justify-center gap-2 bg-cyan-600 hover:bg-cyan-500 disabled:bg-line-strong disabled:text-fg-faint text-white font-semibold text-sm py-2.5 rounded-lg transition-all shadow-lg"
>
{loading ? (
<span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
@ -147,14 +147,14 @@ export default function LoginPage({ onLogin, onNavigateToRegister, authError }:
{azureEnabled && (
<>
<div className="flex items-center gap-3">
<div className="flex-1 h-px bg-slate-800" />
<span className="text-[10px] font-mono text-slate-500 uppercase tracking-widest">or</span>
<div className="flex-1 h-px bg-slate-800" />
<div className="flex-1 h-px bg-line" />
<span className="text-[10px] font-mono text-fg-faint uppercase tracking-widest">or</span>
<div className="flex-1 h-px bg-line" />
</div>
<button
type="button"
onClick={() => { window.location.href = '/api/auth/azure'; }}
className="w-full flex items-center justify-center gap-3 bg-slate-800 hover:bg-slate-700 border border-slate-700 hover:border-slate-600 text-white font-semibold text-sm py-2.5 rounded-lg transition-all"
className="w-full flex items-center justify-center gap-3 bg-inner hover:bg-card border border-line hover:border-line-strong text-fg font-semibold text-sm py-2.5 rounded-lg transition-all"
>
{/* Microsoft M logo */}
<svg width="18" height="18" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
@ -168,11 +168,11 @@ export default function LoginPage({ onLogin, onNavigateToRegister, authError }:
</>
)}
<p className="text-center text-xs text-slate-400">
<p className="text-center text-xs text-fg-muted">
No account yet?{' '}
<button
onClick={onNavigateToRegister}
className="text-cyan-400 hover:text-cyan-300 font-semibold transition-colors"
className="text-info hover:opacity-80 font-semibold transition-colors"
>
Create one
</button>

View File

@ -56,21 +56,21 @@ export default function RegisterPage({ onLogin, onNavigateToLogin }: RegisterPag
};
return (
<div className="min-h-screen bg-[#0B0F19] text-slate-100 font-sans flex items-center justify-center p-4">
<div className="min-h-screen bg-surface text-fg font-sans flex items-center justify-center p-4">
<div className="w-full max-w-md space-y-8">
{/* Logo & Brand */}
<div className="text-center space-y-3">
<div className="inline-flex p-3 bg-slate-950/80 border border-slate-800 rounded-2xl shadow-[0_0_40px_rgba(6,182,212,0.15)]">
<div className="inline-flex p-3 bg-card border border-line rounded-2xl shadow-lg">
<GhostGridLogo className="w-14 h-14" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight text-white">GhostGrid</h1>
<p className="text-[10px] font-mono text-cyan-400 tracking-widest uppercase mt-0.5">
<h1 className="text-2xl font-bold tracking-tight text-fg">GhostGrid</h1>
<p className="text-[10px] font-mono text-info tracking-widest uppercase mt-0.5">
BUILD AND CONTROL INVISIBLE INFRASTRUCTURE
</p>
<div className="flex items-center justify-center gap-1.5 mt-2">
<span className="text-[9px] text-slate-500 font-sans">A product by</span>
<span className="text-[9px] text-fg-faint font-sans">A product by</span>
<span className="airit-badge inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[8px] font-sans font-semibold">
<span className="w-1.5 h-1.5 rounded-sm bg-white/70 inline-block" />
AirIT Systems
@ -80,22 +80,22 @@ export default function RegisterPage({ onLogin, onNavigateToLogin }: RegisterPag
</div>
{/* Register Card */}
<div className="bg-[#0F172A] border border-[#1E293B] rounded-2xl p-8 shadow-2xl space-y-6">
<div className="bg-card border border-line rounded-2xl p-8 shadow-2xl space-y-6">
<div>
<h2 className="text-lg font-semibold text-white">Create account</h2>
<p className="text-xs text-slate-400 mt-1">Register to gain access to the platform.</p>
<h2 className="text-lg font-semibold text-fg">Create account</h2>
<p className="text-xs text-fg-muted mt-1">Register to gain access to the platform.</p>
</div>
{error && (
<div className="flex items-center gap-2 bg-red-950/60 border border-red-800/60 rounded-lg px-3 py-2.5 text-xs text-red-300">
<AlertCircle className="w-4 h-4 shrink-0 text-red-400" />
<div className="flex items-center gap-2 bg-danger-soft border border-danger-line rounded-lg px-3 py-2.5 text-xs text-danger">
<AlertCircle className="w-4 h-4 shrink-0 text-danger" />
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-1.5">
<label className="block text-xs font-semibold text-slate-300" htmlFor="reg-name">
<label className="block text-xs font-semibold text-fg-muted" htmlFor="reg-name">
Full name
</label>
<input
@ -105,12 +105,12 @@ export default function RegisterPage({ onLogin, onNavigateToLogin }: RegisterPag
required
value={name}
onChange={e => setName(e.target.value)}
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2.5 text-sm text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50 transition-all"
className="w-full bg-field border border-line-strong rounded-lg px-3 py-2.5 text-sm text-fg placeholder-fg-faint focus:outline-none focus:ring-2 focus:ring-info/50 focus:border-info transition-all"
/>
</div>
<div className="space-y-1.5">
<label className="block text-xs font-semibold text-slate-300" htmlFor="reg-email">
<label className="block text-xs font-semibold text-fg-muted" htmlFor="reg-email">
Email address
</label>
<input
@ -120,12 +120,12 @@ export default function RegisterPage({ onLogin, onNavigateToLogin }: RegisterPag
required
value={email}
onChange={e => setEmail(e.target.value)}
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2.5 text-sm text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50 transition-all"
className="w-full bg-field border border-line-strong rounded-lg px-3 py-2.5 text-sm text-fg placeholder-fg-faint focus:outline-none focus:ring-2 focus:ring-info/50 focus:border-info transition-all"
/>
</div>
<div className="space-y-1.5">
<label className="block text-xs font-semibold text-slate-300" htmlFor="reg-password">
<label className="block text-xs font-semibold text-fg-muted" htmlFor="reg-password">
Password
</label>
<div className="relative">
@ -136,19 +136,19 @@ export default function RegisterPage({ onLogin, onNavigateToLogin }: RegisterPag
required
value={password}
onChange={e => setPassword(e.target.value)}
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2.5 pr-10 text-sm text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50 transition-all"
className="w-full bg-field border border-line-strong rounded-lg px-3 py-2.5 pr-10 text-sm text-fg placeholder-fg-faint focus:outline-none focus:ring-2 focus:ring-info/50 focus:border-info transition-all"
/>
<button
type="button"
onClick={() => setShowPassword(v => !v)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-200 transition-colors"
className="absolute right-3 top-1/2 -translate-y-1/2 text-fg-muted hover:text-fg transition-colors"
tabIndex={-1}
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
{password.length > 0 && (
<div className={`flex items-center gap-1.5 text-[11px] ${passwordStrong ? 'text-emerald-400' : 'text-amber-400'}`}>
<div className={`flex items-center gap-1.5 text-[11px] ${passwordStrong ? 'text-success' : 'text-warning'}`}>
<CheckCircle2 className="w-3 h-3" />
{passwordStrong ? 'Password meets requirements' : 'At least 8 characters required'}
</div>
@ -156,7 +156,7 @@ export default function RegisterPage({ onLogin, onNavigateToLogin }: RegisterPag
</div>
<div className="space-y-1.5">
<label className="block text-xs font-semibold text-slate-300" htmlFor="reg-confirm">
<label className="block text-xs font-semibold text-fg-muted" htmlFor="reg-confirm">
Confirm password
</label>
<input
@ -166,10 +166,10 @@ export default function RegisterPage({ onLogin, onNavigateToLogin }: RegisterPag
required
value={confirmPassword}
onChange={e => setConfirmPassword(e.target.value)}
className={`w-full bg-slate-900 border rounded-lg px-3 py-2.5 text-sm text-white placeholder-slate-500 focus:outline-none focus:ring-2 transition-all ${
className={`w-full bg-field border rounded-lg px-3 py-2.5 text-sm text-fg placeholder-fg-faint focus:outline-none focus:ring-2 transition-all ${
confirmPassword.length > 0 && confirmPassword !== password
? 'border-red-700 focus:ring-red-500/50'
: 'border-slate-700 focus:ring-cyan-500/50 focus:border-cyan-500/50'
? 'border-danger focus:ring-danger/50'
: 'border-line-strong focus:ring-info/50 focus:border-info'
}`}
/>
</div>
@ -177,7 +177,7 @@ export default function RegisterPage({ onLogin, onNavigateToLogin }: RegisterPag
<button
type="submit"
disabled={loading}
className="w-full flex items-center justify-center gap-2 bg-cyan-600 hover:bg-cyan-500 disabled:bg-slate-700 disabled:text-slate-500 text-white font-semibold text-sm py-2.5 rounded-lg transition-all shadow-lg shadow-cyan-900/30"
className="w-full flex items-center justify-center gap-2 bg-cyan-600 hover:bg-cyan-500 disabled:bg-line-strong disabled:text-fg-faint text-white font-semibold text-sm py-2.5 rounded-lg transition-all shadow-lg"
>
{loading ? (
<span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
@ -188,11 +188,11 @@ export default function RegisterPage({ onLogin, onNavigateToLogin }: RegisterPag
</button>
</form>
<p className="text-center text-xs text-slate-400">
<p className="text-center text-xs text-fg-muted">
Already have an account?{' '}
<button
onClick={onNavigateToLogin}
className="text-cyan-400 hover:text-cyan-300 font-semibold transition-colors"
className="text-info hover:opacity-80 font-semibold transition-colors"
>
Sign in
</button>

View File

@ -50,17 +50,17 @@ interface SettingsProps {
}
function Label({ children }: { children: React.ReactNode }) {
return <label className="block text-[11px] font-semibold text-slate-400 uppercase tracking-wide mb-1.5">{children}</label>;
return <label className="block text-[11px] font-semibold text-fg-muted uppercase tracking-wide mb-1.5">{children}</label>;
}
function Hint({ children }: { children: React.ReactNode }) {
return <p className="mt-1 text-[10px] text-slate-500 font-mono leading-relaxed">{children}</p>;
return <p className="mt-1 text-[10px] text-fg-faint font-mono leading-relaxed">{children}</p>;
}
function ConfiguredBadge() {
return (
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-semibold bg-emerald-950/60 border border-emerald-900/50 text-emerald-400">
<span className="w-1 h-1 rounded-full bg-emerald-400 inline-block" />
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-semibold bg-success-soft border border-success-line text-success">
<span className="w-1 h-1 rounded-full bg-success inline-block" />
CONFIGURED
</span>
);
@ -94,7 +94,7 @@ function Input({ value, onChange, placeholder, monospace, icon }: {
return (
<div className="relative">
{icon && (
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500 pointer-events-none">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-fg-faint pointer-events-none">
{icon}
</span>
)}
@ -103,7 +103,7 @@ function Input({ value, onChange, placeholder, monospace, icon }: {
value={value}
onChange={e => onChange(e.target.value)}
placeholder={placeholder}
className={`w-full bg-slate-900 border border-slate-700 rounded-lg py-2.5 text-sm text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50 transition-all ${icon ? 'pl-9 pr-3' : 'px-3'} ${monospace ? 'font-mono text-xs' : ''}`}
className={`w-full bg-field border border-line-strong rounded-lg py-2.5 text-sm text-fg placeholder-fg-faint focus:outline-none focus:ring-2 focus:ring-info/50 focus:border-info transition-all ${icon ? 'pl-9 pr-3' : 'px-3'} ${monospace ? 'font-mono text-xs' : ''}`}
/>
</div>
);
@ -117,19 +117,19 @@ function SecretInput({ value, onChange, show, onToggleShow }: {
}) {
return (
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500 pointer-events-none">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-fg-faint pointer-events-none">
<KeyRound className="w-3.5 h-3.5" />
</span>
<input
type={show ? 'text' : 'password'}
value={value}
onChange={e => onChange(e.target.value)}
className="w-full bg-slate-900 border border-slate-700 rounded-lg pl-9 pr-10 py-2.5 text-xs font-mono text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50 transition-all"
className="w-full bg-field border border-line-strong rounded-lg pl-9 pr-10 py-2.5 text-xs font-mono text-fg placeholder-fg-faint focus:outline-none focus:ring-2 focus:ring-info/50 focus:border-info transition-all"
/>
<button
type="button"
onClick={onToggleShow}
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-500 hover:text-slate-300 transition-colors"
className="absolute right-3 top-1/2 -translate-y-1/2 text-fg-faint hover:text-fg-muted transition-colors"
tabIndex={-1}
>
{show ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
@ -143,7 +143,7 @@ function SectionCard({ accentColor, children }: {
children: React.ReactNode;
}) {
return (
<div className="bg-[#0F172A] border border-[#1E293B] rounded-2xl overflow-hidden">
<div className="bg-card border border-line rounded-2xl overflow-hidden">
<div className={`h-0.5 w-full ${accentColor}`} />
<div className="p-6 space-y-5">
{children}
@ -520,7 +520,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<span className="w-5 h-5 border-2 border-slate-700 border-t-cyan-400 rounded-full animate-spin" />
<span className="w-5 h-5 border-2 border-line-strong border-t-info rounded-full animate-spin" />
</div>
);
}
@ -531,19 +531,19 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
{/* Page header */}
<div className="flex items-start justify-between gap-4">
<div>
<div className="flex items-center gap-1.5 text-[10px] font-mono text-slate-600 mb-3">
<div className="flex items-center gap-1.5 text-[10px] font-mono text-fg-faint mb-3">
<Settings2 className="w-3 h-3" />
<span>SYSTEM</span>
<ChevronRight className="w-3 h-3" />
<span className="text-slate-400">SETTINGS</span>
<span className="text-fg-muted">SETTINGS</span>
</div>
<h1 className="text-xl font-bold text-white tracking-tight">Settings</h1>
<p className="text-xs text-slate-500 mt-0.5">Configure integrations and authentication providers.</p>
<h1 className="text-xl font-bold text-fg tracking-tight">Settings</h1>
<p className="text-xs text-fg-faint mt-0.5">Configure integrations and authentication providers.</p>
</div>
<button
onClick={handleSave}
disabled={saving}
className="shrink-0 flex items-center gap-2 bg-cyan-600 hover:bg-cyan-500 active:bg-cyan-700 disabled:bg-slate-800 disabled:text-slate-600 disabled:border disabled:border-slate-700 text-white font-semibold text-sm px-5 py-2.5 rounded-xl transition-all shadow-lg shadow-cyan-950/50"
className="shrink-0 flex items-center gap-2 bg-cyan-600 hover:bg-cyan-500 active:bg-cyan-700 disabled:bg-line-strong disabled:text-fg-faint disabled:border disabled:border-line text-white font-semibold text-sm px-5 py-2.5 rounded-xl transition-all shadow-lg"
>
{saving ? (
<span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
@ -556,20 +556,20 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
{/* Feedback banners */}
{error && (
<div className="flex items-start gap-2.5 bg-red-950/40 border border-red-900/50 rounded-xl px-4 py-3 text-xs text-red-300">
<AlertCircle className="w-4 h-4 shrink-0 text-red-400 mt-0.5" />
<div className="flex items-start gap-2.5 bg-danger-soft border border-danger-line rounded-xl px-4 py-3 text-xs text-danger">
<AlertCircle className="w-4 h-4 shrink-0 text-danger mt-0.5" />
<span>{error}</span>
</div>
)}
{successMsg && (
<div className="flex items-center gap-2.5 bg-emerald-950/40 border border-emerald-900/50 rounded-xl px-4 py-3 text-xs text-emerald-300">
<CheckCircle className="w-4 h-4 shrink-0 text-emerald-400" />
<div className="flex items-center gap-2.5 bg-success-soft border border-success-line rounded-xl px-4 py-3 text-xs text-success">
<CheckCircle className="w-4 h-4 shrink-0 text-success" />
{successMsg}
</div>
)}
{/* Section tabs - switch between Integrations and System to keep the page light */}
<div className="inline-flex items-center gap-0.5 p-0.5 bg-slate-900/50 border border-slate-800 rounded-lg w-fit">
<div className="inline-flex items-center gap-0.5 p-0.5 bg-inner border border-line rounded-lg w-fit">
{([
{ id: 'integrations', label: 'Integrations', icon: <Plug className="w-3.5 h-3.5" /> },
{ id: 'system', label: 'System', icon: <Server className="w-3.5 h-3.5" /> },
@ -580,8 +580,8 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
onClick={() => setActiveSection(tab.id)}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
activeSection === tab.id
? 'bg-cyan-950/40 border border-cyan-900/50 text-cyan-400'
: 'border border-transparent text-slate-400 hover:text-slate-200 hover:bg-slate-800'
? 'bg-info-soft border border-info-line text-info'
: 'border border-transparent text-fg-muted hover:text-fg hover:bg-inner'
}`}
>
{tab.icon}
@ -598,37 +598,37 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
<SectionCard accentColor="bg-gradient-to-r from-blue-600 to-indigo-600">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-950/60 border border-blue-900/40 rounded-xl">
<Shield className="w-4 h-4 text-blue-400" />
<div className="p-2 bg-blue-soft border border-blue-line rounded-xl">
<Shield className="w-4 h-4 text-blue" />
</div>
<div>
<div className="flex items-center gap-2">
<h2 className="text-sm font-semibold text-white">Microsoft Entra ID</h2>
<h2 className="text-sm font-semibold text-fg">Microsoft Entra ID</h2>
{azureEnabled && azureSecretSet && (
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-semibold bg-blue-950/60 border border-blue-900/50 text-blue-400">
<span className="w-1 h-1 rounded-full bg-blue-400 animate-pulse inline-block" />
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-semibold bg-blue-soft border border-blue-line text-blue">
<span className="w-1 h-1 rounded-full bg-blue animate-pulse inline-block" />
ACTIVE
</span>
)}
</div>
<p className="text-[11px] text-slate-500 mt-0.5">OAuth 2.0 SSO for organizational accounts</p>
<p className="text-[11px] text-fg-faint mt-0.5">OAuth 2.0 SSO for organizational accounts</p>
</div>
</div>
<div className="flex items-center gap-2.5">
<span className={`text-[10px] font-semibold font-mono ${azureEnabled ? 'text-blue-400' : 'text-slate-600'}`}>
<span className={`text-[10px] font-semibold font-mono ${azureEnabled ? 'text-blue' : 'text-fg-faint'}`}>
{azureEnabled ? 'ENABLED' : 'DISABLED'}
</span>
<button
type="button"
onClick={() => setAzureEnabled(v => !v)}
className={`relative w-10 h-5 rounded-full transition-all duration-200 focus:outline-none ${azureEnabled ? 'bg-blue-600 shadow-[0_0_10px_rgba(37,99,235,0.4)]' : 'bg-slate-800 border border-slate-700'}`}
className={`relative w-10 h-5 rounded-full transition-all duration-200 focus:outline-none ${azureEnabled ? 'bg-blue-600' : 'bg-line-strong border border-line'}`}
>
<span className={`absolute top-0.5 w-4 h-4 bg-white rounded-full shadow transition-all duration-200 ${azureEnabled ? 'left-5' : 'left-0.5'}`} />
</button>
</div>
</div>
<div className="h-px bg-slate-800/60" />
<div className="h-px bg-line" />
<div className={`grid grid-cols-1 sm:grid-cols-2 gap-5 transition-opacity duration-200 ${!azureEnabled ? 'opacity-40 pointer-events-none' : ''}`}>
<FieldRow label="Tenant ID" hint="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx">
@ -675,19 +675,19 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
{/* Redirect URI read-only */}
{effectiveRedirectUri && azureEnabled && (
<div className="flex items-start gap-3 bg-slate-900/60 border border-slate-700/60 rounded-xl px-4 py-3">
<div className="flex items-start gap-3 bg-inner border border-line rounded-xl px-4 py-3">
<div className="flex-1 min-w-0">
<p className="text-[10px] font-semibold text-slate-400 uppercase tracking-wide mb-1">Required Redirect URI</p>
<p className="text-[11px] font-mono text-slate-200 break-all">{effectiveRedirectUri}</p>
<p className="text-[10px] text-slate-500 mt-1">Register this in Azure Portal &gt; App registrations &gt; Authentication &gt; Redirect URIs</p>
<p className="text-[10px] font-semibold text-fg-muted uppercase tracking-wide mb-1">Required Redirect URI</p>
<p className="text-[11px] font-mono text-fg break-all">{effectiveRedirectUri}</p>
<p className="text-[10px] text-fg-faint mt-1">Register this in Azure Portal &gt; App registrations &gt; Authentication &gt; Redirect URIs</p>
</div>
<button
type="button"
onClick={copyRedirectUri}
className="shrink-0 p-1.5 rounded-lg text-slate-500 hover:text-slate-200 hover:bg-slate-800 transition-all"
className="shrink-0 p-1.5 rounded-lg text-fg-faint hover:text-fg hover:bg-inner transition-all"
title="Copy to clipboard"
>
{copied ? <CheckCircle className="w-4 h-4 text-emerald-400" /> : <Copy className="w-4 h-4" />}
{copied ? <CheckCircle className="w-4 h-4 text-success" /> : <Copy className="w-4 h-4" />}
</button>
</div>
)}
@ -700,37 +700,37 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
<SectionCard accentColor="bg-gradient-to-r from-emerald-600 to-teal-600">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-emerald-950/60 border border-emerald-900/40 rounded-xl">
<Activity className="w-4 h-4 text-emerald-400" />
<div className="p-2 bg-success-soft border border-success-line rounded-xl">
<Activity className="w-4 h-4 text-success" />
</div>
<div>
<div className="flex items-center gap-2">
<h2 className="text-sm font-semibold text-white">CheckMK</h2>
<h2 className="text-sm font-semibold text-fg">CheckMK</h2>
{cmkEnabled && cmkApiUrl && cmkSecretSet && (
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-semibold bg-emerald-950/60 border border-emerald-900/50 text-emerald-400">
<span className="w-1 h-1 rounded-full bg-emerald-400 animate-pulse inline-block" />
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-semibold bg-success-soft border border-success-line text-success">
<span className="w-1 h-1 rounded-full bg-success animate-pulse inline-block" />
ACTIVE
</span>
)}
</div>
<p className="text-[11px] text-slate-500 mt-0.5">Device status sync via CheckMK REST API</p>
<p className="text-[11px] text-fg-faint mt-0.5">Device status sync via CheckMK REST API</p>
</div>
</div>
<div className="flex items-center gap-2.5">
<span className={`text-[10px] font-semibold font-mono ${cmkEnabled ? 'text-emerald-400' : 'text-slate-600'}`}>
<span className={`text-[10px] font-semibold font-mono ${cmkEnabled ? 'text-success' : 'text-fg-faint'}`}>
{cmkEnabled ? 'ENABLED' : 'DISABLED'}
</span>
<button
type="button"
onClick={() => setCmkEnabled(v => !v)}
className={`relative w-10 h-5 rounded-full transition-all duration-200 focus:outline-none ${cmkEnabled ? 'bg-emerald-600 shadow-[0_0_10px_rgba(5,150,105,0.4)]' : 'bg-slate-800 border border-slate-700'}`}
className={`relative w-10 h-5 rounded-full transition-all duration-200 focus:outline-none ${cmkEnabled ? 'bg-emerald-600' : 'bg-line-strong border border-line'}`}
>
<span className={`absolute top-0.5 w-4 h-4 bg-white rounded-full shadow transition-all duration-200 ${cmkEnabled ? 'left-5' : 'left-0.5'}`} />
</button>
</div>
</div>
<div className="h-px bg-slate-800/60" />
<div className="h-px bg-line" />
<div className={`space-y-5 transition-opacity duration-200 ${!cmkEnabled ? 'opacity-40 pointer-events-none' : ''}`}>
<FieldRow label="API URL">
@ -776,7 +776,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
type="button"
onClick={runSync}
disabled={syncing}
className="flex items-center gap-2 bg-emerald-950/60 hover:bg-emerald-900/40 border border-emerald-900/50 text-emerald-400 text-xs font-semibold px-3 py-2 rounded-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed"
className="flex items-center gap-2 bg-success-soft hover:opacity-80 border border-success-line text-success text-xs font-semibold px-3 py-2 rounded-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
<RefreshCw className={`w-3.5 h-3.5 ${syncing ? 'animate-spin' : ''}`} />
{syncing ? 'Syncing…' : 'Run sync now'}
@ -789,37 +789,37 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
<SectionCard accentColor="bg-gradient-to-r from-orange-600 to-amber-600">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-orange-950/60 border border-orange-900/40 rounded-xl">
<Terminal className="w-4 h-4 text-orange-400" />
<div className="p-2 bg-orange-soft border border-orange-line rounded-xl">
<Terminal className="w-4 h-4 text-orange" />
</div>
<div>
<div className="flex items-center gap-2">
<h2 className="text-sm font-semibold text-white">Ansible Semaphore</h2>
<h2 className="text-sm font-semibold text-fg">Ansible Semaphore</h2>
{semaphoreEnabled && semaphoreApiUrl && semaphoreTokenSet && semaphoreProjectId && (
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-semibold bg-orange-950/60 border border-orange-900/50 text-orange-400">
<span className="w-1 h-1 rounded-full bg-orange-400 animate-pulse inline-block" />
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-semibold bg-orange-soft border border-orange-line text-orange">
<span className="w-1 h-1 rounded-full bg-orange animate-pulse inline-block" />
ACTIVE
</span>
)}
</div>
<p className="text-[11px] text-slate-500 mt-0.5">Trigger playbooks automatically at booking start and end</p>
<p className="text-[11px] text-fg-faint mt-0.5">Trigger playbooks automatically at booking start and end</p>
</div>
</div>
<div className="flex items-center gap-2.5">
<span className={`text-[10px] font-semibold font-mono ${semaphoreEnabled ? 'text-orange-400' : 'text-slate-600'}`}>
<span className={`text-[10px] font-semibold font-mono ${semaphoreEnabled ? 'text-orange' : 'text-fg-faint'}`}>
{semaphoreEnabled ? 'ENABLED' : 'DISABLED'}
</span>
<button
type="button"
onClick={() => setSemaphoreEnabled(v => !v)}
className={`relative w-10 h-5 rounded-full transition-all duration-200 focus:outline-none ${semaphoreEnabled ? 'bg-orange-600 shadow-[0_0_10px_rgba(234,88,12,0.4)]' : 'bg-slate-800 border border-slate-700'}`}
className={`relative w-10 h-5 rounded-full transition-all duration-200 focus:outline-none ${semaphoreEnabled ? 'bg-orange-600' : 'bg-line-strong border border-line'}`}
>
<span className={`absolute top-0.5 w-4 h-4 bg-white rounded-full shadow transition-all duration-200 ${semaphoreEnabled ? 'left-5' : 'left-0.5'}`} />
</button>
</div>
</div>
<div className="h-px bg-slate-800/60" />
<div className="h-px bg-line" />
<div className={`space-y-5 transition-opacity duration-200 ${!semaphoreEnabled ? 'opacity-40 pointer-events-none' : ''}`}>
<FieldRow label="API URL">
@ -858,13 +858,13 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
type="button"
onClick={testSemaphoreConnection}
disabled={semaphoreTesting}
className="flex items-center gap-2 bg-orange-950/60 hover:bg-orange-900/40 border border-orange-900/50 text-orange-400 text-xs font-semibold px-3 py-2 rounded-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed"
className="flex items-center gap-2 bg-orange-soft hover:opacity-80 border border-orange-line text-orange text-xs font-semibold px-3 py-2 rounded-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
<RefreshCw className={`w-3.5 h-3.5 ${semaphoreTesting ? 'animate-spin' : ''}`} />
{semaphoreTesting ? 'Testing…' : 'Test connection'}
</button>
{semaphoreTestResult && (
<p className={`text-[11px] font-mono ${semaphoreTestResult.startsWith('Error') ? 'text-red-400' : 'text-emerald-400'}`}>
<p className={`text-[11px] font-mono ${semaphoreTestResult.startsWith('Error') ? 'text-danger' : 'text-success'}`}>
{semaphoreTestResult}
</p>
)}
@ -886,46 +886,46 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
<SectionCard accentColor="bg-gradient-to-r from-sky-600 to-cyan-600">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-sky-950/60 border border-sky-900/40 rounded-xl">
<Network className="w-4 h-4 text-sky-400" />
<div className="p-2 bg-sky-soft border border-sky-line rounded-xl">
<Network className="w-4 h-4 text-sky" />
</div>
<div>
<div className="flex items-center gap-2">
<h2 className="text-sm font-semibold text-white">Caddy Reverse Proxy</h2>
<h2 className="text-sm font-semibold text-fg">Caddy Reverse Proxy</h2>
{caddyEnabled && caddyStatus === 'available' && (
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-semibold bg-sky-950/60 border border-sky-900/50 text-sky-400">
<span className="w-1 h-1 rounded-full bg-sky-400 animate-pulse inline-block" />
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-semibold bg-sky-soft border border-sky-line text-sky">
<span className="w-1 h-1 rounded-full bg-sky animate-pulse inline-block" />
ACTIVE
</span>
)}
</div>
<p className="text-[11px] text-slate-500 mt-0.5">Manage reverse proxy routes for internal services</p>
<p className="text-[11px] text-fg-faint mt-0.5">Manage reverse proxy routes for internal services</p>
</div>
</div>
<div className="flex items-center gap-2.5">
{caddyManaged ? (
<>
<span className={`text-[10px] font-semibold font-mono ${caddyEnabled ? 'text-sky-400' : 'text-slate-600'}`}>
<span className={`text-[10px] font-semibold font-mono ${caddyEnabled ? 'text-sky' : 'text-fg-faint'}`}>
{caddyEnabled ? 'ENABLED' : 'DISABLED'}
</span>
<button
type="button"
onClick={() => setCaddyEnabled((v: boolean) => !v)}
className={`relative w-10 h-5 rounded-full transition-all duration-200 focus:outline-none ${caddyEnabled ? 'bg-sky-600 shadow-[0_0_10px_rgba(2,132,199,0.4)]' : 'bg-slate-800 border border-slate-700'}`}
className={`relative w-10 h-5 rounded-full transition-all duration-200 focus:outline-none ${caddyEnabled ? 'bg-sky-600' : 'bg-line-strong border border-line'}`}
>
<span className={`absolute top-0.5 w-4 h-4 bg-white rounded-full shadow transition-all duration-200 ${caddyEnabled ? 'left-5' : 'left-0.5'}`} />
</button>
</>
) : (
<span className="text-[10px] font-semibold font-mono text-slate-500">MANAGED BY PRODUCTION</span>
<span className="text-[10px] font-semibold font-mono text-fg-faint">MANAGED BY PRODUCTION</span>
)}
</div>
</div>
<div className="h-px bg-slate-800/60" />
<div className="h-px bg-line" />
{!caddyManaged && (
<p className="text-[11px] font-mono text-slate-500 leading-relaxed">
<p className="text-[11px] font-mono text-fg-faint leading-relaxed">
Caddy is centrally managed by the production instance (ghostgrid). Manage proxy routes there.
</p>
)}
@ -943,20 +943,20 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
<Hint>Prefix the upstream with https:// for TLS backends - the certificate is not verified.</Hint>
{caddyStatus === 'unavailable' && (
<p className="text-[11px] font-mono text-amber-400 mb-2">
<p className="text-[11px] font-mono text-warning mb-2">
Caddy Admin API not reachable - routes will be applied when Caddy starts.
</p>
)}
{caddyRoutes.length === 0 && (
<p className="text-[11px] font-mono text-slate-500 mb-2">
<p className="text-[11px] font-mono text-fg-faint mb-2">
No proxy routes configured yet.
</p>
)}
{/* Custom routes */}
{caddyRoutes.map((r: CaddyRoute) => (
<div key={r.id} className="bg-slate-900/60 border border-slate-700/60 rounded-xl px-4 py-2.5">
<div key={r.id} className="bg-inner border border-line rounded-xl px-4 py-2.5">
{editingRouteId === r.id ? (
<div className="space-y-2">
<div className="flex items-end gap-2">
@ -969,25 +969,25 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
<Input value={editUpstream} onChange={setEditUpstream} monospace />
</div>
<div className="flex flex-col items-center gap-1 pb-0.5">
<span className="text-[9px] font-semibold text-slate-400 uppercase tracking-wide">TLS</span>
<span className="text-[9px] font-semibold text-fg-muted uppercase tracking-wide">TLS</span>
<button type="button" onClick={() => setEditTls(v => !v)}
className={`relative w-8 h-4 rounded-full transition-all duration-200 focus:outline-none ${editTls ? 'bg-sky-600' : 'bg-slate-800 border border-slate-700'}`}>
className={`relative w-8 h-4 rounded-full transition-all duration-200 focus:outline-none ${editTls ? 'bg-sky-600' : 'bg-line-strong border border-line'}`}>
<span className={`absolute top-0.5 w-3 h-3 bg-white rounded-full shadow transition-all duration-200 ${editTls ? 'left-4' : 'left-0.5'}`} />
</button>
</div>
<div className="flex flex-col items-center gap-1 pb-0.5">
<span className="text-[9px] font-semibold text-slate-400 uppercase tracking-wide">GZ</span>
<span className="text-[9px] font-semibold text-fg-muted uppercase tracking-wide">GZ</span>
<button type="button" onClick={() => setEditCompress(v => !v)}
className={`relative w-8 h-4 rounded-full transition-all duration-200 focus:outline-none ${editCompress ? 'bg-sky-600' : 'bg-slate-800 border border-slate-700'}`}>
className={`relative w-8 h-4 rounded-full transition-all duration-200 focus:outline-none ${editCompress ? 'bg-sky-600' : 'bg-line-strong border border-line'}`}>
<span className={`absolute top-0.5 w-3 h-3 bg-white rounded-full shadow transition-all duration-200 ${editCompress ? 'left-4' : 'left-0.5'}`} />
</button>
</div>
<button type="button" onClick={() => handleEditSave(r.id)} disabled={savingRoute || !editHostname.trim() || !editUpstream.trim()}
className="flex items-center gap-1 bg-sky-950/60 hover:bg-sky-900/40 border border-sky-900/50 text-sky-400 text-xs font-semibold px-2.5 py-2 rounded-lg transition-all disabled:opacity-50 shrink-0">
className="flex items-center gap-1 bg-sky-soft hover:opacity-80 border border-sky-line text-sky text-xs font-semibold px-2.5 py-2 rounded-lg transition-all disabled:opacity-50 shrink-0">
<CheckCircle className="w-3.5 h-3.5" />
</button>
<button type="button" onClick={handleEditCancel}
className="p-2 rounded-lg text-slate-500 hover:text-slate-300 hover:bg-slate-800 transition-all shrink-0">
className="p-2 rounded-lg text-fg-faint hover:text-fg-muted hover:bg-inner transition-all shrink-0">
<X className="w-3.5 h-3.5" />
</button>
</div>
@ -996,20 +996,20 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
) : (
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 min-w-0">
<span className="text-[11px] font-mono text-white truncate">{r.hostname}</span>
<span className="text-slate-600 text-[11px]">&gt;</span>
<span className="text-[11px] font-mono text-slate-400 truncate">{r.upstream}</span>
{r.tls ? <span className="text-[9px] font-semibold text-sky-500 font-mono">TLS</span> : null}
{r.compress ? <span className="text-[9px] font-semibold text-slate-500 font-mono">GZ</span> : null}
{r.redirect ? <span className="text-[9px] font-mono text-slate-500 truncate">&#8627; {r.redirect}</span> : null}
<span className="text-[11px] font-mono text-fg truncate">{r.hostname}</span>
<span className="text-fg-faint text-[11px]">&gt;</span>
<span className="text-[11px] font-mono text-fg-muted truncate">{r.upstream}</span>
{r.tls ? <span className="text-[9px] font-semibold text-sky font-mono">TLS</span> : null}
{r.compress ? <span className="text-[9px] font-semibold text-fg-faint font-mono">GZ</span> : null}
{r.redirect ? <span className="text-[9px] font-mono text-fg-faint truncate">&#8627; {r.redirect}</span> : null}
</div>
<div className="flex items-center gap-1 ml-3 shrink-0">
<button type="button" onClick={() => handleEditStart(r)}
className="p-1.5 rounded-lg text-slate-600 hover:text-sky-400 hover:bg-sky-950/30 transition-all" title="Edit route">
className="p-1.5 rounded-lg text-fg-faint hover:text-sky hover:bg-sky-soft transition-all" title="Edit route">
<Pencil className="w-3.5 h-3.5" />
</button>
<button type="button" onClick={() => handleDeleteRoute(r.id)}
className="p-1.5 rounded-lg text-slate-600 hover:text-red-400 hover:bg-red-950/30 transition-all" title="Remove route">
className="p-1.5 rounded-lg text-fg-faint hover:text-danger hover:bg-danger-soft transition-all" title="Remove route">
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
@ -1030,21 +1030,21 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
<Input value={newUpstream} onChange={setNewUpstream} monospace />
</div>
<div className="flex flex-col items-center gap-1 pb-0.5">
<span className="text-[9px] font-semibold text-slate-400 uppercase tracking-wide">TLS</span>
<span className="text-[9px] font-semibold text-fg-muted uppercase tracking-wide">TLS</span>
<button
type="button"
onClick={() => setNewTls(v => !v)}
className={`relative w-8 h-4 rounded-full transition-all duration-200 focus:outline-none ${newTls ? 'bg-sky-600' : 'bg-slate-800 border border-slate-700'}`}
className={`relative w-8 h-4 rounded-full transition-all duration-200 focus:outline-none ${newTls ? 'bg-sky-600' : 'bg-line-strong border border-line'}`}
>
<span className={`absolute top-0.5 w-3 h-3 bg-white rounded-full shadow transition-all duration-200 ${newTls ? 'left-4' : 'left-0.5'}`} />
</button>
</div>
<div className="flex flex-col items-center gap-1 pb-0.5">
<span className="text-[9px] font-semibold text-slate-400 uppercase tracking-wide">GZ</span>
<span className="text-[9px] font-semibold text-fg-muted uppercase tracking-wide">GZ</span>
<button
type="button"
onClick={() => setNewCompress(v => !v)}
className={`relative w-8 h-4 rounded-full transition-all duration-200 focus:outline-none ${newCompress ? 'bg-sky-600' : 'bg-slate-800 border border-slate-700'}`}
className={`relative w-8 h-4 rounded-full transition-all duration-200 focus:outline-none ${newCompress ? 'bg-sky-600' : 'bg-line-strong border border-line'}`}
>
<span className={`absolute top-0.5 w-3 h-3 bg-white rounded-full shadow transition-all duration-200 ${newCompress ? 'left-4' : 'left-0.5'}`} />
</button>
@ -1053,7 +1053,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
type="button"
onClick={handleAddRoute}
disabled={addingRoute || !newHostname.trim() || !newUpstream.trim()}
className="flex items-center gap-1.5 bg-sky-950/60 hover:bg-sky-900/40 border border-sky-900/50 text-sky-400 text-xs font-semibold px-3 py-2 rounded-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed shrink-0"
className="flex items-center gap-1.5 bg-sky-soft hover:opacity-80 border border-sky-line text-sky text-xs font-semibold px-3 py-2 rounded-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed shrink-0"
>
<Plus className="w-3.5 h-3.5" />
{addingRoute ? 'Adding…' : 'Add'}
@ -1073,25 +1073,25 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
{/* Header: icon + title + file size */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-violet-950/60 border border-violet-900/40 rounded-xl">
<HardDrive className="w-4 h-4 text-violet-400" />
<div className="p-2 bg-violet-soft border border-violet-line rounded-xl">
<HardDrive className="w-4 h-4 text-violet" />
</div>
<div>
<h2 className="text-sm font-semibold text-white">Database</h2>
<p className="text-[11px] text-slate-500 mt-0.5">SQLite · {dbInfo?.path ?? 'ghostgrid.db'}</p>
<h2 className="text-sm font-semibold text-fg">Database</h2>
<p className="text-[11px] text-fg-faint mt-0.5">SQLite · {dbInfo?.path ?? 'ghostgrid.db'}</p>
</div>
</div>
<div className="text-right">
<p className="text-xl font-bold text-white font-mono leading-none">
<p className="text-xl font-bold text-fg font-mono leading-none">
{dbInfo ? formatBytes(dbInfo.sizeBytes) : '-'}
</p>
<p className="text-[10px] text-slate-500 font-mono mt-1">
<p className="text-[10px] text-fg-faint font-mono mt-1">
{dbInfo ? new Date(dbInfo.lastModified).toLocaleDateString() : 'Loading…'}
</p>
</div>
</div>
<div className="h-px bg-slate-800/60" />
<div className="h-px bg-line" />
{/* Proportional usage bar + table stats */}
{dbInfo ? (() => {
@ -1104,9 +1104,9 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
};
return (
<div className="space-y-3">
<div className="flex h-1.5 rounded-full overflow-hidden bg-slate-800 gap-px">
<div className="flex h-1.5 rounded-full overflow-hidden bg-line-strong gap-px">
{total === 0
? <div className="flex-1 bg-slate-700" />
? <div className="flex-1 bg-line-strong" />
: tableEntries.filter(([, n]) => n > 0).map(([t, n]) => (
<div
key={t}
@ -1119,10 +1119,10 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
</div>
<div className="grid grid-cols-4 gap-1.5">
{tableEntries.map(([t, n]) => (
<div key={t} className="bg-slate-900/50 border border-slate-700/60 rounded-lg px-2 py-1.5">
<div key={t} className="bg-inner border border-line rounded-lg px-2 py-1.5">
<div className={`w-1.5 h-1.5 rounded-full mb-1 ${palette[t] ?? 'bg-slate-500'}`} />
<p className="text-[8px] font-semibold text-slate-600 uppercase tracking-wide truncate">{t}</p>
<p className="text-sm font-bold text-white font-mono">{n}</p>
<p className="text-[8px] font-semibold text-fg-faint uppercase tracking-wide truncate">{t}</p>
<p className="text-sm font-bold text-fg font-mono">{n}</p>
</div>
))}
</div>
@ -1130,11 +1130,11 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
);
})() : (
<div className="h-16 flex items-center justify-center">
<span className="w-4 h-4 border-2 border-slate-700 border-t-violet-400 rounded-full animate-spin" />
<span className="w-4 h-4 border-2 border-line-strong border-t-violet rounded-full animate-spin" />
</div>
)}
<div className="h-px bg-slate-800/60" />
<div className="h-px bg-line" />
{/* Actions */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
@ -1146,7 +1146,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
type="button"
onClick={handleBackup}
disabled={backingUp}
className="w-full flex items-center justify-center gap-2 bg-violet-950/60 hover:bg-violet-900/40 border border-violet-900/50 text-violet-400 text-xs font-semibold px-3 py-2 rounded-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed"
className="w-full flex items-center justify-center gap-2 bg-violet-soft hover:opacity-80 border border-violet-line text-violet text-xs font-semibold px-3 py-2 rounded-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
<Download className={`w-3.5 h-3.5 ${backingUp ? 'animate-pulse' : ''}`} />
{backingUp ? 'Creating backup…' : 'Download Backup'}
@ -1157,13 +1157,13 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
{/* Import */}
<div className="space-y-2">
<Label>Import</Label>
<div className="flex items-start gap-2 bg-amber-950/40 border border-amber-900/50 rounded-xl px-3 py-2">
<AlertTriangle className="w-3.5 h-3.5 text-amber-400 shrink-0 mt-0.5" />
<p className="text-[11px] text-amber-300 leading-relaxed">
<div className="flex items-start gap-2 bg-warning-soft border border-warning-line rounded-xl px-3 py-2">
<AlertTriangle className="w-3.5 h-3.5 text-warning shrink-0 mt-0.5" />
<p className="text-[11px] text-warning leading-relaxed">
<strong>Import overwrites the entire database</strong> - this cannot be undone.
</p>
</div>
<label className="w-full flex items-center gap-2 cursor-pointer bg-slate-900 border border-slate-700 hover:border-slate-600 rounded-lg px-3 py-2 text-xs text-slate-400 hover:text-slate-200 transition-all">
<label className="w-full flex items-center gap-2 cursor-pointer bg-inner border border-line-strong hover:border-line-strong rounded-lg px-3 py-2 text-xs text-fg-muted hover:text-fg transition-all">
<Upload className="w-3.5 h-3.5 shrink-0" />
<span className="truncate">{importFile ? importFile.name : 'Select .db file…'}</span>
<input
@ -1186,16 +1186,16 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
onChange={e => setImportConfirmed(e.target.checked)}
className="w-3.5 h-3.5 rounded accent-violet-500"
/>
<span className="text-[11px] text-slate-400">I confirm this will overwrite all existing data</span>
<span className="text-[11px] text-fg-muted">I confirm this will overwrite all existing data</span>
</label>
<button
type="button"
onClick={handleImport}
disabled={importing || !importConfirmed}
className="w-full flex items-center justify-center gap-2 bg-red-950/60 hover:bg-red-900/40 border border-red-900/50 text-red-400 text-xs font-semibold px-3 py-2 rounded-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed"
className="w-full flex items-center justify-center gap-2 bg-danger-soft hover:opacity-80 border border-danger-line text-danger text-xs font-semibold px-3 py-2 rounded-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{importing
? <span className="w-3.5 h-3.5 border-2 border-red-400/30 border-t-red-400 rounded-full animate-spin" />
? <span className="w-3.5 h-3.5 border-2 border-danger/30 border-t-danger rounded-full animate-spin" />
: <Upload className="w-3.5 h-3.5" />
}
{importing ? 'Importing…' : 'Import Database'}
@ -1203,7 +1203,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
</div>
)}
{importResult && (
<p className={`text-[11px] font-mono ${importResult.ok ? 'text-emerald-400' : 'text-red-400'}`}>
<p className={`text-[11px] font-mono ${importResult.ok ? 'text-success' : 'text-danger'}`}>
{importResult.msg}
</p>
)}

View File

@ -100,60 +100,60 @@ export default function TopologyPanel({ devices, links, onSelectDevice }: Topolo
const getDeviceIcon = (type: string) => {
switch (type) {
case 'Firewall':
return <Shield className="w-5 h-5 text-rose-400" />;
return <Shield className="w-5 h-5 text-rose" />;
case 'Access-Point':
return <Wifi className="w-5 h-5 text-amber-400" />;
return <Wifi className="w-5 h-5 text-warning" />;
case 'Controller':
return <Cpu className="w-5 h-5 text-cyan-400" />;
return <Cpu className="w-5 h-5 text-info" />;
case 'Switch':
default:
return <Server className="w-5 h-5 text-teal-400" />;
return <Server className="w-5 h-5 text-success" />;
}
};
const getDeviceColorClass = (type: string) => {
switch (type) {
case 'Firewall':
return 'border-rose-500/60 bg-rose-950/20 text-rose-200';
return 'border-rose-line bg-rose-soft text-rose';
case 'Access-Point':
return 'border-amber-500/60 bg-amber-950/20 text-amber-200';
return 'border-warning-line bg-warning-soft text-warning';
case 'Controller':
return 'border-cyan-500/60 bg-cyan-950/20 text-cyan-200';
return 'border-info-line bg-info-soft text-info';
case 'Switch':
default:
return 'border-teal-500/60 bg-teal-950/20 text-teal-200';
return 'border-success-line bg-success-soft text-success';
}
};
return (
<div className="bg-slate-900 border border-slate-800 rounded-xl p-4 shadow-inner" id="topology-panel">
<div className="bg-card border border-line rounded-xl p-4 shadow-inner" id="topology-panel">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-sm font-semibold text-slate-100 flex items-center gap-2 font-sans">
<Activity className="w-4 h-4 text-emerald-400 animate-pulse" />
<h3 className="text-sm font-semibold text-fg flex items-center gap-2 font-sans">
<Activity className="w-4 h-4 text-success" />
Interactive Topology Diagram (Physical & Logical Links)
</h3>
<p className="text-[11px] text-slate-400 font-sans">Hover links to inspect port details. Click host nodes to view operational recovery playbooks.</p>
<p className="text-[11px] text-fg-muted font-sans">Hover links to inspect port details. Click host nodes to view operational recovery playbooks.</p>
</div>
<div className="flex gap-2 text-[10px] font-mono">
<span className="flex items-center gap-1 text-teal-400 bg-teal-950/40 px-2 py-0.5 rounded border border-teal-900/50">
<span className="w-1.5 h-1.5 rounded-full bg-teal-500"></span> Switch
<span className="flex items-center gap-1 text-success bg-success-soft px-2 py-0.5 rounded border border-success-line">
<span className="w-1.5 h-1.5 rounded-full bg-success"></span> Switch
</span>
<span className="flex items-center gap-1 text-rose-400 bg-rose-950/40 px-2 py-0.5 rounded border border-rose-900/50">
<span className="w-1.5 h-1.5 rounded-full bg-rose-500"></span> Firewall
<span className="flex items-center gap-1 text-rose bg-rose-soft px-2 py-0.5 rounded border border-rose-line">
<span className="w-1.5 h-1.5 rounded-full bg-rose"></span> Firewall
</span>
<span className="flex items-center gap-1 text-amber-400 bg-amber-950/40 px-2 py-0.5 rounded border border-amber-900/50">
<span className="w-1.5 h-1.5 rounded-full bg-amber-500"></span> AP
<span className="flex items-center gap-1 text-warning bg-warning-soft px-2 py-0.5 rounded border border-warning-line">
<span className="w-1.5 h-1.5 rounded-full bg-warning"></span> AP
</span>
<span className="flex items-center gap-1 text-cyan-400 bg-cyan-950/40 px-2 py-0.5 rounded border border-cyan-900/50">
<span className="w-1.5 h-1.5 rounded-full bg-cyan-500"></span> WLC
<span className="flex items-center gap-1 text-info bg-info-soft px-2 py-0.5 rounded border border-info-line">
<span className="w-1.5 h-1.5 rounded-full bg-info"></span> WLC
</span>
</div>
</div>
<div className="relative overflow-auto border border-slate-800 rounded-lg bg-slate-950/60 flex justify-center items-center">
<div className="relative overflow-auto border border-line rounded-lg bg-inner flex justify-center items-center">
{devices.length === 0 ? (
<div className="py-20 text-center text-slate-500 text-xs font-sans">
<div className="py-20 text-center text-fg-faint text-xs font-sans">
No active hardware nodes assigned. Build or update a lab template to associate physical equipment.
</div>
) : (
@ -246,8 +246,8 @@ export default function TopologyPanel({ devices, links, onSelectDevice }: Topolo
key={`badge-${idx}`}
className={`absolute -translate-x-1/2 -translate-y-1/2 px-2 py-0.5 rounded text-[9px] font-mono transition-all z-20 shadow pointer-events-none ${
isHovered
? 'bg-emerald-500 text-slate-950 scale-110 font-bold border border-emerald-400 z-30'
: 'bg-slate-800 text-slate-400 border border-slate-700'
? 'bg-success text-white scale-110 font-bold border border-success z-30'
: 'bg-inner text-fg-muted border border-line'
}`}
style={{ left: layout.apexX, top: layout.apexY }}
>
@ -265,25 +265,25 @@ export default function TopologyPanel({ devices, links, onSelectDevice }: Topolo
<button
key={device.id}
onClick={() => onSelectDevice && onSelectDevice(device)}
className={`absolute group -translate-x-1/2 -translate-y-1/2 p-3 border-2 rounded-xl flex flex-col items-center gap-1.5 transition-all text-center z-20 ${getDeviceColorClass(device.type)} hover:bg-slate-900/90 hover:scale-105 hover:border-emerald-500/80 hover:shadow-[0_0_15px_rgba(16,185,129,0.15)]`}
className={`absolute group -translate-x-1/2 -translate-y-1/2 p-3 border-2 rounded-xl flex flex-col items-center gap-1.5 transition-all text-center z-20 ${getDeviceColorClass(device.type)} hover:bg-card hover:scale-105 hover:border-success hover:shadow-lg`}
style={{ left: pos.x, top: pos.y }}
title={`${device.hostname} (${device.ip}) - Click for troubleshooting details`}
>
{/* Status Indicator (sourced from CheckMK; grey = unknown) */}
<span className={`absolute -top-1 -right-1 w-3 h-3 rounded-full border-2 border-slate-950 ${
device.status === 'online' ? 'bg-emerald-500' :
device.status === 'offline' ? 'bg-rose-500' : 'bg-slate-500'
<span className={`absolute -top-1 -right-1 w-3 h-3 rounded-full border-2 border-card ${
device.status === 'online' ? 'bg-success' :
device.status === 'offline' ? 'bg-rose' : 'bg-fg-faint'
}`} />
<div className="p-1.5 bg-slate-950/60 rounded-lg border border-slate-800/40">
<div className="p-1.5 bg-inner rounded-lg border border-line">
{getDeviceIcon(device.type)}
</div>
<div className="leading-none">
<p className="text-[11px] font-mono font-bold tracking-tight text-white group-hover:text-emerald-400 transition-colors">
<p className="text-[11px] font-mono font-bold tracking-tight text-fg group-hover:text-success transition-colors">
{device.hostname}
</p>
<p className="text-[9px] font-mono text-slate-400 group-hover:text-slate-300 mt-0.5">
<p className="text-[9px] font-mono text-fg-muted group-hover:text-fg mt-0.5">
{device.ip}
</p>
</div>

View File

@ -60,53 +60,53 @@ function EditModal({ user, onClose, onSave }: EditModalProps) {
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-950/70 backdrop-blur-sm">
<div className="bg-[#0F172A] border border-[#1E293B] rounded-2xl shadow-2xl w-full max-w-md">
<div className="flex items-center justify-between px-6 py-4 border-b border-slate-800">
<h3 className="text-sm font-semibold text-white">Edit User</h3>
<button onClick={onClose} className="p-1 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 transition-all">
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-overlay backdrop-blur-sm">
<div className="bg-card border border-line rounded-2xl shadow-2xl w-full max-w-md">
<div className="flex items-center justify-between px-6 py-4 border-b border-line">
<h3 className="text-sm font-semibold text-fg">Edit User</h3>
<button onClick={onClose} className="p-1 rounded-lg text-fg-muted hover:text-fg hover:bg-inner transition-all">
<X className="w-4 h-4" />
</button>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-4">
{error && (
<div className="flex items-center gap-2 bg-red-950/50 border border-red-900/50 rounded-lg px-3 py-2 text-xs text-red-300">
<div className="flex items-center gap-2 bg-danger-soft border border-danger-line rounded-lg px-3 py-2 text-xs text-danger">
<AlertCircle className="w-3.5 h-3.5 shrink-0" />
{error}
</div>
)}
<div className="space-y-1.5">
<label className="block text-xs font-semibold text-slate-400 uppercase tracking-wide">Name</label>
<label className="block text-xs font-semibold text-fg-muted uppercase tracking-wide">Name</label>
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
required
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2.5 text-sm text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50 transition-all"
className="w-full bg-field border border-line-strong rounded-lg px-3 py-2.5 text-sm text-fg placeholder-fg-faint focus:outline-none focus:ring-2 focus:ring-info/50 focus:border-info transition-all"
/>
</div>
<div className="space-y-1.5">
<label className="block text-xs font-semibold text-slate-400 uppercase tracking-wide">Email address</label>
<label className="block text-xs font-semibold text-fg-muted uppercase tracking-wide">Email address</label>
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
required
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2.5 text-sm text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50 transition-all font-mono"
className="w-full bg-field border border-line-strong rounded-lg px-3 py-2.5 text-sm text-fg placeholder-fg-faint focus:outline-none focus:ring-2 focus:ring-info/50 focus:border-info transition-all font-mono"
/>
</div>
<div className="flex justify-end gap-2 pt-2">
<button
type="button"
onClick={onClose}
className="px-4 py-2 rounded-lg text-xs font-semibold text-slate-400 hover:text-white hover:bg-slate-800 transition-all"
className="px-4 py-2 rounded-lg text-xs font-semibold text-fg-muted hover:text-fg hover:bg-inner transition-all"
>
Cancel
</button>
<button
type="submit"
disabled={saving}
className="flex items-center gap-2 px-4 py-2 rounded-lg text-xs font-semibold bg-cyan-600 hover:bg-cyan-500 disabled:bg-slate-700 disabled:text-slate-500 text-white transition-all"
className="flex items-center gap-2 px-4 py-2 rounded-lg text-xs font-semibold bg-cyan-600 hover:bg-cyan-500 disabled:bg-line-strong disabled:text-fg-faint text-white transition-all"
>
{saving ? <span className="w-3.5 h-3.5 border-2 border-white/30 border-t-white rounded-full animate-spin" /> : <Save className="w-3.5 h-3.5" />}
{saving ? 'Saving…' : 'Save'}
@ -177,57 +177,57 @@ export default function UserDirectory({ users, currentUser, bookings, onDeleteUs
<div className="space-y-6 font-sans" id="user-directory-root">
{/* Header banner */}
<div className="bg-gradient-to-br from-[#1E293B] to-[#111827] border border-slate-800 rounded-2xl p-6 shadow-sm relative overflow-hidden">
<div className="absolute top-0 right-0 p-8 text-slate-800 pointer-events-none select-none font-mono text-9xl font-extrabold opacity-10">
<div className="bg-card border border-line rounded-2xl p-6 shadow-sm relative overflow-hidden">
<div className="absolute top-0 right-0 p-8 text-fg-faint pointer-events-none select-none font-mono text-9xl font-extrabold opacity-10">
TEAM
</div>
<div className="relative space-y-1.5">
<h2 className="text-2xl font-bold tracking-tight text-white flex items-center gap-2.5">
<Users className="w-6 h-6 text-emerald-400" />
<h2 className="text-2xl font-bold tracking-tight text-fg flex items-center gap-2.5">
<Users className="w-6 h-6 text-success" />
Registered Operators
</h2>
<p className="text-xs text-slate-300 max-w-xl leading-relaxed">
<p className="text-xs text-fg-muted max-w-xl leading-relaxed">
Everyone with an account on this box. Booking counts come straight from the shared reservation pool.
</p>
<div className="flex flex-wrap gap-2 pt-3">
<span className="inline-flex items-center gap-1.5 bg-slate-950/60 border border-slate-800 rounded-lg px-3 py-1.5 text-[11px] text-slate-300">
<Users className="w-3.5 h-3.5 text-emerald-400" />
<strong className="text-white font-mono">{users.length}</strong> registered
<span className="inline-flex items-center gap-1.5 bg-inner border border-line rounded-lg px-3 py-1.5 text-[11px] text-fg-muted">
<Users className="w-3.5 h-3.5 text-success" />
<strong className="text-fg font-mono">{users.length}</strong> registered
</span>
<span className="inline-flex items-center gap-1.5 bg-slate-950/60 border border-slate-800 rounded-lg px-3 py-1.5 text-[11px] text-slate-300">
<Calendar className="w-3.5 h-3.5 text-indigo-400" />
<strong className="text-white font-mono">{bookings.length}</strong> total bookings
<span className="inline-flex items-center gap-1.5 bg-inner border border-line rounded-lg px-3 py-1.5 text-[11px] text-fg-muted">
<Calendar className="w-3.5 h-3.5 text-primary" />
<strong className="text-fg font-mono">{bookings.length}</strong> total bookings
</span>
<span className="inline-flex items-center gap-1.5 bg-slate-950/60 border border-slate-800 rounded-lg px-3 py-1.5 text-[11px] text-slate-300">
<Activity className="w-3.5 h-3.5 text-emerald-400" />
<strong className="text-white font-mono">{bookings.filter(b => b.status === 'active' || b.status === 'upcoming').length}</strong> active / upcoming
<span className="inline-flex items-center gap-1.5 bg-inner border border-line rounded-lg px-3 py-1.5 text-[11px] text-fg-muted">
<Activity className="w-3.5 h-3.5 text-success" />
<strong className="text-fg font-mono">{bookings.filter(b => b.status === 'active' || b.status === 'upcoming').length}</strong> active / upcoming
</span>
</div>
</div>
</div>
{roleError && (
<div className="flex items-center gap-2 bg-red-950/50 border border-red-900/50 rounded-lg px-3 py-2 text-xs text-red-300">
<div className="flex items-center gap-2 bg-danger-soft border border-danger-line rounded-lg px-3 py-2 text-xs text-danger">
<AlertCircle className="w-3.5 h-3.5 shrink-0" />
{roleError}
<button onClick={() => setRoleError(null)} className="ml-auto text-red-400 hover:text-white"><X className="w-3 h-3" /></button>
<button onClick={() => setRoleError(null)} className="ml-auto text-danger hover:text-fg"><X className="w-3 h-3" /></button>
</div>
)}
{/* Search */}
<div className="relative">
<span className="absolute left-3 top-2.5 text-slate-500"><Search className="w-4 h-4" /></span>
<span className="absolute left-3 top-2.5 text-fg-faint"><Search className="w-4 h-4" /></span>
<input
type="text"
value={search}
onChange={e => setSearch(e.target.value)}
className="w-full bg-[#1E293B] text-slate-100 border border-slate-800 rounded-xl pl-9 pr-4 py-2 text-xs focus:outline-none focus:border-emerald-600"
className="w-full bg-card text-fg border border-line rounded-xl pl-9 pr-4 py-2 text-xs focus:outline-none focus:border-success"
/>
</div>
{/* User grid */}
{filtered.length === 0 ? (
<p className="text-center py-16 text-slate-500 text-xs">No operators match your search.</p>
<p className="text-center py-16 text-fg-faint text-xs">No operators match your search.</p>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-4">
{filtered.map(user => {
@ -238,10 +238,10 @@ export default function UserDirectory({ users, currentUser, bookings, onDeleteUs
return (
<div
key={user.id}
className={`relative bg-[#1E293B] border rounded-xl p-5 transition-all hover:shadow-lg ${isMe ? 'border-emerald-500/50 shadow-[0_0_20px_rgba(16,185,129,0.08)]' : 'border-slate-800 hover:border-slate-700'}`}
className={`relative bg-card border rounded-xl p-5 transition-all hover:shadow-lg ${isMe ? 'border-success shadow-lg' : 'border-line hover:border-line-strong'}`}
>
{isMe && (
<span className="absolute top-3 right-3 text-[9px] font-bold uppercase tracking-wider text-emerald-400 bg-emerald-950/60 border border-emerald-900/60 px-2 py-0.5 rounded-full">
<span className="absolute top-3 right-3 text-[9px] font-bold uppercase tracking-wider text-success bg-success-soft border border-success-line px-2 py-0.5 rounded-full">
You
</span>
)}
@ -251,10 +251,10 @@ export default function UserDirectory({ users, currentUser, bookings, onDeleteUs
{initials(user.name)}
</div>
<div className="min-w-0 flex-1">
<h3 className="text-sm font-bold text-white truncate">{user.name}</h3>
<h3 className="text-sm font-bold text-fg truncate">{user.name}</h3>
<a
href={`mailto:${user.email}`}
className="text-[11px] text-slate-400 hover:text-emerald-400 truncate flex items-center gap-1 mt-0.5 w-fit max-w-full"
className="text-[11px] text-fg-muted hover:text-success truncate flex items-center gap-1 mt-0.5 w-fit max-w-full"
>
<Mail className="w-3 h-3 shrink-0" />
<span className="truncate">{user.email}</span>
@ -262,20 +262,20 @@ export default function UserDirectory({ users, currentUser, bookings, onDeleteUs
</div>
</div>
<div className="mt-4 pt-3 border-t border-slate-800 flex items-center justify-between gap-2">
<div className="mt-4 pt-3 border-t border-line flex items-center justify-between gap-2">
{user.role.toLowerCase() === 'admin'
? <span className="flex items-center gap-1 text-[10px] font-mono text-amber-400 uppercase tracking-wider"><ShieldCheck className="w-3 h-3" />Admin</span>
: <span className="text-[10px] font-mono text-slate-500 uppercase tracking-wider">User</span>
? <span className="flex items-center gap-1 text-[10px] font-mono text-warning uppercase tracking-wider"><ShieldCheck className="w-3 h-3" />Admin</span>
: <span className="text-[10px] font-mono text-fg-faint uppercase tracking-wider">User</span>
}
<div className="flex items-center gap-2">
<div className="flex items-center gap-3 text-[10px] font-mono text-slate-400">
<div className="flex items-center gap-3 text-[10px] font-mono text-fg-muted">
<span className="flex items-center gap-1" title="Total bookings">
<Calendar className="w-3 h-3 text-indigo-400" />
<Calendar className="w-3 h-3 text-primary" />
{total}
</span>
<span className="flex items-center gap-1" title="Active / upcoming bookings">
<Activity className="w-3 h-3 text-emerald-400" />
<Activity className="w-3 h-3 text-success" />
{active}
</span>
</div>
@ -286,11 +286,11 @@ export default function UserDirectory({ users, currentUser, bookings, onDeleteUs
<button
onClick={() => handleToggleRole(user)}
disabled={togglingRoleId === user.id}
className={`p-1.5 rounded-lg transition-all disabled:opacity-40 ${user.role.toLowerCase() === 'admin' ? 'text-amber-400 hover:text-slate-400 hover:bg-slate-900' : 'text-slate-500 hover:text-amber-400 hover:bg-slate-900'}`}
className={`p-1.5 rounded-lg transition-all disabled:opacity-40 ${user.role.toLowerCase() === 'admin' ? 'text-warning hover:text-fg-muted hover:bg-inner' : 'text-fg-faint hover:text-warning hover:bg-inner'}`}
title={user.role.toLowerCase() === 'admin' ? 'Remove admin' : 'Make admin'}
>
{togglingRoleId === user.id
? <span className="w-3.5 h-3.5 border-2 border-slate-600 border-t-amber-400 rounded-full animate-spin inline-block" />
? <span className="w-3.5 h-3.5 border-2 border-line-strong border-t-warning rounded-full animate-spin inline-block" />
: user.role.toLowerCase() === 'admin'
? <ShieldCheck className="w-3.5 h-3.5" />
: <Shield className="w-3.5 h-3.5" />}
@ -298,7 +298,7 @@ export default function UserDirectory({ users, currentUser, bookings, onDeleteUs
)}
<button
onClick={() => setEditingUser(user)}
className="p-1.5 rounded-lg text-slate-500 hover:text-cyan-400 hover:bg-slate-900 transition-all"
className="p-1.5 rounded-lg text-fg-faint hover:text-info hover:bg-inner transition-all"
title="Edit name / email"
>
<Pencil className="w-3.5 h-3.5" />
@ -307,11 +307,11 @@ export default function UserDirectory({ users, currentUser, bookings, onDeleteUs
<button
onClick={() => handleDelete(user.id)}
disabled={isDeleting}
className="p-1.5 rounded-lg text-slate-500 hover:text-rose-400 hover:bg-slate-900 transition-all disabled:opacity-40"
className="p-1.5 rounded-lg text-fg-faint hover:text-rose hover:bg-inner transition-all disabled:opacity-40"
title="Delete user"
>
{isDeleting
? <span className="w-3.5 h-3.5 border-2 border-slate-600 border-t-rose-400 rounded-full animate-spin inline-block" />
? <span className="w-3.5 h-3.5 border-2 border-line-strong border-t-rose rounded-full animate-spin inline-block" />
: <Trash2 className="w-3.5 h-3.5" />}
</button>
)}

View File

@ -6,6 +6,38 @@
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;
}
/* ── Semantic design tokens ────────────────────────────────────────
Wired with `inline` so each utility references the CSS var directly
(e.g. `.bg-card { background: var(--bg-card) }`). The values live in
:root / :root.light below, so theme switching is a pure var swap —
no per-utility light-mode overrides needed. */
@theme inline {
/* neutrals */
--color-surface: var(--bg);
--color-header: var(--bg-header);
--color-card: var(--bg-card);
--color-inner: var(--bg-inner);
--color-field: var(--bg-input);
--color-line: var(--border);
--color-line-strong: var(--border-muted);
--color-fg: var(--text);
--color-fg-muted: var(--text-muted);
--color-fg-faint: var(--text-faint);
--color-overlay: var(--overlay);
/* accents — triplet per hue: vivid (text/icon) / soft (chip bg) / line (chip border) */
--color-success: var(--success); --color-success-soft: var(--success-soft); --color-success-line: var(--success-line);
--color-info: var(--info); --color-info-soft: var(--info-soft); --color-info-line: var(--info-line);
--color-primary: var(--primary); --color-primary-soft: var(--primary-soft); --color-primary-line: var(--primary-line);
--color-warning: var(--warning); --color-warning-soft: var(--warning-soft); --color-warning-line: var(--warning-line);
--color-danger: var(--danger); --color-danger-soft: var(--danger-soft); --color-danger-line: var(--danger-line);
--color-rose: var(--rose); --color-rose-soft: var(--rose-soft); --color-rose-line: var(--rose-line);
--color-violet: var(--violet); --color-violet-soft: var(--violet-soft); --color-violet-line: var(--violet-line);
--color-sky: var(--sky); --color-sky-soft: var(--sky-soft); --color-sky-line: var(--sky-line);
--color-orange: var(--orange); --color-orange-soft: var(--orange-soft); --color-orange-line: var(--orange-line);
--color-blue: var(--blue); --color-blue-soft: var(--blue-soft); --color-blue-line: var(--blue-line);
}
/* ── AirIT brand tokens ────────────────────────────────────────── */
:root {
--airit-navy: #003A70;
@ -18,21 +50,37 @@
--airit-border: #D6DADF;
}
/* ── CSS custom properties ─────────────────────────────────────── */
/* ── Theme values: DARK (default) ──────────────────────────────── */
:root {
/* neutrals */
--bg: #0b0f19;
--bg-header: #0f172a;
--bg-card: #1e293b;
--bg-inner: #090d16;
--bg-input: #020408;
--border: #1e293b;
--border-muted:#334155;
--text: #f1f5f9;
--bg-card: #181f2b;
--bg-inner: #0c1119;
--bg-input: #0a0e16;
--border: #283142;
--border-muted:#3a4659;
--text: #e9eef5;
--text-muted: #94a3b8;
--text-label: #cbd5e1;
--text-faint: #64748b;
--overlay: rgba(2, 6, 12, 0.6);
/* accents — calmer, slightly desaturated; soft chips use alpha tints */
--success: #34d399; --success-soft: rgba(52,211,153,0.12); --success-line: rgba(52,211,153,0.26);
--info: #22d3ee; --info-soft: rgba(34,211,238,0.12); --info-line: rgba(34,211,238,0.26);
--primary: #818cf8; --primary-soft: rgba(129,140,248,0.14); --primary-line: rgba(129,140,248,0.28);
--warning: #fbbf24; --warning-soft: rgba(251,191,36,0.13); --warning-line: rgba(251,191,36,0.28);
--danger: #f87171; --danger-soft: rgba(248,113,113,0.13); --danger-line: rgba(248,113,113,0.30);
--rose: #fb7185; --rose-soft: rgba(251,113,133,0.13); --rose-line: rgba(251,113,133,0.30);
--violet: #a78bfa; --violet-soft: rgba(167,139,250,0.14); --violet-line: rgba(167,139,250,0.28);
--sky: #38bdf8; --sky-soft: rgba(56,189,248,0.13); --sky-line: rgba(56,189,248,0.28);
--orange: #fb923c; --orange-soft: rgba(251,146,60,0.13); --orange-line: rgba(251,146,60,0.28);
--blue: #60a5fa; --blue-soft: rgba(96,165,250,0.13); --blue-line: rgba(96,165,250,0.28);
}
/* ── Theme values: LIGHT ───────────────────────────────────────── */
:root.light {
/* neutrals */
--bg: #f1f5f9;
--bg-header: #ffffff;
--bg-card: #ffffff;
@ -42,879 +90,40 @@
--border-muted:#cbd5e1;
--text: #0f172a;
--text-muted: #475569;
--text-label: #334155;
}
/* ── LIGHT MODE OVERRIDES ─────────────────────────────────────── */
/* Root / body */
:root.light body,
:root.light #main-root {
background-color: var(--bg) !important;
color: var(--text) !important;
}
/* ── Backgrounds: all dark hex variants > card/inner */
:root.light .bg-\[\#0B0F19\],
:root.light .bg-\[\#0b0f19\] {
background-color: var(--bg) !important;
}
:root.light .bg-\[\#0F172A\],
:root.light .bg-\[\#0f172a\] {
background-color: var(--bg-header) !important;
border-color: var(--border) !important;
}
:root.light .bg-\[\#1E293B\],
:root.light .bg-\[\#1e293b\] {
background-color: var(--bg-card) !important;
border-color: var(--border) !important;
}
/* BookingCalendar "Quick Booking" green-tinted card */
:root.light .bg-\[\#1D2535\],
:root.light .bg-\[\#1d2535\] {
background-color: #f0fdf4 !important;
border-color: #bbf7d0 !important;
}
/* ── Header & nav ─────────────────────────────────────────────── */
:root.light header,
:root.light #app-header {
background-color: var(--bg-header) !important;
border-color: var(--border) !important;
color: var(--text) !important;
}
:root.light aside,
:root.light #nav-sidebar {
background-color: #f8fafc !important;
border-color: var(--border) !important;
}
/* ── Slate utility backgrounds ────────────────────────────────── */
:root.light .bg-slate-950,
:root.light .bg-slate-900 {
background-color: #f1f5f9 !important;
border-color: var(--border) !important;
}
:root.light .bg-slate-800 {
background-color: #e2e8f0 !important;
}
/* opacity variants */
:root.light .bg-slate-950\/10,
:root.light .bg-slate-950\/20,
:root.light .bg-slate-950\/30,
:root.light .bg-slate-950\/40,
:root.light .bg-slate-950\/50,
:root.light .bg-slate-950\/60,
:root.light .bg-slate-950\/80 {
background-color: rgba(241, 245, 249, 0.85) !important;
}
:root.light .bg-slate-900\/10,
:root.light .bg-slate-900\/35,
:root.light .bg-slate-900\/40,
:root.light .bg-slate-900\/60,
:root.light .bg-slate-900\/80 {
background-color: #f8fafc !important;
}
:root.light .bg-slate-800\/50,
:root.light .bg-slate-800\/60,
:root.light .bg-slate-800\/80 {
background-color: #e9ecf0 !important;
}
/* ── Dashboard / UserDirectory banner gradients (dark hex only) ── */
/* Targets only the dark-themed banners, not coloured avatar gradients */
:root.light .bg-gradient-to-br.from-\[\#1E293B\],
:root.light .bg-gradient-to-br.from-\[\#1e293b\] {
background-image: linear-gradient(to bottom right, #ffffff, #f1f5f9) !important;
border-color: var(--border) !important;
}
/* ── Inputs, selects, textareas ───────────────────────────────── */
:root.light input,
:root.light select,
:root.light textarea {
background-color: var(--bg-input) !important;
border-color: var(--border-muted) !important;
color: var(--text) !important;
}
:root.light option {
background-color: #ffffff !important;
color: var(--text) !important;
}
:root.light input:focus,
:root.light select:focus,
:root.light textarea:focus {
border-color: #6366f1 !important;
}
/* ── Borders ──────────────────────────────────────────────────── */
:root.light .border-slate-900,
:root.light .border-slate-800,
:root.light .border-slate-850,
:root.light .border-slate-855,
:root.light .border-slate-700,
:root.light .border-\[\#1E293B\],
:root.light .border-\[\#1e293b\] {
border-color: var(--border) !important;
}
:root.light .border-red-900\/50,
:root.light .border-red-900\/40,
:root.light .border-red-900\/30 {
border-color: #fca5a5 !important;
}
:root.light .divide-slate-800 > *,
:root.light .divide-slate-850 > * {
border-color: var(--border) !important;
}
/* ── Text colours ─────────────────────────────────────────────── */
:root.light .text-white,
:root.light .text-white\/90,
:root.light .text-white\/80,
:root.light .text-white\/70,
:root.light .text-slate-100,
:root.light .text-slate-200 {
color: var(--text) !important;
}
:root.light .text-slate-300 {
color: #334155 !important;
}
:root.light .text-slate-400 {
color: #64748b !important;
}
:root.light .text-slate-500 {
color: #94a3b8 !important;
}
/* Accent colours - slightly darkened for readability on white */
:root.light .text-emerald-400 {
color: #059669 !important;
}
:root.light .text-cyan-400 {
color: #0891b2 !important;
}
:root.light .text-indigo-400 {
color: #4f46e5 !important;
}
:root.light .text-amber-400,
:root.light .text-amber-500 {
color: #d97706 !important;
}
:root.light .text-rose-400,
:root.light .text-rose-450 {
color: #be123c !important;
}
/* ── Accent / status badge backgrounds ───────────────────────── */
:root.light .bg-emerald-950\/60,
:root.light .bg-emerald-950\/50,
:root.light .bg-emerald-950\/40,
:root.light .bg-emerald-950\/20,
:root.light .bg-emerald-950\/80 {
background-color: #d1fae5 !important;
border-color: #6ee7b7 !important;
color: #065f46 !important;
}
:root.light .bg-indigo-950\/60,
:root.light .bg-indigo-950\/50,
:root.light .bg-indigo-950\/40 {
background-color: #e0e7ff !important;
border-color: #a5b4fc !important;
color: #3730a3 !important;
}
:root.light .bg-rose-950\/60,
:root.light .bg-rose-950\/40,
:root.light .bg-rose-950\/20 {
background-color: #ffe4e6 !important;
border-color: #fca5a5 !important;
color: #9f1239 !important;
}
:root.light .bg-rose-950\/30,
:root.light .hover\:bg-rose-950\/30:hover {
background-color: #ffe4e6 !important;
}
:root.light .bg-red-950\/60,
:root.light .bg-red-950\/50,
:root.light .bg-red-950\/40,
:root.light .bg-red-950\/20 {
background-color: #fee2e2 !important;
border-color: #fca5a5 !important;
color: #b91c1c !important;
}
:root.light .border-red-800\/60 {
border-color: #fca5a5 !important;
}
:root.light .text-red-300 {
color: #b91c1c !important;
}
:root.light .text-red-400 {
color: #dc2626 !important;
}
:root.light .bg-cyan-950\/40 {
background-color: #cffafe !important;
border-color: #67e8f9 !important;
color: #155e75 !important;
}
:root.light .bg-amber-950\/40,
:root.light .bg-amber-900\/30 {
background-color: #fef3c7 !important;
border-color: #fde68a !important;
color: #92400e !important;
}
/* ── Nav sidebar active item ──────────────────────────────────── */
:root.light #nav-sidebar button {
color: #475569 !important;
}
:root.light #nav-sidebar button:hover:not(.bg-gradient-to-r) {
background-color: rgba(0, 0, 0, 0.05) !important;
color: var(--text) !important;
}
/* ── Sidebar telemetry box ────────────────────────────────────── */
:root.light #nav-sidebar .bg-slate-950 {
background-color: #f1f5f9 !important;
border-color: var(--border-muted) !important;
}
/* ── Dropdown panels (mail, bell) ─────────────────────────────── */
:root.light .bg-\[\#1E293B\].rounded-xl,
:root.light .shadow-2xl.rounded-xl {
background-color: #ffffff !important;
border-color: var(--border-muted) !important;
}
/* ── Table internals ──────────────────────────────────────────── */
:root.light table {
color: var(--text) !important;
}
:root.light thead {
background-color: #f8fafc !important;
}
:root.light tbody tr:hover {
background-color: #f1f5f9 !important;
}
:root.light .bg-\[\#0f172a\]\/60,
:root.light tr.bg-\[\#0f172a\] {
background-color: #f1f5f9 !important;
}
/* ── Dashed empty states ──────────────────────────────────────── */
:root.light .border-dashed {
border-color: var(--border-muted) !important;
}
/* ── DeviceInventory ──────────────────────────────────────────── */
/* Emergency Sheet container (amber-tinted dark card) */
:root.light .bg-\[\#1D2432\],
:root.light .bg-\[\#1d2432\] {
background-color: #fffbeb !important;
border-color: #fde68a !important;
}
/* Emergency sheet markdown content area - light in light mode */
:root.light .bg-\[\#1D2432\] .bg-slate-950\/80,
:root.light .bg-\[\#1d2432\] .bg-slate-950\/80 {
background-color: #ffffff !important;
border-color: var(--border) !important;
color: var(--text) !important;
}
:root.light .bg-\[\#1D2432\] .bg-slate-950\/80 *,
:root.light .bg-\[\#1d2432\] .bg-slate-950\/80 * {
color: var(--text) !important;
}
/* Keep emerald headings readable */
:root.light .bg-\[\#1D2432\] .bg-slate-950\/80 h5,
:root.light .bg-\[\#1d2432\] .bg-slate-950\/80 h5 {
color: #059669 !important;
}
/* Device type icon pill backgrounds */
:root.light .bg-rose-950\/20 { background-color: #fff1f2 !important; border-color: #fecdd3 !important; }
:root.light .bg-amber-950\/20 { background-color: #fffbeb !important; border-color: #fde68a !important; }
:root.light .bg-cyan-950\/20 { background-color: #ecfeff !important; border-color: #a5f3fc !important; }
:root.light .bg-teal-950\/20 { background-color: #f0fdfa !important; border-color: #99f6e4 !important; }
/* Filter toolbar type-filter buttons */
:root.light .bg-slate-850,
:root.light .hover\:bg-slate-850:hover {
background-color: #e2e8f0 !important;
}
/* Device card selected state */
:root.light #inventory-list-container .bg-slate-900.border-emerald-500\/80 {
background-color: #f0fdf4 !important;
border-color: #10b981 !important;
}
/* Device card unselected/hover */
:root.light #inventory-list-container .bg-slate-900\/40 {
background-color: #f8fafc !important;
border-color: var(--border) !important;
}
:root.light #inventory-list-container .hover\:bg-slate-900\/60:hover {
background-color: #f1f5f9 !important;
}
/* SPECS ID badge and code block inside right panel */
:root.light #inventory-details-container .bg-slate-950 {
background-color: #f1f5f9 !important;
border-color: var(--border) !important;
color: var(--text) !important;
}
:root.light #inventory-details-container .bg-slate-900\/50,
:root.light #inventory-details-container .bg-slate-900\/60 {
background-color: #f8fafc !important;
border-color: var(--border) !important;
}
/* Amber rescue badge */
:root.light .bg-amber-950 {
background-color: #fef3c7 !important;
border-color: #fde68a !important;
color: #92400e !important;
}
/* ── Dashboard "NET" watermark: invisible in light mode ───────── */
:root.light #dashboard-cockpit-root .text-slate-800 {
color: transparent !important;
}
/* ── Modal overlays ───────────────────────────────────────────── */
:root.light .fixed.inset-0 {
background-color: rgba(15, 23, 42, 0.45) !important;
}
:root.light .fixed.inset-0 > div,
:root.light .bg-\[\#0F172A\].rounded-2xl {
background-color: #ffffff !important;
border-color: var(--border-muted) !important;
}
/* ── Lab Template Modal internals ─────────────────────────────── */
/* Modal header bar */
:root.light .fixed.inset-0 .bg-slate-900.border-b {
background-color: #f8fafc !important;
border-color: var(--border) !important;
}
/* Modal form body */
:root.light .fixed.inset-0 form {
background-color: #ffffff !important;
}
/* Device-toggle buttons inside modal */
:root.light .fixed.inset-0 .bg-slate-900.border-slate-800 {
background-color: #f1f5f9 !important;
border-color: var(--border) !important;
color: var(--text-label) !important;
}
:root.light .fixed.inset-0 .bg-slate-900.border-slate-850 {
background-color: #f1f5f9 !important;
}
/* Device grid area */
:root.light .bg-slate-950\/60 {
background-color: #f8fafc !important;
border-color: var(--border) !important;
}
/* Link builder row */
:root.light .bg-slate-1000,
:root.light .bg-slate-1000\/40 {
background-color: #f1f5f9 !important;
border-color: var(--border) !important;
}
/* Existing link row badges */
:root.light .fixed.inset-0 .bg-slate-900\/40 {
background-color: #f8fafc !important;
}
/* border-slate-700 in modal context */
:root.light .border-slate-700 {
border-color: var(--border) !important;
}
/* ── Login / Register pages ───────────────────────────────────── */
:root.light .min-h-screen.bg-\[\#0B0F19\] {
background-color: var(--bg) !important;
}
:root.light .bg-slate-950\/80 {
background-color: rgba(255, 255, 255, 0.9) !important;
border-color: var(--border) !important;
}
/* ── Code / terminal blocks - always dark ─────────────────────── */
:root.light pre,
:root.light code,
:root.light .font-mono.bg-slate-950 {
background-color: #0d1117 !important;
color: #00f0ff !important;
border-color: #1f242c !important;
}
:root.light pre *,
:root.light code * {
color: inherit !important;
}
/* ── Settings page blue (Entra ID) tokens ─────────────────────── */
:root.light .bg-blue-950\/60,
:root.light .bg-blue-950\/50,
:root.light .bg-blue-950\/40 {
background-color: #dbeafe !important;
border-color: #93c5fd !important;
color: #1d4ed8 !important;
}
:root.light .border-blue-900\/50,
:root.light .border-blue-900\/40 {
border-color: #93c5fd !important;
}
:root.light .text-blue-400 {
color: #1d4ed8 !important;
}
/* ── AirIT badge - always white text on navy, regardless of theme ─ */
--text-faint: #94a3b8;
--overlay: rgba(15, 23, 42, 0.45);
/* accents */
--success: #059669; --success-soft: #d1fae5; --success-line: #6ee7b7;
--info: #0891b2; --info-soft: #cffafe; --info-line: #67e8f9;
--primary: #4f46e5; --primary-soft: #e0e7ff; --primary-line: #a5b4fc;
--warning: #d97706; --warning-soft: #fef3c7; --warning-line: #fde68a;
--danger: #dc2626; --danger-soft: #fee2e2; --danger-line: #fca5a5;
--rose: #be123c; --rose-soft: #ffe4e6; --rose-line: #fca5a5;
--violet: #7c3aed; --violet-soft: #ede9fe; --violet-line: #c4b5fd;
--sky: #0284c7; --sky-soft: #e0f2fe; --sky-line: #7dd3fc;
--orange: #ea580c; --orange-soft: #fff7ed; --orange-line: #fdba74;
--blue: #1d4ed8; --blue-soft: #dbeafe; --blue-line: #93c5fd;
}
/* ── Base element styling ──────────────────────────────────────────
The component layer now consumes the semantic tokens above directly,
so the old ~260-rule `:root.light` utility-override block is gone.
Only genuinely global, theme-agnostic rules remain here. */
/* AirIT badge - always white text on navy, regardless of theme */
.airit-badge {
color: #ffffff !important;
background-color: var(--airit-navy) !important;
color: #ffffff;
background-color: var(--airit-navy);
}
/* ── Text selection ───────────────────────────────────────────── */
:root.light ::selection {
background-color: rgba(5, 150, 105, 0.2) !important;
color: #047857 !important;
/* Text selection - subtle brand tint in both themes */
::selection {
background-color: rgba(16, 185, 129, 0.25);
}
/* ── Reservation Details Modal (BookingDetailsModal) ──────────── */
/* Device node cards inside right panel */
:root.light #booking-details-modal .bg-slate-950\/65 {
background-color: #f8fafc !important;
border-color: var(--border) !important;
}
/* JSON REST Response panel GitHub Light style in light mode */
:root.light #booking-details-modal .font-mono.bg-slate-950 {
background-color: #f6f8fa !important;
color: #24292f !important;
border-color: #d0d7de !important;
}
/* The <pre> inside inherits the dark pre-rule; override explicitly */
:root.light #booking-details-modal .font-mono.bg-slate-950 pre {
background-color: transparent !important;
color: #24292f !important;
border-color: transparent !important;
}
/* Header bar inside the JSON panel */
:root.light #booking-details-modal .font-mono.bg-slate-950 .bg-slate-900 {
background-color: #eaeef2 !important;
border-color: #d0d7de !important;
}
/* Title label and icon in panel header */
:root.light #booking-details-modal .font-mono.bg-slate-950 .text-indigo-400 {
color: #6366f1 !important;
}
/* Copy button inside panel */
:root.light #booking-details-modal .font-mono.bg-slate-950 button {
background-color: #eaeef2 !important;
border-color: #d0d7de !important;
color: #57606a !important;
}
:root.light #booking-details-modal .font-mono.bg-slate-950 button:hover {
background-color: #d0d7de !important;
color: #24292f !important;
}
/* Ansible status card orange accent in light mode */
:root.light #booking-details-modal .bg-orange-950\/10 {
background-color: #fff7ed !important;
}
:root.light #booking-details-modal .border-orange-900\/40 {
border-color: #fdba74 !important;
}
:root.light .text-orange-400 {
color: #ea580c !important;
}
:root.light .bg-orange-950\/60,
:root.light .bg-orange-900\/40 {
background-color: #fed7aa !important;
border-color: #fb923c !important;
color: #9a3412 !important;
}
:root.light .border-orange-800\/50 {
border-color: #fb923c !important;
}
/* bg-slate-900/40 rows (notice box etc.) light in light mode */
:root.light #booking-details-modal .bg-slate-900\/40 {
background-color: #f1f5f9 !important;
border-color: var(--border) !important;
color: var(--text) !important;
}
/* ── Reserve Slot selects & date pickers (BookingCalendar) ───── */
:root.light #booking-actions-card select,
:root.light #booking-actions-card input[type="text"],
:root.light #booking-actions-card input[type="date"],
:root.light #booking-actions-card textarea {
background-color: #ffffff !important;
border-color: var(--border-muted) !important;
color: var(--text) !important;
}
/* Date picker calendar icon - invert to dark in light mode */
/* Date picker calendar icon - invert to dark so it reads on light inputs */
:root.light input[type="date"]::-webkit-calendar-picker-indicator {
filter: invert(0.6);
cursor: pointer;
}
/* ── Define Ports dropdowns & Link Builder (LabTemplates modal) ── */
/* Non-standard text/border classes used in the link builder */
:root.light .text-slate-250 {
color: var(--text) !important;
}
:root.light .border-slate-805 {
border-color: var(--border) !important;
}
/* Selects and inputs inside any fixed modal overlay */
:root.light .fixed.inset-0 select,
:root.light .fixed.inset-0 input[type="text"],
:root.light .fixed.inset-0 input[type="email"],
:root.light .fixed.inset-0 input[type="password"],
:root.light .fixed.inset-0 input[type="date"],
:root.light .fixed.inset-0 textarea {
background-color: #ffffff !important;
border-color: var(--border-muted) !important;
color: var(--text) !important;
}
/* Link row items inside modal */
:root.light .fixed.inset-0 .bg-slate-950.font-mono {
background-color: #f8fafc !important;
border-color: var(--border) !important;
color: var(--text) !important;
}
/* "Add Link" button - keep readable white label on indigo in light mode
(the global :root.light .text-white override would otherwise darken it) */
:root.light #add-link-btn {
background-color: #4f46e5 !important;
color: #ffffff !important;
}
:root.light #add-link-btn:hover {
background-color: #6366f1 !important;
}
/* ─────────────────────────────────────────────────────────────────── */
/* EXTENDED LIGHT MODE OVERRIDES */
/* ─────────────────────────────────────────────────────────────────── */
/* ── Solid (no-opacity) color backgrounds ────────────────────────── */
/* These are used in Logbook type badges and Dashboard countdown pill */
:root.light .bg-emerald-950 {
background-color: #d1fae5 !important;
border-color: #6ee7b7 !important;
color: #065f46 !important;
}
:root.light .bg-cyan-950 {
background-color: #cffafe !important;
border-color: #67e8f9 !important;
color: #155e75 !important;
}
:root.light .bg-indigo-950 {
background-color: #e0e7ff !important;
border-color: #a5b4fc !important;
color: #3730a3 !important;
}
:root.light .bg-rose-950 {
background-color: #ffe4e6 !important;
border-color: #fca5a5 !important;
color: #9f1239 !important;
}
/* ── emerald-900 opacity variants (quick booking modal tabs) ──────── */
:root.light .bg-emerald-900\/50,
:root.light .bg-emerald-900\/40,
:root.light .bg-emerald-900\/30 {
background-color: #d1fae5 !important;
}
/* ── Violet accent (LinkDashboard) ────────────────────────────────── */
:root.light .bg-violet-950\/60,
:root.light .bg-violet-950\/40,
:root.light .bg-violet-950\/20 {
background-color: #ede9fe !important;
border-color: #c4b5fd !important;
color: #5b21b6 !important;
}
:root.light .text-violet-400 {
color: #7c3aed !important;
}
/* ── 300-level text near-invisible on white backgrounds ────────── */
:root.light .text-amber-300 {
color: #b45309 !important;
}
:root.light .text-emerald-300 {
color: #059669 !important;
}
:root.light .text-cyan-300 {
color: #0891b2 !important;
}
:root.light .text-rose-300 {
color: #be123c !important;
}
:root.light .text-indigo-300 {
color: #4338ca !important;
}
/* ── Missing border opacity variants ─────────────────────────────── */
/* slate-800 with opacity */
:root.light .border-slate-800\/50,
:root.light .border-slate-800\/40 {
border-color: var(--border) !important;
}
/* slate-700 with opacity */
:root.light .border-slate-700\/40,
:root.light .border-slate-700\/50,
:root.light .border-slate-700\/60 {
border-color: var(--border) !important;
}
/* slate-900 opacity (section dividers, row separators) */
:root.light .border-slate-900\/30,
:root.light .border-slate-900\/40,
:root.light .border-slate-900\/60 {
border-color: var(--border) !important;
}
/* emerald borders solid and opacity variants */
:root.light .border-emerald-900,
:root.light .border-emerald-900\/30,
:root.light .border-emerald-900\/40,
:root.light .border-emerald-900\/60 {
border-color: #6ee7b7 !important;
}
:root.light .border-emerald-800\/50,
:root.light .border-emerald-800\/60 {
border-color: #a7f3d0 !important;
}
/* cyan borders */
:root.light .border-cyan-900\/50,
:root.light .border-cyan-900\/60 {
border-color: #67e8f9 !important;
}
/* amber borders */
:root.light .border-amber-900\/40,
:root.light .border-amber-900\/60 {
border-color: #fde68a !important;
}
:root.light .border-amber-800\/50,
:root.light .border-amber-800\/60 {
border-color: #fde68a !important;
}
/* rose borders */
:root.light .border-rose-900\/30,
:root.light .border-rose-900\/50,
:root.light .border-rose-900\/60 {
border-color: #fca5a5 !important;
}
/* indigo borders */
:root.light .border-indigo-900,
:root.light .border-indigo-900\/40,
:root.light .border-indigo-900\/50 {
border-color: #a5b4fc !important;
}
/* violet borders */
:root.light .border-violet-900\/50,
:root.light .border-violet-950\/40 {
border-color: #c4b5fd !important;
}
/* ── Missing bg-slate-900 opacity variants ───────────────────────── */
:root.light .bg-slate-900\/30,
:root.light .bg-slate-900\/50,
:root.light .bg-slate-900\/70,
:root.light .bg-slate-900\/90 {
background-color: #f1f5f9 !important;
}
/* ── bg-slate-800 additional variant ────────────────────────────── */
:root.light .bg-slate-800\/40 {
background-color: #e2e8f0 !important;
}
/* ── Hover-state overrides for dark bg classes ───────────────────── */
/* Without these the hover flashes a dark background in light mode. */
:root.light .hover\:bg-slate-900:hover {
background-color: #f1f5f9 !important;
}
:root.light .hover\:bg-slate-900\/35:hover,
:root.light .hover\:bg-slate-900\/40:hover,
:root.light .hover\:bg-slate-900\/60:hover,
:root.light .hover\:bg-slate-900\/70:hover,
:root.light .hover\:bg-slate-900\/80:hover {
background-color: #f1f5f9 !important;
}
:root.light .hover\:bg-slate-800:hover {
background-color: #e2e8f0 !important;
}
:root.light .hover\:bg-slate-800\/80:hover {
background-color: #e2e8f0 !important;
}
:root.light .hover\:bg-slate-950\/30:hover,
:root.light .hover\:bg-slate-950\/40:hover {
background-color: #f8fafc !important;
}
/* Coloured hover states */
:root.light .hover\:bg-emerald-900\/40:hover,
:root.light .hover\:bg-emerald-900\/60:hover {
background-color: #a7f3d0 !important;
}
:root.light .hover\:bg-rose-900\/60:hover {
background-color: #fecdd3 !important;
}
:root.light .hover\:bg-red-950\/40:hover {
background-color: #fee2e2 !important;
}
/* ── Border-dashed empty slots (calendar grid) ───────────────────── */
:root.light .border-slate-800\/40 {
border-color: var(--border) !important;
}
:root.light .hover\:border-slate-700\/60:hover {
border-color: var(--border-muted) !important;
}
/* ── Hover text colors prevent near-white text on light backgrounds */
:root.light .hover\:text-white:hover,
:root.light .hover\:text-slate-100:hover,
:root.light .hover\:text-slate-200:hover {
color: var(--text) !important;
}
:root.light .group:hover .group-hover\:text-white {
color: var(--text) !important;
}
:root.light .group:hover .group-hover\:text-slate-300 {
color: var(--text-muted) !important;
}
/* ── Settings → Caddy section: sky accent ─────────────────────────── */
/* sky-* is used only by the Caddy card; map its dark tokens to light. */
:root.light .bg-sky-950\/60,
:root.light .bg-sky-950\/40,
:root.light .bg-sky-900\/40 {
background-color: #e0f2fe !important;
border-color: #7dd3fc !important;
color: #0369a1 !important;
}
:root.light .border-sky-900\/50,
:root.light .border-sky-900\/40 {
border-color: #7dd3fc !important;
}
:root.light .text-sky-400,
:root.light .text-sky-500 {
color: #0284c7 !important;
}
:root.light .bg-sky-950\/30,
:root.light .hover\:bg-sky-950\/30:hover {
background-color: #e0f2fe !important;
}
:root.light .hover\:bg-sky-900\/40:hover {
background-color: #bae6fd !important;
}
:root.light .hover\:text-sky-400:hover {
color: #0284c7 !important;
}
/* Delete-icon hover (red-950/30 is the only red opacity not yet mapped) */
:root.light .bg-red-950\/30,
:root.light .hover\:bg-red-950\/30:hover {
background-color: #fee2e2 !important;
}