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

@ -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);