diff --git a/server-db.ts b/server-db.ts index 9fbb4a3..98cff45 100644 --- a/server-db.ts +++ b/server-db.ts @@ -88,6 +88,7 @@ function ensureColumn(table: string, column: string, ddl: string) { } ensureColumn('devices', 'checkMkUrl', "checkMkUrl TEXT NOT NULL DEFAULT ''"); +ensureColumn('devices', 'cmkHostname', "cmkHostname TEXT NOT NULL DEFAULT ''"); // Seed default settings (INSERT OR IGNORE = only if key absent) const _insertDefault = db.prepare('INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)'); diff --git a/server.ts b/server.ts index e7da6d3..8fb6ccb 100644 --- a/server.ts +++ b/server.ts @@ -155,10 +155,12 @@ async function startServer() { 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`; + const cmkApiUrl = getSetting('checkmk_api_url') || process.env.CHECKMK_API_URL || ''; res.json({ azureEnabled: enabled && Boolean(clientId) && Boolean(tenantId) && Boolean(secret), effectiveRedirectUri, checkmkEnabled: getSetting('checkmk_enabled') === 'true', + checkmkBaseUrl: cmkApiUrl.replace(/\/api\/.*$/, ''), }); }); @@ -751,7 +753,7 @@ async function startServer() { const cmkHost = ipToHostname.get(dev.ip); if (!cmkHost) { if (dev.status !== 'unknown') { - db.prepare('UPDATE devices SET status = ?, lastCheckedAt = ? WHERE id = ?').run('unknown', now, dev.id); + 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); } @@ -768,7 +770,7 @@ async function startServer() { const state: number = hostData?.extensions?.state ?? -1; const newStatus = state === 0 ? 'online' : state === 1 || state === 2 ? 'offline' : 'unknown'; - db.prepare('UPDATE devices SET status = ?, lastCheckedAt = ? WHERE id = ?').run(newStatus, now, dev.id); + 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); diff --git a/src/App.tsx b/src/App.tsx index 89f54a0..0c520ba 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -54,6 +54,7 @@ export default function App() { const [inventoryHighlightDevice, setInventoryHighlightDevice] = useState(null); const [checkmkEnabled, setCheckmkEnabled] = useState(false); + const [checkmkBaseUrl, setCheckmkBaseUrl] = useState(''); useEffect(() => { const root = document.documentElement; @@ -142,7 +143,7 @@ export default function App() { if (bookingsRes.ok) setBookings(await bookingsRes.json()); if (logsRes.ok) setLogs(await logsRes.json()); if (linksRes.ok) setLinks(await linksRes.json()); - if (configRes.ok) { const cfg = await configRes.json(); setCheckmkEnabled(!!cfg.checkmkEnabled); } + if (configRes.ok) { const cfg = await configRes.json(); setCheckmkEnabled(!!cfg.checkmkEnabled); setCheckmkBaseUrl(cfg.checkmkBaseUrl || ''); } } catch (err) { console.error('[App] Failed to load data:', err); } finally { @@ -584,6 +585,7 @@ export default function App() { ) => void; onUpdateDevice: (device: Device) => void; onDeleteDevice: (id: string) => void; @@ -24,6 +25,7 @@ interface DeviceInventoryProps { export default function DeviceInventory({ devices, checkmkEnabled, + checkmkBaseUrl, onAddDevice, onUpdateDevice, onDeleteDevice, @@ -62,6 +64,10 @@ export default function DeviceInventory({ }); const effectiveStatus = (d: Device): 'online' | 'offline' | 'unknown' => d.status; + const cmkHostUrl = (d: Device) => + checkmkEnabled && checkmkBaseUrl && d.cmkHostname + ? `${checkmkBaseUrl}/index.py?host=${encodeURIComponent(d.cmkHostname)}` + : 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' }; @@ -330,6 +336,17 @@ export default function DeviceInventory({ {/* Action Panel */}
+ {cmkHostUrl(device) && ( + + + + )}
+ {cmkHostUrl(selectedDevice) && ( + + + Open host in CheckMK + + )} {selectedDevice.lastCheckedAt && (

Last checked: {new Date(selectedDevice.lastCheckedAt).toLocaleString()} diff --git a/src/components/Logbook.tsx b/src/components/Logbook.tsx index 4bdfcf4..e54be35 100644 --- a/src/components/Logbook.tsx +++ b/src/components/Logbook.tsx @@ -21,6 +21,7 @@ interface LogbookProps { export default function Logbook({ logs, devices, users, currentUser, onAddLog }: LogbookProps) { const [searchTerm, setSearchTerm] = useState(''); const [typeFilter, setTypeFilter] = useState('all'); + const [showSystem, setShowSystem] = useState(false); // Custom Maintenance Log state const [showAddLog, setShowAddLog] = useState(false); @@ -32,6 +33,7 @@ export default function Logbook({ logs, devices, users, currentUser, onAddLog }: // Filter logs const filteredLogs = sortedLogs.filter(log => { + if (!showSystem && log.type === 'system') return false; const matchesSearch = log.message.toLowerCase().includes(searchTerm.toLowerCase()); const matchesType = typeFilter === 'all' || log.type === typeFilter; return matchesSearch && matchesType; @@ -121,8 +123,8 @@ export default function Logbook({ logs, devices, users, currentUser, onAddLog }: 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" /> -

- {['all', 'booking', 'maintenance'].map((type) => ( +
+ {['all', 'booking', 'maintenance', 'status'].map((type) => ( ))} +
diff --git a/src/types.ts b/src/types.ts index bb16b5c..83e3918 100644 --- a/src/types.ts +++ b/src/types.ts @@ -16,6 +16,7 @@ export interface Device { type: DeviceType; status: 'online' | 'offline' | 'unknown'; emergencySheet: string; // Markdown text + cmkHostname?: string; lastCheckedAt?: string; }