From be007791dca31a660727145bcd8b0f379a37bd47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Br=C3=BCckner?= Date: Wed, 10 Jun 2026 15:08:35 +0200 Subject: [PATCH] =?UTF-8?q?refactor(db):=20rename=20redirect=5Fpath?= =?UTF-8?q?=E2=86=92redirect,=20add=20uid/addLog=20helpers,=20simplify=20C?= =?UTF-8?q?addy=20CRUD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename caddy.redirect_path to caddy.redirect across schema, server, frontend and docs - Remove obsolete ALTER TABLE migration (fresh-install model has no migrations) - Move uid() from server.ts to server-db.ts for shared use - Add addLog() general helper (prepared statement, shared timestamp support) and replace ~24 inline INSERT INTO logs calls throughout server.ts - Caddy CRUD now takes CaddyRouteInput object instead of positional arguments; add/update reuse getCaddyRouteById() to avoid duplicate SELECT --- ARCHITECTURE.md | 8 +- server-db.ts | 69 +++++++++++----- server.ts | 155 ++++++++++++++---------------------- src/components/Settings.tsx | 10 +-- 4 files changed, 117 insertions(+), 125 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 91ec25c..dd0f1a0 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -282,7 +282,7 @@ CREATE TABLE IF NOT EXISTS caddy ( upstream TEXT NOT NULL, tls INTEGER NOT NULL DEFAULT 1, compress INTEGER NOT NULL DEFAULT 1, - redirect_path TEXT NOT NULL DEFAULT '', -- optional 'redir / ' for the bare root + redirect TEXT NOT NULL DEFAULT '', -- optional 'redir / ' for the bare root created_at TEXT DEFAULT (datetime('now')) ); ``` @@ -464,8 +464,8 @@ Manual: POST /api/semaphore/trigger/{bookingId} body { type: 'setup'|'teardown ``` buildCaddyfile(): { local_certs } # global block - per custom route { [encode] [tls internal] [redir / ] reverse_proxy { … } } - redirect_path set → `redir / ` redirects only the bare root '/' + per custom route { [encode] [tls internal] [redir / ] reverse_proxy { … } } + redirect set → `redir / ` redirects only the bare root '/' (other paths pass through; e.g. CheckMK served at //check_mk/) every reverse_proxy block carries standard forwarding headers: header_up X-Forwarded-Proto {scheme} @@ -716,7 +716,7 @@ Backup : ghostgrid.db + ghostgrid.db-wal + ghostgrid.db-shm ``` GhostGrid/ +-- server.ts # Express app: all routes, auth, integrations, background jobs -+-- server-db.ts # SQLite connection, full schema, settings/Caddy helpers ++-- server-db.ts # SQLite connection, full schema, settings/Caddy/log helpers (uid, addLog) +-- index.html # Vite HTML entry (#root > src/main.tsx); links /favicon.svg +-- public/ | +-- favicon.svg # app favicon (GhostGrid logo; served at site root by Vite) diff --git a/server-db.ts b/server-db.ts index cc560d5..bdb0dd0 100644 --- a/server-db.ts +++ b/server-db.ts @@ -3,6 +3,9 @@ import path from 'path'; export const DB_FILE = path.join(process.cwd(), 'ghostgrid.db'); +/** App-generated primary key: `${prefix}-${epochMs}-${rand}` (e.g. `log-…`, `dev-…`). */ +export const uid = (prefix: string) => `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; + console.log(`[Database] Connecting to SQLite database at: ${DB_FILE}`); const db = new Database(DB_FILE); @@ -91,14 +94,11 @@ db.exec(` upstream TEXT NOT NULL, tls INTEGER NOT NULL DEFAULT 1, compress INTEGER NOT NULL DEFAULT 1, - redirect_path TEXT NOT NULL DEFAULT '', + redirect TEXT NOT NULL DEFAULT '', created_at TEXT DEFAULT (datetime('now')) ); `); -// redirect_path was added later; ensure it exists on databases created before it. -try { db.exec("ALTER TABLE caddy ADD COLUMN redirect_path TEXT NOT NULL DEFAULT ''"); } catch { /* column already exists */ } - // Seed default settings — INSERT OR IGNORE writes a key only if it is absent. const DEFAULT_SETTINGS: Record = { azure_enabled: 'false', @@ -117,7 +117,7 @@ const DEFAULT_SETTINGS: Record = { semaphore_api_token: '', semaphore_project_id: '', caddy_enabled: 'false', - caddy_admin_url: 'http://localhost:2019', + caddy_admin_url: 'http://127.0.0.1:2019', }; const seedSetting = db.prepare('INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)'); @@ -137,39 +137,68 @@ export function getAllSettings(): Record { return Object.fromEntries(rows.map(r => [r.key, r.value])); } +const insertLog = db.prepare( + 'INSERT INTO logs (id, timestamp, type, message, deviceId, userId) VALUES (?, ?, ?, ?, ?, ?)', +); + +/** + * Append a logbook entry. `deviceId`/`userId` default to NULL; `timestamp` + * defaults to now (pass one to share a single timestamp across a batch). + * Returns the generated log id. + */ +export function addLog( + type: string, + message: string, + opts: { deviceId?: string | null; userId?: string | null; timestamp?: string } = {}, +): string { + const id = uid('log'); + insertLog.run(id, opts.timestamp ?? new Date().toISOString(), type, message, opts.deviceId ?? null, opts.userId ?? null); + return id; +} + +/** A reverse-proxy route as stored (booleans are SQLite 0/1 integers). */ export interface CaddyRoute { id: number; hostname: string; upstream: string; tls: number; compress: number; - redirect_path: string; + redirect: string; created_at: string; } +/** Fields a caller supplies to create or update a route (JS booleans). */ +export interface CaddyRouteInput { + hostname: string; + upstream: string; + tls: boolean; + compress: boolean; + redirect?: string; +} + export function getCaddyRoutes(): CaddyRoute[] { return db.prepare('SELECT * FROM caddy ORDER BY id ASC').all() as CaddyRoute[]; } -export function addCaddyRoute(hostname: string, upstream: string, tls: boolean, compress: boolean, redirectPath = ''): CaddyRoute { - const { lastInsertRowid } = db.prepare( - 'INSERT INTO caddy (hostname, upstream, tls, compress, redirect_path) VALUES (?, ?, ?, ?, ?)' - ).run(hostname, upstream, tls ? 1 : 0, compress ? 1 : 0, redirectPath); - return db.prepare('SELECT * FROM caddy WHERE id = ?').get(lastInsertRowid) as CaddyRoute; +export function getCaddyRouteById(id: number): CaddyRoute | undefined { + return db.prepare('SELECT * FROM caddy WHERE id = ?').get(id) as CaddyRoute | undefined; } -export function updateCaddyRoute(id: number, hostname: string, upstream: string, tls: boolean, compress: boolean, redirectPath = ''): CaddyRoute { - db.prepare('UPDATE caddy SET hostname = ?, upstream = ?, tls = ?, compress = ?, redirect_path = ? WHERE id = ?') - .run(hostname, upstream, tls ? 1 : 0, compress ? 1 : 0, redirectPath, id); - return db.prepare('SELECT * FROM caddy WHERE id = ?').get(id) as CaddyRoute; +export function addCaddyRoute(route: CaddyRouteInput): CaddyRoute { + const { lastInsertRowid } = db.prepare( + 'INSERT INTO caddy (hostname, upstream, tls, compress, redirect) VALUES (?, ?, ?, ?, ?)', + ).run(route.hostname, route.upstream, route.tls ? 1 : 0, route.compress ? 1 : 0, route.redirect ?? ''); + return getCaddyRouteById(Number(lastInsertRowid))!; +} + +export function updateCaddyRoute(id: number, route: CaddyRouteInput): CaddyRoute { + db.prepare('UPDATE caddy SET hostname = ?, upstream = ?, tls = ?, compress = ?, redirect = ? WHERE id = ?') + .run(route.hostname, route.upstream, route.tls ? 1 : 0, route.compress ? 1 : 0, route.redirect ?? '', id); + return getCaddyRouteById(id)!; } export function deleteCaddyRoute(id: number): void { db.prepare('DELETE FROM caddy WHERE id = ?').run(id); } -export function getCaddyRouteById(id: number): CaddyRoute | undefined { - return db.prepare('SELECT * FROM caddy WHERE id = ?').get(id) as CaddyRoute | undefined; -} - -export default db; +export default db; \ No newline at end of file diff --git a/server.ts b/server.ts index e54b966..5531859 100644 --- a/server.ts +++ b/server.ts @@ -7,11 +7,9 @@ import bcrypt from 'bcryptjs'; import jwt from 'jsonwebtoken'; import DatabaseConstructor from 'better-sqlite3'; import { ConfidentialClientApplication } from '@azure/msal-node'; -import db, { getSetting, setSetting, getAllSettings, getCaddyRoutes, addCaddyRoute, updateCaddyRoute, deleteCaddyRoute, getCaddyRouteById, DB_FILE } from './server-db'; +import db, { uid, addLog, getSetting, setSetting, getAllSettings, getCaddyRoutes, addCaddyRoute, updateCaddyRoute, deleteCaddyRoute, getCaddyRouteById, DB_FILE } from './server-db'; import { Device, LabTemplate, Booking, LogEntry, User, QuickLink } from './src/types'; -const uid = (prefix: string) => `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; - const JWT_SECRET = process.env.JWT_SECRET || 'ghostgrid-dev-secret-change-in-production'; const JWT_EXPIRY = '24h'; @@ -87,10 +85,10 @@ function buildCaddyfile(): string { lines.push(`${route.hostname} {`); if (route.compress) lines.push(' encode zstd gzip'); if (route.tls) lines.push(' tls internal'); - if (route.redirect_path) { + if (route.redirect) { // Redirect only the bare root ('/') to the given path — other paths pass // through to the backend unchanged (e.g. CheckMK at //check_mk/). - const target = route.redirect_path.startsWith('/') ? route.redirect_path : `/${route.redirect_path}`; + const target = route.redirect.startsWith('/') ? route.redirect : `/${route.redirect}`; lines.push(` redir / ${target}`); } lines.push(` reverse_proxy ${route.upstream} {`); @@ -138,24 +136,21 @@ function importCaddyfileRoutes(userId?: string): void { const upstream = upstreamMatch[1]; const tls = /tls\s+internal/.test(block); const compress = /encode/.test(block); - addCaddyRoute(hostname, upstream, tls, compress); + addCaddyRoute({ hostname, upstream, tls, compress }); imported.push(`${hostname} → ${upstream}`); } } i++; } if (imported.length > 0) { - db.prepare('INSERT INTO logs (id, timestamp, type, message, deviceId, userId) VALUES (?, ?, ?, ?, ?, ?)') - .run(uid('log'), new Date().toISOString(), 'system', - `Caddy: imported ${imported.length} route(s) from Caddyfile — ${imported.join(', ')}`, - null, userId ?? null); + addLog('system', `Caddy: imported ${imported.length} route(s) from Caddyfile — ${imported.join(', ')}`, { userId }); } } async function pushCaddyConfig(): Promise { if (!IS_PRODUCTION) return; if (getSetting('caddy_enabled') !== 'true') return; - const adminUrl = getSetting('caddy_admin_url') || 'http://localhost:2019'; + const adminUrl = getSetting('caddy_admin_url') || 'http://127.0.0.1:2019'; const body = buildCaddyfile(); const res = await fetch(`${adminUrl}/load`, { method: 'POST', @@ -241,9 +236,7 @@ async function startServer() { const token = jwt.sign({ userId: row.id, email: row.email }, JWT_SECRET, { expiresIn: JWT_EXPIRY }); const user: User = { id: row.id, name: row.name, role: row.role, email: row.email }; - const logId = uid("log"); - db.prepare(`INSERT INTO logs (id, timestamp, type, message, deviceId, userId) VALUES (?, ?, ?, ?, ?, ?)`) - .run(logId, new Date().toISOString(), 'system', `${row.name} logged in.`, null, row.id); + addLog('system', `${row.name} logged in.`, { userId: row.id }); res.json({ token, user }); } catch (err: any) { @@ -269,7 +262,7 @@ async function startServer() { const clientId = getSetting('azure_client_id'); const tenantId = getSetting('azure_tenant_id'); const secret = getSetting('azure_client_secret'); - const appUrl = process.env.APP_URL || `http://localhost:${PORT}`; + const appUrl = process.env.APP_URL || `http://127.0.0.1:${PORT}`; const effectiveRedirectUri = getSetting('azure_redirect_uri') || `${appUrl}/api/auth/azure/callback`; const cmkApiUrl = getSetting('checkmk_api_url') || process.env.CHECKMK_API_URL || ''; res.json({ @@ -287,7 +280,7 @@ async function startServer() { if (!msalClient) { return res.redirect('/?auth_error=Azure+login+not+configured'); } - const appUrl = process.env.APP_URL || `http://localhost:${PORT}`; + const appUrl = process.env.APP_URL || `http://127.0.0.1:${PORT}`; const redirectUri = getSetting('azure_redirect_uri') || `${appUrl}/api/auth/azure/callback`; try { const authCodeUrl = await msalClient.getAuthCodeUrl({ @@ -315,7 +308,7 @@ async function startServer() { if (!msalClient) { return res.redirect('/?auth_error=Azure+login+not+configured'); } - const appUrl = process.env.APP_URL || `http://localhost:${PORT}`; + const appUrl = process.env.APP_URL || `http://127.0.0.1:${PORT}`; const redirectUri = getSetting('azure_redirect_uri') || `${appUrl}/api/auth/azure/callback`; try { const result = await msalClient.acquireTokenByCode({ @@ -457,11 +450,9 @@ async function startServer() { VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `).run(id, hostname, ip, location || '', notes || '', type, status || 'unknown', emergencySheet || '', lastCheckedAt || null); - const logId = uid("log"); - db.prepare(`INSERT INTO logs (id, timestamp, type, message, deviceId, userId) VALUES (?, ?, ?, ?, ?, ?)`) - .run(logId, new Date().toISOString(), 'maintenance', - `Provisioned a new host candidate "${hostname}" (${ip}) inside location ${location || 'Inventory'}.`, - id, req.user!.userId); + addLog('maintenance', + `Provisioned a new host candidate "${hostname}" (${ip}) inside location ${location || 'Inventory'}.`, + { deviceId: id, userId: req.user!.userId }); const device = db.prepare('SELECT * FROM devices WHERE id = ?').get(id) as Device; res.status(201).json(device); @@ -480,11 +471,10 @@ async function startServer() { WHERE id = ? `).run(hostname, ip, location, notes, type, status, emergencySheet, lastCheckedAt ?? null, id); - const logId = uid("log"); const operatorText = operatorName ? `${operatorName} finished ` : 'Updated '; - db.prepare(`INSERT INTO logs (id, timestamp, type, message, deviceId, userId) VALUES (?, ?, ?, ?, ?, ?)`) - .run(logId, new Date().toISOString(), 'maintenance', - `${operatorText}refining the device specifications for "${hostname}".`, id, req.user!.userId); + addLog('maintenance', + `${operatorText}refining the device specifications for "${hostname}".`, + { deviceId: id, userId: req.user!.userId }); const device = db.prepare('SELECT * FROM devices WHERE id = ?').get(id) as Device; res.json(device); @@ -513,11 +503,9 @@ async function startServer() { ); } - const logId = uid("log"); - db.prepare(`INSERT INTO logs (id, timestamp, type, message, deviceId, userId) VALUES (?, ?, ?, ?, ?, ?)`) - .run(logId, new Date().toISOString(), 'maintenance', - `Permanently removed the host device "${dev.hostname || id}" from the inventory records.`, - null, req.user!.userId); + addLog('maintenance', + `Permanently removed the host device "${dev.hostname || id}" from the inventory records.`, + { userId: req.user!.userId }); res.json({ success: true, message: 'Device deleted successfully and cleaned from topologies.' }); } catch (err: any) { @@ -555,11 +543,9 @@ async function startServer() { db.prepare(`INSERT INTO labs (id, name, description, contactPerson, location, deviceIds, topology, semaphoreSetupTemplateId, semaphoreTeardownTemplateId) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`) .run(id, name, description || '', contactPerson || '', location || '', JSON.stringify(deviceIds), JSON.stringify(topology || []), semaphoreSetupTemplateId || '', semaphoreTeardownTemplateId || ''); - const logId = uid("log"); - db.prepare(`INSERT INTO logs (id, timestamp, type, message, userId) VALUES (?, ?, ?, ?, ?)`) - .run(logId, new Date().toISOString(), 'maintenance', - `Released a new lab template profile "${name}" (${location || 'Staging Area'}) for engineering teams.`, - req.user!.userId); + addLog('maintenance', + `Released a new lab template profile "${name}" (${location || 'Staging Area'}) for engineering teams.`, + { userId: req.user!.userId }); const r = db.prepare('SELECT * FROM labs WHERE id = ?').get(id) as any; res.status(201).json({ id: r.id, name: r.name, description: r.description, contactPerson: r.contactPerson, location: r.location, deviceIds: JSON.parse(r.deviceIds), topology: JSON.parse(r.topology), semaphoreSetupTemplateId: r.semaphoreSetupTemplateId || '', semaphoreTeardownTemplateId: r.semaphoreTeardownTemplateId || '' }); @@ -576,10 +562,9 @@ async function startServer() { db.prepare(`UPDATE labs SET name = ?, description = ?, contactPerson = ?, location = ?, deviceIds = ?, topology = ?, semaphoreSetupTemplateId = ?, semaphoreTeardownTemplateId = ? WHERE id = ?`) .run(name, description, contactPerson, location, JSON.stringify(deviceIds), JSON.stringify(topology), semaphoreSetupTemplateId || '', semaphoreTeardownTemplateId || '', id); - const logId = uid("log"); - db.prepare(`INSERT INTO logs (id, timestamp, type, message, userId) VALUES (?, ?, ?, ?, ?)`) - .run(logId, new Date().toISOString(), 'maintenance', - `Modified the active topology mapping schema for the "${name}" lab template.`, req.user!.userId); + addLog('maintenance', + `Modified the active topology mapping schema for the "${name}" lab template.`, + { userId: req.user!.userId }); const r = db.prepare('SELECT * FROM labs WHERE id = ?').get(id) as any; res.json({ id: r.id, name: r.name, description: r.description, contactPerson: r.contactPerson, location: r.location, deviceIds: JSON.parse(r.deviceIds), topology: JSON.parse(r.topology), semaphoreSetupTemplateId: r.semaphoreSetupTemplateId || '', semaphoreTeardownTemplateId: r.semaphoreTeardownTemplateId || '' }); @@ -597,10 +582,9 @@ async function startServer() { db.prepare('DELETE FROM labs WHERE id = ?').run(id); db.prepare(`UPDATE bookings SET status = 'cancelled' WHERE labId = ? AND status = 'upcoming'`).run(id); - const logId = uid("log"); - db.prepare(`INSERT INTO logs (id, timestamp, type, message, userId) VALUES (?, ?, ?, ?, ?)`) - .run(logId, new Date().toISOString(), 'booking', - `Withdrew the lab testing template "${lab.name || id}".`, req.user!.userId); + addLog('booking', + `Withdrew the lab testing template "${lab.name || id}".`, + { userId: req.user!.userId }); res.json({ success: true, message: 'Lab template deleted and future reservations cancelled.' }); } catch (err: any) { @@ -642,14 +626,13 @@ async function startServer() { .run(id, labId, userId, startDateTime, endDateTime, notes || '', status || 'upcoming'); const lab = db.prepare('SELECT name FROM labs WHERE id = ?').get(labId) as { name: string } | undefined; - const logId = uid("log"); const operatorText = operatorName || 'An operator'; const startF = new Date(startDateTime).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); const endF = new Date(endDateTime).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); - db.prepare(`INSERT INTO logs (id, timestamp, type, message, userId) VALUES (?, ?, ?, ?, ?)`) - .run(logId, new Date().toISOString(), 'booking', - `${operatorText} booked the lab template "${lab?.name || 'Unknown'}" from ${startF} to ${endF}.`, userId); + addLog('booking', + `${operatorText} booked the lab template "${lab?.name || 'Unknown'}" from ${startF} to ${endF}.`, + { userId }); const r = db.prepare('SELECT * FROM bookings WHERE id = ?').get(id) as any; res.status(201).json({ @@ -673,11 +656,9 @@ async function startServer() { if (status === 'cancelled') { const lab = db.prepare('SELECT name, semaphoreTeardownTemplateId FROM labs WHERE id = ?').get(booking.labId) as { name: string; semaphoreTeardownTemplateId: string } | undefined; - const logId = uid("log"); - db.prepare(`INSERT INTO logs (id, timestamp, type, message, userId) VALUES (?, ?, ?, ?, ?)`) - .run(logId, new Date().toISOString(), 'booking', - `${operatorName || 'An operator'} canceled the reservation for lab template "${lab?.name || 'Unknown'}" and released associated nodes back into the pool.`, - req.user!.userId); + addLog('booking', + `${operatorName || 'An operator'} canceled the reservation for lab template "${lab?.name || 'Unknown'}" and released associated nodes back into the pool.`, + { userId: req.user!.userId }); // Trigger teardown if booking had already started and teardown not yet triggered const now = new Date(); @@ -718,10 +699,9 @@ async function startServer() { db.prepare('DELETE FROM bookings WHERE id = ?').run(id); const lab = db.prepare('SELECT name FROM labs WHERE id = ?').get(booking.labId) as { name: string } | undefined; - const logId = uid("log"); - db.prepare(`INSERT INTO logs (id, timestamp, type, message, userId) VALUES (?, ?, ?, ?, ?)`) - .run(logId, new Date().toISOString(), 'booking', - `Permanently deleted the reservation records for lab "${lab?.name || 'Unknown'}".`, req.user!.userId); + addLog('booking', + `Permanently deleted the reservation records for lab "${lab?.name || 'Unknown'}".`, + { userId: req.user!.userId }); res.json({ success: true, message: 'Reservation and its logs have been resolved correctly.' }); } catch (err: any) { @@ -748,9 +728,7 @@ async function startServer() { return res.status(400).json({ error: 'Missing log message or classification type.' }); } - const id = uid("log"); - db.prepare(`INSERT INTO logs (id, timestamp, type, message, deviceId, userId) VALUES (?, ?, ?, ?, ?, ?)`) - .run(id, new Date().toISOString(), type, message, deviceId || null, userId || null); + const id = addLog(type, message, { deviceId: deviceId || null, userId: userId || null }); const log = db.prepare('SELECT * FROM logs WHERE id = ?').get(id) as LogEntry; res.status(201).json(log); @@ -929,8 +907,7 @@ async function startServer() { const CHECKMK_API_SECRET = getSetting('checkmk_api_secret') || process.env.CHECKMK_API_SECRET; if (!CHECKMK_API_URL || !CHECKMK_API_SECRET) { - db.prepare('INSERT INTO logs (id, timestamp, type, message) VALUES (?, ?, ?, ?)') - .run(uid('log'), now, 'system', 'CheckMK sync skipped — API URL or secret not configured. Check Settings.'); + addLog('system', 'CheckMK sync skipped — API URL or secret not configured. Check Settings.', { timestamp: now }); return; } @@ -958,8 +935,7 @@ async function startServer() { } catch (err: any) { const msg = `CheckMK sync failed — could not fetch host list: ${err?.message ?? err}`; console.error('[CheckMK]', msg); - db.prepare('INSERT INTO logs (id, timestamp, type, message) VALUES (?, ?, ?, ?)') - .run(uid('log'), now, 'system', msg); + addLog('system', msg, { timestamp: now }); return; } @@ -975,8 +951,7 @@ async function startServer() { if (!cmkHost) { if (dev.status !== 'unknown') { db.prepare('UPDATE devices SET status = ?, lastCheckedAt = ?, cmkHostname = ? WHERE id = ?').run('unknown', now, '', dev.id); - db.prepare('INSERT INTO logs (id, timestamp, type, message, deviceId) VALUES (?, ?, ?, ?, ?)') - .run(uid('log'), now, 'status', `CheckMK: ${dev.hostname} (${dev.ip}) not found in monitoring — status set to unknown.`, dev.id); + addLog('status', `CheckMK: ${dev.hostname} (${dev.ip}) not found in monitoring — status set to unknown.`, { deviceId: dev.id, timestamp: now }); } counts.unknown++; continue; @@ -993,22 +968,20 @@ async function startServer() { const newStatus = state === 0 ? 'online' : state === 1 || state === 2 ? 'offline' : 'unknown'; db.prepare('UPDATE devices SET status = ?, lastCheckedAt = ?, cmkHostname = ? WHERE id = ?').run(newStatus, now, cmkHost, dev.id); if (dev.status !== newStatus) { - db.prepare('INSERT INTO logs (id, timestamp, type, message, deviceId) VALUES (?, ?, ?, ?, ?)') - .run(uid('log'), now, 'status', `CheckMK: ${dev.hostname} (${dev.ip}) status changed to ${newStatus} (was: ${dev.status}).`, dev.id); + addLog('status', `CheckMK: ${dev.hostname} (${dev.ip}) status changed to ${newStatus} (was: ${dev.status}).`, { deviceId: dev.id, timestamp: now }); } counts[newStatus as 'online' | 'offline' | 'unknown']++; } catch (err: any) { const msg = `CheckMK: status sync failed for ${dev.hostname} (${dev.ip}) — ${err?.message ?? err}`; console.error('[CheckMK]', msg); - db.prepare('INSERT INTO logs (id, timestamp, type, message, deviceId) VALUES (?, ?, ?, ?, ?)') - .run(uid('log'), now, 'system', msg, dev.id); + addLog('system', msg, { deviceId: dev.id, timestamp: now }); counts.unknown++; } } - db.prepare('INSERT INTO logs (id, timestamp, type, message) VALUES (?, ?, ?, ?)') - .run(uid('log'), now, 'system', - `CheckMK sync completed — ${counts.online} online, ${counts.offline} offline, ${counts.unknown} unknown (${rows.length} devices total).`); + addLog('system', + `CheckMK sync completed — ${counts.online} online, ${counts.offline} offline, ${counts.unknown} unknown (${rows.length} devices total).`, + { timestamp: now }); } async function scheduleSync() { @@ -1034,14 +1007,12 @@ async function startServer() { // as CheckMK. Template IDs are configured per lab template. // ------------------------------------------------------------- async function triggerSemaphoreTask(templateId: number, extraVars: Record): Promise { - const now = new Date().toISOString(); const apiUrl = getSetting('semaphore_api_url'); const token = getSetting('semaphore_api_token'); const projectId = getSetting('semaphore_project_id'); if (!apiUrl || !token || !projectId) { - db.prepare('INSERT INTO logs (id, timestamp, type, message) VALUES (?, ?, ?, ?)') - .run(uid('log'), now, 'system', 'Semaphore trigger skipped — API URL, token, or project ID not configured. Check Settings.'); + addLog('system', 'Semaphore trigger skipped — API URL, token, or project ID not configured. Check Settings.'); return null; } @@ -1064,14 +1035,12 @@ async function startServer() { } const data = await res.json() as { id?: number }; const jobId = data?.id ?? null; - db.prepare('INSERT INTO logs (id, timestamp, type, message) VALUES (?, ?, ?, ?)') - .run(uid('log'), now, 'booking', `Semaphore: triggered template #${templateId} > job #${jobId} (booking ${extraVars.booking_id}).`); + addLog('booking', `Semaphore: triggered template #${templateId} > job #${jobId} (booking ${extraVars.booking_id}).`); return jobId; } catch (err: any) { const msg = `Semaphore trigger failed for template #${templateId} — ${err?.message ?? err}`; console.error('[Semaphore]', msg); - db.prepare('INSERT INTO logs (id, timestamp, type, message) VALUES (?, ?, ?, ?)') - .run(uid('log'), now, 'system', msg); + addLog('system', msg); return null; } } @@ -1187,7 +1156,7 @@ async function startServer() { // ------------------------------------------------------------- app.get('/api/caddy/status', requireAuth, async (_req, res) => { try { - const adminUrl = getSetting('caddy_admin_url') || 'http://localhost:2019'; + const adminUrl = getSetting('caddy_admin_url') || 'http://127.0.0.1:2019'; const r = await fetch(`${adminUrl}/config/`, { signal: AbortSignal.timeout(2000) }); res.json({ available: r.ok }); } catch { @@ -1206,16 +1175,14 @@ async function startServer() { app.post('/api/caddy/routes', requireAuth, async (req, res) => { try { if (!IS_PRODUCTION) return res.status(403).json({ error: 'Caddy is managed by another instance.' }); - const { hostname, upstream, tls, compress, redirectPath } = req.body as { - hostname?: string; upstream?: string; tls?: boolean; compress?: boolean; redirectPath?: string; + const { hostname, upstream, tls, compress, redirect } = req.body as { + hostname?: string; upstream?: string; tls?: boolean; compress?: boolean; redirect?: string; }; if (!hostname || !upstream) return res.status(400).json({ error: 'hostname and upstream are required.' }); if (getCaddyRoutes().some(r => r.hostname === hostname.trim())) return res.status(409).json({ error: `Route for ${hostname.trim()} already exists.` }); - const route = addCaddyRoute(hostname.trim(), upstream.trim(), tls !== false, compress !== false, (redirectPath ?? '').trim()); - db.prepare('INSERT INTO logs (id, timestamp, type, message, deviceId, userId) VALUES (?, ?, ?, ?, ?, ?)') - .run(uid('log'), new Date().toISOString(), 'system', - `Caddy route added: ${hostname.trim()} → ${upstream.trim()}`, null, req.user!.userId); + const route = addCaddyRoute({ hostname: hostname.trim(), upstream: upstream.trim(), tls: tls !== false, compress: compress !== false, redirect: (redirect ?? '').trim() }); + addLog('system', `Caddy route added: ${hostname.trim()} → ${upstream.trim()}`, { userId: req.user!.userId }); pushCaddyConfig().catch(err => console.warn('[Caddy] Could not push config after route add:', err.message)); res.json(route); } catch (err: any) { @@ -1228,14 +1195,12 @@ async function startServer() { if (!IS_PRODUCTION) return res.status(403).json({ error: 'Caddy is managed by another instance.' }); const id = Number(req.params.id); if (!id) return res.status(400).json({ error: 'Invalid route id.' }); - const { hostname, upstream, tls, compress, redirectPath } = req.body as { - hostname?: string; upstream?: string; tls?: boolean; compress?: boolean; redirectPath?: string; + const { hostname, upstream, tls, compress, redirect } = req.body as { + hostname?: string; upstream?: string; tls?: boolean; compress?: boolean; redirect?: string; }; if (!hostname || !upstream) return res.status(400).json({ error: 'hostname and upstream are required.' }); - const route = updateCaddyRoute(id, hostname.trim(), upstream.trim(), tls !== false, compress !== false, (redirectPath ?? '').trim()); - db.prepare('INSERT INTO logs (id, timestamp, type, message, deviceId, userId) VALUES (?, ?, ?, ?, ?, ?)') - .run(uid('log'), new Date().toISOString(), 'system', - `Caddy route updated: ${hostname.trim()} → ${upstream.trim()}`, null, req.user!.userId); + const route = updateCaddyRoute(id, { hostname: hostname.trim(), upstream: upstream.trim(), tls: tls !== false, compress: compress !== false, redirect: (redirect ?? '').trim() }); + addLog('system', `Caddy route updated: ${hostname.trim()} → ${upstream.trim()}`, { userId: req.user!.userId }); pushCaddyConfig().catch(err => console.warn('[Caddy] Could not push config after route update:', err.message)); res.json(route); } catch (err: any) { @@ -1251,9 +1216,7 @@ async function startServer() { const existing = getCaddyRouteById(id); deleteCaddyRoute(id); if (existing) { - db.prepare('INSERT INTO logs (id, timestamp, type, message, deviceId, userId) VALUES (?, ?, ?, ?, ?, ?)') - .run(uid('log'), new Date().toISOString(), 'system', - `Caddy route deleted: ${existing.hostname} → ${existing.upstream}`, null, req.user!.userId); + addLog('system', `Caddy route deleted: ${existing.hostname} → ${existing.upstream}`, { userId: req.user!.userId }); } pushCaddyConfig().catch(err => console.warn('[Caddy] Could not push config after route delete:', err.message)); res.status(204).send(); diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index 2fbf0d1..dae8e56 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -35,7 +35,7 @@ interface CaddyRoute { upstream: string; tls: number; compress: number; - redirect_path: string; + redirect: string; } interface DbInfo { @@ -384,7 +384,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) { try { const res = await authFetch('/api/caddy/routes', { method: 'POST', - body: JSON.stringify({ hostname: newHostname.trim(), upstream: newUpstream.trim(), tls: newTls, compress: newCompress, redirectPath: newRedirect.trim() }), + body: JSON.stringify({ hostname: newHostname.trim(), upstream: newUpstream.trim(), tls: newTls, compress: newCompress, redirect: newRedirect.trim() }), }); if (!res.ok) { const d = await res.json(); @@ -424,7 +424,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) { setEditUpstream(r.upstream); setEditTls(r.tls === 1); setEditCompress(r.compress === 1); - setEditRedirect(r.redirect_path || ''); + setEditRedirect(r.redirect || ''); } function handleEditCancel() { @@ -437,7 +437,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) { try { const res = await authFetch(`/api/caddy/routes/${id}`, { method: 'PUT', - body: JSON.stringify({ hostname: editHostname.trim(), upstream: editUpstream.trim(), tls: editTls, compress: editCompress, redirectPath: editRedirect.trim() }), + body: JSON.stringify({ hostname: editHostname.trim(), upstream: editUpstream.trim(), tls: editTls, compress: editCompress, redirect: editRedirect.trim() }), }); if (!res.ok) { const d = await res.json(); @@ -1013,7 +1013,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) { {r.upstream} {r.tls ? TLS : null} {r.compress ? GZ : null} - {r.redirect_path ? ↳ {r.redirect_path} : null} + {r.redirect ? ↳ {r.redirect} : null}