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:
10
server-db.ts
10
server-db.ts
@ -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 (
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
46
server.ts
46
server.ts
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
12
src/App.tsx
12
src/App.tsx
@ -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}
|
||||||
|
|||||||
@ -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" />
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user