import 'dotenv/config'; import express, { Request, Response, NextFunction } from 'express'; import path from 'path'; import { createServer as createViteServer } from 'vite'; import bcrypt from 'bcryptjs'; import jwt from 'jsonwebtoken'; import { ConfidentialClientApplication } from '@azure/msal-node'; import db, { getSetting, setSetting, getAllSettings } 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'; interface JwtPayload { userId: string; email: string; } declare global { namespace Express { interface Request { user?: JwtPayload; } } } function requireAuth(req: Request, res: Response, next: NextFunction) { const authHeader = req.headers['authorization']; const token = authHeader?.split(' ')[1]; if (!token) { res.status(401).json({ error: 'Authentication required.' }); return; } try { const payload = jwt.verify(token, JWT_SECRET) as JwtPayload; req.user = payload; next(); } catch { res.status(401).json({ error: 'Invalid or expired token.' }); } } function requireAdmin(req: Request, res: Response, next: NextFunction) { const row = db.prepare('SELECT role FROM users WHERE id = ?').get(req.user!.userId) as { role: string } | undefined; if (!row || row.role.toLowerCase() !== 'admin') { res.status(403).json({ error: 'Admin access required.' }); return; } next(); } function getMsalClient(): ConfidentialClientApplication | null { const clientId = getSetting('azure_client_id'); const tenantId = getSetting('azure_tenant_id'); const secret = getSetting('azure_client_secret'); if (getSetting('azure_enabled') !== 'true' || !clientId || !tenantId || !secret) return null; return new ConfidentialClientApplication({ auth: { clientId, authority: `https://login.microsoftonline.com/${tenantId}`, clientSecret: secret }, }); } const SECRET_KEYS = ['azure_client_secret', 'checkmk_api_secret']; function maskSettings(raw: Record): Record { const out = { ...raw }; for (const k of SECRET_KEYS) { if (out[k]) out[k] = '__SET__'; } return out; } async function startServer() { const app = express(); const PORT = Number(process.env.PORT) || 3000; app.use(express.json()); // ------------------------------------------------------------- // AUTH API // ------------------------------------------------------------- app.post('/api/auth/register', (req, res) => { try { const { name, email, password } = req.body; if (!name || !email || !password) { return res.status(400).json({ error: 'Name, email and password are required.' }); } if (password.length < 8) { return res.status(400).json({ error: 'Password must be at least 8 characters.' }); } const existing = db.prepare('SELECT id FROM users WHERE email = ?').get(email); if (existing) { return res.status(409).json({ error: 'An account with this email already exists.' }); } const passwordHash = bcrypt.hashSync(password, 10); const id = uid("u"); db.prepare('INSERT INTO users (id, name, role, email, password_hash) VALUES (?, ?, ?, ?, ?)') .run(id, name, 'User', email, passwordHash); const user = db.prepare('SELECT id, name, role, email FROM users WHERE id = ?').get(id) as User; const token = jwt.sign({ userId: id, email }, JWT_SECRET, { expiresIn: JWT_EXPIRY }); res.status(201).json({ token, user }); } catch (err: any) { res.status(500).json({ error: err.message }); } }); app.post('/api/auth/login', (req, res) => { try { const { email, password } = req.body; if (!email || !password) { return res.status(400).json({ error: 'Email and password are required.' }); } const row = db.prepare('SELECT * FROM users WHERE email = ?').get(email) as (User & { password_hash: string }) | undefined; if (!row || !bcrypt.compareSync(password, row.password_hash)) { return res.status(401).json({ error: 'Invalid email or password.' }); } 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 }; res.json({ token, user }); } catch (err: any) { res.status(500).json({ error: err.message }); } }); app.get('/api/auth/me', requireAuth, (req, res) => { try { const user = db.prepare('SELECT id, name, role, email FROM users WHERE id = ?').get(req.user!.userId) as User | undefined; if (!user) { return res.status(404).json({ error: 'User not found.' }); } res.json(user); } catch (err: any) { res.status(500).json({ error: err.message }); } }); // Public: frontend checks this before rendering the Azure login button app.get('/api/auth/config', (_req, res) => { const enabled = getSetting('azure_enabled') === 'true'; 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 effectiveRedirectUri = getSetting('azure_redirect_uri') || `${appUrl}/api/auth/azure/callback`; res.json({ azureEnabled: enabled && Boolean(clientId) && Boolean(tenantId) && Boolean(secret), effectiveRedirectUri, }); }); // Start Azure OAuth flow app.get('/api/auth/azure', async (_req, res) => { const msalClient = getMsalClient(); if (!msalClient) { return res.redirect('/?auth_error=Azure+login+not+configured'); } const appUrl = process.env.APP_URL || `http://localhost:${PORT}`; const redirectUri = getSetting('azure_redirect_uri') || `${appUrl}/api/auth/azure/callback`; try { const authCodeUrl = await msalClient.getAuthCodeUrl({ scopes: ['openid', 'profile', 'email'], redirectUri, }); res.redirect(authCodeUrl); } catch (err: any) { console.error('[Azure Auth] getAuthCodeUrl error:', err); res.redirect('/?auth_error=Failed+to+start+Microsoft+login'); } }); // Azure OAuth callback app.get('/api/auth/azure/callback', async (req, res) => { const { code, error, error_description } = req.query; if (error) { const msg = encodeURIComponent(String(error_description || error)); return res.redirect(`/?auth_error=${msg}`); } if (!code) { return res.redirect('/?auth_error=No+authorization+code+received'); } const msalClient = getMsalClient(); if (!msalClient) { return res.redirect('/?auth_error=Azure+login+not+configured'); } const appUrl = process.env.APP_URL || `http://localhost:${PORT}`; const redirectUri = getSetting('azure_redirect_uri') || `${appUrl}/api/auth/azure/callback`; try { const result = await msalClient.acquireTokenByCode({ code: String(code), scopes: ['openid', 'profile', 'email'], redirectUri, }); const email = (result.account?.username ?? '').toLowerCase(); const name = result.account?.name || email; if (!email) { return res.redirect('/?auth_error=No+email+returned+by+Microsoft'); } let user = db.prepare('SELECT id, name, role, email FROM users WHERE email = ?').get(email) as User | undefined; if (!user) { const id = uid("u"); db.prepare('INSERT INTO users (id, name, role, email, password_hash) VALUES (?, ?, ?, ?, ?)') .run(id, name, 'User', email, ''); user = db.prepare('SELECT id, name, role, email FROM users WHERE id = ?').get(id) as User; } const token = jwt.sign({ userId: user.id, email: user.email }, JWT_SECRET, { expiresIn: JWT_EXPIRY }); res.redirect(`/?token=${encodeURIComponent(token)}`); } catch (err: any) { console.error('[Azure Auth] acquireTokenByCode error:', err); res.redirect('/?auth_error=Authentication+failed'); } }); // ------------------------------------------------------------- // RESTFUL API: Settings (admin only) // ------------------------------------------------------------- app.get('/api/settings', requireAuth, (_req, res) => { try { res.json(maskSettings(getAllSettings())); } catch (err: any) { res.status(500).json({ error: err.message }); } }); app.put('/api/settings', requireAuth, (req, res) => { try { const allowed = ['azure_enabled', 'azure_client_id', 'azure_tenant_id', 'azure_client_secret', 'azure_redirect_uri', 'checkmk_api_url', 'checkmk_api_secret', 'checkmk_sync_interval_ms']; const updates = req.body as Record; for (const key of allowed) { if (key in updates && updates[key] !== '__SET__') { setSetting(key, String(updates[key])); } } res.json(maskSettings(getAllSettings())); } catch (err: any) { res.status(500).json({ error: err.message }); } }); // ------------------------------------------------------------- // RESTFUL API: Users // ------------------------------------------------------------- app.get('/api/users', requireAuth, (_req, res) => { try { const users = db.prepare('SELECT id, name, role, email FROM users').all() as User[]; res.json(users); } catch (err: any) { res.status(500).json({ error: err.message }); } }); // ------------------------------------------------------------- // RESTFUL API: Devices / Inventory // ------------------------------------------------------------- app.get('/api/devices', requireAuth, (_req, res) => { try { const devices = db.prepare('SELECT * FROM devices').all() as Device[]; res.json(devices); } catch (err: any) { res.status(500).json({ error: err.message }); } }); app.post('/api/devices', requireAuth, (req, res) => { try { const { hostname, ip, location, notes, type, status, emergencySheet, checkMkUrl, lastCheckedAt } = req.body; if (!hostname || !ip || !type) { return res.status(400).json({ error: 'Missing required device specifications.' }); } const id = uid("dev"); db.prepare(` INSERT INTO devices (id, hostname, ip, location, notes, type, status, emergencySheet, checkMkUrl, lastCheckedAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run(id, hostname, ip, location || '', notes || '', type, status || 'unknown', emergencySheet || '', checkMkUrl || '', 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); const device = db.prepare('SELECT * FROM devices WHERE id = ?').get(id) as Device; res.status(201).json(device); } catch (err: any) { res.status(500).json({ error: err.message }); } }); app.put('/api/devices/:id', requireAuth, (req, res) => { try { const id = req.params.id; const { hostname, ip, location, notes, type, status, emergencySheet, checkMkUrl, lastCheckedAt, operatorName } = req.body; db.prepare(` UPDATE devices SET hostname = ?, ip = ?, location = ?, notes = ?, type = ?, status = ?, emergencySheet = ?, checkMkUrl = ?, lastCheckedAt = ? WHERE id = ? `).run(hostname, ip, location, notes, type, status, emergencySheet, checkMkUrl ?? '', 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); const device = db.prepare('SELECT * FROM devices WHERE id = ?').get(id) as Device; res.json(device); } catch (err: any) { res.status(500).json({ error: err.message }); } }); app.delete('/api/devices/:id', requireAuth, (req, res) => { try { const id = req.params.id; const dev = db.prepare('SELECT * FROM devices WHERE id = ?').get(id) as Device; if (!dev) return res.status(404).json({ error: 'Device not found.' }); db.prepare('DELETE FROM devices WHERE id = ?').run(id); const labs = db.prepare('SELECT * FROM labs').all() as any[]; const updateLabStmt = db.prepare('UPDATE labs SET deviceIds = ?, topology = ? WHERE id = ?'); for (const lab of labs) { const deviceIds: string[] = JSON.parse(lab.deviceIds); const topology: any[] = JSON.parse(lab.topology); updateLabStmt.run( JSON.stringify(deviceIds.filter(dId => dId !== id)), JSON.stringify(topology.filter(t => t.fromDevice !== id && t.toDevice !== id)), lab.id ); } 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); res.json({ success: true, message: 'Device deleted successfully and cleaned from topologies.' }); } catch (err: any) { res.status(500).json({ error: err.message }); } }); // ------------------------------------------------------------- // RESTFUL API: Lab Templates // ------------------------------------------------------------- app.get('/api/labs', requireAuth, (_req, res) => { try { const rows = db.prepare('SELECT * FROM labs').all() as any[]; const labs: LabTemplate[] = rows.map(r => ({ 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) })); res.json(labs); } catch (err: any) { res.status(500).json({ error: err.message }); } }); app.post('/api/labs', requireAuth, (req, res) => { try { const { name, description, contactPerson, location, deviceIds, topology } = req.body; if (!name || !deviceIds || !Array.isArray(deviceIds)) { return res.status(400).json({ error: 'Missing name or associated device configurations.' }); } const id = uid("lab"); db.prepare(`INSERT INTO labs (id, name, description, contactPerson, location, deviceIds, topology) VALUES (?, ?, ?, ?, ?, ?, ?)`) .run(id, name, description || '', contactPerson || '', location || '', JSON.stringify(deviceIds), JSON.stringify(topology || [])); 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); 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) }); } catch (err: any) { res.status(500).json({ error: err.message }); } }); app.put('/api/labs/:id', requireAuth, (req, res) => { try { const id = req.params.id; const { name, description, contactPerson, location, deviceIds, topology } = req.body; db.prepare(`UPDATE labs SET name = ?, description = ?, contactPerson = ?, location = ?, deviceIds = ?, topology = ? WHERE id = ?`) .run(name, description, contactPerson, location, JSON.stringify(deviceIds), JSON.stringify(topology), 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); 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) }); } catch (err: any) { res.status(500).json({ error: err.message }); } }); app.delete('/api/labs/:id', requireAuth, (req, res) => { try { const id = req.params.id; const lab = db.prepare('SELECT * FROM labs WHERE id = ?').get(id) as any; if (!lab) return res.status(404).json({ error: 'Lab template not found.' }); 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); res.json({ success: true, message: 'Lab template deleted and future reservations cancelled.' }); } catch (err: any) { res.status(500).json({ error: err.message }); } }); // ------------------------------------------------------------- // RESTFUL API: Bookings / Reservations // ------------------------------------------------------------- app.get('/api/bookings', requireAuth, (_req, res) => { try { const rows = db.prepare('SELECT * FROM bookings').all() as any[]; const bookings: Booking[] = rows.map(r => ({ id: r.id, labId: r.labId, userId: r.userId, startDateTime: r.startDateTime, endDateTime: r.endDateTime, notes: r.notes || '', status: r.status as any, notified: r.notified === 1, emailSent: r.emailSent === 1 })); res.json(bookings); } catch (err: any) { res.status(500).json({ error: err.message }); } }); app.post('/api/bookings', requireAuth, (req, res) => { try { const { labId, userId, startDateTime, endDateTime, notes, status, operatorName } = req.body; if (!labId || !userId || !startDateTime || !endDateTime) { return res.status(400).json({ error: 'Missing reservation timestamps or laboratory ID.' }); } const id = uid("book"); db.prepare(`INSERT INTO bookings (id, labId, userId, startDateTime, endDateTime, notes, status, notified, emailSent) VALUES (?, ?, ?, ?, ?, ?, ?, 0, 1)`) .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); const r = db.prepare('SELECT * FROM bookings WHERE id = ?').get(id) as any; res.status(201).json({ booking: { id: r.id, labId: r.labId, userId: r.userId, startDateTime: r.startDateTime, endDateTime: r.endDateTime, notes: r.notes || '', status: r.status, notified: r.notified === 1, emailSent: r.emailSent === 1 }, alertGenerated: `Reservation successfully approved: Lab "${lab?.name || 'Scenario'}" is reserved for your exclusive usage during this window.` }); } catch (err: any) { res.status(500).json({ error: err.message }); } }); app.put('/api/bookings/:id', requireAuth, (req, res) => { try { const id = req.params.id; const { status, operatorName } = req.body; const booking = db.prepare('SELECT * FROM bookings WHERE id = ?').get(id) as any; if (!booking) return res.status(404).json({ error: 'Reservation not found.' }); db.prepare('UPDATE bookings SET status = ? WHERE id = ?').run(status, id); if (status === 'cancelled') { 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', `${operatorName || 'An operator'} canceled the reservation for lab template "${lab?.name || 'Unknown'}" and released associated nodes back into the pool.`, req.user!.userId); } const r = db.prepare('SELECT * FROM bookings WHERE id = ?').get(id) as any; res.json({ id: r.id, labId: r.labId, userId: r.userId, startDateTime: r.startDateTime, endDateTime: r.endDateTime, notes: r.notes || '', status: r.status, notified: r.notified === 1, emailSent: r.emailSent === 1 }); } catch (err: any) { res.status(500).json({ error: err.message }); } }); app.delete('/api/bookings/:id', requireAuth, (req, res) => { try { const id = req.params.id; const booking = db.prepare('SELECT * FROM bookings WHERE id = ?').get(id) as any; if (!booking) return res.status(404).json({ error: 'Reservation not found.' }); 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); res.json({ success: true, message: 'Reservation and its logs have been resolved correctly.' }); } catch (err: any) { res.status(500).json({ error: err.message }); } }); // ------------------------------------------------------------- // RESTFUL API: Logs // ------------------------------------------------------------- app.get('/api/logs', requireAuth, (_req, res) => { try { const logs = db.prepare('SELECT * FROM logs ORDER BY timestamp DESC').all() as LogEntry[]; res.json(logs); } catch (err: any) { res.status(500).json({ error: err.message }); } }); app.post('/api/logs', requireAuth, (req, res) => { try { const { type, message, deviceId, userId } = req.body; if (!message || !type) { 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 log = db.prepare('SELECT * FROM logs WHERE id = ?').get(id) as LogEntry; res.status(201).json(log); } catch (err: any) { res.status(500).json({ error: err.message }); } }); // ------------------------------------------------------------- // RESTFUL API: Quick Links (shared link dashboard) // ------------------------------------------------------------- app.get('/api/links', requireAuth, (_req, res) => { try { const links = db.prepare('SELECT * FROM links ORDER BY category ASC, title ASC').all() as QuickLink[]; res.json(links); } catch (err: any) { res.status(500).json({ error: err.message }); } }); app.post('/api/links', requireAuth, (req, res) => { try { const { title, url, description, category, color } = req.body; if (!title || !url) { return res.status(400).json({ error: 'A title and a URL are required.' }); } const id = uid("link"); const createdAt = new Date().toISOString(); db.prepare(`INSERT INTO links (id, title, url, description, category, color, createdBy, createdAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`) .run(id, title, url, description || '', category || '', color || 'emerald', req.user!.userId, createdAt); const link = db.prepare('SELECT * FROM links WHERE id = ?').get(id) as QuickLink; res.status(201).json(link); } catch (err: any) { res.status(500).json({ error: err.message }); } }); app.put('/api/links/:id', requireAuth, (req, res) => { try { const id = req.params.id; const { title, url, description, category, color } = req.body; const existing = db.prepare('SELECT id FROM links WHERE id = ?').get(id); if (!existing) return res.status(404).json({ error: 'Link not found.' }); db.prepare(`UPDATE links SET title = ?, url = ?, description = ?, category = ?, color = ? WHERE id = ?`) .run(title, url, description || '', category || '', color || 'emerald', id); const link = db.prepare('SELECT * FROM links WHERE id = ?').get(id) as QuickLink; res.json(link); } catch (err: any) { res.status(500).json({ error: err.message }); } }); app.delete('/api/links/:id', requireAuth, (req, res) => { try { const id = req.params.id; const existing = db.prepare('SELECT id FROM links WHERE id = ?').get(id); if (!existing) return res.status(404).json({ error: 'Link not found.' }); db.prepare('DELETE FROM links WHERE id = ?').run(id); res.json({ success: true, message: 'Link removed from the shared dashboard.' }); } catch (err: any) { res.status(500).json({ error: err.message }); } }); // ------------------------------------------------------------- // VITE / STATIC SERVING // ------------------------------------------------------------- if (process.env.NODE_ENV !== 'production') { const vite = await createViteServer({ server: { middlewareMode: true }, appType: 'spa', }); app.use(vite.middlewares); } else { const distPath = path.join(process.cwd(), 'dist'); app.use(express.static(distPath)); app.get('*', (_req, res) => { res.sendFile(path.join(distPath, 'index.html')); }); } // ------------------------------------------------------------- // CYCLIC CHECKMK STATUS SYNC // The device status shown in the UI is owned by CheckMK, not the app. // This job runs on an interval and reconciles each *linked* device's status // from the CheckMK REST API. The frontend additionally polls /api/devices, // so anything written here surfaces in the inventory & booking screens. // Until CHECKMK_API_URL/SECRET are configured, devices stay 'unknown' (and // therefore not bookable) - which is the intended safe default. // ------------------------------------------------------------- // Sync interval: DB setting takes precedence over env var const CHECKMK_SYNC_INTERVAL_MS = Number(getSetting('checkmk_sync_interval_ms') || process.env.CHECKMK_SYNC_INTERVAL_MS) || 60_000; async function syncCheckMkStatuses() { // DB settings take precedence; fall back to env vars for backwards compatibility const CHECKMK_API_URL = getSetting('checkmk_api_url') || process.env.CHECKMK_API_URL; const CHECKMK_API_SECRET = getSetting('checkmk_api_secret') || process.env.CHECKMK_API_SECRET; if (!CHECKMK_API_URL || !CHECKMK_API_SECRET) return; // not configured - leave statuses as 'unknown' const rows = db.prepare("SELECT id, hostname, checkMkUrl FROM devices WHERE checkMkUrl != ''") .all() as { id: string; hostname: string; checkMkUrl: string }[]; for (const dev of rows) { try { // TODO(checkmk): query the host's hard state from the CheckMK API using the // automation secret, map 0 (UP) -> 'online' and anything else -> 'offline': // const res = await fetch(`${CHECKMK_API_URL}/objects/host/${host}/actions/...`, // { headers: { Authorization: `Bearer automation ${CHECKMK_API_SECRET}` } }); // const state = (await res.json()).extensions.state === 0 ? 'online' : 'offline'; // db.prepare('UPDATE devices SET status = ?, lastCheckedAt = ? WHERE id = ?') // .run(state, new Date().toISOString(), dev.id); } catch (err) { console.error(`[CheckMK] Status sync failed for ${dev.hostname}:`, err); } } } setInterval(syncCheckMkStatuses, CHECKMK_SYNC_INTERVAL_MS); syncCheckMkStatuses(); app.listen(PORT, '0.0.0.0', () => { console.log(`[Server] Core Server running at http://0.0.0.0:${PORT}`); }); } startServer().catch(err => { console.error('[Server] Critical Crash during bootstrap:', err); });