Skip to content

Add Schedules section to the Settings UI (#303)#304

Merged
germanescobar merged 4 commits into
mainfrom
issue-303
Jul 14, 2026
Merged

Add Schedules section to the Settings UI (#303)#304
germanescobar merged 4 commits into
mainfrom
issue-303

Conversation

@germanescobar

Copy link
Copy Markdown
Owner

Summary

Adds a new Schedules entry to the in-app Settings page so the user can manage scheduled sessions on the active project without leaving the app.

  • New client/src/components/schedules-section.tsx — self-contained component mounted from SECTIONS in Settings.tsx (data-testid="settings-nav-schedules").
  • New schedule API helpers in client/src/api.ts (thin client over the existing REST surface in server/routes/schedules.ts; no server changes).
  • SettingsPage now accepts an optional projectId and onOpenSession so the section is project-scoped and can deep-link from a run to its session.

What works end-to-end

  1. List every schedule (including disabled) — worktree, prompt preview, trigger type (cron / runAt), next/last run, enabled state, source, last error.
  2. Create via a form mirroring the CLI: worktree (from project), prompt, and either one-shot ISO timestamp or cron + timezone. Server validation errors surface inline. createdBy: "ui".
  3. Toggle enabled/disabled with a Switch.
  4. Delete with an AlertDialog confirmation (data-testid="schedule-delete-confirm").
  5. View runs in a drawer; each run's sessionId is a button that switches the main view to that session (same path the transcript's controller:// links take).

Non-goals (deferred per the issue)

  • Editing an existing schedule's trigger (the server has no PUT route).
  • A cron helper UI (raw cron + timezone select is enough).

Validation

  • client/src/components/__tests__/schedules-section.test.tsx — 16 new tests covering list rendering (cron + one-shot rows, disabled badge, last run, last error, all action controls), the create form (worktree select, prompt, trigger toggle, both one-shot and cron fields, empty-worktree state, validation errors), the delete confirmation wiring, and the enable/disable switch.
  • Existing tests still pass (settings-responsive.test.tsx, agents-section.test.tsx, etc.). The only failing test in the suite is the pre-existing pty-manager.test.ts failure, unrelated to this change.
  • vite build succeeds.

Issue

Closes #303.

Wires a new 'Schedules' entry into the Settings page so the user
can manage the active project's scheduled sessions without leaving
the app. The component is a thin client over the existing REST
surface in server/routes/schedules.ts (the CLI is still the
source of truth; no server changes):

  * List every schedule (including disabled) with worktree, prompt
    preview, trigger type, next/last run, enabled state, source,
    and last error inline.
  * Create via a form that mirrors the CLI: worktree (from the
    project's worktrees), prompt, and either a one-shot ISO
    timestamp or a cron expression + timezone. Server-side
    validation errors (bad cron / timezone) are surfaced inline.
    'createdBy' defaults to 'ui'.
  * Toggle enabled/disabled with a Switch.
  * Delete with an AlertDialog confirmation.
  * View runs for a schedule in a drawer; each run with a
    'sessionId' is a button that switches the main view to that
    session (mirrors the 'controller://' deep-link path the
    transcript already uses).

Editing an existing schedule is intentionally not exposed — the
server has no PUT route for it (non-goal). The form resets on
close; the section refreshes after every mutation.

Plumbing: 'SettingsPage' now accepts an optional 'projectId'
and 'onOpenSession' prop so the section can be project-scoped
and so runs can deep-link to a session. Both are optional so
the page still renders without an active project.

A regression test in
'client/src/components/__tests__/schedules-section.test.tsx'
covers list rendering (cron and one-shot rows, disabled badge,
last run, last error, action controls), the create form (worktree
select, prompt, trigger toggle, both one-shot and cron fields,
empty-worktree state, validation errors), the delete
confirmation flow (data-testid contract + wiring to
'deleteSchedule'), and the enable/disable switch.
Review feedback on #303: instead of scoping the Schedules section
to the active project, list every project's schedules in one view
so the user can see everything that's about to fire without
project-hopping.

The server still has no cross-project '/api/schedules' endpoint
(the CLI is the source of truth for the data model, per the
issue's non-goals). The section fans out client-side: list
projects, then list each project's schedules, then merge. Each
row carries its projectId plus a denormalised projectName so
the action handlers (toggle, delete, runs) don't need a
separate lookup.

The create form now picks a project first and a worktree second
(worktrees are loaded on demand and cached per project). The
delete confirm dialog names the project so the user is sure
which schedule they're removing.

Regression test: 18 cases in schedules-section.test.tsx now
cover the project badge per row, the project + worktree
selectors, the loading state for worktrees, the empty-projects
state, and the cross-project delete wiring (the confirm action
must call deleteSchedule with the row's projectId, not the
section's). All 632 tests in the suite pass (the one
pre-existing pty-manager failure is unrelated).
Without this, calling 'npm run dev' from a clean shell (no exported
VITE_API_PORT) would have the dev:server script fall back to the
default 3102, while the Vite client reads .env.local via its own
loadEnv and proxies to whatever port is in there (e.g. 3129 for
this worktree). The two would land on different ports, and every
request would 404 — which is exactly the state this worktree was
in before the fix.

The loader is intentionally minimal (KEY=value with optional
quotes, no expansion): we only need the port keys, and pulling in
dotenv for three lines is overkill. Values already set in the
parent shell win, so a script that exports VITE_API_PORT
explicitly is not clobbered.

Discovered while debugging the empty-projects / /api/shortcuts 404
state in the dev tooling on issue #303.
@germanescobar

Copy link
Copy Markdown
Owner Author

@codex review

1 similar comment
@germanescobar

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 23cc9bc07e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +391 to +394
onOpenSession({
sessionId: run.sessionId,
worktreeId: runsFor.worktreeId,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass the schedule project when opening runs

When the Schedules view is showing all projects, this callback only sends the sessionId and worktreeId; App.handleOpenSessionFromSchedule then fills in activeProjectId. If the user opens a run from any project other than the currently active one, the app navigates to the wrong project and SessionView fetches a non-existent or unrelated session. Include runsFor.projectId in the callback payload and use that for navigation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 409bb18. The onOpenSession callback now carries the run's projectId (optional for future single-project callers), the SchedulesSection always forwards runsFor.projectId, and App.handleOpenSessionFromSchedule prefers the payload's projectId over the active one. Regression test added in schedules-section.test.tsx.

Comment thread client/src/api.ts Outdated
Comment on lines +1790 to +1793
} catch {
// One project's schedules failing shouldn't blank the whole
// view — log the failure server-side and skip it.
return [] as ScheduleWithProject[];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Surface partial schedule fetch failures

If one project's /schedules request fails, this catch converts it to an empty list, so fetchAllSchedules still resolves and the Settings UI clears the error while silently omitting that project's schedules. In a corrupt-store or transient 500 scenario, users can see an incomplete list or even “No schedules yet” while active schedules still exist and may fire. Preserve enough failure information to show a partial-load error instead of dropping the project.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 409bb18. fetchAllSchedules now returns { schedules, failedProjectIds } (new AllSchedulesResult type) and the section renders a non-fatal amber banner — data-testid="schedule-partial-error" — with the failed project names. Partial failures no longer disappear. Two new test cases pin the contract.

Comment on lines +212 to +213
const list = await fetchScheduleRuns(schedule.projectId, schedule.id);
setRuns(list);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Ignore stale runs responses

If the user clicks Runs for schedule A and then schedule B before A's request returns, A's later response still calls setRuns(list) while the dialog header now reflects B. On slow networks this can display A's sessions under B and make the run links use B's worktree context. Track the requested schedule id or abort the previous request before updating runs.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 409bb18. openRuns bumps a useRef on every call and the async fetcher bails out of all three setState calls (runs, runsError, runsLoading) if a newer request has started. A new test case pins the contract via source matching.

Three fixes for the cross-project Schedules section, all from the
Codex review on the PR.

P2 #1: Pass the run's projectId to the deep-link callback. In the
cross-project view, opening a run for a non-active project was
navigating to the wrong session because the click payload dropped
the projectId and the App handler fell back to activeProjectId.
The onOpenSession contract now carries an optional projectId, the
SchedulesSection always forwards runsFor.projectId, and the App
handler prefers payload.projectId over activeProjectId. The
optional annotation is non-breaking for future single-project
callers.

P2 #2: Surface partial schedule fetch failures. The cross-project
fan-out used to swallow per-project failures and return an empty
list, so the UI showed 'No schedules yet' (or an incomplete list)
while active schedules were still around. fetchAllSchedules now
returns { schedules, failedProjectIds } and the section renders a
non-fatal amber banner with the failed project names. AllSchedulesResult
is the new public type so future callers don't make the same
mistake.

P3: Drop stale runs responses. Clicking Runs for A and then B
before A resolved would let A's late response overwrite B's
dialog. openRuns now bumps a useRef on every call and the async
fetcher bails out of the setState calls if a newer request has
started. Cheap and self-contained.

Tests: 4 new cases pin the source contracts (P2 partial-load
banner + testid, run onClick projectId, App handler preference,
stale-response guard). 22 of 22 in this file pass; 636 of 637
across the suite (the lone pty-manager failure is pre-existing
and unrelated).
@germanescobar germanescobar merged commit b75c398 into main Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Schedules section to the Settings UI

1 participant