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:
@ -3,7 +3,7 @@ import { authFetch } from '../lib/auth';
|
||||
import { User } from '../types';
|
||||
import {
|
||||
Shield, Activity, Save, CheckCircle, AlertCircle, Eye, EyeOff,
|
||||
Settings2, KeyRound, Globe, Clock, ChevronRight, Copy, Users,
|
||||
Settings2, KeyRound, Globe, Clock, ChevronRight, Copy, Users, RefreshCw,
|
||||
} from 'lucide-react';
|
||||
|
||||
const SECRET_SENTINEL = '__SET__';
|
||||
@ -13,8 +13,11 @@ interface RawSettings {
|
||||
azure_client_id: string;
|
||||
azure_tenant_id: string;
|
||||
azure_client_secret: string;
|
||||
azure_redirect_uri: string;
|
||||
azure_allowed_group: string;
|
||||
checkmk_enabled: string;
|
||||
checkmk_api_url: string;
|
||||
checkmk_api_user: string;
|
||||
checkmk_api_secret: string;
|
||||
checkmk_sync_interval_ms: string;
|
||||
}
|
||||
@ -142,14 +145,18 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
|
||||
const [azureTenantId, setAzureTenantId] = useState('');
|
||||
const [azureClientSecret, setAzureClientSecret] = useState('');
|
||||
const [azureSecretSet, setAzureSecretSet] = useState(false);
|
||||
const [azureRedirectUri, setAzureRedirectUri] = useState('');
|
||||
const [azureAllowedGroup, setAzureAllowedGroup] = useState('');
|
||||
const [showAzureSecret, setShowAzureSecret] = useState(false);
|
||||
|
||||
const [checkmkEnabled, setCheckmkEnabled] = useState(false);
|
||||
const [checkmkApiUrl, setCheckmkApiUrl] = useState('');
|
||||
const [checkmkApiUser, setCheckmkApiUser] = useState('automation');
|
||||
const [checkmkApiSecret, setCheckmkApiSecret] = useState('');
|
||||
const [checkmkSecretSet, setCheckmkSecretSet] = useState(false);
|
||||
const [checkmkSyncInterval, setCheckmkSyncInterval] = useState('60000');
|
||||
const [showCheckmkSecret, setShowCheckmkSecret] = useState(false);
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
@ -176,8 +183,11 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
|
||||
setAzureTenantId(data.azure_tenant_id || '');
|
||||
setAzureSecretSet(data.azure_client_secret === SECRET_SENTINEL);
|
||||
setAzureClientSecret('');
|
||||
setAzureRedirectUri(data.azure_redirect_uri || '');
|
||||
setAzureAllowedGroup(data.azure_allowed_group || '');
|
||||
setCheckmkEnabled(data.checkmk_enabled === 'true');
|
||||
setCheckmkApiUrl(data.checkmk_api_url || '');
|
||||
setCheckmkApiUser(data.checkmk_api_user || 'automation');
|
||||
setCheckmkSecretSet(data.checkmk_api_secret === SECRET_SENTINEL);
|
||||
setCheckmkApiSecret('');
|
||||
setCheckmkSyncInterval(data.checkmk_sync_interval_ms || '60000');
|
||||
@ -196,8 +206,11 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
|
||||
azure_enabled: azureEnabled ? 'true' : 'false',
|
||||
azure_client_id: azureClientId,
|
||||
azure_tenant_id: azureTenantId,
|
||||
azure_redirect_uri: azureRedirectUri,
|
||||
azure_allowed_group: azureAllowedGroup,
|
||||
checkmk_enabled: checkmkEnabled ? 'true' : 'false',
|
||||
checkmk_api_url: checkmkApiUrl,
|
||||
checkmk_api_user: checkmkApiUser,
|
||||
checkmk_sync_interval_ms: checkmkSyncInterval,
|
||||
};
|
||||
if (azureClientSecret) payload.azure_client_secret = azureClientSecret;
|
||||
@ -223,6 +236,25 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
|
||||
}
|
||||
}
|
||||
|
||||
async function runSync() {
|
||||
setSyncing(true);
|
||||
setError('');
|
||||
try {
|
||||
const res = await authFetch('/api/checkmk/sync', { method: 'POST' });
|
||||
if (!res.ok) {
|
||||
const d = await res.json();
|
||||
setError(d.error || 'Sync failed.');
|
||||
return;
|
||||
}
|
||||
setSuccessMsg('CheckMK sync triggered successfully.');
|
||||
setTimeout(() => setSuccessMsg(''), 4000);
|
||||
} catch {
|
||||
setError('Network error during sync.');
|
||||
} finally {
|
||||
setSyncing(false);
|
||||
}
|
||||
}
|
||||
|
||||
function copyRedirectUri() {
|
||||
if (!effectiveRedirectUri) return;
|
||||
navigator.clipboard.writeText(effectiveRedirectUri).then(() => {
|
||||
@ -359,27 +391,41 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
|
||||
|
||||
{/* ── CheckMK ── */}
|
||||
<SectionCard accentColor="bg-gradient-to-r from-emerald-600 to-teal-600">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-emerald-950/60 border border-emerald-900/40 rounded-xl">
|
||||
<Activity className="w-4 h-4 text-emerald-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-sm font-semibold text-white">CheckMK</h2>
|
||||
{checkmkApiUrl && checkmkSecretSet && (
|
||||
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-semibold bg-emerald-950/60 border border-emerald-900/50 text-emerald-400">
|
||||
<span className="w-1 h-1 rounded-full bg-emerald-400 animate-pulse inline-block" />
|
||||
ACTIVE
|
||||
</span>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-emerald-950/60 border border-emerald-900/40 rounded-xl">
|
||||
<Activity className="w-4 h-4 text-emerald-400" />
|
||||
</div>
|
||||
<p className="text-[11px] text-slate-500 mt-0.5">Device status sync via CheckMK REST API</p>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-sm font-semibold text-white">CheckMK</h2>
|
||||
{checkmkEnabled && checkmkApiUrl && checkmkSecretSet && (
|
||||
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-semibold bg-emerald-950/60 border border-emerald-900/50 text-emerald-400">
|
||||
<span className="w-1 h-1 rounded-full bg-emerald-400 animate-pulse inline-block" />
|
||||
ACTIVE
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[11px] text-slate-500 mt-0.5">Device status sync via CheckMK REST API</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className={`text-[10px] font-semibold font-mono ${checkmkEnabled ? 'text-emerald-400' : 'text-slate-600'}`}>
|
||||
{checkmkEnabled ? 'ENABLED' : 'DISABLED'}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCheckmkEnabled(v => !v)}
|
||||
className={`relative w-10 h-5 rounded-full transition-all duration-200 focus:outline-none ${checkmkEnabled ? 'bg-emerald-600 shadow-[0_0_10px_rgba(5,150,105,0.4)]' : 'bg-slate-800 border border-slate-700'}`}
|
||||
>
|
||||
<span className={`absolute top-0.5 w-4 h-4 bg-white rounded-full shadow transition-all duration-200 ${checkmkEnabled ? 'left-5' : 'left-0.5'}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-px bg-slate-800/60" />
|
||||
|
||||
<div className="space-y-5">
|
||||
<div className={`space-y-5 transition-opacity duration-200 ${!checkmkEnabled ? 'opacity-40 pointer-events-none' : ''}`}>
|
||||
<FieldRow label="API URL">
|
||||
<Input
|
||||
value={checkmkApiUrl}
|
||||
@ -390,6 +436,15 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
|
||||
/>
|
||||
</FieldRow>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
||||
<FieldRow label="Automation User" hint="CheckMK automation user (default: automation)">
|
||||
<Input
|
||||
value={checkmkApiUser}
|
||||
onChange={setCheckmkApiUser}
|
||||
placeholder="automation"
|
||||
monospace
|
||||
icon={<KeyRound className="w-3.5 h-3.5" />}
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow
|
||||
label="Automation Secret"
|
||||
badge={checkmkSecretSet ? <ConfiguredBadge /> : undefined}
|
||||
@ -412,6 +467,17 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
|
||||
/>
|
||||
</FieldRow>
|
||||
</div>
|
||||
{checkmkApiUrl && checkmkSecretSet && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={runSync}
|
||||
disabled={syncing}
|
||||
className="flex items-center gap-2 bg-emerald-950/60 hover:bg-emerald-900/40 border border-emerald-900/50 text-emerald-400 text-xs font-semibold px-3 py-2 rounded-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<RefreshCw className={`w-3.5 h-3.5 ${syncing ? 'animate-spin' : ''}`} />
|
||||
{syncing ? 'Syncing…' : 'Run sync now'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user