…ctions, fires
Implementation layer of docs/acceptance/studio-schedules.md, built
against plan commit d3300a6.
BFF: /api/v1/schedules list (supported flag and reason read live from
the negotiated snapshot's scheduling capability; 501 schedule_unsupported
on mutations when off), create and update mapping the product mode /
profile / cron-or-once trigger vocabulary onto the SDK spec while
carrying the daemon's unexposed spec fields over untouched, fire / pause
/ resume actions, delete, and newest-first fire history with a derived
in-flight flag; status derived from enabled / in-flight / pending-claim
state. Mutations sit behind the bootstrap's CSRF and session gates.
Web: the schedules workspace, form with cron builder and phrase parser,
and detail page; "Scheduled" joins the nav.
Answers four review findings. A one-shot instant no longer round-trips
through an offset-less datetime-local value: the form carries the
schedule's original instant and re-sends it unchanged while the displayed
wall clock is unedited, so saving an untouched schedule whose instant is
the second occurrence of an ambiguous local time can no longer move the
fire an hour; an edited wall clock CANONICALISES with a fixed rule
rather than restoring the previous instant (the earlier occurrence at a
fall-back transition, the first instant after the gap at a spring-forward
one), and the form shows the absolute time it will save. A proof states
the canonicalisation explicitly, so the preservation rule is not read as
a general round-trip. A schedule the daemon reports with no next fire after at least one
fire is completed rather than scheduled. maxFires and oneShotMaxRetries
are bounded to 0..2147483647 before any SDK call, so no value reaches a
signed 32-bit daemon field that cannot hold it. The phrase parser accepts
"next <weekday> at <time>", not only the bare form, and ?schedule=<name>
opens that schedule editor once, without reopening it after close.
77 server and 213 web Vitest proofs; the unsupported path smoke-tested
against a spawned mecated --mock (which runs with scheduling off).
Tracking: #1736
Signed-off-by: Samuele Verzi <samu@stacklok.com>
Summary
What lands
/api/v1/schedules…list with the supported flag and reason read live from the negotiated snapshot, create and update mapping mode / profile / cron-or-once trigger onto the SDK spec, fire / pause / resume, delete, newest-first fire history with a derived in-flight flag; status derived from enabled / in-flight / pending-claim state.Amended after review
Implements the amended contract and fixes the two implementation findings:
Europe/Rome, which falls back on 2026-10-25 and springs forward on 2026-03-29); they pass underUTC,America/New_York, andAsia/Tokyoas well as the local zone.completed.0..2147483647in the contract schema, so an oversized or non-safe value is a400before any SDK call.next <weekday> at <time>now parses: the pattern accepts the optionalattoken, with coverage for the bare form, theatform, and odd spacing and case.?schedule=<name>now opens that schedule's editor once the inventory has loaded, and a ref keeps closing the editor from reopening it.Verification
69 server and 209 web Vitest proofs (17 ACs, 0 missing);
task studio:check,task docspass; the unsupported path smoke-tested againstmecated --mock, which runs with scheduling off. A manual run against a deployment with scheduling enabled is still owed.Further amendments
Implements the amended schedules contract (see the plan layer below):
The adapter refuses a changed trigger before calling
update. It validates the timezone throughIntland derivescompletedahead ofpaused.The form sends the stored mode and trigger unchanged, and locks trigger editing for an existing schedule.
Run now, Pause, and Resume are gated on status, and Run now refreshes that schedule's fire history.
An empty timezone reads UTC.
Trigger lock. The form now routes every trigger edit through
withTriggerPatchand renders fromdisplayedValue. An existing schedule can no longer show one trigger while sending another.Plan status. This layer's diff moves its acceptance plan to
landed(header and index), per the plan lifecycle; the transition is authoritative only when this PR merges, andtask ac-trace-strictnow gates the plan.Re-verified on the rebased stack:
task studio:checkpasses (106 server + 248 web Vitest), andtask ac-trace-strictpasses with every Studio plan gated aslanded(141 ACs, 0 failures).Development stage
Contract linkage
/api/v1/schedules…) inside the boundary ADR 0351 fixedgh stack) delivery for the whole Studio series: nothing merges until every layer is reviewed, so the plan's approval-by-merge event is deferred to the end (recorded in each plan's Delivery line), and each implementation layer records the exact plan commit it built against.docs/acceptance/studio-schedules.mdd3300a6aaaec9744f7744eaf9b26f24a39d642e9(the plan layer below; not yet merged — stacking exception above)Interface conformance
Issue relationship
Relates to #1748 (parent #1736; stack #1766). Non-closing: the step completes when both of its layers merge.
Type of change
Test plan
Baseline checks
task lint)task test)go run ./cmd/mecademo)task docs)Studio gates (this workspace is not covered by
task lint/task test, which are Go-only):task studio:check(Biome, typecheck, Vitest, generated-drift),task ac-trace— all pass; see Verification above.User-facing change
Studio gains the Scheduled page: create, edit, pause, resume, fire, and inspect schedules.
Special notes for reviewers
Part of a fourteen-layer stack; review bottom-up. Nothing merges until the whole series is reviewed.