feat: CheckMK global IP-based integration with enable toggle

Replace per-device CheckMK URL field with a global, IP-based lookup.
The sync job fetches all host configs from CheckMK once per cycle,
matches each device by IP address, and updates its status accordingly.
Devices not found in CheckMK are reset to 'unknown'.

- Add checkmk_enabled / checkmk_api_user settings; toggle in Settings
  mirrors the Entra ID pattern (fields dim when disabled)
- Sync job uses self-scheduling setTimeout so interval changes apply
  without a server restart; POST /api/checkmk/sync for manual triggers
- Status changes and a per-cycle summary are written to the Logbook
- Remove checkMkUrl from Device type, form, list view, and detail panel;
  status badge and CheckMK panel only render when CheckMK is enabled
- Booking offline warning suppressed when CheckMK is disabled
- Topology status dot color driven purely by device.status
This commit is contained in:
Brückner
2026-06-04 14:07:54 +02:00
parent e9fb79041e
commit f12f92aea8
8 changed files with 194 additions and 118 deletions

View File

@ -6,8 +6,8 @@
import React, { useState, useMemo } from 'react';
import { Device, DeviceType } from '../types';
import {
Server, Search, Plus, Trash, Edit2, MapPin, Info,
BookOpen, Save, X, ExternalLink, Gauge
Server, Search, Plus, Trash, Edit2, MapPin, Gauge,
BookOpen, Save, X, Info
} from 'lucide-react';
// Built-in device class presets shown in the dropdown.
@ -15,6 +15,7 @@ const DEVICE_CLASS_PRESETS = ['Switch', 'Firewall', 'Access-Point', 'Controller'
interface DeviceInventoryProps {
devices: Device[];
checkmkEnabled: boolean;
onAddDevice: (device: Omit<Device, 'id'>) => void;
onUpdateDevice: (device: Device) => void;
onDeleteDevice: (id: string) => void;
@ -22,6 +23,7 @@ interface DeviceInventoryProps {
export default function DeviceInventory({
devices,
checkmkEnabled,
onAddDevice,
onUpdateDevice,
onDeleteDevice,
@ -50,7 +52,6 @@ export default function DeviceInventory({
notes: string;
type: DeviceType;
emergencySheet: string;
checkMkUrl: string;
}>({
hostname: '',
ip: '',
@ -58,12 +59,9 @@ export default function DeviceInventory({
notes: '',
type: 'Switch',
emergencySheet: '',
checkMkUrl: ''
});
// Effective status: nothing is known until CheckMK is linked and reports a state.
const effectiveStatus = (d: Device): 'online' | 'offline' | 'unknown' =>
d.checkMkUrl ? (d.status === 'online' || d.status === 'offline' ? d.status : 'unknown') : 'unknown';
const effectiveStatus = (d: Device): 'online' | 'offline' | 'unknown' => d.status;
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' };
@ -90,7 +88,6 @@ export default function DeviceInventory({
location: '',
notes: '',
type: 'Switch',
checkMkUrl: '',
emergencySheet: `### EMERGENCY MANUAL [HOSTNAME]
**Device Type:** [Enter Model]
@ -119,7 +116,6 @@ export default function DeviceInventory({
location: dev.location,
notes: dev.notes,
type: dev.type,
checkMkUrl: dev.checkMkUrl ?? '',
emergencySheet: dev.emergencySheet
});
setIsEditing(true);
@ -138,7 +134,6 @@ export default function DeviceInventory({
type: formData.type,
status: 'unknown',
emergencySheet: formData.emergencySheet,
checkMkUrl: formData.checkMkUrl
});
} else if (formMode === 'edit' && formData.id) {
const match = devices.find(d => d.id === formData.id);
@ -151,7 +146,6 @@ export default function DeviceInventory({
notes: formData.notes,
type: formData.type,
emergencySheet: formData.emergencySheet,
checkMkUrl: formData.checkMkUrl
});
}
}
@ -324,30 +318,18 @@ export default function DeviceInventory({
{/* Right: Actions and Status */}
<div className="flex items-center gap-3" onClick={(e) => e.stopPropagation()}>
{/* CheckMK Monitoring Badge */}
{(() => { const m = statusMeta(effectiveStatus(device)); return (
{/* CheckMK Status Badge only when CheckMK is enabled */}
{checkmkEnabled && (() => { const m = statusMeta(effectiveStatus(device)); return (
<div className="flex flex-col items-end gap-1 font-sans">
<span className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[10px] font-medium border capitalize ${m.badge}`}>
<span className={`w-1.5 h-1.5 rounded-full ${m.dot}`} />
{m.label}
</span>
<span className="text-[9px] font-mono text-slate-500">{device.checkMkUrl ? 'via CheckMK' : 'not linked'}</span>
</div>
); })()}
{/* Action Panel */}
<div className="flex items-center gap-1.5 border-l border-slate-800/80 pl-3">
{device.checkMkUrl && (
<a
href={device.checkMkUrl}
target="_blank"
rel="noopener noreferrer"
className="p-1 px-1.5 rounded hover:bg-slate-800 text-slate-400 hover:text-cyan-400 transition-colors"
title="Open in CheckMK"
>
<ExternalLink className="w-3.5 h-3.5" />
</a>
)}
<button
onClick={() => handleOpenEdit(device)}
className="p-1 px-1.5 rounded hover:bg-slate-800 text-slate-400 hover:text-indigo-400 transition-colors"
@ -403,7 +385,8 @@ export default function DeviceInventory({
</div>
</div>
{/* CheckMK Monitoring Panel */}
{/* CheckMK Monitoring Panel only when CheckMK is enabled */}
{checkmkEnabled && (
<div className="mt-4 pt-4 border-t border-slate-800 space-y-2.5">
<div className="flex items-center justify-between">
<span className="text-xs text-slate-400 font-sans font-medium flex items-center gap-1.5">
@ -417,22 +400,13 @@ export default function DeviceInventory({
</span>
); })()}
</div>
{selectedDevice.checkMkUrl ? (
<a
href={selectedDevice.checkMkUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-1.5 px-3 py-1.5 bg-slate-900 border border-slate-700 text-slate-200 hover:text-cyan-400 hover:border-cyan-500 rounded text-xs transition-colors font-mono"
>
<ExternalLink className="w-3.5 h-3.5" />
Open host in CheckMK
</a>
) : (
<p className="text-[11px] text-slate-500 italic bg-slate-900/50 border border-slate-800 rounded p-2 leading-relaxed">
No CheckMK host linked yet. Add the host URL in the modify window - live status will be pulled from the CheckMK API.
{selectedDevice.lastCheckedAt && (
<p className="text-[10px] text-slate-500 font-mono">
Last checked: {new Date(selectedDevice.lastCheckedAt).toLocaleString()}
</p>
)}
</div>
)}
</div>
{/* Emergency rescue guidelines sheet */}
@ -569,29 +543,6 @@ Pick a box from the list to see its specs and break-glass playbook.
/>
</div>
{/* CheckMK Monitoring integration */}
<div className="bg-slate-900/60 border border-cyan-900/40 rounded-lg p-3.5 space-y-3">
<div className="flex items-center gap-2 text-cyan-400 font-semibold">
<Gauge className="w-4 h-4" />
CheckMK Monitoring
</div>
<p className="text-[11px] text-slate-400 leading-relaxed flex gap-1.5">
<Info className="w-3.5 h-3.5 text-cyan-400 shrink-0 mt-0.5" />
Link this host to CheckMK. Connectivity is monitored there and pulled in via the CheckMK API - no in-app ping checks.
</p>
<div>
<label className="block text-slate-300 font-semibold mb-1">CheckMK Host URL</label>
<input
type="text"
value={formData.checkMkUrl}
onChange={(e) => setFormData({ ...formData, checkMkUrl: e.target.value })}
className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 focus:outline-none focus:border-cyan-500 font-mono"
placeholder="https://checkmk.internal/site/check_mk/index.py?host=SW-CORE-03"
/>
<p className="text-[10px] text-slate-500 mt-1.5 italic">Without a linked CheckMK host the status stays unknown.</p>
</div>
</div>
<div>
<label className="block text-slate-300 font-semibold mb-1">Disaster Recovery Guide (Markdown formatting supported)</label>
<textarea