Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesDaily ProgrammingDaily programming

Daily programming

What this is

Daily programming is the class-type × date → workouts join. Coaches use it to publish the “WOD board” for a given class type on a given day: “WOD on 2026-06-03 = Fran”. One row in daily_programming per (class_type, date, workout). Status is draft or published; members only see published rows. This is the broadcast/whiteboard surface — orthogonal to workout-assignments, which are per-user. Schedule-mode program-templates materialize daily-programming rows alongside class_sessions on apply.

Who uses it

PersonaRole
Coach / Admin / OwnerSets the day’s programming for each class type, toggles draft↔published.
MemberReads via findByWeek / findByDate. Sees only status='published' rows.
Class sessions surfaceClassSessionsService reads via findByDateRange(publishedOnly=true) to surface the day’s workouts on a session card.

Persona impact

  • Coach writes once per class type per day; the same prescription is visible to every member booked into any session of that class type on that day.
  • Member sees the day’s programming on the class card before booking, and on the whiteboard / class session view.
  • Owner uses schedule templates to fill weeks of programming in one apply.

Capabilities

  1. Set — PUT /daily-programming with { classTypeId, date, status?, workouts:[{ workoutId, sortOrder }] }. Whole-day replace: deletes existing rows for (classTypeId, date) and inserts the new set in one transaction.
  2. Read by week — GET /daily-programming?weekStart=YYYY-MM-DD. Returns [{ classTypeId, classType, date, status, workouts[] }] grouped per (classType, date).
  3. Read by date — GET /daily-programming?date=YYYY-MM-DD.
  4. Toggle status — PATCH /daily-programming/status { classTypeId, date, status }. Affects all rows for that slot.
  5. Remove — DELETE /daily-programming?classTypeId&date. Physical delete (no soft-delete column).
  6. Member visibility — members see only status='published' (service filters after the SQL fetch).

Programming week board

Where the grid lives follows workout-text-first-authoring. On, it is the Workouts page’s Plan tab (/dashboard/workouts, ?view=plan, the default view when the org runs an active schedule program; WeeklyCalendar surface="programming"), beside Library; an org that only coaches gets the saved list alone and no tabs; Schedule keeps the timetable (Expanded and Condensed) and has no programming view. Off, the grid is the Schedule’s third view (the Programming toggle beside Expanded and Condensed, Schedule screen only) and the Workouts page offers Library and Posted by day.

Plan is a class-type × weekday matrix: every active cell represents all live sessions of that type on that date, so one workout applies across their time slots. With two or more halls, the matrix is grouped by hall like the Schedule. The class-day workout still appears on each session card and condensed row, and the “Workouts set” stat tile counts class days with one. A cell opens the shared day sheet (WorkoutPickerSheet, see workout-assignments): text first, published to every session that day on save. Saved workouts sit behind a collapsed toggle; a cell holding this day’s own text workout reopens as editable text, with Detailed edit (/dashboard/workouts/:id/edit?mode=builder) as its one door to the structured editor when the org has workout_builder. Each cell shows its workout’s structure summary (movements linked, what is left to review) once the background parse (workout-parse) reports. With no class type that carries a workout, the empty grid links to the Schedule’s settings (?settings=class-types).

The Plan toolbar (programming-week-toolbar.tsx) holds the week actions:

  • Copy last week previews, cell by cell, what copy-week will do (copied, already written, no session), then copies on confirm. Copied days land as drafts.
  • Publish the week · N drafts appears while the week has drafts and calls publish-week.
  • Plan the week with Spotter opens Spotter with the week’s status (use-programming-week-actions.ts).

The board response remains week-shaped: rows are the org’s schedule-delivery-mode class types with hasWorkout=true, columns are the 7 dates of whichever week the calendar is currently showing (the board endpoint accepts any 7-day-range start date, and optionally a programId filter). The Programming view renders that matrix directly.

  • Cell state — explicit enum, not left for a client to derive: published, draft, empty, no_session. no_session wins regardless of board content — a class type with no session that day has nothing to post to.
  • GET /daily-programming/board?weekStart= — composes findByWeek-shaped data with a class_sessions count per (classType, date).
  • POST /daily-programming/copy-week { fromWeekStart, toWeekStart } — copies PUBLISHED cells to the same weekday of the target week as drafts. Never overwrites a target cell that’s already published; skips a target day with no session. Returns { copied, skippedPublished, skippedNoSession }. Called by the Plan toolbar’s “Copy last week”.
  • POST /daily-programming/publish-week { weekStart } — publishes every draft cell on the visible week. Returns { published }. Called by the Plan toolbar’s “Publish the week”.
  • POST /daily-programming/quick-workout { classTypeId, date, title?, description } — creates a description-only, isLibrary:false workout (same non-pollution rule as the session sheet’s inline quick-create) and publishes it as the day’s programming in one call. No current UI caller (was the removed DaySheet’s “Quick freeform note”).
  • Entries (PUT /daily-programming with entry) — the Plan grid’s save sends entry: 'freeform' for text written in the sheet, 'pick' for a saved workout, and 'paste' for one structured through the review; “Publish to a day” on a saved workout sends 'pick'. The session detail sheet still sets the workout with day / session scope on Schedule and sends no entry.
  • Analytics — wod_published fires whenever a write lands a cell as published (set, setStatus, publish-week, one event per cell). wod_post_completed fires only for writes that carry an entry, { entry, scope: 'day' } — the session sheet and workout builder never emit it.
  • Workouts page “Posted by day” (offered with workout-text-first-authoring off; on, the Plan grid already shows every posted day) — GET /workouts/posted?weekStart=&classTypeId=&page= stays either way, a daily_programming-keyed listing (date, class type, workout, mode, draft/published, isLibrary), newest first. “Save to Workouts” is PATCH /workouts/:id { isLibrary: true } — flips the flag only, doesn’t touch anything else about the row.

Build the week with Spotter

Spotter never authors a workout. The Plan toolbar’s “Plan the week with Spotter” button opens Spotter with one fully-specified status message — this week’s sessions and WOD coverage per class type, coach gaps, draft counts, what ran last week but not this week, and the program’s saved timetable templates, all built client-side by buildSpotterWeekPrompt (apps/web/src/lib/programming/spotter-week-prompt.ts) — so the model’s first move is to list what’s missing and then wait for the coach’s instructions, never to invent programming.

Once told what to do, Spotter executes with the same tools a coach would reach for directly: class_sessions.create/update/cancel/publish/bulk_publish to change sessions, class_sessions.copy_week to repeat last week’s live sessions into this week as drafts, program_templates.apply for a saved timetable, and — only for a workout the coach hands it as text — workouts.create (freeform, saved exactly as written) then daily_programming.set to post it to the class day named. See spotter-agent for the full tool catalog.

Relationship to other features

  • workouts — daily_programming.workout_id references a library workout. Service validates org ownership before insert.
  • program-templates — applyToSchedule writes both class_sessions and daily_programming rows in one transaction.
  • scheduling/class sessions — ClassSessionsService.findByDateRange(publishedOnly=true) reads daily programming so a class session card shows the day’s workouts.
  • workout-assignments — completely orthogonal. Assignments are per-user; daily programming is per-class-type. A schedule-mode program drives daily programming; a coaching-mode program drives assignments.

Current status

Shipped. Whole-day replace pattern, draft/published flip, week/date reads, schedule-template-driven materialization. ~510-line service.

Known gaps / open Linear issues

  • No soft delete — DELETE physically removes rows. Historical accountability lost (if a coach unpublishes/edits the day, prior content is gone).
  • No daily_programming.organization_id denormalization — every query joins through class_types → programs.organization_id to scope (getOrgClassTypeIds). Adequate at current scale; potential index target later.
  • No audit trail for daily-programming edits. Different from workout_assignments bulk ops, which write audit_logs.
  • Status enum is a plain varchar(20) — only draft / published accepted in practice; not enforced at DB level. The controller’s SetStatusDto enforces it via class-validator.
  • findByWeek member filter is JS-side after the SQL fetch — readable but loads draft rows the member won’t see.