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
| Persona | Role |
|---|---|
| Coach / Admin / Owner | Sets the day’s programming for each class type, toggles draft↔published. |
| Member | Reads via findByWeek / findByDate. Sees only status='published' rows. |
| Class sessions surface | ClassSessionsService 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
- Set —
PUT /daily-programmingwith{ classTypeId, date, status?, workouts:[{ workoutId, sortOrder }] }. Whole-day replace: deletes existing rows for(classTypeId, date)and inserts the new set in one transaction. - Read by week —
GET /daily-programming?weekStart=YYYY-MM-DD. Returns[{ classTypeId, classType, date, status, workouts[] }]grouped per (classType, date). - Read by date —
GET /daily-programming?date=YYYY-MM-DD. - Toggle status —
PATCH /daily-programming/status{ classTypeId, date, status }. Affects all rows for that slot. - Remove —
DELETE /daily-programming?classTypeId&date. Physical delete (no soft-delete column). - 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-weekwill 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_sessionwins regardless of board content — a class type with no session that day has nothing to post to. GET /daily-programming/board?weekStart=— composesfindByWeek-shaped data with aclass_sessionscount 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:falseworkout (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 removedDaySheet’s “Quick freeform note”).- Entries (
PUT /daily-programmingwithentry) — the Plan grid’s save sendsentry: '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_publishedfires whenever a write lands a cell as published (set,setStatus,publish-week, one event per cell).wod_post_completedfires only for writes that carry anentry,{ entry, scope: 'day' }— the session sheet and workout builder never emit it. - Workouts page “Posted by day” (offered with
workout-text-first-authoringoff; on, the Plan grid already shows every posted day) —GET /workouts/posted?weekStart=&classTypeId=&page=stays either way, adaily_programming-keyed listing (date, class type, workout, mode, draft/published,isLibrary), newest first. “Save to Workouts” isPATCH /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_idreferences a library workout. Service validates org ownership before insert. - program-templates —
applyToSchedulewrites bothclass_sessionsanddaily_programmingrows 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 —
DELETEphysically removes rows. Historical accountability lost (if a coach unpublishes/edits the day, prior content is gone). - No
daily_programming.organization_iddenormalization — every query joins throughclass_types → programs.organization_idto scope (getOrgClassTypeIds). Adequate at current scale; potential index target later. - No audit trail for daily-programming edits. Different from
workout_assignmentsbulk ops, which writeaudit_logs. - Status enum is a plain
varchar(20)— onlydraft/publishedaccepted in practice; not enforced at DB level. The controller’sSetStatusDtoenforces it viaclass-validator. findByWeekmember filter is JS-side after the SQL fetch — readable but loads draft rows the member won’t see.