@open-mercato/scheduler 0.6.5-develop.4534.1.b459babe6d → 0.6.5-develop.4544.1.71c003c861

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.
@@ -62,7 +62,7 @@ function EditSchedulePage({ params }) {
62
62
  }, [params.id, t]);
63
63
  const formSchema = React.useMemo(() => scheduledJobFormSchema(t), [t]);
64
64
  const fields = React.useMemo(
65
- () => scheduledJobFields(t, { loadQueueOptions, loadCommandOptions, loadTimezoneOptions }),
65
+ () => scheduledJobFields(t, { loadQueueOptions, loadCommandOptions, loadTimezoneOptions }, { lockScope: true }),
66
66
  [t, loadQueueOptions, loadCommandOptions]
67
67
  );
68
68
  const groups = React.useMemo(() => scheduledJobGroups(t), [t]);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../../src/modules/scheduler/backend/config/scheduled-jobs/%5Bid%5D/edit/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 { CrudForm } from '@open-mercato/ui/backend/CrudForm'\nimport { updateCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport {\n type ScheduleFormValues,\n createTargetOptionsLoader,\n loadTimezoneOptions,\n scheduledJobFormSchema,\n scheduledJobFields,\n scheduledJobGroups,\n ScheduledJobEnabledSwitch,\n buildScheduledJobPayload,\n} from '../../../../../lib/scheduledJobFormConfig'\n\ntype ScheduleData = {\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 targetPayload?: Record<string, unknown> | null\n isEnabled: boolean\n}\n\nexport default function EditSchedulePage({ params }: { params: { id: string } }) {\n const t = useT()\n const router = useRouter()\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [initialData, setInitialData] = React.useState<Partial<ScheduleFormValues> | null>(null)\n const [isEnabled, setIsEnabled] = React.useState(false)\n\n const { loadQueueOptions, loadCommandOptions } = React.useMemo(\n () => createTargetOptionsLoader(apiCall),\n []\n )\n\n React.useEffect(() => {\n async function fetchSchedule() {\n try {\n const { result } = await apiCallOrThrow<{ items: ScheduleData[] }>(\n `/api/scheduler/jobs?id=${params.id}`\n )\n\n const schedule = result?.items?.[0]\n if (schedule) {\n setIsEnabled(schedule.isEnabled)\n\n setInitialData({\n name: schedule.name,\n description: schedule.description || undefined,\n scopeType: schedule.scopeType,\n scheduleType: schedule.scheduleType,\n scheduleValue: schedule.scheduleValue,\n timezone: schedule.timezone,\n targetType: schedule.targetType,\n targetQueue: schedule.targetQueue || undefined,\n targetCommand: schedule.targetCommand || undefined,\n targetPayload: (schedule.targetPayload && Object.keys(schedule.targetPayload).length > 0)\n ? schedule.targetPayload\n : undefined,\n isEnabled: schedule.isEnabled,\n })\n }\n } catch (err) {\n setError(t('scheduler.error.load_failed', 'Failed to load schedule'))\n } finally {\n setLoading(false)\n }\n }\n\n fetchSchedule()\n }, [params.id, t])\n\n const formSchema = React.useMemo(() => scheduledJobFormSchema(t), [t])\n\n const fields = React.useMemo(\n () => scheduledJobFields(t, { loadQueueOptions, loadCommandOptions, loadTimezoneOptions }),\n [t, loadQueueOptions, loadCommandOptions]\n )\n\n const groups = React.useMemo(() => scheduledJobGroups(t), [t])\n\n if (loading) {\n return (\n <Page>\n <PageBody>\n <LoadingMessage label={t('scheduler.loading', 'Loading schedule...')} />\n </PageBody>\n </Page>\n )\n }\n\n if (error || !initialData) {\n return (\n <Page>\n <PageBody>\n <ErrorMessage label={error || t('scheduler.error.not_found', 'Schedule not found')} />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <PageBody>\n <CrudForm<ScheduleFormValues>\n title={t('scheduler.edit.title', 'Edit Schedule')}\n backHref=\"/backend/config/scheduled-jobs\"\n fields={fields}\n groups={groups}\n initialValues={initialData}\n submitLabel={t('scheduler.form.save', 'Save Changes')}\n cancelHref=\"/backend/config/scheduled-jobs\"\n schema={formSchema}\n extraActions={\n <ScheduledJobEnabledSwitch isEnabled={isEnabled} setIsEnabled={setIsEnabled} t={t} />\n }\n onSubmit={async (values) => {\n const payload = buildScheduledJobPayload(values, isEnabled)\n\n await updateCrud(\n 'scheduler/jobs',\n { id: params.id, ...payload }\n )\n\n flash(t('scheduler.success.updated', 'Schedule updated successfully'), 'success')\n router.push('/backend/config/scheduled-jobs')\n }}\n />\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 { CrudForm } from '@open-mercato/ui/backend/CrudForm'\nimport { updateCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport {\n type ScheduleFormValues,\n createTargetOptionsLoader,\n loadTimezoneOptions,\n scheduledJobFormSchema,\n scheduledJobFields,\n scheduledJobGroups,\n ScheduledJobEnabledSwitch,\n buildScheduledJobPayload,\n} from '../../../../../lib/scheduledJobFormConfig'\n\ntype ScheduleData = {\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 targetPayload?: Record<string, unknown> | null\n isEnabled: boolean\n}\n\nexport default function EditSchedulePage({ params }: { params: { id: string } }) {\n const t = useT()\n const router = useRouter()\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [initialData, setInitialData] = React.useState<Partial<ScheduleFormValues> | null>(null)\n const [isEnabled, setIsEnabled] = React.useState(false)\n\n const { loadQueueOptions, loadCommandOptions } = React.useMemo(\n () => createTargetOptionsLoader(apiCall),\n []\n )\n\n React.useEffect(() => {\n async function fetchSchedule() {\n try {\n const { result } = await apiCallOrThrow<{ items: ScheduleData[] }>(\n `/api/scheduler/jobs?id=${params.id}`\n )\n\n const schedule = result?.items?.[0]\n if (schedule) {\n setIsEnabled(schedule.isEnabled)\n\n setInitialData({\n name: schedule.name,\n description: schedule.description || undefined,\n scopeType: schedule.scopeType,\n scheduleType: schedule.scheduleType,\n scheduleValue: schedule.scheduleValue,\n timezone: schedule.timezone,\n targetType: schedule.targetType,\n targetQueue: schedule.targetQueue || undefined,\n targetCommand: schedule.targetCommand || undefined,\n targetPayload: (schedule.targetPayload && Object.keys(schedule.targetPayload).length > 0)\n ? schedule.targetPayload\n : undefined,\n isEnabled: schedule.isEnabled,\n })\n }\n } catch (err) {\n setError(t('scheduler.error.load_failed', 'Failed to load schedule'))\n } finally {\n setLoading(false)\n }\n }\n\n fetchSchedule()\n }, [params.id, t])\n\n const formSchema = React.useMemo(() => scheduledJobFormSchema(t), [t])\n\n const fields = React.useMemo(\n () => scheduledJobFields(t, { loadQueueOptions, loadCommandOptions, loadTimezoneOptions }, { lockScope: true }),\n [t, loadQueueOptions, loadCommandOptions]\n )\n\n const groups = React.useMemo(() => scheduledJobGroups(t), [t])\n\n if (loading) {\n return (\n <Page>\n <PageBody>\n <LoadingMessage label={t('scheduler.loading', 'Loading schedule...')} />\n </PageBody>\n </Page>\n )\n }\n\n if (error || !initialData) {\n return (\n <Page>\n <PageBody>\n <ErrorMessage label={error || t('scheduler.error.not_found', 'Schedule not found')} />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <PageBody>\n <CrudForm<ScheduleFormValues>\n title={t('scheduler.edit.title', 'Edit Schedule')}\n backHref=\"/backend/config/scheduled-jobs\"\n fields={fields}\n groups={groups}\n initialValues={initialData}\n submitLabel={t('scheduler.form.save', 'Save Changes')}\n cancelHref=\"/backend/config/scheduled-jobs\"\n schema={formSchema}\n extraActions={\n <ScheduledJobEnabledSwitch isEnabled={isEnabled} setIsEnabled={setIsEnabled} t={t} />\n }\n onSubmit={async (values) => {\n const payload = buildScheduledJobPayload(values, isEnabled)\n\n await updateCrud(\n 'scheduler/jobs',\n { id: params.id, ...payload }\n )\n\n flash(t('scheduler.success.updated', 'Schedule updated successfully'), 'success')\n router.push('/backend/config/scheduled-jobs')\n }}\n />\n </PageBody>\n </Page>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -99,6 +99,7 @@
99
99
  "scheduler.form.schedule_value.description": "Für Cron: verwenden Sie Cron-Ausdruck (z.B. \"0 0 * * *\"). Für Intervall: verwenden Sie Format wie \"15m\", \"2h\", \"1d\" (s=Sekunden, m=Minuten, h=Stunden, d=Tage)",
100
100
  "scheduler.form.schedule_value.placeholder": "z.B. 0 */6 * * * oder 15m",
101
101
  "scheduler.form.scope_type": "Bereich",
102
+ "scheduler.form.scope_type.locked_description": "Der Bereich wird bei der Erstellung des Zeitplans festgelegt und kann danach nicht mehr geändert werden.",
102
103
  "scheduler.form.submit": "Zeitplan erstellen",
103
104
  "scheduler.form.target_command": "Zielbefehl",
104
105
  "scheduler.form.target_command.placeholder": "Befehle suchen...",
@@ -99,6 +99,7 @@
99
99
  "scheduler.form.schedule_value.description": "For cron: use cron expression (e.g., \"0 0 * * *\"). For interval: use format like \"15m\", \"2h\", \"1d\" (s=seconds, m=minutes, h=hours, d=days)",
100
100
  "scheduler.form.schedule_value.placeholder": "e.g. 0 */6 * * * or 15m",
101
101
  "scheduler.form.scope_type": "Scope",
102
+ "scheduler.form.scope_type.locked_description": "Scope is set when the schedule is created and cannot be changed afterwards.",
102
103
  "scheduler.form.submit": "Create Schedule",
103
104
  "scheduler.form.target_command": "Target Command",
104
105
  "scheduler.form.target_command.placeholder": "Search commands...",
@@ -99,6 +99,7 @@
99
99
  "scheduler.form.schedule_value.description": "Para cron: use expresión cron (ej. \"0 0 * * *\"). Para intervalo: use formato como \"15m\", \"2h\", \"1d\" (s=segundos, m=minutos, h=horas, d=días)",
100
100
  "scheduler.form.schedule_value.placeholder": "ej. 0 */6 * * * o 15m",
101
101
  "scheduler.form.scope_type": "Ámbito",
102
+ "scheduler.form.scope_type.locked_description": "El ámbito se establece al crear la programación y no se puede cambiar posteriormente.",
102
103
  "scheduler.form.submit": "Crear programación",
103
104
  "scheduler.form.target_command": "Comando de destino",
104
105
  "scheduler.form.target_command.placeholder": "Buscar comandos...",
@@ -99,6 +99,7 @@
99
99
  "scheduler.form.schedule_value.description": "Dla cron: użyj wyrażenia cron (np. \"0 0 * * *\"). Dla interwału: użyj formatu jak \"15m\", \"2h\", \"1d\" (s=sekundy, m=minuty, h=godziny, d=dni)",
100
100
  "scheduler.form.schedule_value.placeholder": "np. 0 */6 * * * lub 15m",
101
101
  "scheduler.form.scope_type": "Zakres",
102
+ "scheduler.form.scope_type.locked_description": "Zakres jest ustalany podczas tworzenia harmonogramu i nie można go później zmienić.",
102
103
  "scheduler.form.submit": "Utwórz harmonogram",
103
104
  "scheduler.form.target_command": "Polecenie docelowe",
104
105
  "scheduler.form.target_command.placeholder": "Szukaj poleceń...",
@@ -85,7 +85,8 @@ function scheduledJobFormSchema(t) {
85
85
  isEnabled: z.boolean()
86
86
  });
87
87
  }
88
- function scheduledJobFields(t, loaders) {
88
+ function scheduledJobFields(t, loaders, options) {
89
+ const lockScope = options?.lockScope ?? false;
89
90
  return [
90
91
  {
91
92
  id: "name",
@@ -103,6 +104,12 @@ function scheduledJobFields(t, loaders) {
103
104
  type: "select",
104
105
  label: t("scheduler.form.scope_type", "Scope"),
105
106
  required: true,
107
+ // Scope is derived from the creator's auth context at create time and is
108
+ // immutable afterwards (the update schema/command never persist it). Lock
109
+ // the field on edit so it does not deceptively accept input that is then
110
+ // silently dropped on save.
111
+ disabled: lockScope,
112
+ description: lockScope ? t("scheduler.form.scope_type.locked_description", "Scope is set when the schedule is created and cannot be changed afterwards.") : void 0,
106
113
  options: [
107
114
  { value: "system", label: t("scheduler.scope.system", "System") },
108
115
  { value: "organization", label: t("scheduler.scope.organization", "Organization") },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/scheduler/lib/scheduledJobFormConfig.tsx"],
4
- "sourcesContent": ["import * as React from 'react'\nimport { z } from 'zod'\nimport { ComboboxInput, type ComboboxOption } from '@open-mercato/ui/backend/inputs/ComboboxInput'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { JsonBuilder } from '@open-mercato/ui/backend/JsonBuilder'\nimport type { CrudField, CrudFormGroup, CrudCustomFieldRenderProps } from '@open-mercato/ui/backend/CrudForm'\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type ScheduleFormValues = {\n name: string\n description?: string\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}\n\nexport type TargetOptions = {\n queues: ComboboxOption[]\n commands: ComboboxOption[]\n}\n\n// ---------------------------------------------------------------------------\n// Components\n// ---------------------------------------------------------------------------\n\nexport function PayloadJsonEditor({ value, setValue, disabled }: CrudCustomFieldRenderProps) {\n return (\n <JsonBuilder\n value={value || {}}\n onChange={setValue}\n disabled={disabled}\n />\n )\n}\n\nexport function ScheduledJobEnabledSwitch({\n isEnabled,\n setIsEnabled,\n t,\n}: {\n isEnabled: boolean\n setIsEnabled: (value: boolean) => void\n t: (key: string, fallback: string) => string\n}) {\n return (\n <div className=\"flex items-center gap-2\">\n <Label htmlFor=\"enabled-switch\" className=\"text-sm font-medium cursor-pointer\">\n {isEnabled ? t('scheduler.form.enabled', 'Enabled') : t('scheduler.form.disabled', 'Disabled')}\n </Label>\n <Switch\n id=\"enabled-switch\"\n checked={isEnabled}\n onCheckedChange={setIsEnabled}\n />\n </div>\n )\n}\n\n// ---------------------------------------------------------------------------\n// Option loaders\n// ---------------------------------------------------------------------------\n\nexport function createTargetOptionsLoader(\n apiCallFn: <T>(url: string, init?: RequestInit, options?: Record<string, unknown>) => Promise<{ result: T | null }>\n) {\n const targetOptionsRef = { current: null as TargetOptions | null }\n\n async function loadTargetOptions(): Promise<TargetOptions> {\n if (targetOptionsRef.current) return targetOptionsRef.current\n try {\n const { result } = await apiCallFn<TargetOptions>('/api/scheduler/targets')\n const options = result ?? { queues: [], commands: [] }\n targetOptionsRef.current = options\n return options\n } catch {\n return { queues: [], commands: [] }\n }\n }\n\n async function loadQueueOptions(query?: string): Promise<ComboboxOption[]> {\n const options = await loadTargetOptions()\n if (!query) return options.queues\n const lower = query.toLowerCase()\n return options.queues.filter((q) => q.label.toLowerCase().includes(lower))\n }\n\n async function loadCommandOptions(query?: string): Promise<ComboboxOption[]> {\n const options = await loadTargetOptions()\n if (!query) return options.commands\n const lower = query.toLowerCase()\n return options.commands.filter((c) => c.label.toLowerCase().includes(lower))\n }\n\n return { loadTargetOptions, loadQueueOptions, loadCommandOptions, targetOptionsRef }\n}\n\nexport async function loadTimezoneOptions(query?: string): Promise<ComboboxOption[]> {\n try {\n const allTz = Array.from(new Set(['UTC', ...Intl.supportedValuesOf('timeZone')]))\n const filtered = query\n ? allTz.filter((tz) => tz.toLowerCase().includes(query.toLowerCase()))\n : allTz\n return filtered.slice(0, 100).map((tz) => ({\n value: tz,\n label: tz,\n }))\n } catch {\n return [{ value: 'UTC', label: 'UTC' }]\n }\n}\n\n// ---------------------------------------------------------------------------\n// Schema\n// ---------------------------------------------------------------------------\n\nexport function scheduledJobFormSchema(t: (key: string, fallback: string) => string) {\n return z.object({\n name: z.string().min(1, t('scheduler.form.name.required', 'Name is required')),\n description: z.string().optional(),\n scopeType: z.enum(['system', 'organization', 'tenant']),\n scheduleType: z.enum(['cron', 'interval']),\n scheduleValue: z.string().min(1, t('scheduler.form.schedule.required', 'Schedule is required')),\n timezone: z.string(),\n targetType: z.enum(['queue', 'command']),\n targetQueue: z.string().optional(),\n targetCommand: z.string().optional(),\n targetPayload: z.record(z.string(), z.unknown()).optional(),\n isEnabled: z.boolean(),\n })\n}\n\n// ---------------------------------------------------------------------------\n// Fields\n// ---------------------------------------------------------------------------\n\nexport function scheduledJobFields(\n t: (key: string, fallback: string) => string,\n loaders: {\n loadQueueOptions: (query?: string) => Promise<ComboboxOption[]>\n loadCommandOptions: (query?: string) => Promise<ComboboxOption[]>\n loadTimezoneOptions: (query?: string) => Promise<ComboboxOption[]>\n }\n): CrudField[] {\n return [\n {\n id: 'name',\n type: 'text',\n label: t('scheduler.form.name', 'Name'),\n required: true,\n },\n {\n id: 'description',\n type: 'textarea',\n label: t('scheduler.form.description', 'Description'),\n },\n {\n id: 'scopeType',\n type: 'select',\n label: t('scheduler.form.scope_type', 'Scope'),\n required: true,\n options: [\n { value: 'system', label: t('scheduler.scope.system', 'System') },\n { value: 'organization', label: t('scheduler.scope.organization', 'Organization') },\n { value: 'tenant', label: t('scheduler.scope.tenant', 'Tenant') },\n ],\n },\n {\n id: 'scheduleType',\n type: 'select',\n label: t('scheduler.form.schedule_type', 'Schedule Type'),\n required: true,\n options: [\n { value: 'cron', label: t('scheduler.type.cron', 'Cron Expression') },\n { value: 'interval', label: t('scheduler.type.interval', 'Simple Interval') },\n ],\n },\n {\n id: 'scheduleValue',\n type: 'text',\n label: t('scheduler.form.schedule_value', 'Schedule Value'),\n placeholder: t('scheduler.form.schedule_value.placeholder', 'e.g. 0 */6 * * * or 15m'),\n description: t('scheduler.form.schedule_value.description', 'For cron: use cron expression (e.g., \"0 0 * * *\"). For interval: use format like \"15m\", \"2h\", \"1d\" (s=seconds, m=minutes, h=hours, d=days)'),\n required: true,\n },\n {\n id: 'timezone',\n type: 'combobox',\n label: t('scheduler.form.timezone', 'Timezone'),\n placeholder: t('scheduler.form.timezone.placeholder', 'Search timezone...'),\n required: true,\n loadOptions: loaders.loadTimezoneOptions,\n allowCustomValues: false,\n },\n {\n id: 'targetType',\n type: 'select',\n label: t('scheduler.form.target_type', 'Target Type'),\n required: true,\n options: [\n { value: 'queue', label: t('scheduler.target.queue', 'Queue') },\n { value: 'command', label: t('scheduler.target.command', 'Command') },\n ],\n },\n {\n id: 'targetFields',\n type: 'custom',\n label: '',\n component: ({ values, setFormValue }) => {\n const targetType = values?.targetType as string | undefined\n const targetQueue = (values?.targetQueue as string) || ''\n const targetCommand = (values?.targetCommand as string) || ''\n\n return (\n <div className=\"space-y-4\">\n {targetType === 'queue' && (\n <div className=\"space-y-1\">\n <Label htmlFor=\"targetQueue\">\n {t('scheduler.form.target_queue', 'Target Queue')}\n <span className=\"text-status-error-icon ml-0.5\" aria-hidden=\"true\">*</span>\n </Label>\n <ComboboxInput\n value={targetQueue}\n onChange={(next) => setFormValue && setFormValue('targetQueue', next)}\n placeholder={t('scheduler.form.target_queue.placeholder', 'Search queues...')}\n loadSuggestions={loaders.loadQueueOptions}\n allowCustomValues={true}\n />\n </div>\n )}\n {targetType === 'command' && (\n <div className=\"space-y-1\">\n <Label htmlFor=\"targetCommand\">\n {t('scheduler.form.target_command', 'Target Command')}\n <span className=\"text-status-error-icon ml-0.5\" aria-hidden=\"true\">*</span>\n </Label>\n <ComboboxInput\n value={targetCommand}\n onChange={(next) => setFormValue && setFormValue('targetCommand', next)}\n placeholder={t('scheduler.form.target_command.placeholder', 'Search commands...')}\n loadSuggestions={loaders.loadCommandOptions}\n allowCustomValues={false}\n />\n </div>\n )}\n </div>\n )\n },\n },\n {\n id: 'targetPayload',\n type: 'custom',\n label: t('scheduler.form.target_payload', 'Job Arguments (JSON)'),\n description: t('scheduler.form.target_payload.description', 'Optional JSON payload. Fields tenantId and organizationId are injected automatically at execution time.'),\n component: (props) => <PayloadJsonEditor {...props} />,\n },\n ]\n}\n\n// ---------------------------------------------------------------------------\n// Groups\n// ---------------------------------------------------------------------------\n\nexport function scheduledJobGroups(t: (key: string, fallback: string) => string): CrudFormGroup[] {\n return [\n {\n id: 'basic',\n title: t('scheduler.form.group.basic', 'Basic Information'),\n fields: ['name', 'description', 'scopeType'],\n },\n {\n id: 'schedule',\n title: t('scheduler.form.group.schedule', 'Schedule Configuration'),\n fields: ['scheduleType', 'scheduleValue', 'timezone'],\n },\n {\n id: 'target',\n title: t('scheduler.form.group.target', 'Target Configuration'),\n fields: ['targetType', 'targetFields', 'targetPayload'],\n },\n ]\n}\n\n// ---------------------------------------------------------------------------\n// Payload builder\n// ---------------------------------------------------------------------------\n\nexport function buildScheduledJobPayload(\n values: ScheduleFormValues,\n isEnabled: boolean\n): Record<string, unknown> {\n const targetPayload = values.targetPayload && Object.keys(values.targetPayload).length > 0\n ? values.targetPayload\n : null\n\n return {\n ...values,\n targetPayload,\n isEnabled,\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import * as React from 'react'\nimport { z } from 'zod'\nimport { ComboboxInput, type ComboboxOption } from '@open-mercato/ui/backend/inputs/ComboboxInput'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { JsonBuilder } from '@open-mercato/ui/backend/JsonBuilder'\nimport type { CrudField, CrudFormGroup, CrudCustomFieldRenderProps } from '@open-mercato/ui/backend/CrudForm'\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type ScheduleFormValues = {\n name: string\n description?: string\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}\n\nexport type TargetOptions = {\n queues: ComboboxOption[]\n commands: ComboboxOption[]\n}\n\n// ---------------------------------------------------------------------------\n// Components\n// ---------------------------------------------------------------------------\n\nexport function PayloadJsonEditor({ value, setValue, disabled }: CrudCustomFieldRenderProps) {\n return (\n <JsonBuilder\n value={value || {}}\n onChange={setValue}\n disabled={disabled}\n />\n )\n}\n\nexport function ScheduledJobEnabledSwitch({\n isEnabled,\n setIsEnabled,\n t,\n}: {\n isEnabled: boolean\n setIsEnabled: (value: boolean) => void\n t: (key: string, fallback: string) => string\n}) {\n return (\n <div className=\"flex items-center gap-2\">\n <Label htmlFor=\"enabled-switch\" className=\"text-sm font-medium cursor-pointer\">\n {isEnabled ? t('scheduler.form.enabled', 'Enabled') : t('scheduler.form.disabled', 'Disabled')}\n </Label>\n <Switch\n id=\"enabled-switch\"\n checked={isEnabled}\n onCheckedChange={setIsEnabled}\n />\n </div>\n )\n}\n\n// ---------------------------------------------------------------------------\n// Option loaders\n// ---------------------------------------------------------------------------\n\nexport function createTargetOptionsLoader(\n apiCallFn: <T>(url: string, init?: RequestInit, options?: Record<string, unknown>) => Promise<{ result: T | null }>\n) {\n const targetOptionsRef = { current: null as TargetOptions | null }\n\n async function loadTargetOptions(): Promise<TargetOptions> {\n if (targetOptionsRef.current) return targetOptionsRef.current\n try {\n const { result } = await apiCallFn<TargetOptions>('/api/scheduler/targets')\n const options = result ?? { queues: [], commands: [] }\n targetOptionsRef.current = options\n return options\n } catch {\n return { queues: [], commands: [] }\n }\n }\n\n async function loadQueueOptions(query?: string): Promise<ComboboxOption[]> {\n const options = await loadTargetOptions()\n if (!query) return options.queues\n const lower = query.toLowerCase()\n return options.queues.filter((q) => q.label.toLowerCase().includes(lower))\n }\n\n async function loadCommandOptions(query?: string): Promise<ComboboxOption[]> {\n const options = await loadTargetOptions()\n if (!query) return options.commands\n const lower = query.toLowerCase()\n return options.commands.filter((c) => c.label.toLowerCase().includes(lower))\n }\n\n return { loadTargetOptions, loadQueueOptions, loadCommandOptions, targetOptionsRef }\n}\n\nexport async function loadTimezoneOptions(query?: string): Promise<ComboboxOption[]> {\n try {\n const allTz = Array.from(new Set(['UTC', ...Intl.supportedValuesOf('timeZone')]))\n const filtered = query\n ? allTz.filter((tz) => tz.toLowerCase().includes(query.toLowerCase()))\n : allTz\n return filtered.slice(0, 100).map((tz) => ({\n value: tz,\n label: tz,\n }))\n } catch {\n return [{ value: 'UTC', label: 'UTC' }]\n }\n}\n\n// ---------------------------------------------------------------------------\n// Schema\n// ---------------------------------------------------------------------------\n\nexport function scheduledJobFormSchema(t: (key: string, fallback: string) => string) {\n return z.object({\n name: z.string().min(1, t('scheduler.form.name.required', 'Name is required')),\n description: z.string().optional(),\n scopeType: z.enum(['system', 'organization', 'tenant']),\n scheduleType: z.enum(['cron', 'interval']),\n scheduleValue: z.string().min(1, t('scheduler.form.schedule.required', 'Schedule is required')),\n timezone: z.string(),\n targetType: z.enum(['queue', 'command']),\n targetQueue: z.string().optional(),\n targetCommand: z.string().optional(),\n targetPayload: z.record(z.string(), z.unknown()).optional(),\n isEnabled: z.boolean(),\n })\n}\n\n// ---------------------------------------------------------------------------\n// Fields\n// ---------------------------------------------------------------------------\n\nexport function scheduledJobFields(\n t: (key: string, fallback: string) => string,\n loaders: {\n loadQueueOptions: (query?: string) => Promise<ComboboxOption[]>\n loadCommandOptions: (query?: string) => Promise<ComboboxOption[]>\n loadTimezoneOptions: (query?: string) => Promise<ComboboxOption[]>\n },\n options?: { lockScope?: boolean }\n): CrudField[] {\n const lockScope = options?.lockScope ?? false\n return [\n {\n id: 'name',\n type: 'text',\n label: t('scheduler.form.name', 'Name'),\n required: true,\n },\n {\n id: 'description',\n type: 'textarea',\n label: t('scheduler.form.description', 'Description'),\n },\n {\n id: 'scopeType',\n type: 'select',\n label: t('scheduler.form.scope_type', 'Scope'),\n required: true,\n // Scope is derived from the creator's auth context at create time and is\n // immutable afterwards (the update schema/command never persist it). Lock\n // the field on edit so it does not deceptively accept input that is then\n // silently dropped on save.\n disabled: lockScope,\n description: lockScope\n ? t('scheduler.form.scope_type.locked_description', 'Scope is set when the schedule is created and cannot be changed afterwards.')\n : undefined,\n options: [\n { value: 'system', label: t('scheduler.scope.system', 'System') },\n { value: 'organization', label: t('scheduler.scope.organization', 'Organization') },\n { value: 'tenant', label: t('scheduler.scope.tenant', 'Tenant') },\n ],\n },\n {\n id: 'scheduleType',\n type: 'select',\n label: t('scheduler.form.schedule_type', 'Schedule Type'),\n required: true,\n options: [\n { value: 'cron', label: t('scheduler.type.cron', 'Cron Expression') },\n { value: 'interval', label: t('scheduler.type.interval', 'Simple Interval') },\n ],\n },\n {\n id: 'scheduleValue',\n type: 'text',\n label: t('scheduler.form.schedule_value', 'Schedule Value'),\n placeholder: t('scheduler.form.schedule_value.placeholder', 'e.g. 0 */6 * * * or 15m'),\n description: t('scheduler.form.schedule_value.description', 'For cron: use cron expression (e.g., \"0 0 * * *\"). For interval: use format like \"15m\", \"2h\", \"1d\" (s=seconds, m=minutes, h=hours, d=days)'),\n required: true,\n },\n {\n id: 'timezone',\n type: 'combobox',\n label: t('scheduler.form.timezone', 'Timezone'),\n placeholder: t('scheduler.form.timezone.placeholder', 'Search timezone...'),\n required: true,\n loadOptions: loaders.loadTimezoneOptions,\n allowCustomValues: false,\n },\n {\n id: 'targetType',\n type: 'select',\n label: t('scheduler.form.target_type', 'Target Type'),\n required: true,\n options: [\n { value: 'queue', label: t('scheduler.target.queue', 'Queue') },\n { value: 'command', label: t('scheduler.target.command', 'Command') },\n ],\n },\n {\n id: 'targetFields',\n type: 'custom',\n label: '',\n component: ({ values, setFormValue }) => {\n const targetType = values?.targetType as string | undefined\n const targetQueue = (values?.targetQueue as string) || ''\n const targetCommand = (values?.targetCommand as string) || ''\n\n return (\n <div className=\"space-y-4\">\n {targetType === 'queue' && (\n <div className=\"space-y-1\">\n <Label htmlFor=\"targetQueue\">\n {t('scheduler.form.target_queue', 'Target Queue')}\n <span className=\"text-status-error-icon ml-0.5\" aria-hidden=\"true\">*</span>\n </Label>\n <ComboboxInput\n value={targetQueue}\n onChange={(next) => setFormValue && setFormValue('targetQueue', next)}\n placeholder={t('scheduler.form.target_queue.placeholder', 'Search queues...')}\n loadSuggestions={loaders.loadQueueOptions}\n allowCustomValues={true}\n />\n </div>\n )}\n {targetType === 'command' && (\n <div className=\"space-y-1\">\n <Label htmlFor=\"targetCommand\">\n {t('scheduler.form.target_command', 'Target Command')}\n <span className=\"text-status-error-icon ml-0.5\" aria-hidden=\"true\">*</span>\n </Label>\n <ComboboxInput\n value={targetCommand}\n onChange={(next) => setFormValue && setFormValue('targetCommand', next)}\n placeholder={t('scheduler.form.target_command.placeholder', 'Search commands...')}\n loadSuggestions={loaders.loadCommandOptions}\n allowCustomValues={false}\n />\n </div>\n )}\n </div>\n )\n },\n },\n {\n id: 'targetPayload',\n type: 'custom',\n label: t('scheduler.form.target_payload', 'Job Arguments (JSON)'),\n description: t('scheduler.form.target_payload.description', 'Optional JSON payload. Fields tenantId and organizationId are injected automatically at execution time.'),\n component: (props) => <PayloadJsonEditor {...props} />,\n },\n ]\n}\n\n// ---------------------------------------------------------------------------\n// Groups\n// ---------------------------------------------------------------------------\n\nexport function scheduledJobGroups(t: (key: string, fallback: string) => string): CrudFormGroup[] {\n return [\n {\n id: 'basic',\n title: t('scheduler.form.group.basic', 'Basic Information'),\n fields: ['name', 'description', 'scopeType'],\n },\n {\n id: 'schedule',\n title: t('scheduler.form.group.schedule', 'Schedule Configuration'),\n fields: ['scheduleType', 'scheduleValue', 'timezone'],\n },\n {\n id: 'target',\n title: t('scheduler.form.group.target', 'Target Configuration'),\n fields: ['targetType', 'targetFields', 'targetPayload'],\n },\n ]\n}\n\n// ---------------------------------------------------------------------------\n// Payload builder\n// ---------------------------------------------------------------------------\n\nexport function buildScheduledJobPayload(\n values: ScheduleFormValues,\n isEnabled: boolean\n): Record<string, unknown> {\n const targetPayload = values.targetPayload && Object.keys(values.targetPayload).length > 0\n ? values.targetPayload\n : null\n\n return {\n ...values,\n targetPayload,\n isEnabled,\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/scheduler",
3
- "version": "0.6.5-develop.4534.1.b459babe6d",
3
+ "version": "0.6.5-develop.4544.1.71c003c861",
4
4
  "description": "Database-managed scheduled jobs with admin UI",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -58,13 +58,13 @@
58
58
  "./*/*/*/*/*/*.json": "./src/*/*/*/*/*/*.json"
59
59
  },
60
60
  "dependencies": {
61
- "@open-mercato/events": "0.6.5-develop.4534.1.b459babe6d",
62
- "@open-mercato/queue": "0.6.5-develop.4534.1.b459babe6d",
61
+ "@open-mercato/events": "0.6.5-develop.4544.1.71c003c861",
62
+ "@open-mercato/queue": "0.6.5-develop.4544.1.71c003c861",
63
63
  "cron-parser": "^5.5.0"
64
64
  },
65
65
  "peerDependencies": {
66
66
  "@mikro-orm/core": "^7.0.14",
67
- "@open-mercato/shared": "0.6.5-develop.4534.1.b459babe6d",
67
+ "@open-mercato/shared": "0.6.5-develop.4544.1.71c003c861",
68
68
  "bullmq": "^5.0.0",
69
69
  "ioredis": "^5.0.0",
70
70
  "zod": ">=3.23.0"
@@ -78,7 +78,7 @@
78
78
  }
79
79
  },
80
80
  "devDependencies": {
81
- "@open-mercato/shared": "0.6.5-develop.4534.1.b459babe6d",
81
+ "@open-mercato/shared": "0.6.5-develop.4544.1.71c003c861",
82
82
  "@types/jest": "^30.0.0",
83
83
  "@types/node": "^25.9.1",
84
84
  "jest": "^30.4.2",
@@ -88,7 +88,7 @@ export default function EditSchedulePage({ params }: { params: { id: string } })
88
88
  const formSchema = React.useMemo(() => scheduledJobFormSchema(t), [t])
89
89
 
90
90
  const fields = React.useMemo(
91
- () => scheduledJobFields(t, { loadQueueOptions, loadCommandOptions, loadTimezoneOptions }),
91
+ () => scheduledJobFields(t, { loadQueueOptions, loadCommandOptions, loadTimezoneOptions }, { lockScope: true }),
92
92
  [t, loadQueueOptions, loadCommandOptions]
93
93
  )
94
94
 
@@ -506,6 +506,23 @@ describe('scheduleUpdateSchema', () => {
506
506
 
507
507
  expect(result.targetPayload).toBeNull()
508
508
  })
509
+
510
+ it('should strip scope fields so scope stays immutable after creation', () => {
511
+ const result = scheduleUpdateSchema.parse({
512
+ id: scheduleId,
513
+ name: 'Updated name',
514
+ scopeType: 'organization',
515
+ organizationId,
516
+ tenantId,
517
+ })
518
+
519
+ // Scope is derived from the creator's auth context at create time and must
520
+ // not be mutable via update. The edit form locks the Scope field to mirror
521
+ // this contract (see scheduledJobFormConfig lockScope).
522
+ expect((result as Record<string, unknown>).scopeType).toBeUndefined()
523
+ expect((result as Record<string, unknown>).organizationId).toBeUndefined()
524
+ expect((result as Record<string, unknown>).tenantId).toBeUndefined()
525
+ })
509
526
  })
510
527
 
511
528
  describe('scheduleDeleteSchema', () => {
@@ -99,6 +99,7 @@
99
99
  "scheduler.form.schedule_value.description": "Für Cron: verwenden Sie Cron-Ausdruck (z.B. \"0 0 * * *\"). Für Intervall: verwenden Sie Format wie \"15m\", \"2h\", \"1d\" (s=Sekunden, m=Minuten, h=Stunden, d=Tage)",
100
100
  "scheduler.form.schedule_value.placeholder": "z.B. 0 */6 * * * oder 15m",
101
101
  "scheduler.form.scope_type": "Bereich",
102
+ "scheduler.form.scope_type.locked_description": "Der Bereich wird bei der Erstellung des Zeitplans festgelegt und kann danach nicht mehr geändert werden.",
102
103
  "scheduler.form.submit": "Zeitplan erstellen",
103
104
  "scheduler.form.target_command": "Zielbefehl",
104
105
  "scheduler.form.target_command.placeholder": "Befehle suchen...",
@@ -99,6 +99,7 @@
99
99
  "scheduler.form.schedule_value.description": "For cron: use cron expression (e.g., \"0 0 * * *\"). For interval: use format like \"15m\", \"2h\", \"1d\" (s=seconds, m=minutes, h=hours, d=days)",
100
100
  "scheduler.form.schedule_value.placeholder": "e.g. 0 */6 * * * or 15m",
101
101
  "scheduler.form.scope_type": "Scope",
102
+ "scheduler.form.scope_type.locked_description": "Scope is set when the schedule is created and cannot be changed afterwards.",
102
103
  "scheduler.form.submit": "Create Schedule",
103
104
  "scheduler.form.target_command": "Target Command",
104
105
  "scheduler.form.target_command.placeholder": "Search commands...",
@@ -99,6 +99,7 @@
99
99
  "scheduler.form.schedule_value.description": "Para cron: use expresión cron (ej. \"0 0 * * *\"). Para intervalo: use formato como \"15m\", \"2h\", \"1d\" (s=segundos, m=minutos, h=horas, d=días)",
100
100
  "scheduler.form.schedule_value.placeholder": "ej. 0 */6 * * * o 15m",
101
101
  "scheduler.form.scope_type": "Ámbito",
102
+ "scheduler.form.scope_type.locked_description": "El ámbito se establece al crear la programación y no se puede cambiar posteriormente.",
102
103
  "scheduler.form.submit": "Crear programación",
103
104
  "scheduler.form.target_command": "Comando de destino",
104
105
  "scheduler.form.target_command.placeholder": "Buscar comandos...",
@@ -99,6 +99,7 @@
99
99
  "scheduler.form.schedule_value.description": "Dla cron: użyj wyrażenia cron (np. \"0 0 * * *\"). Dla interwału: użyj formatu jak \"15m\", \"2h\", \"1d\" (s=sekundy, m=minuty, h=godziny, d=dni)",
100
100
  "scheduler.form.schedule_value.placeholder": "np. 0 */6 * * * lub 15m",
101
101
  "scheduler.form.scope_type": "Zakres",
102
+ "scheduler.form.scope_type.locked_description": "Zakres jest ustalany podczas tworzenia harmonogramu i nie można go później zmienić.",
102
103
  "scheduler.form.submit": "Utwórz harmonogram",
103
104
  "scheduler.form.target_command": "Polecenie docelowe",
104
105
  "scheduler.form.target_command.placeholder": "Szukaj poleceń...",
@@ -83,6 +83,30 @@ describe('scheduledJobFormConfig target fields', () => {
83
83
  })
84
84
  })
85
85
 
86
+ describe('scheduledJobFormConfig scope field', () => {
87
+ const t = (_key: string, fallback: string) => fallback
88
+ const loaders = {
89
+ loadQueueOptions: async () => [],
90
+ loadCommandOptions: async () => [],
91
+ loadTimezoneOptions: async () => [],
92
+ }
93
+
94
+ it('keeps the scope field editable on create (no lockScope)', () => {
95
+ const fields = scheduledJobFields(t, loaders)
96
+ const scope = fields.find((f) => f.id === 'scopeType')
97
+ expect(scope).toBeDefined()
98
+ expect(scope?.disabled).toBeFalsy()
99
+ })
100
+
101
+ it('locks the scope field on edit so the value cannot be deceptively changed then silently dropped', () => {
102
+ const fields = scheduledJobFields(t, loaders, { lockScope: true })
103
+ const scope = fields.find((f) => f.id === 'scopeType')
104
+ expect(scope).toBeDefined()
105
+ expect(scope?.disabled).toBe(true)
106
+ expect(scope?.description).toBeTruthy()
107
+ })
108
+ })
109
+
86
110
  describe('loadTimezoneOptions', () => {
87
111
  it('includes UTC even when Intl.supportedValuesOf does not list it', async () => {
88
112
  const options = await loadTimezoneOptions('utc')
@@ -149,8 +149,10 @@ export function scheduledJobFields(
149
149
  loadQueueOptions: (query?: string) => Promise<ComboboxOption[]>
150
150
  loadCommandOptions: (query?: string) => Promise<ComboboxOption[]>
151
151
  loadTimezoneOptions: (query?: string) => Promise<ComboboxOption[]>
152
- }
152
+ },
153
+ options?: { lockScope?: boolean }
153
154
  ): CrudField[] {
155
+ const lockScope = options?.lockScope ?? false
154
156
  return [
155
157
  {
156
158
  id: 'name',
@@ -168,6 +170,14 @@ export function scheduledJobFields(
168
170
  type: 'select',
169
171
  label: t('scheduler.form.scope_type', 'Scope'),
170
172
  required: true,
173
+ // Scope is derived from the creator's auth context at create time and is
174
+ // immutable afterwards (the update schema/command never persist it). Lock
175
+ // the field on edit so it does not deceptively accept input that is then
176
+ // silently dropped on save.
177
+ disabled: lockScope,
178
+ description: lockScope
179
+ ? t('scheduler.form.scope_type.locked_description', 'Scope is set when the schedule is created and cannot be changed afterwards.')
180
+ : undefined,
171
181
  options: [
172
182
  { value: 'system', label: t('scheduler.scope.system', 'System') },
173
183
  { value: 'organization', label: t('scheduler.scope.organization', 'Organization') },