feat(semaphore): trigger Ansible tasks at booking start/end via Semaphore

- Background scheduler checks every 30s for bookings that need setup or teardown
- Per-lab Semaphore template IDs stored on the labs table
- Booking flags track which jobs have been triggered and their Semaphore job IDs
- Immediate teardown triggered when an active booking is cancelled
- Settings UI section for Semaphore API URL, token, and project ID
- Lab template form fields for setup/teardown template IDs
- BookingDetailsModal shows live Ansible job status with manual trigger buttons
This commit is contained in:
Brückner
2026-06-05 09:39:58 +02:00
parent 11eb06c5ad
commit 70399a00ec
6 changed files with 488 additions and 24 deletions

View File

@ -8,7 +8,7 @@ import { LabTemplate, Device, TopologyLink } from '../types';
import TopologyPanel from './TopologyPanel';
import {
Server, Plus, Edit3, Trash, User, MapPin,
Layers, ChevronRight, Save, X, Check, Pencil
Layers, ChevronRight, Save, X, Check, Pencil, Terminal,
} from 'lucide-react';
interface LabTemplatesProps {
@ -47,12 +47,16 @@ export default function LabTemplates({
contactPerson: string;
location: string;
deviceIds: string[];
semaphoreSetupTemplateId: string;
semaphoreTeardownTemplateId: string;
}>({
name: '',
description: '',
contactPerson: '',
location: '',
deviceIds: []
deviceIds: [],
semaphoreSetupTemplateId: '',
semaphoreTeardownTemplateId: '',
});
// Calculate filtered devices associated with selected lab
@ -68,7 +72,9 @@ export default function LabTemplates({
description: '',
contactPerson: '',
location: '',
deviceIds: []
deviceIds: [],
semaphoreSetupTemplateId: '',
semaphoreTeardownTemplateId: '',
});
setIsEditing(true);
};
@ -82,7 +88,9 @@ export default function LabTemplates({
description: lab.description,
contactPerson: lab.contactPerson,
location: lab.location,
deviceIds: [...lab.deviceIds]
deviceIds: [...lab.deviceIds],
semaphoreSetupTemplateId: lab.semaphoreSetupTemplateId || '',
semaphoreTeardownTemplateId: lab.semaphoreTeardownTemplateId || '',
});
setIsEditing(true);
};
@ -126,7 +134,9 @@ export default function LabTemplates({
contactPerson: formData.contactPerson,
location: formData.location,
deviceIds: formData.deviceIds,
topology: tempLinks
topology: tempLinks,
semaphoreSetupTemplateId: formData.semaphoreSetupTemplateId,
semaphoreTeardownTemplateId: formData.semaphoreTeardownTemplateId,
};
if (formMode === 'add') {
@ -525,6 +535,39 @@ export default function LabTemplates({
)}
</div>
{/* Ansible Semaphore Automation */}
<div className="border-t border-slate-800 pt-3">
<label className="block text-slate-300 font-bold mb-1.5 flex items-center gap-1.5">
<Terminal className="w-3.5 h-3.5 text-orange-400" />
3. Ansible Automation (optional)
</label>
<p className="text-[10px] text-slate-400 mb-2">Semaphore task template IDs to trigger at booking start and end. Leave empty to skip automation for this lab.</p>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-[10px] text-slate-400 mb-1">Setup Template ID</label>
<input
type="text"
inputMode="numeric"
value={formData.semaphoreSetupTemplateId}
onChange={(e) => setFormData({ ...formData, semaphoreSetupTemplateId: e.target.value })}
className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 font-mono text-xs focus:outline-none focus:border-orange-500/60"
placeholder="e.g. 3"
/>
</div>
<div>
<label className="block text-[10px] text-slate-400 mb-1">Teardown Template ID</label>
<input
type="text"
inputMode="numeric"
value={formData.semaphoreTeardownTemplateId}
onChange={(e) => setFormData({ ...formData, semaphoreTeardownTemplateId: e.target.value })}
className="w-full bg-slate-950 text-slate-100 border border-slate-800 rounded p-2 font-mono text-xs focus:outline-none focus:border-orange-500/60"
placeholder="e.g. 4"
/>
</div>
</div>
</div>
{/* Form submit handlers */}
<div className="pt-3 border-t border-slate-800 flex justify-end gap-2.5">
<button