From 72c2c02e495a3bdaf0df8ffba057f19a740cb4e2 Mon Sep 17 00:00:00 2001 From: orangecoding Date: Sat, 13 Jun 2026 13:14:07 +0200 Subject: [PATCH] fixing job state setting when job is disabled --- lib/api/routes/jobRouter.js | 2 +- lib/services/storage/jobStorage.js | 12 ++++- test/storage/jobStorage.test.js | 64 +++++++++++++++++++++++++ ui/src/components/grid/jobs/JobGrid.jsx | 1 + 4 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 test/storage/jobStorage.test.js diff --git a/lib/api/routes/jobRouter.js b/lib/api/routes/jobRouter.js index c6b6f4d..280f613 100644 --- a/lib/api/routes/jobRouter.js +++ b/lib/api/routes/jobRouter.js @@ -29,7 +29,7 @@ export default async function jobPlugin(fastify) { fastify.get('/', async (request) => { const isUserAdmin = isAdmin(request); return jobStorage - .getJobs() + .getJobs({ includeDisabled: true }) .filter( (job) => isUserAdmin || diff --git a/lib/services/storage/jobStorage.js b/lib/services/storage/jobStorage.js index 1796d54..3e386c7 100644 --- a/lib/services/storage/jobStorage.js +++ b/lib/services/storage/jobStorage.js @@ -169,9 +169,17 @@ export const removeJobsByUserId = (userId) => { /** * Get all jobs. + * + * By default only enabled jobs are returned, since most callers (scheduler, + * geocoding cron, tracker, dashboard) operate on active jobs only. The UI, + * however, must also be able to load disabled jobs (e.g. to edit them or view + * their listings), so it passes `includeDisabled: true`. + * + * @param {Object} [params] + * @param {boolean} [params.includeDisabled=false] - When true, disabled jobs are included. * @returns {Job[]} List of jobs ordered by name (NULLs last). */ -export const getJobs = () => { +export const getJobs = ({ includeDisabled = false } = {}) => { const rows = SqliteConnection.query( `SELECT j.id, j.user_id AS userId, @@ -186,7 +194,7 @@ export const getJobs = () => { j.last_run_at AS lastRunAt, (SELECT COUNT(1) FROM listings l WHERE l.job_id = j.id AND l.is_active = 1 AND l.manually_deleted = 0) AS numberOfFoundListings FROM jobs j - WHERE j.enabled = 1 + ${includeDisabled ? '' : 'WHERE j.enabled = 1'} ORDER BY j.name IS NULL, j.name`, ); return rows.map((row) => ({ diff --git a/test/storage/jobStorage.test.js b/test/storage/jobStorage.test.js new file mode 100644 index 0000000..19b0a85 --- /dev/null +++ b/test/storage/jobStorage.test.js @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026 by Christian Kellner. + * Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause + */ + +import { vi, describe, it, expect, beforeEach } from 'vitest'; + +// Mock SqliteConnection so we can assert which SQL the storage layer runs +// without spinning up a real SQLite DB. + +const calls = { + execute: [], + query: [], +}; + +const sqliteMock = { + execute: (sql, params) => { + calls.execute.push({ sql, params }); + return { changes: 1 }; + }, + query: (sql, params) => { + calls.query.push({ sql, params }); + if (sqliteMock.__queryHandler) return sqliteMock.__queryHandler(sql, params); + return []; + }, + __queryHandler: null, +}; + +vi.mock('../../lib/services/storage/SqliteConnection.js', () => ({ + default: sqliteMock, +})); + +describe('jobStorage.getJobs', () => { + let jobStorage; + + beforeEach(async () => { + calls.execute.length = 0; + calls.query.length = 0; + sqliteMock.__queryHandler = null; + jobStorage = await import('../../lib/services/storage/jobStorage.js'); + }); + + it('filters out disabled jobs by default (WHERE j.enabled = 1)', () => { + jobStorage.getJobs(); + expect(calls.query).toHaveLength(1); + expect(calls.query[0].sql).toMatch(/WHERE j\.enabled = 1/); + }); + + it('includes disabled jobs when includeDisabled is true', () => { + jobStorage.getJobs({ includeDisabled: true }); + expect(calls.query).toHaveLength(1); + expect(calls.query[0].sql).not.toMatch(/WHERE j\.enabled = 1/); + }); + + it('coerces the enabled column to a boolean', () => { + sqliteMock.__queryHandler = () => [ + { id: 'enabled-job', enabled: 1 }, + { id: 'disabled-job', enabled: 0 }, + ]; + const jobs = jobStorage.getJobs({ includeDisabled: true }); + expect(jobs.find((j) => j.id === 'enabled-job').enabled).toBe(true); + expect(jobs.find((j) => j.id === 'disabled-job').enabled).toBe(false); + }); +}); diff --git a/ui/src/components/grid/jobs/JobGrid.jsx b/ui/src/components/grid/jobs/JobGrid.jsx index 6210a63..3186688 100644 --- a/ui/src/components/grid/jobs/JobGrid.jsx +++ b/ui/src/components/grid/jobs/JobGrid.jsx @@ -185,6 +185,7 @@ const JobGrid = () => { await xhrPut(`/api/jobs/${jobId}/status`, { status }); Toast.success(t('jobs.toastStatusChanged')); loadData(); + actions.jobsData.getJobs(); // refresh the jobs slice read by the edit form so its switch isn't stale } catch (error) { Toast.error(error.error); }