* feat(scheduler): schedule container restart, stop, and start
Add container as a scheduled-task target type so operators can automate lifecycle actions against standalone containers by node and name, with matching UI pickers, validation, execution on local and remote nodes, and tests.
* fix(scheduler): stack service matching and container picker hygiene
Backfill Service on smartFallback containers so per-service stack restarts work when container_name is set. Match services by compose label and container name in stack routes and scheduled restarts. Exclude Sencho from GET /api/containers lists. Hide the Restart Stack service picker when a stack has only one service.
* test(scheduler): scope service checkbox assertion to Services block
The create dialog also has a Delete after run checkbox. Count checkboxes only inside the Services section so CI does not include unrelated form controls.
* fix(scheduler): narrow closest() result to HTMLElement in schedule test
The service-checkbox assertion passed an Element from closest() into
within(), which requires an HTMLElement, failing tsc -b in the frontend
build and Docker build stages. Use the closest<HTMLElement>() type
argument so the value type-checks without an unsafe cast.
* fix(scheduler): hide Sencho container on remote node picker lists
Remote container lists are proxied from peer Sencho instances, so id-only self filtering missed peers on older builds. Await SelfIdentity init, match ImageID, and drop official saelix/sencho images. Apply the same heuristic in the scheduled-operations UI and when the hub fetches remote containers for scheduled runs.
* test(monitor): add missing DatabaseService mocks for scan history cleanup
* test(scheduler): add missing markStaleScansAsFailed mock
SchedulerService.tick() calls db.markStaleScansAsFailed() to sweep stale
vulnerability scans. The scheduler-service test was missing this method in
its DatabaseService mock, causing TypeError failures during test initialization.
Added mockMarkStaleScansAsFailed to hoisted mocks and DatabaseService mock
object, returning safe default of 0 scans marked as failed.
* test(compose): add missing FileSystemService mocks for getStackContent/getEnvContent
* test(containers-route): mock SelfIdentityService to prevent initialize() crash
The excludeSelfContainers() helper calls SelfIdentityService.initialize(), which tries to access DockerController. Without a proper SelfIdentityService mock, the initialize() call fails silently, causing a 500 error on GET /api/containers.
Added SelfIdentityService mock with initialize(), isOwnContainer(), and isOwnImage() methods to prevent the crash.
PR #1497 pinned an enabled one-shot's next_run_at on creation, but the chosen
instant did not survive two paths: editing reconstructed the date from the
yearless cron (current year), and a disabled one-shot nulled next_run_at with no
other store, so enabling it later recomputed from the cron. Both moved a
future-year one-shot to a different annual occurrence than the date displayed.
Persist the one-shot's absolute fire time in a dedicated run_at column
(additive, nullable; recurring schedules leave it null). Create and update store
run_at independently of the enabled state; next_run_at is derived from it when
enabled and null while disabled, so a disabled one-shot keeps its run_at and the
enable toggle restores the exact instant from the column rather than the cron.
The editor reconstructs a one-shot's date from the persisted run_at, so opening
and re-saving without changes preserves the originally chosen year.
No behavior change for recurring schedules or fresh installs; the column is added
by an additive migration safe for upgrades from v0.92.0.
Opening the New Schedule modal from a stack's Schedule action, or editing
a stack-targeted task, set the node and the stack, but the node-change
effect then ran and cleared the stack, forcing the user to reselect it.
Move the stack-clear out of that effect into the Node picker's change
handler so it only fires on a user-driven node change. A programmatic node
set from prefill or edit now preserves the stack.
The schedule editor exposed only a raw 5-field cron input, which is
unfriendly for the common "daily at 3am" or "one-time next week" cases.
Add a Simple mode (now the default) that builds the cron from a
frequency and time: Once, Hourly, Daily, Weekly, and Monthly. Advanced
mode keeps the raw cron input as an escape hatch.
- Simple mode compiles to the existing cron_expression on save; no
schema or scheduler changes.
- One-time schedules reuse the existing delete-after-run flag: selecting
Once turns it on and locks it so the task runs a single time (cron has
no year field, so without it the task would repeat yearly).
- Editing a task opens in Simple mode when its cron maps to one of the
simple shapes, otherwise in Advanced; switching a custom cron to Simple
warns that it will be replaced.
- Day-of-week uses frosted toggle chips and the time uses hour/minute
selects so the controls match the rest of the editor.
Timeline pills and the mobile schedule list now identify what each
scheduled run acts on instead of repeating the task name. Pills stay
compact (firing time plus a category-aware target) and carry the full
detail on hover:
- Stack actions show the stack name.
- Fleet snapshots show "Entire fleet".
- Fleet auto-updates and node-scoped prune/scan show the selected node.
- The hover tooltip adds the action label, task name, and node.
The mobile list resolves node names too, so prune and scan rows name the
node rather than the literal "system". A shared scheduleTargetDescriptor
helper removes the target-label logic that was duplicated across the
desktop and mobile views. The lifecycle lane is renamed "Stack lifecycle"
to match the action-picker category wording.
* feat(scheduler): add helper text and risk badges to scheduled action picker
Add a concise helper text and risk level badge to every scheduled action
in the create/edit modal. The six risk levels (Safe, Read-only, Interruptive,
Runtime change, Removes containers, Destructive) map to the four existing
design-system tones and render as a small dot+label chip next to the helper
text, following the same pattern as SeverityBadge.
Fix an ambiguous mobile label: update + target_type: fleet now resolves
through resolveTaskAction and renders 'update node stacks' instead of the
misleading 'update fleet'.
Add exact helper-text and risk-level assertions for all 10 actions, plus
component tests for default modal state, action-switch scenarios, and
mobile update+fleet rendering.
* docs: update stale scheduled-operations alt text for changed helper text
* feat(scheduler): group schedule action picker by operator intent
Reorganize the New Schedule action picker from a flat dropdown to a
category-grouped list (Lifecycle, Updates, Security, Maintenance, Backups).
- Extend Combobox component with optional group field on ComboboxOption,
rendering grouped sections with non-interactive headers when groups are
present. Flat rendering is unchanged for all other callers.
- Reorder SCHEDULED_ACTIONS by category group and update seven action
labels per the operator-intent spec.
- Add DEFAULT_SCHEDULED_ACTION_ID constant so picker order and form
defaults are independently controllable.
- Wire grouped actionOptions into ScheduledOperationsView.
- Update all label references in docs and tests.
- Add Combobox grouping tests, registry order test, and default-constant
test.
* fix(scheduler): correct Combobox grouping for interleaved groups, docs labels
- Replace last-group-append with Map-based group partitioning so
interleaved or mixed-group options land in the correct group.
- Add interleaved-groups test and restore non-interactivity test.
- Update stale "Start Stack" references to "Start / Bring Up Stack"
in doc action-label contexts.
- Update action-picker alt text to describe the new grouped order.
* fix(scheduler): reject 6-field cron in Scheduled Operations
Create and edit validation parsed cron with cron-parser, which accepts both
5- and 6-field expressions, while the form, presets, and docs all describe a
5-field cron. Because the scheduler ticks once per minute, a leading seconds
field can never improve precision, so a 6-field expression was silently
accepted but never honored on its stated schedule.
Add a field-count guard on both sides: the API rejects 6-field input at
create and edit with a clear message, and the form surfaces the same error
inline and disables save. Cron nicknames such as @daily still pass. Document
the five-field requirement in the cron reference.
* chore: merge main into scheduled cron validation
* fix: avoid logging policy bypass actor in debug output
Give every scheduled action an explicit, predictable target model
(Action then Node then Stack then Options then Schedule):
- System Prune now exposes a Node picker and requires a node, so it can
no longer run silently on the default node.
- Vulnerability Scan and System Prune list local nodes only; both run on
the hub-local Docker daemon and reject remote nodes on the backend.
- Restart Stack service discovery loads services from the selected node
via fetchForNode instead of the active or local node.
- Fleet Snapshot shows a read-only "Scope: Entire fleet" summary.
Backend gains a shared local-node guard and prune node validation on
create and update, plus an executor-level remote-node guard, so the
frontend and backend validation now agree for every action.
Scheduled-operation action metadata was duplicated across the backend route
validator, the DatabaseService action union, the desktop action picker, the
Timeline lanes, and the mobile labels/tones. Adding or renaming one action meant
editing all of them.
Introduce one registry per package as the single source within that package:
- backend/src/services/scheduledActionRegistry.ts owns the action list and
target-type validation; routes/scheduledTasks.ts and DatabaseService import
from it (BackendScheduledAction type, VALID_ACTIONS, validateActionTarget).
- frontend/src/lib/scheduledActions.ts owns the UI metadata (labels, short
labels, categories, tones, target/node/stack/service flags, helper text) and
drives the create-flow picker, the All Tasks label, the Timeline lanes, and
the mobile schedule view.
Timeline lanes now group by semantic category (Lifecycle, Updates, Security,
Maintenance, Backups) sourced from the registry. The update-fleet UI alias is
made explicit via a backendAction field. Backend validation stays authoritative;
parity tests on each side keep the action sets in lockstep.
A one-shot task (delete after successful run) self-deletes only on a
successful run; a failed one-shot is kept so it can be retried or
debugged. The table gave no signal for this, so a lingering failed
one-shot was indistinguishable from a stuck recurring task. Add a
"One-shot" chip to the status cell for every delete-after-run task,
with a tooltip that explains the keep-on-failure behavior when the
task has failed.
* fix(scheduled-ops): run stack lifecycle schedules on remote nodes and harden run visibility
Stack lifecycle schedules (Restart, Stop, Take Down, Start, Backup Stack
Files) now run against whichever node the schedule targets, local or
remote. Each remote run proxies to that node's own stack-operation
endpoint, so a hub-managed schedule reaches the node that actually holds
the stack. Restart with a service subset restarts each selected service
and, if one fails, names the services already restarted so run history
reflects the stack's partial state. Auto-start on a remote node runs that
node's own pre-deploy scan-policy check against the images it holds.
Add POST /api/stacks/:name/backup to trigger an on-demand backup of a
stack's compose and env files (the same rollback snapshot a deploy
takes); it backs the remote backup schedule and is available to operators
on its own.
A scheduled task that reaches execution on an unpaid licence is now
skipped and written to run history as a failed run, so a manual trigger
that returned a queued response never silently disappears.
Test plan:
- Backend unit + integration: scheduler-service (remote proxy per action,
per-service fan-out, auto-start policy delegation, remote-failure and
no-credentials paths, unpaid-tier skip), stack-backup-route
(auth/role/paid/404/400/500), scheduled-tasks-routes.
- Frontend component test for the schedules view (list, prefill, node
filter, create payload).
- tsc and lint clean on both packages.
* fix(scheduled-ops): lock the stack-files backup route against concurrent stack ops
The stack-files backup writes the same slot the pre-deploy rollback
snapshot uses, so running it while a deploy, update, or rollback is in
flight on the same stack could overwrite the rollback point. The backup
route now takes the per-stack operation lock (as deploy/down/restart do)
and returns 409 when the stack is busy, keeping the rollback snapshot
intact. Adds the 'backup' action to the stack-op lock type and a busy
participle for the 409 message.
* fix(scheduled-ops): enforce backup-path containment inline at the filesystem sink
The on-demand backup route passes the stack name straight into
backupStackFiles, so resolve the backup directory against the backup root
and confirm containment with an inline startsWith check before the
mkdir/copy/write sinks, matching the barrier restoreStackFiles already
uses. The stack name is validated at the route and again by
resolveStackDir, so this is defense in depth that also closes a
static path-injection finding on the new call path.