@open-mercato/scheduler 0.7.0 → 0.7.1-develop.7103.1.41ff100d93

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/scheduler/__integration__/TC-SCHED-001.spec.js +2 -2
  3. package/dist/modules/scheduler/__integration__/TC-SCHED-001.spec.js.map +2 -2
  4. package/dist/modules/scheduler/__integration__/TC-SCHED-004.spec.js +9 -4
  5. package/dist/modules/scheduler/__integration__/TC-SCHED-004.spec.js.map +2 -2
  6. package/dist/modules/scheduler/__integration__/TC-SCHED-007.spec.js +3 -3
  7. package/dist/modules/scheduler/__integration__/TC-SCHED-007.spec.js.map +2 -2
  8. package/dist/modules/scheduler/__integration__/TC-SCHED-008-safe-targets.spec.js +72 -0
  9. package/dist/modules/scheduler/__integration__/TC-SCHED-008-safe-targets.spec.js.map +7 -0
  10. package/dist/modules/scheduler/__integration__/TC-SCHED-008.spec.js +66 -0
  11. package/dist/modules/scheduler/__integration__/TC-SCHED-008.spec.js.map +7 -0
  12. package/dist/modules/scheduler/__integration__/TC-SCHED-CRUDFORM-001.spec.js +3 -3
  13. package/dist/modules/scheduler/__integration__/TC-SCHED-CRUDFORM-001.spec.js.map +1 -1
  14. package/dist/modules/scheduler/__integration__/TC-UNDO-001-scheduler-jobs.spec.js +107 -44
  15. package/dist/modules/scheduler/__integration__/TC-UNDO-001-scheduler-jobs.spec.js.map +2 -2
  16. package/dist/modules/scheduler/__integration__/helpers/scheduler.js +13 -3
  17. package/dist/modules/scheduler/__integration__/helpers/scheduler.js.map +2 -2
  18. package/dist/modules/scheduler/api/jobs/[id]/executions/route.js +7 -13
  19. package/dist/modules/scheduler/api/jobs/[id]/executions/route.js.map +2 -2
  20. package/dist/modules/scheduler/api/targets/route.js +2 -12
  21. package/dist/modules/scheduler/api/targets/route.js.map +2 -2
  22. package/dist/modules/scheduler/api/trigger/route.js +7 -13
  23. package/dist/modules/scheduler/api/trigger/route.js.map +2 -2
  24. package/dist/modules/scheduler/backend/config/scheduled-jobs/page.js +3 -1
  25. package/dist/modules/scheduler/backend/config/scheduled-jobs/page.js.map +2 -2
  26. package/dist/modules/scheduler/cli.js +32 -1
  27. package/dist/modules/scheduler/cli.js.map +2 -2
  28. package/dist/modules/scheduler/commands/jobs.js +84 -2
  29. package/dist/modules/scheduler/commands/jobs.js.map +2 -2
  30. package/dist/modules/scheduler/data/validators.js +27 -3
  31. package/dist/modules/scheduler/data/validators.js.map +2 -2
  32. package/dist/modules/scheduler/i18n/de.json +2 -0
  33. package/dist/modules/scheduler/i18n/en.json +2 -0
  34. package/dist/modules/scheduler/i18n/es.json +2 -0
  35. package/dist/modules/scheduler/i18n/ko.json +2 -0
  36. package/dist/modules/scheduler/i18n/pl.json +2 -0
  37. package/dist/modules/scheduler/lib/safeQueueTargets.js +157 -0
  38. package/dist/modules/scheduler/lib/safeQueueTargets.js.map +7 -0
  39. package/dist/modules/scheduler/lib/scheduleAccess.js +22 -0
  40. package/dist/modules/scheduler/lib/scheduleAccess.js.map +7 -0
  41. package/dist/modules/scheduler/services/localSchedulerService.js +31 -6
  42. package/dist/modules/scheduler/services/localSchedulerService.js.map +2 -2
  43. package/dist/modules/scheduler/workers/execute-schedule.worker.js +64 -6
  44. package/dist/modules/scheduler/workers/execute-schedule.worker.js.map +2 -2
  45. package/dist/modules/scheduler/workers/test-echo-queue.worker.js +22 -0
  46. package/dist/modules/scheduler/workers/test-echo-queue.worker.js.map +7 -0
  47. package/package.json +7 -6
  48. package/src/modules/scheduler/__integration__/TC-SCHED-001.spec.ts +2 -2
  49. package/src/modules/scheduler/__integration__/TC-SCHED-004.spec.ts +14 -8
  50. package/src/modules/scheduler/__integration__/TC-SCHED-007.spec.ts +3 -3
  51. package/src/modules/scheduler/__integration__/TC-SCHED-008-safe-targets.spec.ts +90 -0
  52. package/src/modules/scheduler/__integration__/TC-SCHED-008.spec.ts +94 -0
  53. package/src/modules/scheduler/__integration__/TC-SCHED-CRUDFORM-001.spec.ts +3 -3
  54. package/src/modules/scheduler/__integration__/TC-UNDO-001-scheduler-jobs.spec.ts +154 -66
  55. package/src/modules/scheduler/__integration__/helpers/scheduler.ts +24 -5
  56. package/src/modules/scheduler/api/jobs/[id]/executions/route.ts +9 -19
  57. package/src/modules/scheduler/api/targets/__tests__/route.test.ts +3 -4
  58. package/src/modules/scheduler/api/targets/route.ts +6 -17
  59. package/src/modules/scheduler/api/trigger/route.ts +9 -19
  60. package/src/modules/scheduler/backend/config/scheduled-jobs/page.tsx +4 -1
  61. package/src/modules/scheduler/cli.ts +44 -1
  62. package/src/modules/scheduler/commands/__tests__/jobs.target-guard.test.ts +177 -0
  63. package/src/modules/scheduler/commands/jobs.ts +137 -3
  64. package/src/modules/scheduler/data/__tests__/safe-targets.test.ts +110 -0
  65. package/src/modules/scheduler/data/__tests__/validators.test.ts +26 -4
  66. package/src/modules/scheduler/data/validators.ts +37 -2
  67. package/src/modules/scheduler/i18n/de.json +2 -0
  68. package/src/modules/scheduler/i18n/en.json +2 -0
  69. package/src/modules/scheduler/i18n/es.json +2 -0
  70. package/src/modules/scheduler/i18n/ko.json +2 -0
  71. package/src/modules/scheduler/i18n/pl.json +2 -0
  72. package/src/modules/scheduler/lib/__tests__/safeQueueTargets.test.ts +311 -0
  73. package/src/modules/scheduler/lib/__tests__/scheduleAccess.test.ts +164 -0
  74. package/src/modules/scheduler/lib/safeQueueTargets.ts +324 -0
  75. package/src/modules/scheduler/lib/scheduleAccess.ts +104 -0
  76. package/src/modules/scheduler/services/__tests__/localSchedulerService.test.ts +179 -0
  77. package/src/modules/scheduler/services/localSchedulerService.ts +39 -6
  78. package/src/modules/scheduler/workers/__tests__/execute-schedule.worker.test.ts +25 -3
  79. package/src/modules/scheduler/workers/execute-schedule.worker.ts +76 -7
  80. package/src/modules/scheduler/workers/test-echo-queue.worker.ts +25 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/scheduler/__integration__/TC-UNDO-001-scheduler-jobs.spec.ts"],
4
- "sourcesContent": ["import { expect, test } from '@playwright/test'\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api'\nimport {\n extractOperation,\n runCrudUndoRoundTrip,\n skipIfUndoTestsDisabled,\n} from '@open-mercato/core/helpers/integration/undoHarness'\nimport {\n SCHEDULER_JOBS_PATH,\n SCHEDULER_TRIGGER_PATH,\n createScheduleJob,\n deleteScheduleJob,\n} from './helpers/scheduler'\n\n/**\n * TC-UNDO-001 (#2582, refs #2468) \u2014 scheduler.jobs undo/redo correctness.\n *\n * Regression lock for #2504: scheduler.jobs undo was a silent no-op because every\n * undo handler read `logEntry.payload` (always undefined \u2014 the command bus persists\n * the undo snapshot under `commandPayload`), so undo returned `{ ok: true }` while\n * changing nothing. Fixed in PR #2514 via `extractUndoPayload`. This spec asserts the\n * corrected behavior (full restoration), driving the real command bus through the\n * public API + the audit-log undo/redo endpoints.\n *\n * scheduler.jobs is a standard `makeCrudRoute` entity (POST/PUT/DELETE on the\n * collection, `?id=` read-back, camelCase serializer, no custom fields), so the shared\n * `runCrudUndoRoundTrip` harness covers the full round-trip in one pass:\n * - I1 update \u2192 undo restores every scalar (and bumps `updatedAt`)\n * - I2 delete \u2192 undo re-materializes the soft-deleted job\n * - I3 create \u2192 undo soft-deletes (never hard-deletes)\n * - I5 a consumed undo token is rejected on a second undo\n * - I6 redo re-applies the command's after-snapshot\n *\n * I4 (custom fields) is N/A \u2014 `scheduled_job` declares no `ce.ts`, so it has no `cf_*`.\n */\nfunction jobCreatePayload(stamp: string): Record<string, unknown> {\n return {\n name: `TC-UNDO-001 Scheduler ${stamp}`,\n description: 'TC-UNDO-001 undo/redo probe',\n scopeType: 'organization',\n scheduleType: 'interval',\n scheduleValue: '15m',\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: 'scheduler-execution',\n isEnabled: true,\n sourceType: 'user',\n }\n}\n\ntest.describe('TC-UNDO-001 scheduler.jobs undo/redo (#2504)', () => {\n test.beforeAll(() => {\n skipIfUndoTestsDisabled()\n })\n\n test('jobs CRUD commands restore scalar state on undo/redo (I1/I2/I3/I5/I6)', async ({ request }) => {\n const token = await getAuthToken(request, 'admin')\n\n await runCrudUndoRoundTrip(request, token, {\n label: 'scheduler.jobs',\n collectionPath: SCHEDULER_JOBS_PATH,\n field: 'name',\n createPayload: jobCreatePayload,\n updatePayload: (id, stamp) => ({ id, name: `TC-UNDO-001 Scheduler Renamed ${stamp}` }),\n })\n })\n\n // \u00A74 \u2014 manual trigger is a fire-and-forget enqueue that never goes through the\n // command bus, so it exposes no undo affordance. The assertion holds regardless of\n // QUEUE_STRATEGY: async returns 200 (enqueued), local returns 400 (rejected) \u2014 neither\n // issues an `x-om-operation` undo token.\n test('\u00A74 trigger exposes no undo token', async ({ request }) => {\n const token = await getAuthToken(request, 'admin')\n let jobId: string | null = null\n try {\n jobId = await createScheduleJob(request, token)\n const triggerRes = await apiRequest(request, 'POST', SCHEDULER_TRIGGER_PATH, {\n token,\n data: { id: jobId },\n })\n expect(extractOperation(triggerRes), 'trigger response carries no undo token (\u00A74)').toBeNull()\n } finally {\n await deleteScheduleJob(request, token, jobId)\n }\n })\n})\n"],
5
- "mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,YAAY,oBAAoB;AACzC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAuBP,SAAS,iBAAiB,OAAwC;AAChE,SAAO;AAAA,IACL,MAAM,yBAAyB,KAAK;AAAA,IACpC,aAAa;AAAA,IACb,WAAW;AAAA,IACX,cAAc;AAAA,IACd,eAAe;AAAA,IACf,UAAU;AAAA,IACV,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,WAAW;AAAA,IACX,YAAY;AAAA,EACd;AACF;AAEA,KAAK,SAAS,gDAAgD,MAAM;AAClE,OAAK,UAAU,MAAM;AACnB,4BAAwB;AAAA,EAC1B,CAAC;AAED,OAAK,yEAAyE,OAAO,EAAE,QAAQ,MAAM;AACnG,UAAM,QAAQ,MAAM,aAAa,SAAS,OAAO;AAEjD,UAAM,qBAAqB,SAAS,OAAO;AAAA,MACzC,OAAO;AAAA,MACP,gBAAgB;AAAA,MAChB,OAAO;AAAA,MACP,eAAe;AAAA,MACf,eAAe,CAAC,IAAI,WAAW,EAAE,IAAI,MAAM,iCAAiC,KAAK,GAAG;AAAA,IACtF,CAAC;AAAA,EACH,CAAC;AAMD,OAAK,uCAAoC,OAAO,EAAE,QAAQ,MAAM;AAC9D,UAAM,QAAQ,MAAM,aAAa,SAAS,OAAO;AACjD,QAAI,QAAuB;AAC3B,QAAI;AACF,cAAQ,MAAM,kBAAkB,SAAS,KAAK;AAC9C,YAAM,aAAa,MAAM,WAAW,SAAS,QAAQ,wBAAwB;AAAA,QAC3E;AAAA,QACA,MAAM,EAAE,IAAI,MAAM;AAAA,MACpB,CAAC;AACD,aAAO,iBAAiB,UAAU,GAAG,gDAA6C,EAAE,SAAS;AAAA,IAC/F,UAAE;AACA,YAAM,kBAAkB,SAAS,OAAO,KAAK;AAAA,IAC/C;AAAA,EACF,CAAC;AACH,CAAC;",
4
+ "sourcesContent": ["import { expect, test, type APIRequestContext, type APIResponse } from '@playwright/test'\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api'\n\n/**\n * TC-UNDO-001: scheduler.jobs undo round-trip (regression for issue #2504).\n *\n * Before the fix, every scheduler.jobs undo handler read `logEntry.payload`\n * (always undefined \u2014 the command bus persists the undo snapshot under\n * `commandPayload`), so undo returned `{ ok: true }` while silently changing\n * nothing. This spec exercises the real HTTP path end-to-end:\n * - CREATE -> undo => job is soft-deleted (no longer listed)\n * - UPDATE -> undo => renamed field is restored to its prior value\n * - DELETE -> undo => soft-deleted job is re-materialized\n *\n * Endpoints covered:\n * - POST /api/scheduler/jobs (create)\n * - GET /api/scheduler/jobs?id= (read)\n * - PUT /api/scheduler/jobs (update)\n * - DELETE /api/scheduler/jobs (delete, cleanup)\n * - POST /api/audit_logs/audit-logs/actions/undo (undo)\n */\n\ntype ScheduleRow = {\n id: string\n name: string\n description: string | null\n scheduleValue: string\n}\n\n// The undo endpoint only honors the *latest* undoable log for a resource, and\n// `latestUndoableForResource` orders by `created_at` alone. Audit `created_at` is\n// millisecond-precision (`new Date()` at log time), so a create + a follow-up\n// mutation issued within the same millisecond tie and the lookup may resolve to\n// the wrong log. A short settle guarantees the operation under undo is strictly\n// the most recent, keeping the round-trip deterministic.\nasync function settleAuditClock(): Promise<void> {\n await new Promise((resolve) => setTimeout(resolve, 50))\n}\n\nfunction readUndoToken(res: APIResponse): string {\n const header = res.headers()['x-om-operation'] ?? ''\n const enc = header.startsWith('omop:') ? header.slice(5) : ''\n expect(enc, 'x-om-operation header should carry an omop: payload').not.toBe('')\n const payload = JSON.parse(decodeURIComponent(enc)) as { undoToken?: string }\n expect(typeof payload.undoToken, 'undoToken present in operation payload').toBe('string')\n return payload.undoToken as string\n}\n\nasync function getById(\n request: APIRequestContext,\n token: string,\n id: string,\n): Promise<ScheduleRow | undefined> {\n const res = await apiRequest(request, 'GET', `/api/scheduler/jobs?id=${encodeURIComponent(id)}`, { token })\n expect(res.status()).toBe(200)\n const body = (await res.json()) as { items?: ScheduleRow[] }\n return (body.items ?? [])[0]\n}\n\nasync function createJob(request: APIRequestContext, token: string, name: string): Promise<{ id: string; res: APIResponse }> {\n const res = await apiRequest(request, 'POST', '/api/scheduler/jobs', {\n token,\n data: {\n name,\n description: 'TC-UNDO-001 probe',\n scopeType: 'tenant',\n scheduleType: 'cron',\n scheduleValue: '0 0 * * *',\n timezone: 'UTC',\n targetType: 'command',\n targetCommand: 'scheduler.test.echo',\n isEnabled: true,\n sourceType: 'user',\n },\n })\n expect(res.status(), 'create returns 201').toBe(201)\n const id = ((await res.json()) as { id: string }).id\n expect(id, 'create returns an id').toBeTruthy()\n return { id, res }\n}\n\nasync function undo(request: APIRequestContext, token: string, undoToken: string): Promise<void> {\n const res = await apiRequest(request, 'POST', '/api/audit_logs/audit-logs/actions/undo', {\n token,\n data: { undoToken },\n })\n const raw = await res.text()\n expect(res.status(), `undo returns 200 (body: ${raw})`).toBe(200)\n const body = JSON.parse(raw) as { ok?: boolean }\n expect(body.ok, 'undo body is { ok: true }').toBe(true)\n}\n\nasync function cleanup(request: APIRequestContext, token: string | null, id: string | null): Promise<void> {\n if (!token || !id) return\n try {\n await apiRequest(request, 'DELETE', '/api/scheduler/jobs', { token, data: { id } })\n } catch {\n /* ignore */\n }\n}\n\ntest.describe('TC-UNDO-001: scheduler.jobs undo actually restores state (#2504)', () => {\n test('UPDATE -> undo restores the prior name', async ({ request }) => {\n let token: string | null = null\n let id: string | null = null\n try {\n token = await getAuthToken(request, 'admin')\n const originalName = `TC-UNDO-001 Update ${Date.now()}`\n ;({ id } = await createJob(request, token, originalName))\n await settleAuditClock()\n\n const updateRes = await apiRequest(request, 'PUT', '/api/scheduler/jobs', {\n token,\n data: { id, name: `${originalName} RENAMED` },\n })\n expect(updateRes.status(), 'update returns 200').toBe(200)\n expect((await getById(request, token, id))?.name).toBe(`${originalName} RENAMED`)\n\n await undo(request, token, readUndoToken(updateRes))\n\n const restored = await getById(request, token, id)\n expect(restored, 'job still exists after update-undo').toBeTruthy()\n expect(restored!.name, 'name restored to original by undo').toBe(originalName)\n } finally {\n await cleanup(request, token, id)\n }\n })\n\n test('DELETE -> undo re-lists the soft-deleted job', async ({ request }) => {\n let token: string | null = null\n let id: string | null = null\n try {\n token = await getAuthToken(request, 'admin')\n const name = `TC-UNDO-001 Delete ${Date.now()}`\n ;({ id } = await createJob(request, token, name))\n await settleAuditClock()\n\n const deleteRes = await apiRequest(request, 'DELETE', '/api/scheduler/jobs', {\n token,\n data: { id },\n })\n expect(deleteRes.status(), 'delete returns 200').toBe(200)\n expect(await getById(request, token, id), 'job is gone after delete').toBeFalsy()\n\n await undo(request, token, readUndoToken(deleteRes))\n\n const restored = await getById(request, token, id)\n expect(restored, 'job re-materialized by delete-undo').toBeTruthy()\n expect(restored!.name).toBe(name)\n } finally {\n await cleanup(request, token, id)\n }\n })\n\n test('CREATE -> undo removes the created job', async ({ request }) => {\n let token: string | null = null\n let id: string | null = null\n try {\n token = await getAuthToken(request, 'admin')\n const name = `TC-UNDO-001 Create ${Date.now()}`\n const created = await createJob(request, token, name)\n id = created.id\n\n expect(await getById(request, token, id), 'job exists after create').toBeTruthy()\n\n await undo(request, token, readUndoToken(created.res))\n\n expect(await getById(request, token, id), 'job removed by create-undo').toBeFalsy()\n id = null // already undone; nothing to clean up\n } finally {\n await cleanup(request, token, id)\n }\n })\n})\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -5,13 +5,20 @@ const SCHEDULER_JOBS_PATH = "/api/scheduler/jobs";
5
5
  const SCHEDULER_TARGETS_PATH = "/api/scheduler/targets";
6
6
  const SCHEDULER_TRIGGER_PATH = "/api/scheduler/trigger";
7
7
  const SCHEDULER_EXECUTION_QUEUE = "scheduler-execution";
8
+ const SCHEDULER_TEST_QUEUE = "scheduler-test";
9
+ const SCHEDULER_FORBIDDEN_INTERNAL_QUEUES = [
10
+ SCHEDULER_EXECUTION_QUEUE,
11
+ "stripe-webhook",
12
+ "payment-gateways-webhook"
13
+ ];
14
+ const SCHEDULER_TEST_COMMAND = "scheduler.test.echo";
8
15
  let sequence = 0;
9
16
  function uniqueScheduleName(prefix) {
10
17
  sequence += 1;
11
18
  return `${prefix} ${Date.now()}-${sequence}`;
12
19
  }
13
20
  async function createScheduleJob(request, token, overrides = {}) {
14
- const targetType = overrides.targetType ?? "queue";
21
+ const targetType = overrides.targetType ?? "command";
15
22
  const data = {
16
23
  name: overrides.name ?? uniqueScheduleName("Integration Scheduler"),
17
24
  description: overrides.description ?? "Created by scheduler integration tests",
@@ -25,9 +32,9 @@ async function createScheduleJob(request, token, overrides = {}) {
25
32
  sourceType: overrides.sourceType ?? "user"
26
33
  };
27
34
  if (targetType === "command") {
28
- data.targetCommand = overrides.targetCommand;
35
+ data.targetCommand = overrides.targetCommand ?? SCHEDULER_TEST_COMMAND;
29
36
  } else {
30
- data.targetQueue = overrides.targetQueue ?? SCHEDULER_EXECUTION_QUEUE;
37
+ data.targetQueue = overrides.targetQueue ?? SCHEDULER_TEST_QUEUE;
31
38
  }
32
39
  const response = await apiRequest(request, "POST", SCHEDULER_JOBS_PATH, { token, data });
33
40
  const body = await readJsonSafe(response);
@@ -48,8 +55,11 @@ async function deleteScheduleJob(request, token, id) {
48
55
  }
49
56
  export {
50
57
  SCHEDULER_EXECUTION_QUEUE,
58
+ SCHEDULER_FORBIDDEN_INTERNAL_QUEUES,
51
59
  SCHEDULER_JOBS_PATH,
52
60
  SCHEDULER_TARGETS_PATH,
61
+ SCHEDULER_TEST_COMMAND,
62
+ SCHEDULER_TEST_QUEUE,
53
63
  SCHEDULER_TRIGGER_PATH,
54
64
  createScheduleJob,
55
65
  deleteScheduleJob,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/scheduler/__integration__/helpers/scheduler.ts"],
4
- "sourcesContent": ["import { expect, type APIRequestContext } from '@playwright/test'\nimport { apiRequest } from '@open-mercato/core/helpers/integration/api'\nimport { readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures'\n\nexport const SCHEDULER_JOBS_PATH = '/api/scheduler/jobs'\nexport const SCHEDULER_TARGETS_PATH = '/api/scheduler/targets'\nexport const SCHEDULER_TRIGGER_PATH = '/api/scheduler/trigger'\n\n/** Queue registered by the scheduler module's execute-schedule worker. */\nexport const SCHEDULER_EXECUTION_QUEUE = 'scheduler-execution'\n\nexport type SchedulerJob = {\n id: string\n name: string\n description: string | null\n scopeType: 'system' | 'organization' | 'tenant'\n organizationId: string | null\n tenantId: string | null\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone: string\n targetType: 'queue' | 'command'\n targetQueue: string | null\n targetCommand: string | null\n isEnabled: boolean\n createdAt: string | null\n updatedAt: string | null\n}\n\nexport type CreateScheduleOverrides = {\n name?: string\n description?: string | null\n scopeType?: 'system' | 'organization' | 'tenant'\n scheduleType?: 'cron' | 'interval'\n scheduleValue?: string\n timezone?: string\n targetType?: 'queue' | 'command'\n targetQueue?: string\n targetCommand?: string\n targetPayload?: Record<string, unknown>\n isEnabled?: boolean\n sourceType?: 'user' | 'module'\n}\n\nlet sequence = 0\n\n/** Stable-ish unique label so concurrent fixtures never collide on name. */\nexport function uniqueScheduleName(prefix: string): string {\n sequence += 1\n return `${prefix} ${Date.now()}-${sequence}`\n}\n\n/**\n * Create an organization-scoped queue-target schedule via the API and return its id.\n * Org/tenant are derived server-side from the caller's auth context, so the caller\n * only needs an admin (or otherwise scheduler.jobs.manage) token.\n */\nexport async function createScheduleJob(\n request: APIRequestContext,\n token: string,\n overrides: CreateScheduleOverrides = {},\n): Promise<string> {\n const targetType = overrides.targetType ?? 'queue'\n const data: Record<string, unknown> = {\n name: overrides.name ?? uniqueScheduleName('Integration Scheduler'),\n description: overrides.description ?? 'Created by scheduler integration tests',\n scopeType: overrides.scopeType ?? 'organization',\n scheduleType: overrides.scheduleType ?? 'interval',\n scheduleValue: overrides.scheduleValue ?? '15m',\n timezone: overrides.timezone ?? 'UTC',\n targetType,\n targetPayload: overrides.targetPayload ?? { source: 'integration-test' },\n isEnabled: overrides.isEnabled ?? true,\n sourceType: overrides.sourceType ?? 'user',\n }\n if (targetType === 'command') {\n data.targetCommand = overrides.targetCommand\n } else {\n data.targetQueue = overrides.targetQueue ?? SCHEDULER_EXECUTION_QUEUE\n }\n\n const response = await apiRequest(request, 'POST', SCHEDULER_JOBS_PATH, { token, data })\n const body = await readJsonSafe<{ id?: string }>(response)\n expect(response.status(), 'POST /api/scheduler/jobs should return 201').toBe(201)\n const id = body?.id\n expect(typeof id === 'string' && /^[0-9a-f-]{36}$/i.test(id), 'create response should include a UUID id').toBe(true)\n return String(id)\n}\n\n/** Fetch a single schedule by id from the list endpoint; null when absent (e.g. soft-deleted). */\nexport async function getScheduleJobById(\n request: APIRequestContext,\n token: string,\n id: string,\n): Promise<SchedulerJob | null> {\n const response = await apiRequest(request, 'GET', `${SCHEDULER_JOBS_PATH}?id=${encodeURIComponent(id)}`, { token })\n expect(response.status(), 'GET /api/scheduler/jobs?id should return 200').toBe(200)\n const body = await readJsonSafe<{ items?: SchedulerJob[] }>(response)\n return (body?.items ?? []).find((item) => item.id === id) ?? null\n}\n\n/** Best-effort cleanup; swallows errors so teardown never masks the real assertion. */\nexport async function deleteScheduleJob(\n request: APIRequestContext,\n token: string,\n id: string | null,\n): Promise<void> {\n if (!id) return\n await apiRequest(request, 'DELETE', SCHEDULER_JOBS_PATH, { token, data: { id } }).catch(() => null)\n}\n"],
5
- "mappings": "AAAA,SAAS,cAAsC;AAC/C,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAEtB,MAAM,sBAAsB;AAC5B,MAAM,yBAAyB;AAC/B,MAAM,yBAAyB;AAG/B,MAAM,4BAA4B;AAmCzC,IAAI,WAAW;AAGR,SAAS,mBAAmB,QAAwB;AACzD,cAAY;AACZ,SAAO,GAAG,MAAM,IAAI,KAAK,IAAI,CAAC,IAAI,QAAQ;AAC5C;AAOA,eAAsB,kBACpB,SACA,OACA,YAAqC,CAAC,GACrB;AACjB,QAAM,aAAa,UAAU,cAAc;AAC3C,QAAM,OAAgC;AAAA,IACpC,MAAM,UAAU,QAAQ,mBAAmB,uBAAuB;AAAA,IAClE,aAAa,UAAU,eAAe;AAAA,IACtC,WAAW,UAAU,aAAa;AAAA,IAClC,cAAc,UAAU,gBAAgB;AAAA,IACxC,eAAe,UAAU,iBAAiB;AAAA,IAC1C,UAAU,UAAU,YAAY;AAAA,IAChC;AAAA,IACA,eAAe,UAAU,iBAAiB,EAAE,QAAQ,mBAAmB;AAAA,IACvE,WAAW,UAAU,aAAa;AAAA,IAClC,YAAY,UAAU,cAAc;AAAA,EACtC;AACA,MAAI,eAAe,WAAW;AAC5B,SAAK,gBAAgB,UAAU;AAAA,EACjC,OAAO;AACL,SAAK,cAAc,UAAU,eAAe;AAAA,EAC9C;AAEA,QAAM,WAAW,MAAM,WAAW,SAAS,QAAQ,qBAAqB,EAAE,OAAO,KAAK,CAAC;AACvF,QAAM,OAAO,MAAM,aAA8B,QAAQ;AACzD,SAAO,SAAS,OAAO,GAAG,4CAA4C,EAAE,KAAK,GAAG;AAChF,QAAM,KAAK,MAAM;AACjB,SAAO,OAAO,OAAO,YAAY,mBAAmB,KAAK,EAAE,GAAG,0CAA0C,EAAE,KAAK,IAAI;AACnH,SAAO,OAAO,EAAE;AAClB;AAGA,eAAsB,mBACpB,SACA,OACA,IAC8B;AAC9B,QAAM,WAAW,MAAM,WAAW,SAAS,OAAO,GAAG,mBAAmB,OAAO,mBAAmB,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC;AAClH,SAAO,SAAS,OAAO,GAAG,8CAA8C,EAAE,KAAK,GAAG;AAClF,QAAM,OAAO,MAAM,aAAyC,QAAQ;AACpE,UAAQ,MAAM,SAAS,CAAC,GAAG,KAAK,CAAC,SAAS,KAAK,OAAO,EAAE,KAAK;AAC/D;AAGA,eAAsB,kBACpB,SACA,OACA,IACe;AACf,MAAI,CAAC,GAAI;AACT,QAAM,WAAW,SAAS,UAAU,qBAAqB,EAAE,OAAO,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,MAAM,MAAM,IAAI;AACpG;",
4
+ "sourcesContent": ["import { expect, type APIRequestContext } from '@playwright/test'\nimport { apiRequest } from '@open-mercato/core/helpers/integration/api'\nimport { readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures'\n\nexport const SCHEDULER_JOBS_PATH = '/api/scheduler/jobs'\nexport const SCHEDULER_TARGETS_PATH = '/api/scheduler/targets'\nexport const SCHEDULER_TRIGGER_PATH = '/api/scheduler/trigger'\n\n/** Queue registered by the scheduler module's execute-schedule worker (internal, not schedulable). */\nexport const SCHEDULER_EXECUTION_QUEUE = 'scheduler-execution'\n\n/** Deliberately side-effect-free queue whose worker opted into scheduling (#5213). */\nexport const SCHEDULER_TEST_QUEUE = 'scheduler-test'\n\n/**\n * Internal queues that must never be offered as scheduler targets (#5213).\n * Workers must opt in via `schedulerSafe: true` to become schedulable.\n */\nexport const SCHEDULER_FORBIDDEN_INTERNAL_QUEUES = [\n SCHEDULER_EXECUTION_QUEUE,\n 'stripe-webhook',\n 'payment-gateways-webhook',\n] as const\n\n/** Always-registered command used by fixtures now that queue targets are restricted. */\nexport const SCHEDULER_TEST_COMMAND = 'scheduler.test.echo'\n\nexport type SchedulerJob = {\n id: string\n name: string\n description: string | null\n scopeType: 'system' | 'organization' | 'tenant'\n organizationId: string | null\n tenantId: string | null\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone: string\n targetType: 'queue' | 'command'\n targetQueue: string | null\n targetCommand: string | null\n isEnabled: boolean\n createdAt: string | null\n updatedAt: string | null\n}\n\nexport type CreateScheduleOverrides = {\n name?: string\n description?: string | null\n scopeType?: 'system' | 'organization' | 'tenant'\n scheduleType?: 'cron' | 'interval'\n scheduleValue?: string\n timezone?: string\n targetType?: 'queue' | 'command'\n targetQueue?: string\n targetCommand?: string\n targetPayload?: Record<string, unknown>\n isEnabled?: boolean\n sourceType?: 'user' | 'module'\n}\n\nlet sequence = 0\n\n/** Stable-ish unique label so concurrent fixtures never collide on name. */\nexport function uniqueScheduleName(prefix: string): string {\n sequence += 1\n return `${prefix} ${Date.now()}-${sequence}`\n}\n\n/**\n * Create an organization-scoped schedule via the API and return its id.\n * Org/tenant are derived server-side from the caller's auth context, so the caller\n * only needs an admin (or otherwise scheduler.jobs.manage) token.\n *\n * Defaults to a command target: user-authored queue targets are restricted to\n * scheduler-safe queues (#5213), so fixtures must not assume a queue is allowed.\n */\nexport async function createScheduleJob(\n request: APIRequestContext,\n token: string,\n overrides: CreateScheduleOverrides = {},\n): Promise<string> {\n const targetType = overrides.targetType ?? 'command'\n const data: Record<string, unknown> = {\n name: overrides.name ?? uniqueScheduleName('Integration Scheduler'),\n description: overrides.description ?? 'Created by scheduler integration tests',\n scopeType: overrides.scopeType ?? 'organization',\n scheduleType: overrides.scheduleType ?? 'interval',\n scheduleValue: overrides.scheduleValue ?? '15m',\n timezone: overrides.timezone ?? 'UTC',\n targetType,\n targetPayload: overrides.targetPayload ?? { source: 'integration-test' },\n isEnabled: overrides.isEnabled ?? true,\n sourceType: overrides.sourceType ?? 'user',\n }\n if (targetType === 'command') {\n data.targetCommand = overrides.targetCommand ?? SCHEDULER_TEST_COMMAND\n } else {\n data.targetQueue = overrides.targetQueue ?? SCHEDULER_TEST_QUEUE\n }\n\n const response = await apiRequest(request, 'POST', SCHEDULER_JOBS_PATH, { token, data })\n const body = await readJsonSafe<{ id?: string }>(response)\n expect(response.status(), 'POST /api/scheduler/jobs should return 201').toBe(201)\n const id = body?.id\n expect(typeof id === 'string' && /^[0-9a-f-]{36}$/i.test(id), 'create response should include a UUID id').toBe(true)\n return String(id)\n}\n\n/** Fetch a single schedule by id from the list endpoint; null when absent (e.g. soft-deleted). */\nexport async function getScheduleJobById(\n request: APIRequestContext,\n token: string,\n id: string,\n): Promise<SchedulerJob | null> {\n const response = await apiRequest(request, 'GET', `${SCHEDULER_JOBS_PATH}?id=${encodeURIComponent(id)}`, { token })\n expect(response.status(), 'GET /api/scheduler/jobs?id should return 200').toBe(200)\n const body = await readJsonSafe<{ items?: SchedulerJob[] }>(response)\n return (body?.items ?? []).find((item) => item.id === id) ?? null\n}\n\n/** Best-effort cleanup; swallows errors so teardown never masks the real assertion. */\nexport async function deleteScheduleJob(\n request: APIRequestContext,\n token: string,\n id: string | null,\n): Promise<void> {\n if (!id) return\n await apiRequest(request, 'DELETE', SCHEDULER_JOBS_PATH, { token, data: { id } }).catch(() => null)\n}\n"],
5
+ "mappings": "AAAA,SAAS,cAAsC;AAC/C,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAEtB,MAAM,sBAAsB;AAC5B,MAAM,yBAAyB;AAC/B,MAAM,yBAAyB;AAG/B,MAAM,4BAA4B;AAGlC,MAAM,uBAAuB;AAM7B,MAAM,sCAAsC;AAAA,EACjD;AAAA,EACA;AAAA,EACA;AACF;AAGO,MAAM,yBAAyB;AAmCtC,IAAI,WAAW;AAGR,SAAS,mBAAmB,QAAwB;AACzD,cAAY;AACZ,SAAO,GAAG,MAAM,IAAI,KAAK,IAAI,CAAC,IAAI,QAAQ;AAC5C;AAUA,eAAsB,kBACpB,SACA,OACA,YAAqC,CAAC,GACrB;AACjB,QAAM,aAAa,UAAU,cAAc;AAC3C,QAAM,OAAgC;AAAA,IACpC,MAAM,UAAU,QAAQ,mBAAmB,uBAAuB;AAAA,IAClE,aAAa,UAAU,eAAe;AAAA,IACtC,WAAW,UAAU,aAAa;AAAA,IAClC,cAAc,UAAU,gBAAgB;AAAA,IACxC,eAAe,UAAU,iBAAiB;AAAA,IAC1C,UAAU,UAAU,YAAY;AAAA,IAChC;AAAA,IACA,eAAe,UAAU,iBAAiB,EAAE,QAAQ,mBAAmB;AAAA,IACvE,WAAW,UAAU,aAAa;AAAA,IAClC,YAAY,UAAU,cAAc;AAAA,EACtC;AACA,MAAI,eAAe,WAAW;AAC5B,SAAK,gBAAgB,UAAU,iBAAiB;AAAA,EAClD,OAAO;AACL,SAAK,cAAc,UAAU,eAAe;AAAA,EAC9C;AAEA,QAAM,WAAW,MAAM,WAAW,SAAS,QAAQ,qBAAqB,EAAE,OAAO,KAAK,CAAC;AACvF,QAAM,OAAO,MAAM,aAA8B,QAAQ;AACzD,SAAO,SAAS,OAAO,GAAG,4CAA4C,EAAE,KAAK,GAAG;AAChF,QAAM,KAAK,MAAM;AACjB,SAAO,OAAO,OAAO,YAAY,mBAAmB,KAAK,EAAE,GAAG,0CAA0C,EAAE,KAAK,IAAI;AACnH,SAAO,OAAO,EAAE;AAClB;AAGA,eAAsB,mBACpB,SACA,OACA,IAC8B;AAC9B,QAAM,WAAW,MAAM,WAAW,SAAS,OAAO,GAAG,mBAAmB,OAAO,mBAAmB,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC;AAClH,SAAO,SAAS,OAAO,GAAG,8CAA8C,EAAE,KAAK,GAAG;AAClF,QAAM,OAAO,MAAM,aAAyC,QAAQ;AACpE,UAAQ,MAAM,SAAS,CAAC,GAAG,KAAK,CAAC,SAAS,KAAK,OAAO,EAAE,KAAK;AAC/D;AAGA,eAAsB,kBACpB,SACA,OACA,IACe;AACf,MAAI,CAAC,GAAI;AACT,QAAM,WAAW,SAAS,UAAU,qBAAqB,EAAE,OAAO,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,MAAM,MAAM,IAAI;AACpG;",
6
6
  "names": []
7
7
  }
@@ -3,6 +3,7 @@ import { z } from "zod";
3
3
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
4
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
5
  import { ScheduledJob } from "../../../../data/entities.js";
6
+ import { resolveScheduleAccess } from "../../../../lib/scheduleAccess.js";
6
7
  import { getRedisUrlOrThrow, parseRedisUrl } from "@open-mercato/shared/lib/redis/connection";
7
8
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
8
9
  import { createLogger } from "@open-mercato/shared/lib/logger";
@@ -21,22 +22,15 @@ async function GET(req, { params }) {
21
22
  const em = container.resolve("em");
22
23
  const scheduleId = params.id;
23
24
  try {
24
- const findFilter = {
25
- id: scheduleId,
26
- deletedAt: null
27
- };
28
- if (auth.tenantId) {
29
- findFilter.tenantId = auth.tenantId;
30
- }
31
- if (auth.orgId) {
32
- findFilter.organizationId = auth.orgId;
33
- }
34
- const schedule = await em.findOne(ScheduledJob, findFilter);
25
+ const schedule = await em.findOne(ScheduledJob, { id: scheduleId, deletedAt: null });
35
26
  if (!schedule) {
36
27
  return NextResponse.json({ error: translate("scheduler.error.not_found", "Schedule not found") }, { status: 404 });
37
28
  }
38
- const isSuperAdmin = auth.isSuperAdmin === true;
39
- if (!schedule.tenantId && !schedule.organizationId && !isSuperAdmin) {
29
+ const access = resolveScheduleAccess(schedule, auth);
30
+ if (access === "not_found") {
31
+ return NextResponse.json({ error: translate("scheduler.error.not_found", "Schedule not found") }, { status: 404 });
32
+ }
33
+ if (access === "forbidden") {
40
34
  return NextResponse.json({ error: translate("scheduler.error.access_denied", "Access denied") }, { status: 403 });
41
35
  }
42
36
  const queueStrategy = process.env.QUEUE_STRATEGY || "local";
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/scheduler/api/jobs/%5Bid%5D/executions/route.ts"],
4
- "sourcesContent": ["import { NextRequest, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/core'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { ScheduledJob } from '../../../../data/entities.js'\nimport { getRedisUrlOrThrow, parseRedisUrl } from '@open-mercato/shared/lib/redis/connection'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('scheduler').child({ component: 'executions' })\n\n\nexport const metadata = {\n requireAuth: true,\n requireFeatures: ['scheduler.jobs.view'],\n}\n\n/**\n * GET /api/scheduler/jobs/[id]/executions\n * Fetch execution history for a schedule from BullMQ\n * \n * Returns jobs from the scheduler-execution queue filtered by scheduleId.\n * This replaces the old /api/scheduler/runs endpoint that used the database table.\n */\nexport async function GET(\n req: NextRequest,\n { params }: { params: { id: string } }\n) {\n const { translate } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: translate('scheduler.error.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve<EntityManager>('em')\n\n const scheduleId = params.id\n\n try {\n // Verify schedule exists and user has access (with tenant/org scope filter)\n const findFilter: Record<string, unknown> = {\n id: scheduleId,\n deletedAt: null,\n }\n\n // Apply tenant isolation: scope the query to the user's tenant/org\n if (auth.tenantId) {\n findFilter.tenantId = auth.tenantId\n }\n if (auth.orgId) {\n findFilter.organizationId = auth.orgId\n }\n\n const schedule = await em.findOne(ScheduledJob, findFilter)\n\n if (!schedule) {\n return NextResponse.json({ error: translate('scheduler.error.not_found', 'Schedule not found') }, { status: 404 })\n }\n\n // System-scoped schedules (no tenantId/orgId) require super-admin. Use the\n // immutable `isSuperAdmin` flag \u2014 never compare mutable/spoofable role names.\n const isSuperAdmin = auth.isSuperAdmin === true\n if (!schedule.tenantId && !schedule.organizationId && !isSuperAdmin) {\n return NextResponse.json({ error: translate('scheduler.error.access_denied', 'Access denied') }, { status: 403 })\n }\n\n // Check if using async strategy\n const queueStrategy = process.env.QUEUE_STRATEGY || 'local'\n if (queueStrategy !== 'async') {\n return NextResponse.json({\n error: translate('scheduler.error.async_strategy_required', 'Execution history requires QUEUE_STRATEGY=async'),\n message: translate('scheduler.error.async_strategy_hint', 'Please set QUEUE_STRATEGY=async to view execution history'),\n items: [],\n }, { status: 400 })\n }\n\n // Fetch jobs from BullMQ scheduler-execution queue\n const { Queue } = await import('bullmq')\n const queue = new Queue('scheduler-execution', {\n connection: parseRedisUrl(getRedisUrlOrThrow('QUEUE')),\n })\n\n try {\n // Validate query params with Zod schema\n const queryResult = executionsQuerySchema.safeParse({\n pageSize: req.nextUrl.searchParams.get('pageSize') ?? undefined,\n })\n const limit = queryResult.success ? queryResult.data.pageSize : 20\n\n const [completed, failed, active, waiting, delayed] = await Promise.all([\n queue.getCompleted(0, limit - 1),\n queue.getFailed(0, limit - 1),\n queue.getActive(0, limit - 1),\n queue.getWaiting(0, limit - 1),\n queue.getDelayed(0, limit - 1),\n ])\n\n // Combine all jobs and filter by scheduleId\n const allJobs = [...completed, ...failed, ...active, ...waiting, ...delayed]\n type BullJobData = { payload?: { scheduleId?: string; triggerType?: string; triggeredByUserId?: string }; scheduleId?: string; triggerType?: string; triggeredByUserId?: string }\n\n const filteredJobs = allJobs\n .filter(job => {\n const data = job.data as BullJobData | undefined\n return data?.payload?.scheduleId === scheduleId || data?.scheduleId === scheduleId\n })\n .slice(0, limit)\n\n // Get state for each job (parallel)\n const jobsWithState = await Promise.all(\n filteredJobs.map(async (job) => {\n const state = await job.getState()\n const data = job.data as BullJobData | undefined\n \n return {\n id: job.id,\n scheduleId: data?.payload?.scheduleId || data?.scheduleId,\n startedAt: job.processedOn ? new Date(job.processedOn).toISOString() : new Date(job.timestamp).toISOString(),\n finishedAt: job.finishedOn ? new Date(job.finishedOn).toISOString() : null,\n status: state === 'completed' ? 'completed' : state === 'failed' ? 'failed' : state === 'active' ? 'running' : 'waiting',\n triggerType: data?.payload?.triggerType || data?.triggerType || 'scheduled',\n triggeredByUserId: data?.payload?.triggeredByUserId || data?.triggeredByUserId || null,\n errorMessage: job.failedReason || null,\n errorStack: job.stacktrace ? job.stacktrace.join('\\n') : null,\n durationMs: (job.finishedOn && job.processedOn) ? (job.finishedOn - job.processedOn) : null,\n queueJobId: job.id,\n queueName: 'scheduler-execution',\n attemptsMade: job.attemptsMade,\n result: job.returnvalue,\n }\n })\n )\n\n // Sort by startedAt descending (most recent first)\n jobsWithState.sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime())\n\n return NextResponse.json({\n items: jobsWithState,\n total: jobsWithState.length,\n page: 1,\n pageSize: limit,\n })\n } finally {\n await queue.close()\n }\n\n } catch (error: unknown) {\n logger.error('Error fetching execution history', { err: error })\n return NextResponse.json(\n { error: error instanceof Error ? error.message : translate('scheduler.error.fetch_executions_failed', 'Failed to fetch execution history') },\n { status: 500 }\n )\n }\n}\n\n// Query schemas\nconst executionsQuerySchema = z.object({\n pageSize: z.coerce.number().int().min(1).max(100).default(20),\n})\n\n// Response schemas\nconst executionItemSchema = z.object({\n id: z.string(),\n scheduleId: z.string().uuid(),\n startedAt: z.string(),\n finishedAt: z.string().nullable(),\n status: z.enum(['running', 'completed', 'failed', 'waiting']),\n triggerType: z.enum(['scheduled', 'manual']),\n triggeredByUserId: z.string().uuid().nullable(),\n errorMessage: z.string().nullable(),\n errorStack: z.string().nullable(),\n durationMs: z.number().nullable(),\n queueJobId: z.string(),\n queueName: z.string(),\n attemptsMade: z.number(),\n result: z.unknown().nullable(),\n})\n\nconst executionsResponseSchema = z.object({\n items: z.array(executionItemSchema),\n total: z.number(),\n page: z.number(),\n pageSize: z.number(),\n})\n\nconst errorResponseSchema = z.object({\n error: z.string(),\n message: z.string().optional(),\n items: z.array(z.any()).optional(),\n})\n\n// OpenAPI specification\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Scheduler',\n summary: 'Get execution history',\n description: 'Fetches recent execution history for a scheduled job from BullMQ.',\n methods: {\n GET: {\n operationId: 'getScheduleExecutions',\n summary: 'Get execution history for a schedule',\n description: 'Fetch recent executions from BullMQ for a scheduled job. Requires QUEUE_STRATEGY=async.',\n query: executionsQuerySchema,\n responses: [\n {\n status: 200,\n description: 'Execution history',\n schema: executionsResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Local strategy not supported', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n { status: 403, description: 'Access denied', schema: errorResponseSchema },\n { status: 404, description: 'Schedule not found', schema: errorResponseSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextRequest, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/core'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { ScheduledJob } from '../../../../data/entities.js'\nimport { resolveScheduleAccess } from '../../../../lib/scheduleAccess.js'\nimport { getRedisUrlOrThrow, parseRedisUrl } from '@open-mercato/shared/lib/redis/connection'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('scheduler').child({ component: 'executions' })\n\n\nexport const metadata = {\n requireAuth: true,\n requireFeatures: ['scheduler.jobs.view'],\n}\n\n/**\n * GET /api/scheduler/jobs/[id]/executions\n * Fetch execution history for a schedule from BullMQ\n * \n * Returns jobs from the scheduler-execution queue filtered by scheduleId.\n * This replaces the old /api/scheduler/runs endpoint that used the database table.\n */\nexport async function GET(\n req: NextRequest,\n { params }: { params: { id: string } }\n) {\n const { translate } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: translate('scheduler.error.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve<EntityManager>('em')\n\n const scheduleId = params.id\n\n try {\n // Load by id alone, then apply isolation to the loaded row. Folding the actor's\n // tenant/org into the lookup would make every system-scoped schedule unmatchable.\n const schedule = await em.findOne(ScheduledJob, { id: scheduleId, deletedAt: null })\n\n if (!schedule) {\n return NextResponse.json({ error: translate('scheduler.error.not_found', 'Schedule not found') }, { status: 404 })\n }\n\n const access = resolveScheduleAccess(schedule, auth)\n if (access === 'not_found') {\n return NextResponse.json({ error: translate('scheduler.error.not_found', 'Schedule not found') }, { status: 404 })\n }\n if (access === 'forbidden') {\n return NextResponse.json({ error: translate('scheduler.error.access_denied', 'Access denied') }, { status: 403 })\n }\n\n // Check if using async strategy\n const queueStrategy = process.env.QUEUE_STRATEGY || 'local'\n if (queueStrategy !== 'async') {\n return NextResponse.json({\n error: translate('scheduler.error.async_strategy_required', 'Execution history requires QUEUE_STRATEGY=async'),\n message: translate('scheduler.error.async_strategy_hint', 'Please set QUEUE_STRATEGY=async to view execution history'),\n items: [],\n }, { status: 400 })\n }\n\n // Fetch jobs from BullMQ scheduler-execution queue\n const { Queue } = await import('bullmq')\n const queue = new Queue('scheduler-execution', {\n connection: parseRedisUrl(getRedisUrlOrThrow('QUEUE')),\n })\n\n try {\n // Validate query params with Zod schema\n const queryResult = executionsQuerySchema.safeParse({\n pageSize: req.nextUrl.searchParams.get('pageSize') ?? undefined,\n })\n const limit = queryResult.success ? queryResult.data.pageSize : 20\n\n const [completed, failed, active, waiting, delayed] = await Promise.all([\n queue.getCompleted(0, limit - 1),\n queue.getFailed(0, limit - 1),\n queue.getActive(0, limit - 1),\n queue.getWaiting(0, limit - 1),\n queue.getDelayed(0, limit - 1),\n ])\n\n // Combine all jobs and filter by scheduleId\n const allJobs = [...completed, ...failed, ...active, ...waiting, ...delayed]\n type BullJobData = { payload?: { scheduleId?: string; triggerType?: string; triggeredByUserId?: string }; scheduleId?: string; triggerType?: string; triggeredByUserId?: string }\n\n const filteredJobs = allJobs\n .filter(job => {\n const data = job.data as BullJobData | undefined\n return data?.payload?.scheduleId === scheduleId || data?.scheduleId === scheduleId\n })\n .slice(0, limit)\n\n // Get state for each job (parallel)\n const jobsWithState = await Promise.all(\n filteredJobs.map(async (job) => {\n const state = await job.getState()\n const data = job.data as BullJobData | undefined\n \n return {\n id: job.id,\n scheduleId: data?.payload?.scheduleId || data?.scheduleId,\n startedAt: job.processedOn ? new Date(job.processedOn).toISOString() : new Date(job.timestamp).toISOString(),\n finishedAt: job.finishedOn ? new Date(job.finishedOn).toISOString() : null,\n status: state === 'completed' ? 'completed' : state === 'failed' ? 'failed' : state === 'active' ? 'running' : 'waiting',\n triggerType: data?.payload?.triggerType || data?.triggerType || 'scheduled',\n triggeredByUserId: data?.payload?.triggeredByUserId || data?.triggeredByUserId || null,\n errorMessage: job.failedReason || null,\n errorStack: job.stacktrace ? job.stacktrace.join('\\n') : null,\n durationMs: (job.finishedOn && job.processedOn) ? (job.finishedOn - job.processedOn) : null,\n queueJobId: job.id,\n queueName: 'scheduler-execution',\n attemptsMade: job.attemptsMade,\n result: job.returnvalue,\n }\n })\n )\n\n // Sort by startedAt descending (most recent first)\n jobsWithState.sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime())\n\n return NextResponse.json({\n items: jobsWithState,\n total: jobsWithState.length,\n page: 1,\n pageSize: limit,\n })\n } finally {\n await queue.close()\n }\n\n } catch (error: unknown) {\n logger.error('Error fetching execution history', { err: error })\n return NextResponse.json(\n { error: error instanceof Error ? error.message : translate('scheduler.error.fetch_executions_failed', 'Failed to fetch execution history') },\n { status: 500 }\n )\n }\n}\n\n// Query schemas\nconst executionsQuerySchema = z.object({\n pageSize: z.coerce.number().int().min(1).max(100).default(20),\n})\n\n// Response schemas\nconst executionItemSchema = z.object({\n id: z.string(),\n scheduleId: z.string().uuid(),\n startedAt: z.string(),\n finishedAt: z.string().nullable(),\n status: z.enum(['running', 'completed', 'failed', 'waiting']),\n triggerType: z.enum(['scheduled', 'manual']),\n triggeredByUserId: z.string().uuid().nullable(),\n errorMessage: z.string().nullable(),\n errorStack: z.string().nullable(),\n durationMs: z.number().nullable(),\n queueJobId: z.string(),\n queueName: z.string(),\n attemptsMade: z.number(),\n result: z.unknown().nullable(),\n})\n\nconst executionsResponseSchema = z.object({\n items: z.array(executionItemSchema),\n total: z.number(),\n page: z.number(),\n pageSize: z.number(),\n})\n\nconst errorResponseSchema = z.object({\n error: z.string(),\n message: z.string().optional(),\n items: z.array(z.any()).optional(),\n})\n\n// OpenAPI specification\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Scheduler',\n summary: 'Get execution history',\n description: 'Fetches recent execution history for a scheduled job from BullMQ.',\n methods: {\n GET: {\n operationId: 'getScheduleExecutions',\n summary: 'Get execution history for a schedule',\n description: 'Fetch recent executions from BullMQ for a scheduled job. Requires QUEUE_STRATEGY=async.',\n query: executionsQuerySchema,\n responses: [\n {\n status: 200,\n description: 'Execution history',\n schema: executionsResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Local strategy not supported', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n { status: 403, description: 'Access denied', schema: errorResponseSchema },\n { status: 404, description: 'Schedule not found', schema: errorResponseSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "AAAA,SAAsB,oBAAoB;AAC1C,SAAS,SAAS;AAClB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAGvC,SAAS,oBAAoB;AAC7B,SAAS,6BAA6B;AACtC,SAAS,oBAAoB,qBAAqB;AAClD,SAAS,2BAA2B;AACpC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,WAAW,EAAE,MAAM,EAAE,WAAW,aAAa,CAAC;AAGnE,MAAM,WAAW;AAAA,EACtB,aAAa;AAAA,EACb,iBAAiB,CAAC,qBAAqB;AACzC;AASA,eAAsB,IACpB,KACA,EAAE,OAAO,GACT;AACA,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,KAAK;AACd,WAAO,aAAa,KAAK,EAAE,OAAO,UAAU,gCAAgC,cAAc,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAChH;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,KAAK,UAAU,QAAuB,IAAI;AAEhD,QAAM,aAAa,OAAO;AAE1B,MAAI;AAGF,UAAM,WAAW,MAAM,GAAG,QAAQ,cAAc,EAAE,IAAI,YAAY,WAAW,KAAK,CAAC;AAEnF,QAAI,CAAC,UAAU;AACb,aAAO,aAAa,KAAK,EAAE,OAAO,UAAU,6BAA6B,oBAAoB,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACnH;AAEA,UAAM,SAAS,sBAAsB,UAAU,IAAI;AACnD,QAAI,WAAW,aAAa;AAC1B,aAAO,aAAa,KAAK,EAAE,OAAO,UAAU,6BAA6B,oBAAoB,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACnH;AACA,QAAI,WAAW,aAAa;AAC1B,aAAO,aAAa,KAAK,EAAE,OAAO,UAAU,iCAAiC,eAAe,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAClH;AAGA,UAAM,gBAAgB,QAAQ,IAAI,kBAAkB;AACpD,QAAI,kBAAkB,SAAS;AAC7B,aAAO,aAAa,KAAK;AAAA,QACvB,OAAO,UAAU,2CAA2C,iDAAiD;AAAA,QAC7G,SAAS,UAAU,uCAAuC,2DAA2D;AAAA,QACrH,OAAO,CAAC;AAAA,MACV,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACpB;AAGA,UAAM,EAAE,MAAM,IAAI,MAAM,OAAO,QAAQ;AACvC,UAAM,QAAQ,IAAI,MAAM,uBAAuB;AAAA,MAC7C,YAAY,cAAc,mBAAmB,OAAO,CAAC;AAAA,IACvD,CAAC;AAED,QAAI;AAEF,YAAM,cAAc,sBAAsB,UAAU;AAAA,QAClD,UAAU,IAAI,QAAQ,aAAa,IAAI,UAAU,KAAK;AAAA,MACxD,CAAC;AACD,YAAM,QAAQ,YAAY,UAAU,YAAY,KAAK,WAAW;AAEhE,YAAM,CAAC,WAAW,QAAQ,QAAQ,SAAS,OAAO,IAAI,MAAM,QAAQ,IAAI;AAAA,QACtE,MAAM,aAAa,GAAG,QAAQ,CAAC;AAAA,QAC/B,MAAM,UAAU,GAAG,QAAQ,CAAC;AAAA,QAC5B,MAAM,UAAU,GAAG,QAAQ,CAAC;AAAA,QAC5B,MAAM,WAAW,GAAG,QAAQ,CAAC;AAAA,QAC7B,MAAM,WAAW,GAAG,QAAQ,CAAC;AAAA,MAC/B,CAAC;AAGD,YAAM,UAAU,CAAC,GAAG,WAAW,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO;AAG3E,YAAM,eAAe,QAClB,OAAO,SAAO;AACb,cAAM,OAAO,IAAI;AACjB,eAAO,MAAM,SAAS,eAAe,cAAc,MAAM,eAAe;AAAA,MAC1E,CAAC,EACA,MAAM,GAAG,KAAK;AAGjB,YAAM,gBAAgB,MAAM,QAAQ;AAAA,QAClC,aAAa,IAAI,OAAO,QAAQ;AAC9B,gBAAM,QAAQ,MAAM,IAAI,SAAS;AACjC,gBAAM,OAAO,IAAI;AAEjB,iBAAO;AAAA,YACL,IAAI,IAAI;AAAA,YACR,YAAY,MAAM,SAAS,cAAc,MAAM;AAAA,YAC/C,WAAW,IAAI,cAAc,IAAI,KAAK,IAAI,WAAW,EAAE,YAAY,IAAI,IAAI,KAAK,IAAI,SAAS,EAAE,YAAY;AAAA,YAC3G,YAAY,IAAI,aAAa,IAAI,KAAK,IAAI,UAAU,EAAE,YAAY,IAAI;AAAA,YACtE,QAAQ,UAAU,cAAc,cAAc,UAAU,WAAW,WAAW,UAAU,WAAW,YAAY;AAAA,YAC/G,aAAa,MAAM,SAAS,eAAe,MAAM,eAAe;AAAA,YAChE,mBAAmB,MAAM,SAAS,qBAAqB,MAAM,qBAAqB;AAAA,YAClF,cAAc,IAAI,gBAAgB;AAAA,YAClC,YAAY,IAAI,aAAa,IAAI,WAAW,KAAK,IAAI,IAAI;AAAA,YACzD,YAAa,IAAI,cAAc,IAAI,cAAgB,IAAI,aAAa,IAAI,cAAe;AAAA,YACvF,YAAY,IAAI;AAAA,YAChB,WAAW;AAAA,YACX,cAAc,IAAI;AAAA,YAClB,QAAQ,IAAI;AAAA,UACd;AAAA,QACF,CAAC;AAAA,MACH;AAGA,oBAAc,KAAK,CAAC,GAAG,MAAM,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ,IAAI,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ,CAAC;AAE9F,aAAO,aAAa,KAAK;AAAA,QACvB,OAAO;AAAA,QACP,OAAO,cAAc;AAAA,QACrB,MAAM;AAAA,QACN,UAAU;AAAA,MACZ,CAAC;AAAA,IACH,UAAE;AACA,YAAM,MAAM,MAAM;AAAA,IACpB;AAAA,EAEF,SAAS,OAAgB;AACvB,WAAO,MAAM,oCAAoC,EAAE,KAAK,MAAM,CAAC;AAC/D,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,iBAAiB,QAAQ,MAAM,UAAU,UAAU,2CAA2C,mCAAmC,EAAE;AAAA,MAC5I,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACF;AAGA,MAAM,wBAAwB,EAAE,OAAO;AAAA,EACrC,UAAU,EAAE,OAAO,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,QAAQ,EAAE;AAC9D,CAAC;AAGD,MAAM,sBAAsB,EAAE,OAAO;AAAA,EACnC,IAAI,EAAE,OAAO;AAAA,EACb,YAAY,EAAE,OAAO,EAAE,KAAK;AAAA,EAC5B,WAAW,EAAE,OAAO;AAAA,EACpB,YAAY,EAAE,OAAO,EAAE,SAAS;AAAA,EAChC,QAAQ,EAAE,KAAK,CAAC,WAAW,aAAa,UAAU,SAAS,CAAC;AAAA,EAC5D,aAAa,EAAE,KAAK,CAAC,aAAa,QAAQ,CAAC;AAAA,EAC3C,mBAAmB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EAC9C,cAAc,EAAE,OAAO,EAAE,SAAS;AAAA,EAClC,YAAY,EAAE,OAAO,EAAE,SAAS;AAAA,EAChC,YAAY,EAAE,OAAO,EAAE,SAAS;AAAA,EAChC,YAAY,EAAE,OAAO;AAAA,EACrB,WAAW,EAAE,OAAO;AAAA,EACpB,cAAc,EAAE,OAAO;AAAA,EACvB,QAAQ,EAAE,QAAQ,EAAE,SAAS;AAC/B,CAAC;AAED,MAAM,2BAA2B,EAAE,OAAO;AAAA,EACxC,OAAO,EAAE,MAAM,mBAAmB;AAAA,EAClC,OAAO,EAAE,OAAO;AAAA,EAChB,MAAM,EAAE,OAAO;AAAA,EACf,UAAU,EAAE,OAAO;AACrB,CAAC;AAED,MAAM,sBAAsB,EAAE,OAAO;AAAA,EACnC,OAAO,EAAE,OAAO;AAAA,EAChB,SAAS,EAAE,OAAO,EAAE,SAAS;AAAA,EAC7B,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,SAAS;AACnC,CAAC;AAGM,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,aAAa;AAAA,EACb,SAAS;AAAA,IACP,KAAK;AAAA,MACH,aAAa;AAAA,MACb,SAAS;AAAA,MACT,aAAa;AAAA,MACb,OAAO;AAAA,MACP,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,QAAQ;AAAA,QACV;AAAA,MACF;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,gCAAgC,QAAQ,oBAAoB;AAAA,QACxF,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,oBAAoB;AAAA,QACxE,EAAE,QAAQ,KAAK,aAAa,iBAAiB,QAAQ,oBAAoB;AAAA,QACzE,EAAE,QAAQ,KAAK,aAAa,sBAAsB,QAAQ,oBAAoB;AAAA,MAChF;AAAA,IACF;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,9 +1,9 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { z } from "zod";
3
3
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
- import { getModules } from "@open-mercato/shared/lib/modules/registry";
5
4
  import { commandRegistry } from "@open-mercato/shared/lib/commands/registry";
6
5
  import { listSchedulerSafeCommands } from "../../lib/scheduler-safe-commands.js";
6
+ import { listSchedulerSafeQueueTargets } from "../../lib/safeQueueTargets.js";
7
7
  const metadata = {
8
8
  requireAuth: true,
9
9
  requireFeatures: ["scheduler.jobs.view"]
@@ -13,16 +13,7 @@ async function GET(req) {
13
13
  if (!auth) {
14
14
  return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
15
15
  }
16
- const modules = getModules();
17
- const queueSet = /* @__PURE__ */ new Set();
18
- for (const mod of modules) {
19
- if (mod.workers) {
20
- for (const worker of mod.workers) {
21
- queueSet.add(worker.queue);
22
- }
23
- }
24
- }
25
- const queues = Array.from(queueSet).sort((a, b) => a.localeCompare(b)).map((queue) => ({ value: queue, label: queue }));
16
+ const queues = listSchedulerSafeQueueTargets().map((target) => ({ value: target.queue, label: target.queue }));
26
17
  const commands = listSchedulerSafeCommands().filter((command) => commandRegistry.has(command.commandId)).map((command) => ({ value: command.commandId, label: command.commandId }));
27
18
  return NextResponse.json({ queues, commands });
28
19
  }
@@ -45,7 +36,6 @@ const openApi = {
45
36
  GET: {
46
37
  operationId: "listScheduleTargets",
47
38
  summary: "List available queues and commands",
48
- description: "Returns all registered queue names (from module workers) and explicitly scheduler-safe command IDs that can be used as schedule targets.",
49
39
  responses: [
50
40
  {
51
41
  status: 200,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/scheduler/api/targets/route.ts"],
4
- "sourcesContent": ["import { NextRequest, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { getModules } from '@open-mercato/shared/lib/modules/registry'\nimport { commandRegistry } from '@open-mercato/shared/lib/commands/registry'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { listSchedulerSafeCommands } from '../../lib/scheduler-safe-commands'\n\nexport const metadata = {\n requireAuth: true,\n requireFeatures: ['scheduler.jobs.view'],\n}\n\n/**\n * GET /api/scheduler/targets\n * Returns available queue names and command IDs for schedule target selection.\n */\nexport async function GET(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const modules = getModules()\n\n const queueSet = new Set<string>()\n for (const mod of modules) {\n if (mod.workers) {\n for (const worker of mod.workers) {\n queueSet.add(worker.queue)\n }\n }\n }\n\n const queues = Array.from(queueSet)\n .sort((a, b) => a.localeCompare(b))\n .map((queue) => ({ value: queue, label: queue }))\n\n const commands = listSchedulerSafeCommands()\n .filter((command) => commandRegistry.has(command.commandId))\n .map((command) => ({ value: command.commandId, label: command.commandId }))\n\n return NextResponse.json({ queues, commands })\n}\n\n// Response schemas\nconst targetOptionSchema = z.object({\n value: z.string(),\n label: z.string(),\n})\n\nconst targetsResponseSchema = z.object({\n queues: z.array(targetOptionSchema),\n commands: z.array(targetOptionSchema),\n})\n\nconst errorResponseSchema = z.object({\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Scheduler',\n summary: 'List available schedule targets',\n description: 'Returns available queue names and scheduler-safe command IDs for schedule target selection.',\n methods: {\n GET: {\n operationId: 'listScheduleTargets',\n summary: 'List available queues and commands',\n description: 'Returns all registered queue names (from module workers) and explicitly scheduler-safe command IDs that can be used as schedule targets.',\n responses: [\n {\n status: 200,\n description: 'Available targets',\n schema: targetsResponseSchema,\n },\n ],\n errors: [\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "AAAA,SAAsB,oBAAoB;AAC1C,SAAS,SAAS;AAClB,SAAS,0BAA0B;AACnC,SAAS,kBAAkB;AAC3B,SAAS,uBAAuB;AAEhC,SAAS,iCAAiC;AAEnC,MAAM,WAAW;AAAA,EACtB,aAAa;AAAA,EACb,iBAAiB,CAAC,qBAAqB;AACzC;AAMA,eAAsB,IAAI,KAAkB;AAC1C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM;AACT,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,QAAM,UAAU,WAAW;AAE3B,QAAM,WAAW,oBAAI,IAAY;AACjC,aAAW,OAAO,SAAS;AACzB,QAAI,IAAI,SAAS;AACf,iBAAW,UAAU,IAAI,SAAS;AAChC,iBAAS,IAAI,OAAO,KAAK;AAAA,MAC3B;AAAA,IACF;AAAA,EACF;AAEA,QAAM,SAAS,MAAM,KAAK,QAAQ,EAC/B,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,CAAC,CAAC,EACjC,IAAI,CAAC,WAAW,EAAE,OAAO,OAAO,OAAO,MAAM,EAAE;AAElD,QAAM,WAAW,0BAA0B,EACxC,OAAO,CAAC,YAAY,gBAAgB,IAAI,QAAQ,SAAS,CAAC,EAC1D,IAAI,CAAC,aAAa,EAAE,OAAO,QAAQ,WAAW,OAAO,QAAQ,UAAU,EAAE;AAE5E,SAAO,aAAa,KAAK,EAAE,QAAQ,SAAS,CAAC;AAC/C;AAGA,MAAM,qBAAqB,EAAE,OAAO;AAAA,EAClC,OAAO,EAAE,OAAO;AAAA,EAChB,OAAO,EAAE,OAAO;AAClB,CAAC;AAED,MAAM,wBAAwB,EAAE,OAAO;AAAA,EACrC,QAAQ,EAAE,MAAM,kBAAkB;AAAA,EAClC,UAAU,EAAE,MAAM,kBAAkB;AACtC,CAAC;AAED,MAAM,sBAAsB,EAAE,OAAO;AAAA,EACnC,OAAO,EAAE,OAAO;AAClB,CAAC;AAEM,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,aAAa;AAAA,EACb,SAAS;AAAA,IACP,KAAK;AAAA,MACH,aAAa;AAAA,MACb,SAAS;AAAA,MACT,aAAa;AAAA,MACb,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,QAAQ;AAAA,QACV;AAAA,MACF;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,oBAAoB;AAAA,MAC1E;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { NextRequest, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { commandRegistry } from '@open-mercato/shared/lib/commands/registry'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { listSchedulerSafeCommands } from '../../lib/scheduler-safe-commands'\nimport { listSchedulerSafeQueueTargets } from '../../lib/safeQueueTargets'\n\nexport const metadata = {\n requireAuth: true,\n requireFeatures: ['scheduler.jobs.view'],\n}\n\n/**\n * GET /api/scheduler/targets\n * Returns scheduler-safe queue names and command IDs for schedule target selection.\n * Only queues whose workers opted into scheduling (`schedulerSafe: true`) are\n * advertised; internal and system-only workers stay undiscoverable (#5213).\n */\nexport async function GET(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const queues = listSchedulerSafeQueueTargets()\n .map((target) => ({ value: target.queue, label: target.queue }))\n\n const commands = listSchedulerSafeCommands()\n .filter((command) => commandRegistry.has(command.commandId))\n .map((command) => ({ value: command.commandId, label: command.commandId }))\n\n return NextResponse.json({ queues, commands })\n}\n\n// Response schemas\nconst targetOptionSchema = z.object({\n value: z.string(),\n label: z.string(),\n})\n\nconst targetsResponseSchema = z.object({\n queues: z.array(targetOptionSchema),\n commands: z.array(targetOptionSchema),\n})\n\nconst errorResponseSchema = z.object({\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Scheduler',\n summary: 'List available schedule targets',\n description: 'Returns available queue names and scheduler-safe command IDs for schedule target selection.',\n methods: {\n GET: {\n operationId: 'listScheduleTargets',\n summary: 'List available queues and commands',\n responses: [\n {\n status: 200,\n description: 'Available targets',\n schema: targetsResponseSchema,\n },\n ],\n errors: [\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "AAAA,SAAsB,oBAAoB;AAC1C,SAAS,SAAS;AAClB,SAAS,0BAA0B;AACnC,SAAS,uBAAuB;AAEhC,SAAS,iCAAiC;AAC1C,SAAS,qCAAqC;AAEvC,MAAM,WAAW;AAAA,EACtB,aAAa;AAAA,EACb,iBAAiB,CAAC,qBAAqB;AACzC;AAQA,eAAsB,IAAI,KAAkB;AAC1C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM;AACT,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,QAAM,SAAS,8BAA8B,EAC1C,IAAI,CAAC,YAAY,EAAE,OAAO,OAAO,OAAO,OAAO,OAAO,MAAM,EAAE;AAEjE,QAAM,WAAW,0BAA0B,EACxC,OAAO,CAAC,YAAY,gBAAgB,IAAI,QAAQ,SAAS,CAAC,EAC1D,IAAI,CAAC,aAAa,EAAE,OAAO,QAAQ,WAAW,OAAO,QAAQ,UAAU,EAAE;AAE5E,SAAO,aAAa,KAAK,EAAE,QAAQ,SAAS,CAAC;AAC/C;AAGA,MAAM,qBAAqB,EAAE,OAAO;AAAA,EAClC,OAAO,EAAE,OAAO;AAAA,EAChB,OAAO,EAAE,OAAO;AAClB,CAAC;AAED,MAAM,wBAAwB,EAAE,OAAO;AAAA,EACrC,QAAQ,EAAE,MAAM,kBAAkB;AAAA,EAClC,UAAU,EAAE,MAAM,kBAAkB;AACtC,CAAC;AAED,MAAM,sBAAsB,EAAE,OAAO;AAAA,EACnC,OAAO,EAAE,OAAO;AAClB,CAAC;AAEM,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,aAAa;AAAA,EACb,SAAS;AAAA,IACP,KAAK;AAAA,MACH,aAAa;AAAA,MACb,SAAS;AAAA,MACT,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,QAAQ;AAAA,QACV;AAAA,MACF;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,oBAAoB;AAAA,MAC1E;AAAA,IACF;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -5,6 +5,7 @@ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
5
  import { createQueue } from "@open-mercato/queue";
6
6
  import { getRedisUrlOrThrow } from "@open-mercato/shared/lib/redis/connection";
7
7
  import { ScheduledJob } from "../../data/entities.js";
8
+ import { resolveScheduleAccess } from "../../lib/scheduleAccess.js";
8
9
  import { scheduleTriggerSchema } from "../../data/validators.js";
9
10
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
10
11
  import { createLogger } from "@open-mercato/shared/lib/logger";
@@ -24,22 +25,15 @@ async function POST(req) {
24
25
  try {
25
26
  const body = await req.json();
26
27
  const input = scheduleTriggerSchema.parse(body);
27
- const findFilter = {
28
- id: input.id,
29
- deletedAt: null
30
- };
31
- if (auth.tenantId) {
32
- findFilter.tenantId = auth.tenantId;
33
- }
34
- if (auth.orgId) {
35
- findFilter.organizationId = auth.orgId;
36
- }
37
- const schedule = await em.findOne(ScheduledJob, findFilter);
28
+ const schedule = await em.findOne(ScheduledJob, { id: input.id, deletedAt: null });
38
29
  if (!schedule) {
39
30
  return NextResponse.json({ error: translate("scheduler.error.not_found", "Schedule not found") }, { status: 404 });
40
31
  }
41
- const isSuperAdmin = auth.isSuperAdmin === true;
42
- if (!schedule.tenantId && !schedule.organizationId && !isSuperAdmin) {
32
+ const access = resolveScheduleAccess(schedule, auth);
33
+ if (access === "not_found") {
34
+ return NextResponse.json({ error: translate("scheduler.error.not_found", "Schedule not found") }, { status: 404 });
35
+ }
36
+ if (access === "forbidden") {
43
37
  return NextResponse.json({ error: translate("scheduler.error.access_denied", "Access denied") }, { status: 403 });
44
38
  }
45
39
  const queueStrategy = process.env.QUEUE_STRATEGY || "local";
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/scheduler/api/trigger/route.ts"],
4
- "sourcesContent": ["import { NextRequest, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/core'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createQueue } from '@open-mercato/queue'\nimport { getRedisUrlOrThrow } from '@open-mercato/shared/lib/redis/connection'\nimport { ScheduledJob } from '../../data/entities.js'\nimport { scheduleTriggerSchema } from '../../data/validators.js'\nimport type { ExecuteSchedulePayload } from '../../workers/execute-schedule.worker.js'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('scheduler').child({ component: 'trigger' })\n\nexport const metadata = {\n requireAuth: true,\n requireFeatures: ['scheduler.jobs.trigger'],\n}\n\n/**\n * POST /api/scheduler/trigger\n * Manually trigger a schedule\n * \n * This enqueues the schedule execution job in BullMQ.\n * Execution history is tracked in BullMQ job state.\n */\nexport async function POST(req: NextRequest) {\n const { translate } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: translate('scheduler.error.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve<EntityManager>('em')\n\n try {\n const body = await req.json()\n const input = scheduleTriggerSchema.parse(body)\n\n // Find the schedule with tenant/org scope filter\n const findFilter: Record<string, unknown> = {\n id: input.id,\n deletedAt: null,\n }\n\n // Apply tenant isolation: scope the query to the user's tenant/org\n if (auth.tenantId) {\n findFilter.tenantId = auth.tenantId\n }\n if (auth.orgId) {\n findFilter.organizationId = auth.orgId\n }\n\n const schedule = await em.findOne(ScheduledJob, findFilter)\n\n if (!schedule) {\n return NextResponse.json({ error: translate('scheduler.error.not_found', 'Schedule not found') }, { status: 404 })\n }\n\n // System-scoped schedules (no tenantId/orgId) require super-admin. Use the\n // immutable `isSuperAdmin` flag \u2014 never compare mutable/spoofable role names.\n const isSuperAdmin = auth.isSuperAdmin === true\n if (!schedule.tenantId && !schedule.organizationId && !isSuperAdmin) {\n return NextResponse.json({ error: translate('scheduler.error.access_denied', 'Access denied') }, { status: 403 })\n }\n\n // Check if using async queue strategy\n const queueStrategy = (process.env.QUEUE_STRATEGY || 'local') as 'local' | 'async'\n \n if (queueStrategy !== 'async') {\n return NextResponse.json(\n { \n error: translate('scheduler.error.trigger_async_required', 'Manual trigger requires QUEUE_STRATEGY=async'),\n message: translate('scheduler.error.trigger_async_hint', 'Execution history and manual triggers are only available with BullMQ (async strategy)')\n },\n { status: 400 }\n )\n }\n\n // Enqueue execution job to scheduler-execution queue\n const executionQueue = createQueue<ExecuteSchedulePayload>('scheduler-execution', queueStrategy, {\n connection: { url: getRedisUrlOrThrow('QUEUE') },\n })\n\n const payload: ExecuteSchedulePayload = {\n scheduleId: schedule.id,\n tenantId: schedule.tenantId,\n organizationId: schedule.organizationId,\n scopeType: schedule.scopeType,\n triggerType: 'manual',\n triggeredByUserId: auth.sub,\n }\n\n const jobId = await executionQueue.enqueue(payload)\n await executionQueue.close()\n\n logger.info('Manually triggered schedule', {\n scheduleId: schedule.id,\n scheduleName: schedule.name,\n jobId,\n triggeredBy: auth.sub,\n })\n\n return NextResponse.json({\n ok: true,\n jobId, // BullMQ job ID\n message: translate('scheduler.success.triggered', 'Schedule queued for execution'),\n })\n\n } catch (error) {\n logger.error('Manual trigger failed', { err: error })\n return NextResponse.json(\n { error: error instanceof Error ? error.message : translate('scheduler.error.trigger_failed', 'Failed to trigger schedule') },\n { status: 400 }\n )\n }\n}\n\n// Response schemas\nconst triggerResponseSchema = z.object({\n ok: z.boolean(),\n jobId: z.string(),\n message: z.string(),\n})\n\nconst errorResponseSchema = z.object({\n error: z.string(),\n message: z.string().optional(),\n})\n\n// OpenAPI specification\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Scheduler',\n summary: 'Manually trigger a schedule',\n description: 'Execute a schedule immediately by enqueueing it in the scheduler-execution queue. Requires QUEUE_STRATEGY=async.',\n methods: {\n POST: {\n operationId: 'triggerScheduledJob',\n summary: 'Manually trigger a schedule',\n description: 'Executes a scheduled job immediately, bypassing the scheduled time. Only works with async queue strategy.',\n requestBody: {\n schema: scheduleTriggerSchema,\n contentType: 'application/json',\n },\n responses: [\n {\n status: 200,\n description: 'Schedule triggered successfully',\n schema: triggerResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request or local strategy not supported', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n { status: 403, description: 'Access denied', schema: errorResponseSchema },\n { status: 404, description: 'Schedule not found', schema: errorResponseSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextRequest, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/core'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createQueue } from '@open-mercato/queue'\nimport { getRedisUrlOrThrow } from '@open-mercato/shared/lib/redis/connection'\nimport { ScheduledJob } from '../../data/entities.js'\nimport { resolveScheduleAccess } from '../../lib/scheduleAccess.js'\nimport { scheduleTriggerSchema } from '../../data/validators.js'\nimport type { ExecuteSchedulePayload } from '../../workers/execute-schedule.worker.js'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('scheduler').child({ component: 'trigger' })\n\nexport const metadata = {\n requireAuth: true,\n requireFeatures: ['scheduler.jobs.trigger'],\n}\n\n/**\n * POST /api/scheduler/trigger\n * Manually trigger a schedule\n * \n * This enqueues the schedule execution job in BullMQ.\n * Execution history is tracked in BullMQ job state.\n */\nexport async function POST(req: NextRequest) {\n const { translate } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: translate('scheduler.error.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve<EntityManager>('em')\n\n try {\n const body = await req.json()\n const input = scheduleTriggerSchema.parse(body)\n\n // Load by id alone, then apply isolation to the loaded row. Folding the actor's\n // tenant/org into the lookup would make every system-scoped schedule unmatchable.\n const schedule = await em.findOne(ScheduledJob, { id: input.id, deletedAt: null })\n\n if (!schedule) {\n return NextResponse.json({ error: translate('scheduler.error.not_found', 'Schedule not found') }, { status: 404 })\n }\n\n const access = resolveScheduleAccess(schedule, auth)\n if (access === 'not_found') {\n return NextResponse.json({ error: translate('scheduler.error.not_found', 'Schedule not found') }, { status: 404 })\n }\n if (access === 'forbidden') {\n return NextResponse.json({ error: translate('scheduler.error.access_denied', 'Access denied') }, { status: 403 })\n }\n\n // Check if using async queue strategy\n const queueStrategy = (process.env.QUEUE_STRATEGY || 'local') as 'local' | 'async'\n \n if (queueStrategy !== 'async') {\n return NextResponse.json(\n { \n error: translate('scheduler.error.trigger_async_required', 'Manual trigger requires QUEUE_STRATEGY=async'),\n message: translate('scheduler.error.trigger_async_hint', 'Execution history and manual triggers are only available with BullMQ (async strategy)')\n },\n { status: 400 }\n )\n }\n\n // Enqueue execution job to scheduler-execution queue\n const executionQueue = createQueue<ExecuteSchedulePayload>('scheduler-execution', queueStrategy, {\n connection: { url: getRedisUrlOrThrow('QUEUE') },\n })\n\n const payload: ExecuteSchedulePayload = {\n scheduleId: schedule.id,\n tenantId: schedule.tenantId,\n organizationId: schedule.organizationId,\n scopeType: schedule.scopeType,\n triggerType: 'manual',\n triggeredByUserId: auth.sub,\n }\n\n const jobId = await executionQueue.enqueue(payload)\n await executionQueue.close()\n\n logger.info('Manually triggered schedule', {\n scheduleId: schedule.id,\n scheduleName: schedule.name,\n jobId,\n triggeredBy: auth.sub,\n })\n\n return NextResponse.json({\n ok: true,\n jobId, // BullMQ job ID\n message: translate('scheduler.success.triggered', 'Schedule queued for execution'),\n })\n\n } catch (error) {\n logger.error('Manual trigger failed', { err: error })\n return NextResponse.json(\n { error: error instanceof Error ? error.message : translate('scheduler.error.trigger_failed', 'Failed to trigger schedule') },\n { status: 400 }\n )\n }\n}\n\n// Response schemas\nconst triggerResponseSchema = z.object({\n ok: z.boolean(),\n jobId: z.string(),\n message: z.string(),\n})\n\nconst errorResponseSchema = z.object({\n error: z.string(),\n message: z.string().optional(),\n})\n\n// OpenAPI specification\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Scheduler',\n summary: 'Manually trigger a schedule',\n description: 'Execute a schedule immediately by enqueueing it in the scheduler-execution queue. Requires QUEUE_STRATEGY=async.',\n methods: {\n POST: {\n operationId: 'triggerScheduledJob',\n summary: 'Manually trigger a schedule',\n description: 'Executes a scheduled job immediately, bypassing the scheduled time. Only works with async queue strategy.',\n requestBody: {\n schema: scheduleTriggerSchema,\n contentType: 'application/json',\n },\n responses: [\n {\n status: 200,\n description: 'Schedule triggered successfully',\n schema: triggerResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request or local strategy not supported', schema: errorResponseSchema },\n { status: 401, description: 'Unauthorized', schema: errorResponseSchema },\n { status: 403, description: 'Access denied', schema: errorResponseSchema },\n { status: 404, description: 'Schedule not found', schema: errorResponseSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -44,6 +44,7 @@ function SchedulerPage() {
44
44
  const [pageSize] = React.useState(20);
45
45
  const [total, setTotal] = React.useState(0);
46
46
  const [totalPages, setTotalPages] = React.useState(1);
47
+ const [totalIsCapped, setTotalIsCapped] = React.useState(false);
47
48
  const [search, setSearch] = React.useState("");
48
49
  const [isLoading, setIsLoading] = React.useState(false);
49
50
  const scopeVersion = useOrganizationScopeVersion();
@@ -63,6 +64,7 @@ function SchedulerPage() {
63
64
  setRows(mapped);
64
65
  setTotal(result?.total ?? 0);
65
66
  setTotalPages(result?.totalPages ?? 1);
67
+ setTotalIsCapped(result?.totalIsCapped === true);
66
68
  } catch (error) {
67
69
  flash(t("scheduler.error.fetch_failed", "Failed to load schedules"), "error");
68
70
  setRows([]);
@@ -212,7 +214,7 @@ function SchedulerPage() {
212
214
  createLabel: t("scheduler.action.create", "New Schedule")
213
215
  }
214
216
  ),
215
- pagination: { page, pageSize, total, totalPages, onPageChange: setPage },
217
+ pagination: { page, pageSize, total, totalPages, totalIsCapped, onPageChange: setPage },
216
218
  isLoading,
217
219
  searchValue: search,
218
220
  onSearchChange: (value) => {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/scheduler/backend/config/scheduled-jobs/page.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { RowActions, type RowActionItem } from '@open-mercato/ui/backend/RowActions'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { formatDateTime } from '@open-mercato/shared/lib/time'\nimport { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'\n\ntype ScheduleRow = {\n id: string\n name: string\n description?: string | null\n scopeType: 'system' | 'organization' | 'tenant'\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone: string\n targetType: 'queue' | 'command'\n targetQueue?: string | null\n targetCommand?: string | null\n isEnabled: boolean\n sourceType: 'user' | 'module'\n sourceModule?: string | null\n nextRunAt?: string | null\n lastRunAt?: string | null\n}\n\ntype SchedulesResponse = {\n items?: Array<Record<string, unknown>>\n total?: number\n page?: number\n totalPages?: number\n}\n\nfunction mapApiItem(item: Record<string, unknown>): ScheduleRow | null {\n const id = typeof item.id === 'string' ? item.id : null\n if (!id) return null\n\n return {\n id,\n name: typeof item.name === 'string' ? item.name : '',\n description: typeof item.description === 'string' ? item.description : null,\n scopeType: (item.scopeType === 'system' || item.scopeType === 'organization' || item.scopeType === 'tenant')\n ? item.scopeType\n : 'tenant',\n scheduleType: (item.scheduleType === 'cron' || item.scheduleType === 'interval')\n ? item.scheduleType\n : 'cron',\n scheduleValue: typeof item.scheduleValue === 'string' ? item.scheduleValue : '',\n timezone: typeof item.timezone === 'string' ? item.timezone : 'UTC',\n targetType: (item.targetType === 'queue' || item.targetType === 'command')\n ? item.targetType\n : 'queue',\n targetQueue: typeof item.targetQueue === 'string' ? item.targetQueue : null,\n targetCommand: typeof item.targetCommand === 'string' ? item.targetCommand : null,\n isEnabled: item.isEnabled === true,\n sourceType: (item.sourceType === 'user' || item.sourceType === 'module')\n ? item.sourceType\n : 'user',\n sourceModule: typeof item.sourceModule === 'string' ? item.sourceModule : null,\n nextRunAt: typeof item.nextRunAt === 'string' ? item.nextRunAt : null,\n lastRunAt: typeof item.lastRunAt === 'string' ? item.lastRunAt : null,\n }\n}\n\n\nexport default function SchedulerPage() {\n const t = useT()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const router = useRouter()\n const [rows, setRows] = React.useState<ScheduleRow[]>([])\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(20)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [search, setSearch] = React.useState('')\n const [isLoading, setIsLoading] = React.useState(false)\n const scopeVersion = useOrganizationScopeVersion()\n\n const fetchSchedules = React.useCallback(async () => {\n setIsLoading(true)\n try {\n const params = new URLSearchParams({\n page: page.toString(),\n pageSize: pageSize.toString(),\n })\n if (search) params.set('search', search)\n\n const { result } = await apiCallOrThrow<SchedulesResponse>(\n `/api/scheduler/jobs?${params.toString()}`\n )\n\n const items = result?.items ?? []\n const mapped = items.map(mapApiItem).filter((x): x is ScheduleRow => x !== null)\n setRows(mapped)\n setTotal(result?.total ?? 0)\n setTotalPages(result?.totalPages ?? 1)\n } catch (error) {\n flash(t('scheduler.error.fetch_failed', 'Failed to load schedules'), 'error')\n setRows([])\n setTotal(0)\n setTotalPages(1)\n } finally {\n setIsLoading(false)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [page, pageSize, search, scopeVersion, t])\n\n React.useEffect(() => {\n fetchSchedules()\n }, [fetchSchedules])\n\n const handleDelete = React.useCallback(\n async (row: ScheduleRow) => {\n const confirmed = await confirm({\n title: t('scheduler.confirm.delete', 'Are you sure you want to delete this schedule?'),\n variant: 'destructive',\n })\n if (!confirmed) {\n return\n }\n\n try {\n await apiCallOrThrow(`/api/scheduler/jobs`, {\n method: 'DELETE',\n body: JSON.stringify({ id: row.id }),\n })\n flash(t('scheduler.success.deleted', 'Schedule deleted successfully'), 'success')\n fetchSchedules()\n } catch (error) {\n flash(t('scheduler.error.delete_failed', 'Failed to delete schedule'), 'error')\n }\n },\n [confirm, t, fetchSchedules]\n )\n\n const handleTrigger = React.useCallback(\n async (row: ScheduleRow) => {\n try {\n await apiCallOrThrow(`/api/scheduler/trigger`, {\n method: 'POST',\n body: JSON.stringify({ id: row.id }),\n })\n flash(t('scheduler.success.triggered', 'Schedule triggered successfully'), 'success')\n } catch (error) {\n const message = error instanceof Error ? error.message : t('scheduler.error.trigger_failed', 'Failed to trigger schedule')\n flash(message, 'error')\n }\n },\n [t]\n )\n\n const columns = React.useMemo<ColumnDef<ScheduleRow>[]>(\n () => [\n {\n id: 'name',\n accessorKey: 'name',\n header: t('scheduler.field.name', 'Name'),\n cell: ({ row }) => (\n <span className=\"font-medium\">{row.original.name}</span>\n ),\n },\n {\n id: 'scheduleType',\n accessorKey: 'scheduleType',\n header: t('scheduler.field.schedule_type', 'Type'),\n cell: ({ row }) => (\n <span className=\"capitalize\">{row.original.scheduleType}</span>\n ),\n },\n {\n id: 'scheduleValue',\n accessorKey: 'scheduleValue',\n header: t('scheduler.field.schedule', 'Schedule'),\n cell: ({ row }) => (\n <span className=\"text-sm\">\n {row.original.scheduleValue}\n </span>\n ),\n },\n {\n id: 'targetType',\n accessorKey: 'targetType',\n header: t('scheduler.field.target', 'Target'),\n cell: ({ row }) => {\n const target = row.original.targetType === 'queue'\n ? row.original.targetQueue\n : row.original.targetCommand\n return (\n <div className=\"flex flex-col\">\n <span className=\"text-xs text-muted-foreground capitalize\">{row.original.targetType}</span>\n <span className=\"text-sm\">{target || '-'}</span>\n </div>\n )\n },\n },\n {\n id: 'nextRunAt',\n accessorKey: 'nextRunAt',\n header: t('scheduler.field.next_run', 'Next Run'),\n cell: ({ row }) => (\n <span className=\"text-sm\">\n {formatDateTime(row.original.nextRunAt) || '-'}\n </span>\n ),\n },\n {\n id: 'isEnabled',\n accessorKey: 'isEnabled',\n header: t('scheduler.field.active', 'Active'),\n cell: ({ row }) => <BooleanIcon value={row.original.isEnabled} />,\n },\n {\n id: 'sourceType',\n accessorKey: 'sourceType',\n header: t('scheduler.field.source', 'Source'),\n cell: ({ row }) => (\n <span className=\"text-xs capitalize\">\n {row.original.sourceType}\n {row.original.sourceModule && ` (${row.original.sourceModule})`}\n </span>\n ),\n },\n ],\n [t]\n )\n\n const rowActions = React.useCallback(\n (row: ScheduleRow): RowActionItem[] => [\n {\n id: 'view',\n label: t('scheduler.action.view', 'View Details'),\n onSelect: () => router.push(`/backend/config/scheduled-jobs/${row.id}`),\n },\n {\n id: 'edit',\n label: t('scheduler.action.edit', 'Edit'),\n onSelect: () => router.push(`/backend/config/scheduled-jobs/${row.id}/edit`),\n },\n {\n id: 'trigger',\n label: t('scheduler.action.trigger', 'Run Now'),\n onSelect: () => handleTrigger(row),\n },\n {\n id: 'delete',\n label: t('scheduler.action.delete', 'Delete'),\n onSelect: () => handleDelete(row),\n destructive: true,\n },\n ],\n [t, router, handleDelete, handleTrigger]\n )\n\n return (\n <Page>\n <PageBody>\n <DataTable<ScheduleRow>\n title={t('scheduler.title', 'Scheduled Jobs')}\n actions={\n <Button onClick={() => router.push('/backend/config/scheduled-jobs/new')}>\n {t('scheduler.action.create', 'New Schedule')}\n </Button>\n }\n columns={columns}\n data={rows}\n onRowClick={(row) => router.push(`/backend/config/scheduled-jobs/${row.id}`)}\n rowActions={(row) => <RowActions items={rowActions(row)} />}\n emptyState={(\n <ListEmptyState\n entityName={t('scheduler.title', 'Scheduled Jobs')}\n createHref=\"/backend/config/scheduled-jobs/new\"\n createLabel={t('scheduler.action.create', 'New Schedule')}\n />\n )}\n pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}\n isLoading={isLoading}\n searchValue={search}\n onSearchChange={(value) => { setSearch(value); setPage(1) }}\n searchPlaceholder={t('scheduler.search.placeholder', 'Search schedules...')}\n refreshButton={{\n label: t('scheduler.action.refresh', 'Refresh'),\n onRefresh: fetchSchedules,\n }}\n />\n {ConfirmDialogElement}\n </PageBody>\n </Page>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { RowActions, type RowActionItem } from '@open-mercato/ui/backend/RowActions'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { formatDateTime } from '@open-mercato/shared/lib/time'\nimport { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'\n\ntype ScheduleRow = {\n id: string\n name: string\n description?: string | null\n scopeType: 'system' | 'organization' | 'tenant'\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone: string\n targetType: 'queue' | 'command'\n targetQueue?: string | null\n targetCommand?: string | null\n isEnabled: boolean\n sourceType: 'user' | 'module'\n sourceModule?: string | null\n nextRunAt?: string | null\n lastRunAt?: string | null\n}\n\ntype SchedulesResponse = {\n items?: Array<Record<string, unknown>>\n total?: number\n page?: number\n totalPages?: number\n totalIsCapped?: boolean\n}\n\nfunction mapApiItem(item: Record<string, unknown>): ScheduleRow | null {\n const id = typeof item.id === 'string' ? item.id : null\n if (!id) return null\n\n return {\n id,\n name: typeof item.name === 'string' ? item.name : '',\n description: typeof item.description === 'string' ? item.description : null,\n scopeType: (item.scopeType === 'system' || item.scopeType === 'organization' || item.scopeType === 'tenant')\n ? item.scopeType\n : 'tenant',\n scheduleType: (item.scheduleType === 'cron' || item.scheduleType === 'interval')\n ? item.scheduleType\n : 'cron',\n scheduleValue: typeof item.scheduleValue === 'string' ? item.scheduleValue : '',\n timezone: typeof item.timezone === 'string' ? item.timezone : 'UTC',\n targetType: (item.targetType === 'queue' || item.targetType === 'command')\n ? item.targetType\n : 'queue',\n targetQueue: typeof item.targetQueue === 'string' ? item.targetQueue : null,\n targetCommand: typeof item.targetCommand === 'string' ? item.targetCommand : null,\n isEnabled: item.isEnabled === true,\n sourceType: (item.sourceType === 'user' || item.sourceType === 'module')\n ? item.sourceType\n : 'user',\n sourceModule: typeof item.sourceModule === 'string' ? item.sourceModule : null,\n nextRunAt: typeof item.nextRunAt === 'string' ? item.nextRunAt : null,\n lastRunAt: typeof item.lastRunAt === 'string' ? item.lastRunAt : null,\n }\n}\n\n\nexport default function SchedulerPage() {\n const t = useT()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const router = useRouter()\n const [rows, setRows] = React.useState<ScheduleRow[]>([])\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(20)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [totalIsCapped, setTotalIsCapped] = React.useState(false)\n const [search, setSearch] = React.useState('')\n const [isLoading, setIsLoading] = React.useState(false)\n const scopeVersion = useOrganizationScopeVersion()\n\n const fetchSchedules = React.useCallback(async () => {\n setIsLoading(true)\n try {\n const params = new URLSearchParams({\n page: page.toString(),\n pageSize: pageSize.toString(),\n })\n if (search) params.set('search', search)\n\n const { result } = await apiCallOrThrow<SchedulesResponse>(\n `/api/scheduler/jobs?${params.toString()}`\n )\n\n const items = result?.items ?? []\n const mapped = items.map(mapApiItem).filter((x): x is ScheduleRow => x !== null)\n setRows(mapped)\n setTotal(result?.total ?? 0)\n setTotalPages(result?.totalPages ?? 1)\n setTotalIsCapped(result?.totalIsCapped === true)\n } catch (error) {\n flash(t('scheduler.error.fetch_failed', 'Failed to load schedules'), 'error')\n setRows([])\n setTotal(0)\n setTotalPages(1)\n } finally {\n setIsLoading(false)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [page, pageSize, search, scopeVersion, t])\n\n React.useEffect(() => {\n fetchSchedules()\n }, [fetchSchedules])\n\n const handleDelete = React.useCallback(\n async (row: ScheduleRow) => {\n const confirmed = await confirm({\n title: t('scheduler.confirm.delete', 'Are you sure you want to delete this schedule?'),\n variant: 'destructive',\n })\n if (!confirmed) {\n return\n }\n\n try {\n await apiCallOrThrow(`/api/scheduler/jobs`, {\n method: 'DELETE',\n body: JSON.stringify({ id: row.id }),\n })\n flash(t('scheduler.success.deleted', 'Schedule deleted successfully'), 'success')\n fetchSchedules()\n } catch (error) {\n flash(t('scheduler.error.delete_failed', 'Failed to delete schedule'), 'error')\n }\n },\n [confirm, t, fetchSchedules]\n )\n\n const handleTrigger = React.useCallback(\n async (row: ScheduleRow) => {\n try {\n await apiCallOrThrow(`/api/scheduler/trigger`, {\n method: 'POST',\n body: JSON.stringify({ id: row.id }),\n })\n flash(t('scheduler.success.triggered', 'Schedule triggered successfully'), 'success')\n } catch (error) {\n const message = error instanceof Error ? error.message : t('scheduler.error.trigger_failed', 'Failed to trigger schedule')\n flash(message, 'error')\n }\n },\n [t]\n )\n\n const columns = React.useMemo<ColumnDef<ScheduleRow>[]>(\n () => [\n {\n id: 'name',\n accessorKey: 'name',\n header: t('scheduler.field.name', 'Name'),\n cell: ({ row }) => (\n <span className=\"font-medium\">{row.original.name}</span>\n ),\n },\n {\n id: 'scheduleType',\n accessorKey: 'scheduleType',\n header: t('scheduler.field.schedule_type', 'Type'),\n cell: ({ row }) => (\n <span className=\"capitalize\">{row.original.scheduleType}</span>\n ),\n },\n {\n id: 'scheduleValue',\n accessorKey: 'scheduleValue',\n header: t('scheduler.field.schedule', 'Schedule'),\n cell: ({ row }) => (\n <span className=\"text-sm\">\n {row.original.scheduleValue}\n </span>\n ),\n },\n {\n id: 'targetType',\n accessorKey: 'targetType',\n header: t('scheduler.field.target', 'Target'),\n cell: ({ row }) => {\n const target = row.original.targetType === 'queue'\n ? row.original.targetQueue\n : row.original.targetCommand\n return (\n <div className=\"flex flex-col\">\n <span className=\"text-xs text-muted-foreground capitalize\">{row.original.targetType}</span>\n <span className=\"text-sm\">{target || '-'}</span>\n </div>\n )\n },\n },\n {\n id: 'nextRunAt',\n accessorKey: 'nextRunAt',\n header: t('scheduler.field.next_run', 'Next Run'),\n cell: ({ row }) => (\n <span className=\"text-sm\">\n {formatDateTime(row.original.nextRunAt) || '-'}\n </span>\n ),\n },\n {\n id: 'isEnabled',\n accessorKey: 'isEnabled',\n header: t('scheduler.field.active', 'Active'),\n cell: ({ row }) => <BooleanIcon value={row.original.isEnabled} />,\n },\n {\n id: 'sourceType',\n accessorKey: 'sourceType',\n header: t('scheduler.field.source', 'Source'),\n cell: ({ row }) => (\n <span className=\"text-xs capitalize\">\n {row.original.sourceType}\n {row.original.sourceModule && ` (${row.original.sourceModule})`}\n </span>\n ),\n },\n ],\n [t]\n )\n\n const rowActions = React.useCallback(\n (row: ScheduleRow): RowActionItem[] => [\n {\n id: 'view',\n label: t('scheduler.action.view', 'View Details'),\n onSelect: () => router.push(`/backend/config/scheduled-jobs/${row.id}`),\n },\n {\n id: 'edit',\n label: t('scheduler.action.edit', 'Edit'),\n onSelect: () => router.push(`/backend/config/scheduled-jobs/${row.id}/edit`),\n },\n {\n id: 'trigger',\n label: t('scheduler.action.trigger', 'Run Now'),\n onSelect: () => handleTrigger(row),\n },\n {\n id: 'delete',\n label: t('scheduler.action.delete', 'Delete'),\n onSelect: () => handleDelete(row),\n destructive: true,\n },\n ],\n [t, router, handleDelete, handleTrigger]\n )\n\n return (\n <Page>\n <PageBody>\n <DataTable<ScheduleRow>\n title={t('scheduler.title', 'Scheduled Jobs')}\n actions={\n <Button onClick={() => router.push('/backend/config/scheduled-jobs/new')}>\n {t('scheduler.action.create', 'New Schedule')}\n </Button>\n }\n columns={columns}\n data={rows}\n onRowClick={(row) => router.push(`/backend/config/scheduled-jobs/${row.id}`)}\n rowActions={(row) => <RowActions items={rowActions(row)} />}\n emptyState={(\n <ListEmptyState\n entityName={t('scheduler.title', 'Scheduled Jobs')}\n createHref=\"/backend/config/scheduled-jobs/new\"\n createLabel={t('scheduler.action.create', 'New Schedule')}\n />\n )}\n pagination={{ page, pageSize, total, totalPages, totalIsCapped, onPageChange: setPage }}\n isLoading={isLoading}\n searchValue={search}\n onSearchChange={(value) => { setSearch(value); setPage(1) }}\n searchPlaceholder={t('scheduler.search.placeholder', 'Search schedules...')}\n refreshButton={{\n label: t('scheduler.action.refresh', 'Refresh'),\n onRefresh: fetchSchedules,\n }}\n />\n {ConfirmDialogElement}\n </PageBody>\n </Page>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,7 @@
1
1
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
2
2
  import { ScheduledJob } from "./data/entities.js";
3
3
  import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
4
+ import { auditSchedulerModuleQueueRows } from "./lib/safeQueueTargets.js";
4
5
  const writeLine = (text = "") => {
5
6
  process.stdout.write(`${text}
6
7
  `);
@@ -182,7 +183,37 @@ const startCommand = {
182
183
  }
183
184
  }
184
185
  };
185
- var cli_default = [listCommand, statusCommand, runCommand, startCommand];
186
+ const auditQueueTargetsCommand = {
187
+ command: "audit-queue-targets",
188
+ async run(rest) {
189
+ const args = parseArgs(rest);
190
+ const { resolve } = await createRequestContainer();
191
+ const em = resolve("em");
192
+ const where = { deletedAt: null, sourceType: "module" };
193
+ if (args.tenant || args.tenantId) where.tenantId = args.tenant || args.tenantId;
194
+ const rows = await em.find(ScheduledJob, where, {
195
+ orderBy: { name: "ASC" }
196
+ });
197
+ const audits = auditSchedulerModuleQueueRows(rows);
198
+ writeLine(`Module-authored queue-target schedules: ${audits.length}`);
199
+ let allowedCount = 0;
200
+ for (const audit of audits) {
201
+ const verdict = audit.dispatchAllowed ? "ALLOWED" : "blocked";
202
+ if (audit.dispatchAllowed) allowedCount += 1;
203
+ writeLine(
204
+ `[${verdict}] ${audit.scheduleId}${audit.name ? ` \u2014 ${audit.name}` : ""} module=${audit.sourceModule ?? "(none)"} queue=${audit.targetQueue ?? "(none)"} enabled=${audit.isEnabled ? "yes" : "no"}`
205
+ );
206
+ }
207
+ if (allowedCount > 0) {
208
+ writeLine("");
209
+ writeLine(`Review the ${allowedCount} ALLOWED row(s) above against your known module integrations.`);
210
+ writeLine("Pre-upgrade rows created with a non-user-bound API key cannot be distinguished");
211
+ writeLine("from genuine registrations at rest; disable anything unrecognized:");
212
+ writeLine(" yarn mercato scheduler list --tenant <tenantId>");
213
+ }
214
+ }
215
+ };
216
+ var cli_default = [listCommand, statusCommand, runCommand, startCommand, auditQueueTargetsCommand];
186
217
  export {
187
218
  cli_default as default
188
219
  };