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
The Programming surface lives on Schedule — see dashboard-shell — and the Workouts page offers a “Posted by day” view beside its list.
Schedule has Calendar, Condensed, and Programming views. Programming 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. The class-day workout still appears on each session card and condensed row, and the “Workouts set” stat tile counts class days with one. Build with Spotter remains in the schedule toolbar (apps/web/src/hooks/use-programming-week-actions.ts); copy-last-week and publish-all-drafts have no UI caller. On a schedule class day, posting publishes it.
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 }. No current UI caller (lived on the removed standalone board) — kept as valid API surface.POST /daily-programming/publish-week{ weekStart }— publishes every draft cell on the visible week. Returns{ published }. No current UI caller, same as above.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”).- Pick / Paste entries (
PUT /daily-programmingwithentry:'pick'/'paste') — also no current UI caller; theDaySheetthat drove them was removed with the standalone board. The session detail sheet sets the workout with day / session scope instead on Schedule. - 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” —
GET /workouts/posted?weekStart=&classTypeId=&page=, 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 board’s “Build 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.