refactor(naming): unify service abbreviations (cmk, semaphore)

Standardise CheckMK variables to the cmk prefix (checkMkUrl -> cmkUrl,
checkmk* -> cmk*) and resolve the ansible/semaphore split by renaming
all booking fields to semaphore*. Includes DB migrations 0001/0002 for
existing databases.
This commit is contained in:
Brückner
2026-06-10 17:06:17 +02:00
parent e0fd19f471
commit 150557ce2c
9 changed files with 95 additions and 87 deletions

View File

@ -30,7 +30,7 @@ db.exec(`
status TEXT NOT NULL, status TEXT NOT NULL,
emergencySheet TEXT NOT NULL, emergencySheet TEXT NOT NULL,
lastCheckedAt TEXT, lastCheckedAt TEXT,
checkMkUrl TEXT NOT NULL DEFAULT '', cmkUrl TEXT NOT NULL DEFAULT '',
cmkHostname TEXT NOT NULL DEFAULT '' cmkHostname TEXT NOT NULL DEFAULT ''
); );
@ -56,10 +56,10 @@ db.exec(`
status TEXT NOT NULL, status TEXT NOT NULL,
notified INTEGER NOT NULL DEFAULT 0, notified INTEGER NOT NULL DEFAULT 0,
emailSent INTEGER NOT NULL DEFAULT 0, emailSent INTEGER NOT NULL DEFAULT 0,
ansibleSetupTriggered INTEGER NOT NULL DEFAULT 0, semaphoreSetupTriggered INTEGER NOT NULL DEFAULT 0,
ansibleTeardownTriggered INTEGER NOT NULL DEFAULT 0, semaphoreTeardownTriggered INTEGER NOT NULL DEFAULT 0,
ansibleSetupJobId TEXT NOT NULL DEFAULT '', semaphoreSetupJobId TEXT NOT NULL DEFAULT '',
ansibleTeardownJobId TEXT NOT NULL DEFAULT '' semaphoreTeardownJobId TEXT NOT NULL DEFAULT ''
); );
CREATE TABLE IF NOT EXISTS logs ( CREATE TABLE IF NOT EXISTS logs (

View File

@ -8,13 +8,21 @@ interface Migration {
// Append only. Never reorder or remove entries — that would corrupt tracking. // Append only. Never reorder or remove entries — that would corrupt tracking.
// Each `up` function receives the open DB handle inside an already-open transaction. // Each `up` function receives the open DB handle inside an already-open transaction.
const migrations: Migration[] = [ const migrations: Migration[] = [
// Example: {
// { id: '0001_rename_device_checkMkUrl_to_cmkUrl',
// id: '0001_bookings_add_color', up: (db) => {
// up: (db) => { db.exec(`ALTER TABLE devices RENAME COLUMN checkMkUrl TO cmkUrl`);
// db.exec(`ALTER TABLE bookings ADD COLUMN color TEXT NOT NULL DEFAULT 'blue'`); },
// }, },
// }, {
id: '0002_rename_booking_ansible_to_semaphore',
up: (db) => {
db.exec(`ALTER TABLE bookings RENAME COLUMN ansibleSetupTriggered TO semaphoreSetupTriggered`);
db.exec(`ALTER TABLE bookings RENAME COLUMN ansibleTeardownTriggered TO semaphoreTeardownTriggered`);
db.exec(`ALTER TABLE bookings RENAME COLUMN ansibleSetupJobId TO semaphoreSetupJobId`);
db.exec(`ALTER TABLE bookings RENAME COLUMN ansibleTeardownJobId TO semaphoreTeardownJobId`);
},
},
]; ];
export function runMigrations(db: InstanceType<typeof Database>): void { export function runMigrations(db: InstanceType<typeof Database>): void {

View File

@ -271,9 +271,9 @@ async function startServer() {
res.json({ res.json({
azureEnabled: enabled && Boolean(clientId) && Boolean(tenantId) && Boolean(secret), azureEnabled: enabled && Boolean(clientId) && Boolean(tenantId) && Boolean(secret),
effectiveRedirectUri, effectiveRedirectUri,
checkmkEnabled: getSetting('checkmk_enabled') === 'true', cmkEnabled: getSetting('checkmk_enabled') === 'true',
semaphoreEnabled: getSetting('semaphore_enabled') === 'true', semaphoreEnabled: getSetting('semaphore_enabled') === 'true',
checkmkBaseUrl: cmkApiUrl.replace(/\/api\/.*$/, ''), cmkBaseUrl: cmkApiUrl.replace(/\/api\/.*$/, ''),
isProduction: IS_PRODUCTION, isProduction: IS_PRODUCTION,
}); });
}); });
@ -655,10 +655,10 @@ async function startServer() {
startDateTime: r.startDateTime, endDateTime: r.endDateTime, startDateTime: r.startDateTime, endDateTime: r.endDateTime,
notes: r.notes || '', status: r.status as any, notes: r.notes || '', status: r.status as any,
notified: r.notified === 1, emailSent: r.emailSent === 1, notified: r.notified === 1, emailSent: r.emailSent === 1,
ansibleSetupTriggered: r.ansibleSetupTriggered === 1, semaphoreSetupTriggered: r.semaphoreSetupTriggered === 1,
ansibleTeardownTriggered: r.ansibleTeardownTriggered === 1, semaphoreTeardownTriggered: r.semaphoreTeardownTriggered === 1,
ansibleSetupJobId: r.ansibleSetupJobId || '', semaphoreSetupJobId: r.semaphoreSetupJobId || '',
ansibleTeardownJobId: r.ansibleTeardownJobId || '', semaphoreTeardownJobId: r.semaphoreTeardownJobId || '',
})); }));
res.json(bookings); res.json(bookings);
} catch (err: any) { } catch (err: any) {
@ -714,14 +714,14 @@ async function startServer() {
// Trigger teardown if booking had already started and teardown not yet triggered // Trigger teardown if booking had already started and teardown not yet triggered
const now = new Date(); const now = new Date();
if (new Date(booking.startDateTime) <= now && !booking.ansibleTeardownTriggered) { if (new Date(booking.startDateTime) <= now && !booking.semaphoreTeardownTriggered) {
const templateId = lab?.semaphoreTeardownTemplateId; const templateId = lab?.semaphoreTeardownTemplateId;
if (templateId) { if (templateId) {
const jobId = await triggerSemaphoreTask(Number(templateId), { const jobId = await triggerSemaphoreTask(Number(templateId), {
booking_id: booking.id, lab_name: lab?.name || '', user_id: booking.userId, booking_id: booking.id, lab_name: lab?.name || '', user_id: booking.userId,
start_time: booking.startDateTime, end_time: booking.endDateTime, start_time: booking.startDateTime, end_time: booking.endDateTime,
}); });
db.prepare('UPDATE bookings SET ansibleTeardownTriggered = 1, ansibleTeardownJobId = ? WHERE id = ?') db.prepare('UPDATE bookings SET semaphoreTeardownTriggered = 1, semaphoreTeardownJobId = ? WHERE id = ?')
.run(jobId !== null ? String(jobId) : '', booking.id); .run(jobId !== null ? String(jobId) : '', booking.id);
} }
} }
@ -732,10 +732,10 @@ async function startServer() {
id: r.id, labId: r.labId, userId: r.userId, startDateTime: r.startDateTime, id: r.id, labId: r.labId, userId: r.userId, startDateTime: r.startDateTime,
endDateTime: r.endDateTime, notes: r.notes || '', status: r.status, endDateTime: r.endDateTime, notes: r.notes || '', status: r.status,
notified: r.notified === 1, emailSent: r.emailSent === 1, notified: r.notified === 1, emailSent: r.emailSent === 1,
ansibleSetupTriggered: r.ansibleSetupTriggered === 1, semaphoreSetupTriggered: r.semaphoreSetupTriggered === 1,
ansibleTeardownTriggered: r.ansibleTeardownTriggered === 1, semaphoreTeardownTriggered: r.semaphoreTeardownTriggered === 1,
ansibleSetupJobId: r.ansibleSetupJobId || '', semaphoreSetupJobId: r.semaphoreSetupJobId || '',
ansibleTeardownJobId: r.ansibleTeardownJobId || '', semaphoreTeardownJobId: r.semaphoreTeardownJobId || '',
}); });
} catch (err: any) { } catch (err: any) {
res.status(500).json({ error: err.message }); res.status(500).json({ error: err.message });
@ -942,7 +942,7 @@ async function startServer() {
// to 'unknown'. Runs on a self-scheduling setTimeout so that interval changes // to 'unknown'. Runs on a self-scheduling setTimeout so that interval changes
// in Settings take effect on the next cycle without a server restart. // in Settings take effect on the next cycle without a server restart.
// ------------------------------------------------------------- // -------------------------------------------------------------
function checkmkHttpHint(status: number): string { function cmkHttpHint(status: number): string {
if (status === 401) return 'HTTP 401 Unauthorized - wrong automation user or secret (check Settings > CheckMK)'; if (status === 401) return 'HTTP 401 Unauthorized - wrong automation user or secret (check Settings > CheckMK)';
if (status === 403) return 'HTTP 403 Forbidden - automation user lacks permission in CheckMK'; if (status === 403) return 'HTTP 403 Forbidden - automation user lacks permission in CheckMK';
if (status === 404) return 'HTTP 404 Not Found - API URL incorrect or site name wrong'; if (status === 404) return 'HTTP 404 Not Found - API URL incorrect or site name wrong';
@ -974,7 +974,7 @@ async function startServer() {
`${CHECKMK_API_URL}/domain-types/host_config/collections/all`, `${CHECKMK_API_URL}/domain-types/host_config/collections/all`,
{ headers } { headers }
); );
if (!cfgRes.ok) throw new Error(checkmkHttpHint(cfgRes.status)); if (!cfgRes.ok) throw new Error(cmkHttpHint(cfgRes.status));
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 ?? []) {
@ -1013,7 +1013,7 @@ async function startServer() {
`${CHECKMK_API_URL}/objects/host/${encodeURIComponent(cmkHost)}?columns=state&columns=hard_state&columns=has_been_checked`, `${CHECKMK_API_URL}/objects/host/${encodeURIComponent(cmkHost)}?columns=state&columns=hard_state&columns=has_been_checked`,
{ headers } { headers }
); );
if (!hostRes.ok) throw new Error(checkmkHttpHint(hostRes.status)); if (!hostRes.ok) throw new Error(cmkHttpHint(hostRes.status));
const hostData = await hostRes.json(); const hostData = await hostRes.json();
const state: number = hostData?.extensions?.state ?? -1; const state: number = hostData?.extensions?.state ?? -1;
@ -1106,20 +1106,20 @@ async function startServer() {
const setupPending = db.prepare( const setupPending = db.prepare(
`SELECT b.*, l.semaphoreSetupTemplateId, l.name AS labName `SELECT b.*, l.semaphoreSetupTemplateId, l.name AS labName
FROM bookings b LEFT JOIN labs l ON b.labId = l.id FROM bookings b LEFT JOIN labs l ON b.labId = l.id
WHERE b.startDateTime <= ? AND b.ansibleSetupTriggered = 0 AND b.status != 'cancelled'` WHERE b.startDateTime <= ? AND b.semaphoreSetupTriggered = 0 AND b.status != 'cancelled'`
).all(now) as any[]; ).all(now) as any[];
for (const row of setupPending) { for (const row of setupPending) {
const templateId = row.semaphoreSetupTemplateId; const templateId = row.semaphoreSetupTemplateId;
if (!templateId) { if (!templateId) {
db.prepare('UPDATE bookings SET ansibleSetupTriggered = 1 WHERE id = ?').run(row.id); db.prepare('UPDATE bookings SET semaphoreSetupTriggered = 1 WHERE id = ?').run(row.id);
continue; continue;
} }
const jobId = await triggerSemaphoreTask(Number(templateId), { const jobId = await triggerSemaphoreTask(Number(templateId), {
booking_id: row.id, lab_name: row.labName || '', user_id: row.userId, booking_id: row.id, lab_name: row.labName || '', user_id: row.userId,
start_time: row.startDateTime, end_time: row.endDateTime, start_time: row.startDateTime, end_time: row.endDateTime,
}); });
db.prepare('UPDATE bookings SET ansibleSetupTriggered = 1, ansibleSetupJobId = ? WHERE id = ?') db.prepare('UPDATE bookings SET semaphoreSetupTriggered = 1, semaphoreSetupJobId = ? WHERE id = ?')
.run(jobId !== null ? String(jobId) : '', row.id); .run(jobId !== null ? String(jobId) : '', row.id);
} }
@ -1127,20 +1127,20 @@ async function startServer() {
const teardownPending = db.prepare( const teardownPending = db.prepare(
`SELECT b.*, l.semaphoreTeardownTemplateId, l.name AS labName `SELECT b.*, l.semaphoreTeardownTemplateId, l.name AS labName
FROM bookings b LEFT JOIN labs l ON b.labId = l.id FROM bookings b LEFT JOIN labs l ON b.labId = l.id
WHERE b.endDateTime <= ? AND b.ansibleTeardownTriggered = 0 AND b.status != 'cancelled'` WHERE b.endDateTime <= ? AND b.semaphoreTeardownTriggered = 0 AND b.status != 'cancelled'`
).all(now) as any[]; ).all(now) as any[];
for (const row of teardownPending) { for (const row of teardownPending) {
const templateId = row.semaphoreTeardownTemplateId; const templateId = row.semaphoreTeardownTemplateId;
if (!templateId) { if (!templateId) {
db.prepare('UPDATE bookings SET ansibleTeardownTriggered = 1 WHERE id = ?').run(row.id); db.prepare('UPDATE bookings SET semaphoreTeardownTriggered = 1 WHERE id = ?').run(row.id);
continue; continue;
} }
const jobId = await triggerSemaphoreTask(Number(templateId), { const jobId = await triggerSemaphoreTask(Number(templateId), {
booking_id: row.id, lab_name: row.labName || '', user_id: row.userId, booking_id: row.id, lab_name: row.labName || '', user_id: row.userId,
start_time: row.startDateTime, end_time: row.endDateTime, start_time: row.startDateTime, end_time: row.endDateTime,
}); });
db.prepare('UPDATE bookings SET ansibleTeardownTriggered = 1, ansibleTeardownJobId = ? WHERE id = ?') db.prepare('UPDATE bookings SET semaphoreTeardownTriggered = 1, semaphoreTeardownJobId = ? WHERE id = ?')
.run(jobId !== null ? String(jobId) : '', row.id); .run(jobId !== null ? String(jobId) : '', row.id);
} }
} }
@ -1190,10 +1190,10 @@ async function startServer() {
}); });
if (type === 'setup') { if (type === 'setup') {
db.prepare('UPDATE bookings SET ansibleSetupTriggered = 1, ansibleSetupJobId = ? WHERE id = ?') db.prepare('UPDATE bookings SET semaphoreSetupTriggered = 1, semaphoreSetupJobId = ? WHERE id = ?')
.run(jobId !== null ? String(jobId) : '', bookingId); .run(jobId !== null ? String(jobId) : '', bookingId);
} else { } else {
db.prepare('UPDATE bookings SET ansibleTeardownTriggered = 1, ansibleTeardownJobId = ? WHERE id = ?') db.prepare('UPDATE bookings SET semaphoreTeardownTriggered = 1, semaphoreTeardownJobId = ? WHERE id = ?')
.run(jobId !== null ? String(jobId) : '', bookingId); .run(jobId !== null ? String(jobId) : '', bookingId);
} }

View File

@ -53,8 +53,8 @@ export default function App() {
const [remindedBookings, setRemindedBookings] = useState<Set<string>>(new Set()); const [remindedBookings, setRemindedBookings] = useState<Set<string>>(new Set());
const [inventoryHighlightDevice, setInventoryHighlightDevice] = useState<Device | null>(null); const [inventoryHighlightDevice, setInventoryHighlightDevice] = useState<Device | null>(null);
const [checkmkEnabled, setCheckmkEnabled] = useState(false); const [cmkEnabled, setCmkEnabled] = useState(false);
const [checkmkBaseUrl, setCheckmkBaseUrl] = useState(''); const [cmkBaseUrl, setCmkBaseUrl] = useState('');
const [isProduction, setIsProduction] = useState(false); const [isProduction, setIsProduction] = useState(false);
const [semaphoreEnabled, setSemaphoreEnabled] = useState(false); const [semaphoreEnabled, setSemaphoreEnabled] = useState(false);
@ -145,7 +145,7 @@ export default function App() {
if (bookingsRes.ok) setBookings(await bookingsRes.json()); if (bookingsRes.ok) setBookings(await bookingsRes.json());
if (logsRes.ok) setLogs(await logsRes.json()); if (logsRes.ok) setLogs(await logsRes.json());
if (linksRes.ok) setLinks(await linksRes.json()); if (linksRes.ok) setLinks(await linksRes.json());
if (configRes.ok) { const cfg = await configRes.json(); setCheckmkEnabled(!!cfg.checkmkEnabled); setCheckmkBaseUrl(cfg.checkmkBaseUrl || ''); setIsProduction(!!cfg.isProduction); setSemaphoreEnabled(!!cfg.semaphoreEnabled); } if (configRes.ok) { const cfg = await configRes.json(); setCmkEnabled(!!cfg.cmkEnabled); setCmkBaseUrl(cfg.cmkBaseUrl || ''); setIsProduction(!!cfg.isProduction); setSemaphoreEnabled(!!cfg.semaphoreEnabled); }
} catch (err) { } catch (err) {
console.error('[App] Failed to load data:', err); console.error('[App] Failed to load data:', err);
} finally { } finally {
@ -588,7 +588,7 @@ export default function App() {
labs={labs} labs={labs}
devices={devices} devices={devices}
currentUser={currentUser} currentUser={currentUser}
checkmkEnabled={checkmkEnabled} cmkEnabled={cmkEnabled}
onAddBooking={handleAddBooking} onAddBooking={handleAddBooking}
onCancelBooking={handleCancelBooking} onCancelBooking={handleCancelBooking}
onDeleteBooking={handleDeleteBooking} onDeleteBooking={handleDeleteBooking}
@ -598,8 +598,8 @@ export default function App() {
{activeTab === 'devices' && ( {activeTab === 'devices' && (
<DeviceInventory <DeviceInventory
devices={devices} devices={devices}
checkmkEnabled={checkmkEnabled} cmkEnabled={cmkEnabled}
checkmkBaseUrl={checkmkBaseUrl} cmkBaseUrl={cmkBaseUrl}
onAddDevice={handleAddDevice} onAddDevice={handleAddDevice}
onUpdateDevice={handleUpdateDevice} onUpdateDevice={handleUpdateDevice}
onDeleteDevice={handleDeleteDevice} onDeleteDevice={handleDeleteDevice}

View File

@ -17,7 +17,7 @@ interface BookingCalendarProps {
labs: LabTemplate[]; labs: LabTemplate[];
devices: Device[]; devices: Device[];
currentUser: User; currentUser: User;
checkmkEnabled: boolean; cmkEnabled: boolean;
onAddBooking: (booking: Omit<Booking, 'id' | 'notified' | 'emailSent'>) => void; onAddBooking: (booking: Omit<Booking, 'id' | 'notified' | 'emailSent'>) => void;
onCancelBooking: (id: string) => void; onCancelBooking: (id: string) => void;
onDeleteBooking: (id: string) => void; onDeleteBooking: (id: string) => void;
@ -73,7 +73,7 @@ export default function BookingCalendar({
labs, labs,
devices, devices,
currentUser, currentUser,
checkmkEnabled, cmkEnabled,
onAddBooking, onAddBooking,
onCancelBooking, onCancelBooking,
onDeleteBooking, onDeleteBooking,
@ -698,7 +698,7 @@ export default function BookingCalendar({
</div> </div>
); );
} }
if (checkmkEnabled && offline.length > 0) { if (cmkEnabled && offline.length > 0) {
return ( return (
<div className="bg-amber-950/40 p-2.5 rounded border border-amber-800/60 flex gap-2 text-amber-300 text-[11px] leading-normal"> <div className="bg-amber-950/40 p-2.5 rounded border border-amber-800/60 flex gap-2 text-amber-300 text-[11px] leading-normal">
<AlertTriangle className="w-4 h-4 text-amber-400 shrink-0" /> <AlertTriangle className="w-4 h-4 text-amber-400 shrink-0" />

View File

@ -47,10 +47,10 @@ export default function BookingDetailsModal({
const [localTeardownTriggered, setLocalTeardownTriggered] = useState(false); const [localTeardownTriggered, setLocalTeardownTriggered] = useState(false);
const [localTeardownJobId, setLocalTeardownJobId] = useState(''); const [localTeardownJobId, setLocalTeardownJobId] = useState('');
const setupTriggered = booking.ansibleSetupTriggered || localSetupTriggered; const setupTriggered = booking.semaphoreSetupTriggered || localSetupTriggered;
const setupJobId = booking.ansibleSetupJobId || localSetupJobId; const setupJobId = booking.semaphoreSetupJobId || localSetupJobId;
const teardownTriggered = booking.ansibleTeardownTriggered || localTeardownTriggered; const teardownTriggered = booking.semaphoreTeardownTriggered || localTeardownTriggered;
const teardownJobId = booking.ansibleTeardownJobId || localTeardownJobId; const teardownJobId = booking.semaphoreTeardownJobId || localTeardownJobId;
async function manualTrigger(type: 'setup' | 'teardown') { async function manualTrigger(type: 'setup' | 'teardown') {
setTriggering(true); setTriggering(true);

View File

@ -15,8 +15,8 @@ const DEVICE_CLASS_PRESETS = ['Switch', 'Firewall', 'Access-Point', 'Controller'
interface DeviceInventoryProps { interface DeviceInventoryProps {
devices: Device[]; devices: Device[];
checkmkEnabled: boolean; cmkEnabled: boolean;
checkmkBaseUrl: string; cmkBaseUrl: string;
onAddDevice: (device: Omit<Device, 'id'>) => void; onAddDevice: (device: Omit<Device, 'id'>) => void;
onUpdateDevice: (device: Device) => void; onUpdateDevice: (device: Device) => void;
onDeleteDevice: (id: string) => void; onDeleteDevice: (id: string) => void;
@ -24,8 +24,8 @@ interface DeviceInventoryProps {
export default function DeviceInventory({ export default function DeviceInventory({
devices, devices,
checkmkEnabled, cmkEnabled,
checkmkBaseUrl, cmkBaseUrl,
onAddDevice, onAddDevice,
onUpdateDevice, onUpdateDevice,
onDeleteDevice, onDeleteDevice,
@ -65,8 +65,8 @@ export default function DeviceInventory({
const effectiveStatus = (d: Device): 'online' | 'offline' | 'unknown' => d.status; const effectiveStatus = (d: Device): 'online' | 'offline' | 'unknown' => d.status;
const cmkHostUrl = (d: Device) => const cmkHostUrl = (d: Device) =>
checkmkEnabled && checkmkBaseUrl && d.cmkHostname cmkEnabled && cmkBaseUrl && d.cmkHostname
? `${checkmkBaseUrl}/index.py?host=${encodeURIComponent(d.cmkHostname)}` ? `${cmkBaseUrl}/index.py?host=${encodeURIComponent(d.cmkHostname)}`
: null; : null;
const statusMeta = (s: 'online' | 'offline' | 'unknown') => { const statusMeta = (s: 'online' | 'offline' | 'unknown') => {
@ -324,7 +324,7 @@ export default function DeviceInventory({
{/* Right: Actions and Status */} {/* Right: Actions and Status */}
<div className="flex items-center gap-3" onClick={(e) => e.stopPropagation()}> <div className="flex items-center gap-3" onClick={(e) => e.stopPropagation()}>
{/* CheckMK Status Badge only when CheckMK is enabled */} {/* CheckMK Status Badge only when CheckMK is enabled */}
{checkmkEnabled && (() => { const m = statusMeta(effectiveStatus(device)); return ( {cmkEnabled && (() => { const m = statusMeta(effectiveStatus(device)); return (
<div className="flex flex-col items-end gap-1 font-sans"> <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={`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}`} /> <span className={`w-1.5 h-1.5 rounded-full ${m.dot}`} />
@ -402,7 +402,7 @@ export default function DeviceInventory({
</div> </div>
{/* CheckMK Monitoring Panel only when CheckMK is enabled */} {/* CheckMK Monitoring Panel only when CheckMK is enabled */}
{checkmkEnabled && ( {cmkEnabled && (
<div className="mt-4 pt-4 border-t border-slate-800 space-y-2.5"> <div className="mt-4 pt-4 border-t border-slate-800 space-y-2.5">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-xs text-slate-400 font-sans font-medium flex items-center gap-1.5"> <span className="text-xs text-slate-400 font-sans font-medium flex items-center gap-1.5">

View File

@ -171,13 +171,13 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
const [azureAllowedGroup, setAzureAllowedGroup] = useState(''); const [azureAllowedGroup, setAzureAllowedGroup] = useState('');
const [showAzureSecret, setShowAzureSecret] = useState(false); const [showAzureSecret, setShowAzureSecret] = useState(false);
const [checkmkEnabled, setCheckmkEnabled] = useState(false); const [cmkEnabled, setCmkEnabled] = useState(false);
const [checkmkApiUrl, setCheckmkApiUrl] = useState(''); const [cmkApiUrl, setCheckmkApiUrl] = useState('');
const [checkmkApiUser, setCheckmkApiUser] = useState(''); const [cmkApiUser, setCheckmkApiUser] = useState('');
const [checkmkApiSecret, setCheckmkApiSecret] = useState(''); const [cmkApiSecret, setCheckmkApiSecret] = useState('');
const [checkmkSecretSet, setCheckmkSecretSet] = useState(false); const [cmkSecretSet, setCheckmkSecretSet] = useState(false);
const [checkmkSyncInterval, setCheckmkSyncInterval] = useState('60000'); const [cmkSyncInterval, setCheckmkSyncInterval] = useState('60000');
const [showCheckmkSecret, setShowCheckmkSecret] = useState(false); const [showCmkSecret, setShowCheckmkSecret] = useState(false);
const [syncing, setSyncing] = useState(false); const [syncing, setSyncing] = useState(false);
const [semaphoreEnabled, setSemaphoreEnabled] = useState(false); const [semaphoreEnabled, setSemaphoreEnabled] = useState(false);
@ -251,7 +251,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
setAzureClientSecret(''); setAzureClientSecret('');
setAzureRedirectUri(data.azure_redirect_uri || ''); setAzureRedirectUri(data.azure_redirect_uri || '');
setAzureAllowedGroup(data.azure_allowed_group || ''); setAzureAllowedGroup(data.azure_allowed_group || '');
setCheckmkEnabled(data.checkmk_enabled === 'true'); setCmkEnabled(data.checkmk_enabled === 'true');
setCheckmkApiUrl(data.checkmk_api_url || ''); setCheckmkApiUrl(data.checkmk_api_url || '');
setCheckmkApiUser(data.checkmk_api_user || ''); setCheckmkApiUser(data.checkmk_api_user || '');
setCheckmkSecretSet(data.checkmk_api_secret === SECRET_SENTINEL); setCheckmkSecretSet(data.checkmk_api_secret === SECRET_SENTINEL);
@ -281,10 +281,10 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
azure_tenant_id: azureTenantId, azure_tenant_id: azureTenantId,
azure_redirect_uri: azureRedirectUri, azure_redirect_uri: azureRedirectUri,
azure_allowed_group: azureAllowedGroup, azure_allowed_group: azureAllowedGroup,
checkmk_enabled: checkmkEnabled ? 'true' : 'false', checkmk_enabled: cmkEnabled ? 'true' : 'false',
checkmk_api_url: checkmkApiUrl, checkmk_api_url: cmkApiUrl,
checkmk_api_user: checkmkApiUser, checkmk_api_user: cmkApiUser,
checkmk_sync_interval_ms: checkmkSyncInterval, checkmk_sync_interval_ms: cmkSyncInterval,
semaphore_enabled: semaphoreEnabled ? 'true' : 'false', semaphore_enabled: semaphoreEnabled ? 'true' : 'false',
semaphore_api_url: semaphoreApiUrl, semaphore_api_url: semaphoreApiUrl,
semaphore_project_id: semaphoreProjectId, semaphore_project_id: semaphoreProjectId,
@ -292,7 +292,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
caddy_admin_url: caddyAdminUrl, caddy_admin_url: caddyAdminUrl,
}; };
if (azureClientSecret) payload.azure_client_secret = azureClientSecret; if (azureClientSecret) payload.azure_client_secret = azureClientSecret;
if (checkmkApiSecret) payload.checkmk_api_secret = checkmkApiSecret; if (cmkApiSecret) payload.checkmk_api_secret = cmkApiSecret;
if (semaphoreApiToken) payload.semaphore_api_token = semaphoreApiToken; if (semaphoreApiToken) payload.semaphore_api_token = semaphoreApiToken;
try { try {
const res = await authFetch('/api/settings', { method: 'PUT', body: JSON.stringify(payload) }); const res = await authFetch('/api/settings', { method: 'PUT', body: JSON.stringify(payload) });
@ -706,7 +706,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
<div> <div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h2 className="text-sm font-semibold text-white">CheckMK</h2> <h2 className="text-sm font-semibold text-white">CheckMK</h2>
{checkmkEnabled && checkmkApiUrl && checkmkSecretSet && ( {cmkEnabled && cmkApiUrl && cmkSecretSet && (
<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="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" /> <span className="w-1 h-1 rounded-full bg-emerald-400 animate-pulse inline-block" />
ACTIVE ACTIVE
@ -717,25 +717,25 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
</div> </div>
</div> </div>
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<span className={`text-[10px] font-semibold font-mono ${checkmkEnabled ? 'text-emerald-400' : 'text-slate-600'}`}> <span className={`text-[10px] font-semibold font-mono ${cmkEnabled ? 'text-emerald-400' : 'text-slate-600'}`}>
{checkmkEnabled ? 'ENABLED' : 'DISABLED'} {cmkEnabled ? 'ENABLED' : 'DISABLED'}
</span> </span>
<button <button
type="button" type="button"
onClick={() => setCheckmkEnabled(v => !v)} onClick={() => setCmkEnabled(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'}`} className={`relative w-10 h-5 rounded-full transition-all duration-200 focus:outline-none ${cmkEnabled ? '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'}`} /> <span className={`absolute top-0.5 w-4 h-4 bg-white rounded-full shadow transition-all duration-200 ${cmkEnabled ? 'left-5' : 'left-0.5'}`} />
</button> </button>
</div> </div>
</div> </div>
<div className="h-px bg-slate-800/60" /> <div className="h-px bg-slate-800/60" />
<div className={`space-y-5 transition-opacity duration-200 ${!checkmkEnabled ? 'opacity-40 pointer-events-none' : ''}`}> <div className={`space-y-5 transition-opacity duration-200 ${!cmkEnabled ? 'opacity-40 pointer-events-none' : ''}`}>
<FieldRow label="API URL"> <FieldRow label="API URL">
<Input <Input
value={checkmkApiUrl} value={cmkApiUrl}
onChange={setCheckmkApiUrl} onChange={setCheckmkApiUrl}
monospace monospace
icon={<Globe className="w-3.5 h-3.5" />} icon={<Globe className="w-3.5 h-3.5" />}
@ -744,7 +744,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
<FieldRow label="Automation User" hint="Setup > Users > Automation user"> <FieldRow label="Automation User" hint="Setup > Users > Automation user">
<Input <Input
value={checkmkApiUser} value={cmkApiUser}
onChange={setCheckmkApiUser} onChange={setCheckmkApiUser}
monospace monospace
icon={<KeyRound className="w-3.5 h-3.5" />} icon={<KeyRound className="w-3.5 h-3.5" />}
@ -753,25 +753,25 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
<FieldRow <FieldRow
label="Automation Secret" label="Automation Secret"
hint="Setup > Users > Automation user > Automation secret" hint="Setup > Users > Automation user > Automation secret"
badge={checkmkSecretSet ? <ConfiguredBadge /> : undefined} badge={cmkSecretSet ? <ConfiguredBadge /> : undefined}
> >
<SecretInput <SecretInput
value={checkmkApiSecret} value={cmkApiSecret}
onChange={setCheckmkApiSecret} onChange={setCheckmkApiSecret}
show={showCheckmkSecret} show={showCmkSecret}
onToggleShow={() => setShowCheckmkSecret(v => !v)} onToggleShow={() => setShowCheckmkSecret(v => !v)}
/> />
</FieldRow> </FieldRow>
<FieldRow label="Sync Interval" hint="Milliseconds between status polls (default: 60000)"> <FieldRow label="Sync Interval" hint="Milliseconds between status polls (default: 60000)">
<Input <Input
value={checkmkSyncInterval} value={cmkSyncInterval}
onChange={setCheckmkSyncInterval} onChange={setCheckmkSyncInterval}
monospace monospace
icon={<Clock className="w-3.5 h-3.5" />} icon={<Clock className="w-3.5 h-3.5" />}
/> />
</FieldRow> </FieldRow>
</div> </div>
{checkmkApiUrl && checkmkSecretSet && ( {cmkApiUrl && cmkSecretSet && (
<button <button
type="button" type="button"
onClick={runSync} onClick={runSync}

View File

@ -50,10 +50,10 @@ export interface Booking {
status: 'active' | 'upcoming' | 'completed' | 'cancelled'; status: 'active' | 'upcoming' | 'completed' | 'cancelled';
notified: boolean; notified: boolean;
emailSent?: boolean; emailSent?: boolean;
ansibleSetupTriggered?: boolean; semaphoreSetupTriggered?: boolean;
ansibleTeardownTriggered?: boolean; semaphoreTeardownTriggered?: boolean;
ansibleSetupJobId?: string; semaphoreSetupJobId?: string;
ansibleTeardownJobId?: string; semaphoreTeardownJobId?: string;
} }
export interface LogEntry { export interface LogEntry {