feat: log login events in logbook; improve CheckMK error reporting

Successful logins now write a system-type logbook entry.
CheckMK sync reports configuration errors, host-fetch failures,
and per-device sync failures as logbook entries instead of silently
dropping them; inherits effective_attributes IP fallback.
This commit is contained in:
Brückner
2026-06-04 14:21:05 +02:00
parent 1289e2476c
commit e13e11ce6a

View File

@ -125,6 +125,10 @@ async function startServer() {
const token = jwt.sign({ userId: row.id, email: row.email }, JWT_SECRET, { expiresIn: JWT_EXPIRY }); 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 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);
res.json({ token, user }); res.json({ token, user });
} catch (err: any) { } catch (err: any) {
res.status(500).json({ error: err.message }); res.status(500).json({ error: err.message });
@ -686,36 +690,51 @@ async function startServer() {
// in Settings take effect on the next cycle without a server restart. // in Settings take effect on the next cycle without a server restart.
// ------------------------------------------------------------- // -------------------------------------------------------------
async function syncCheckMkStatuses() { async function syncCheckMkStatuses() {
const now = new Date().toISOString();
if (getSetting('checkmk_enabled') !== 'true') return; if (getSetting('checkmk_enabled') !== 'true') return;
const CHECKMK_API_URL = getSetting('checkmk_api_url') || process.env.CHECKMK_API_URL; const CHECKMK_API_URL = getSetting('checkmk_api_url') || process.env.CHECKMK_API_URL;
const CHECKMK_API_USER = getSetting('checkmk_api_user') || process.env.CHECKMK_API_USER || 'automation'; const CHECKMK_API_USER = getSetting('checkmk_api_user') || process.env.CHECKMK_API_USER || 'automation';
const CHECKMK_API_SECRET = getSetting('checkmk_api_secret') || process.env.CHECKMK_API_SECRET; const CHECKMK_API_SECRET = getSetting('checkmk_api_secret') || process.env.CHECKMK_API_SECRET;
if (!CHECKMK_API_URL || !CHECKMK_API_SECRET) return;
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.');
return;
}
const authHeader = `Bearer ${CHECKMK_API_USER} ${CHECKMK_API_SECRET}`;
// Step 1: build IP → hostname map from CheckMK host configurations // Step 1: build IP → hostname map from CheckMK host configurations
// Falls back to effective_attributes for IPs inherited from a parent folder.
let ipToHostname: Map<string, string>; let ipToHostname: Map<string, string>;
try { try {
const cfgRes = await fetch( const cfgRes = await fetch(
`${CHECKMK_API_URL}/domain-types/host_config/collections/all`, `${CHECKMK_API_URL}/domain-types/host_config/collections/all`,
{ headers: { Authorization: `Bearer ${CHECKMK_API_USER} ${CHECKMK_API_SECRET}`, Accept: 'application/json' } } { headers: { Authorization: authHeader, Accept: 'application/json' } }
); );
if (!cfgRes.ok) throw new Error(`HTTP ${cfgRes.status}`); if (!cfgRes.ok) throw new Error(`HTTP ${cfgRes.status}${await cfgRes.text()}`);
const cfgData = await cfgRes.json(); const cfgData = await cfgRes.json();
ipToHostname = new Map<string, string>(); ipToHostname = new Map<string, string>();
for (const host of cfgData?.value ?? []) { for (const host of cfgData?.value ?? []) {
const ip: string | undefined = host?.extensions?.attributes?.ipaddress; const ext = host?.extensions;
const ip: string | undefined =
ext?.attributes?.ipaddress || ext?.effective_attributes?.ipaddress;
const name: string | undefined = host?.id; const name: string | undefined = host?.id;
if (ip && name) ipToHostname.set(ip, name); if (ip && name) ipToHostname.set(ip, name);
} }
} catch (err) { } catch (err: any) {
console.error('[CheckMK] Failed to fetch host configs:', err); 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);
return; return;
} }
// Step 2: update each device based on IP lookup and log status changes // Step 2: update each device based on IP lookup and log status changes
const rows = db.prepare('SELECT id, hostname, ip, status FROM devices').all() as { id: string; hostname: string; ip: string; status: string }[]; const rows = db.prepare('SELECT id, hostname, ip, status FROM devices').all() as { id: string; hostname: string; ip: string; status: string }[];
const counts = { online: 0, offline: 0, unknown: 0 }; const counts = { online: 0, offline: 0, unknown: 0 };
const now = new Date().toISOString();
for (const dev of rows) { for (const dev of rows) {
const cmkHost = ipToHostname.get(dev.ip); const cmkHost = ipToHostname.get(dev.ip);
@ -731,9 +750,9 @@ async function startServer() {
try { try {
const res = await fetch( const res = await fetch(
`${CHECKMK_API_URL}/objects/host/${encodeURIComponent(cmkHost)}`, `${CHECKMK_API_URL}/objects/host/${encodeURIComponent(cmkHost)}`,
{ headers: { Authorization: `Bearer ${CHECKMK_API_USER} ${CHECKMK_API_SECRET}`, Accept: 'application/json' } } { headers: { Authorization: authHeader, Accept: 'application/json' } }
); );
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) throw new Error(`HTTP ${res.status}${await res.text()}`);
const data = await res.json(); const data = await res.json();
const hardState: number = data?.extensions?.state ?? -1; const hardState: number = data?.extensions?.state ?? -1;
const newStatus = hardState === 0 ? 'online' : 'offline'; const newStatus = hardState === 0 ? 'online' : 'offline';
@ -743,13 +762,15 @@ async function startServer() {
.run(uid('log'), now, 'status', `CheckMK: ${dev.hostname} (${dev.ip}) status changed to ${newStatus} (was: ${dev.status}).`, dev.id); .run(uid('log'), now, 'status', `CheckMK: ${dev.hostname} (${dev.ip}) status changed to ${newStatus} (was: ${dev.status}).`, dev.id);
} }
counts[newStatus as 'online' | 'offline']++; counts[newStatus as 'online' | 'offline']++;
} catch (err) { } catch (err: any) {
console.error(`[CheckMK] Status sync failed for ${dev.hostname} (${dev.ip}):`, err); 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);
counts.unknown++; counts.unknown++;
} }
} }
// Summary log entry for every sync run
db.prepare('INSERT INTO logs (id, timestamp, type, message) VALUES (?, ?, ?, ?)') db.prepare('INSERT INTO logs (id, timestamp, type, message) VALUES (?, ?, ?, ?)')
.run(uid('log'), now, 'system', .run(uid('log'), now, 'system',
`CheckMK sync completed — ${counts.online} online, ${counts.offline} offline, ${counts.unknown} unknown (${rows.length} devices total).`); `CheckMK sync completed — ${counts.online} online, ${counts.offline} offline, ${counts.unknown} unknown (${rows.length} devices total).`);