diff --git a/js/ui.js b/js/ui.js
index 7156fc5..a7ddd7f 100644
--- a/js/ui.js
+++ b/js/ui.js
@@ -463,6 +463,13 @@ async function loadJobDetail(jobId) {
+
+
+
${_renderJobConfigFields(job.key, cfg)}
@@ -521,10 +528,12 @@ async function saveJobDetail(jobId) {
const apiKey = document.getElementById('job-cfg-api-key');
const apiUrl = document.getElementById('job-cfg-api-url');
const apiToken = document.getElementById('job-cfg-api-token');
- if (tailnet) cfg.tailnet = tailnet.value.trim();
- if (apiKey) cfg.api_key = apiKey.value;
- if (apiUrl) cfg.api_url = apiUrl.value.trim();
- if (apiToken) cfg.api_token = apiToken.value;
+ if (tailnet) cfg.tailnet = tailnet.value.trim();
+ if (apiKey) cfg.api_key = apiKey.value;
+ if (apiUrl) cfg.api_url = apiUrl.value.trim();
+ if (apiToken) cfg.api_token = apiToken.value;
+ const runOnCreate = document.getElementById('job-run-on-create');
+ if (runOnCreate) cfg.run_on_create = runOnCreate.checked;
const res = await fetch(`/api/jobs/${jobId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
diff --git a/server/jobs.js b/server/jobs.js
index cfcb20e..f6b1a59 100644
--- a/server/jobs.js
+++ b/server/jobs.js
@@ -129,6 +129,13 @@ export async function runJob(jobId) {
const _intervals = new Map();
+export async function runJobsOnCreate() {
+ for (const job of getJobs()) {
+ const cfg = JSON.parse(job.config || '{}');
+ if (cfg.run_on_create) runJob(job.id).catch(e => console.error(`runJobsOnCreate job ${job.id}:`, e));
+ }
+}
+
export function restartJobs() {
for (const iv of _intervals.values()) clearInterval(iv);
_intervals.clear();
diff --git a/server/routes.js b/server/routes.js
index eed881c..fba5552 100644
--- a/server/routes.js
+++ b/server/routes.js
@@ -5,7 +5,7 @@ import {
getConfig, setConfig, getJobs, getJob, updateJob, getJobRuns,
getAllJobs, getAllJobRuns, importJobs,
} from './db.js';
-import { runJob, restartJobs } from './jobs.js';
+import { runJob, restartJobs, runJobsOnCreate } from './jobs.js';
export const router = Router();
@@ -102,6 +102,7 @@ router.post('/instances', (req, res) => {
createInstance(data);
const created = getInstance(data.vmid);
res.status(201).json(created);
+ runJobsOnCreate().catch(() => {});
} catch (e) {
handleDbError('POST /api/instances', e, res);
}
diff --git a/tests/api.test.js b/tests/api.test.js
index 6b7e652..5373d62 100644
--- a/tests/api.test.js
+++ b/tests/api.test.js
@@ -623,6 +623,25 @@ describe('POST /api/jobs/:id/run', () => {
expect(res.status).toBe(500)
})
+ it('run_on_create: triggers matching jobs when an instance is created', async () => {
+ createJob({ ...testJob, config: JSON.stringify({ api_key: 'k', tailnet: 't', run_on_create: true }) })
+ const id = (await request(app).get('/api/jobs')).body[0].id
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, json: async () => ({ devices: [] }) }))
+ await request(app).post('/api/instances').send(base)
+ await new Promise(r => setImmediate(r))
+ const detail = await request(app).get(`/api/jobs/${id}`)
+ expect(detail.body.runs).toHaveLength(1)
+ expect(detail.body.runs[0].status).toBe('success')
+ })
+
+ it('run_on_create: does not trigger jobs without the flag', async () => {
+ createJob(testJob)
+ const id = (await request(app).get('/api/jobs')).body[0].id
+ await request(app).post('/api/instances').send(base)
+ await new Promise(r => setImmediate(r))
+ expect((await request(app).get(`/api/jobs/${id}`)).body.runs).toHaveLength(0)
+ })
+
it('semaphore_sync: parses ansible inventory and updates instances', async () => {
const semaphoreJob = {
key: 'semaphore_sync', name: 'Semaphore Sync', description: 'test',