feat(schedules): B3 — schedule management SPA + enablement#564
Conversation
Adds the user-facing schedules feature: a signal-based list/create/edit page under frontend/ai.client/src/app/schedules/ (bounded cadence UI — daily/weekday/weekly + hour + IANA timezone, optional assistant + tool snapshot, pause/resume/delete with confirm), the headless-grant enablement UX (status banner, "Enable scheduled runs", paused_error reauth_required / oauth_required affordances), and capability-gated nav visibility that rides the /schedules list call's 403/404 rather than a dedicated client signal. Backend: adds POST /runs/grant to apis/app_api/runs/routes.py, sharing the existing _resolve_grant create-on-enable logic with /runs/now so a user can turn on scheduled runs without running a prompt first. Same kill-switch + scheduled-runs capability gating. Co-Authored-By: Claude Opus 4.8 <[email protected]>
Review (orchestrator pass)Verdict: clean, no blockers. Backend Real follow-up (flagged by the agent, confirmed)The B1 NoteThe intermittent |
The schedule-form and schedules-list specs used provideRouter([]), but the components navigate programmatically (router.navigate(['/schedules']) etc.) on save/cancel/create/edit. With no matching route the navigation rejects (NG04002) AFTER the test body, surfacing as unhandled rejections that fail the whole vitest process even though all 1378 tests pass. Register the target routes so the real router resolves them. Scoped ng test: 46 passed, 0 errors. Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
CI fixed (commit 3355dcc). The frontend job failed on 4 unhandled rejections — |
Summary
frontend/ai.client/src/app/schedules/): signal-based list (label, cadence summary, next run, state badge, last-run status + link to session), create/edit form (label, prompt, bounded cadence — daily/weekday/weekly + hour + IANA timezone, optional assistant, optional tool selection), pause/resume, delete-with-confirm.POST /runs/grant);paused_errorwithstateReason: "reauth_required"surfaces "Your access expired — log in and re-enable";stateReason: "oauth_required"surfaces a "reconnect a tool" hint linking to/settings/connectors./scheduleslist call — a 403/404 flipsScheduleService.accessible$tofalseand the UI fails gracefully (page shows a "not available" state; nav entry stays hidden).POST /runs/grantadded toapis/app_api/runs/routes.py, reusing the exact create-on-enable_resolve_grantlogicPOST /runs/nowalready used (no duplication) — same cookie + kill-switch +scheduled-runscapability gating. Lets a user enable scheduled runs without running a prompt first.Known gap (flagged per the brief)
The B1
PATCH /schedules/{id}(update_scheduled_prompt) only writes a field when the incoming value is notNone— there is currently no way to clearassistantIdorenabledToolsover the wire. The edit form has explicit "Detach assistant" / "Reset to all my tools" checkboxes to make the intent explicit, but they can only omit the field from the PATCH (a no-op against the current backend), not actually clear a previously-set value. A real fix needs a backend follow-up (e.g. a sentinel value, or splitting into a dedicated clear affordance) — out of scope for this PR per the brief's instruction to consume the B1 CRUD as-is.Test plan
uv run python -m pytest tests/apis/app_api/test_runs_routes.py tests/routes/test_schedules_routes.py tests/architecture/ -q— 62 passednpx tsc --noEmitandnpx ng build— cleannpx ng test --watch=false --include='src/app/schedules/**/*.spec.ts'— 46 passednpx ng test --watch=false --include='src/app/components/sidenav/sidenav.spec.ts'— 8 passedng testsuite — passes; one pre-existing spec (shared-view.page.spec.ts) intermittently times out under full-suite worker-pool load (reproduces the same way ondevelopwithout these changes once the suite is large enough — documented cross-spec pollution in shared worker pools, not a regression from this PR)No new npm/uv packages were added — everything is built from existing dependencies (
@angular/forms,@angular/cdk/dialog,@ng-icons/heroicons,Intl.supportedValuesOfat runtime with a static fallback).🤖 Generated with Claude Code