Skip to content

Timetable two-step roster iteration 2 — break audience parity and weekday supervisors

1. Problem distillation

  • Break rosters can select only departments, curricula, grades and homerooms, while activity rosters can also select tracks and individual students. This blocks legitimate per-student lunch/break participation.
  • One activity owns a repeat-set weekdays[], but every supervising-teacher row currently applies to the whole set. The backend therefore cannot express a different supervisor on Tuesday without splitting the activity identity.
  • Weekday-aware supervision is authorization- and diagnostics-relevant: it must flow through timetable conflicts, teacher views, attendance day resolution, activity-register access, revision copies and retained versions.
  • The activity remains one logical entity. Its name, audience, room, time and attendance cohort are still shared across the repeat-set; only supervisor coverage varies by weekday.

Success criteria (observable behavior that proves this works):

  • A break accepts { trackId } and { studentId } audience selectors, echoes their names, resolves them into the live participant union, and carries them through duplicate/revision copies and teacher-day duty detail.
  • An activity created for Monday and Tuesday can assign T1 to Monday and T2 to Tuesday. Diagnostics charge/conflict T1 only on Monday and T2 only on Tuesday.
  • The teacher-day surface shows that activity only to the supervisor assigned on the requested weekday; the activity register admits the same dated supervisor and rejects a teacher assigned only on a different weekday.
  • Existing clients may continue sending teacherIds; those teachers supervise every weekday in the activity repeat-set, including weekdays added by a later legacy-style edit.
  • Copy, revision, publish-revision and archived-version retention preserve the supervisor schedule exactly.

Non-goals (in-scope-shaped things this iteration is explicitly not doing):

  • Per-weekday activity time, room, audience or name. Those still require a second activity.
  • Client-side fan-out into independent single-day activities.
  • A new audience-to-student count endpoint or studentCount response field.
  • Replacing the existing write-time audience collapse with a granularity-preserving request model.
  • Making breaks attendance anchors.

2. Patterns survey

Analogous module/spec What we'd borrow What doesn't fit
prisma/schema.prisma ScheduledBreakAudience + src/timetables/timetables.queries.ts resolveBreakAudiences Nullable one-of selector refs, live union resolution, raw one-of CHECK and NULLS-NOT-DISTINCT selector index The existing row has four refs; TRACK needs the dated curriculum-selection track and STUDENT needs a direct student limb
prisma/schema.prisma ScheduledActivity / ScheduledActivityTeacher + 2026-07-13-timetable-activities-design.md One stable activity id with a repeat-set, optional supervisors, real teacher occupancy, attendance anchor semantics The original design deliberately made the teacher relation uniform across the repeat-set; that is the product gap being revised
src/timetables/timetables.queries.ts toSnapshotActivities Expand one activity row into one diagnostics slot per weekday Teacher ids must now be selected for the expanded weekday instead of copied wholesale to every occurrence
src/attendance/attendance-day.queries.ts loadDaySlots + src/attendance/attendance-access.service.ts assertTeacherCohortRead The requested date is already converted to a weekday and activity supervisors already feed the expected slot and the cohort gate Both paths currently treat row existence as activity-wide and must apply the weekday subset
src/timetables/timetables.queries.ts copyTimetableContent Deep-copy timetable-owned children during duplicate, revision, republish and archive retention The teacher child copy must include its weekday coverage; break audience copy must include the two new refs

On-axis assessment (chapter 16 §4–§5): break selector expansion and timetable-child copying are paved extensions. Weekday-qualified activity supervision is a new dimension on an existing relation, but it stays inside the existing timetable snapshot and attendance expected-schedule seams; it introduces no new module or cross-module write coordinator.


3. Architecture mapping

Primitive Apply? How Justify
Tenant scope yes Existing required tenantId columns, service filters and Class-S RLS policies remain unchanged No new tenant-bearing model is introduced
Academic-year scope indirect Breaks/activities remain scoped through timetableId -> Timetable.academicYearId; attendance resolves the version governing the requested date Existing timetable and expected-schedule contract
RBAC entity key existing TIMETABLES / ATTENDANCE No entity-key delta CRUD and attendance routes do not change ownership
Scopes existing Timetable writes keep timetables.configuration:write; attendance reads/writes keep their current register scopes No new fields are independently permissioned
Actions none Existing timetable and attendance actions apply This refines stored content and dated authorization only
Service base custom TimetablesService, timetable query helpers and attendance services These modules do not use BaseTenantedCrudService for aggregate slot mutations
queries.ts shape existing includes + named functions Widen break/activity graph includes; keep audience resolution and copy logic in timetables.queries.ts; dated activity access remains in attendance Canonical module pattern
Error codes existing Use VALIDATION_FAILED for mutually exclusive legacy/new teacher fields, duplicate teacher assignments, empty/duplicate/out-of-activity weekday subsets No new client recovery branch is needed
DTO conventions existing flat timetable DTOs Extend scheduled-break.dto.ts and scheduled-activity.dto.ts; extend attendance-owned duty DTO for break selector kinds Public contracts stay in their owning modules
File-backed sub-resources n/a No files
Custom fields no Not a custom-fields entity
Profile completeness no No person-profile field changes

4. Data model plan

Schema deltas

  • ScheduledBreakAudience: add nullable trackId -> CurriculumTrack and studentId -> Student relations. Keep the existing inferred-kind six-ref shape; do not add a redundant kind column.
  • ScheduledActivityTeacher: add weekdays DayOfWeek[] @default([]).
  • An empty stored array means “every weekday in ScheduledActivity.weekdays” and is the storage form written by the legacy teacherIds contract.
  • A non-empty value is a duplicate-free subset of the parent activity's weekdays and is the storage form written by the new weekday-aware contract.
  • The existing @@unique([activityId, teacherId]) remains: one teacher has one coverage declaration per activity, avoiding redundant global-plus-specific rows.
  • No new models, tenant columns, RLS policies or tenant registries.

When ScheduledActivity.weekdays changes without a teacher payload, the same timetable mutation transaction intersects every explicit teacher subset with the new repeat-set and deletes assignments whose intersection is empty. An empty legacy/global assignment remains empty, so it automatically follows added or removed activity weekdays. Adding a weekday never widens an explicit subset implicitly.

Migration shape

  • Additive / destructive / renaming: additive nullable break columns and foreign keys, one non-null activity-teacher array with a constant empty-array default, plus replacement of the existing break raw CHECK/index definitions to include the two new refs.
  • Data backfill: no explicit update. ADD COLUMN ... NOT NULL DEFAULT '{}' supplies the global sentinel to existing rows and to old application instances that omit the new field, preserving their current all-weekdays meaning.
  • Hazards from chapter 12 checklist:
  • The break selector unique index is dropped and recreated wider. Existing rows have both new refs null, so their existing uniqueness is preserved; old application writes remain valid during a rolling deploy.
  • The two new break foreign keys are added on nullable, initially-null columns, so no orphan cleanup is required.
  • No activity-teacher raw CHECK is added: the empty array is a valid global sentinel. Duplicate and parent-subset validation stays service-enforced because a normal CHECK cannot inspect the parent row.
  • Update chapter 12 and test/db-constraints.e2e-spec.ts for the widened break CHECK/index and the activity-teacher default. The existing break CHECK/index names are retained.
  • Repository inspection on 2026-08-26 found no uncommitted migration to fold.

Indexes and uniqueness

  • Recreate scheduled_break_audiences_selector_key over (break_id, COALESCE(department_id,...), COALESCE(curriculum_id,...), COALESCE(grade_id,...), COALESCE(track_id,...), COALESCE(homeroom_id,...), COALESCE(student_id,...)).
  • Retain scheduled_activity_teachers_activity_id_teacher_id_key; the new array is not part of identity.
  • No weekday index is needed: activity children are loaded through activityId, and the repeat-set is at most seven enum values.

5. API surface

Verb Path Decorators Request DTO Response DTO
POST /timetables/:id/breaks existing @RequireScopes(TIMETABLES, 'write') audience[] additionally accepts { trackId } and { studentId } each audience row additionally exposes nullable track and student named refs
PATCH /timetables/:id/breaks/:breakId existing Same break audience extension; list remains full-replace Existing mutation envelope, widened break response
POST /timetables/:id/activities existing @RequireScopes(TIMETABLES, 'write') New teachers?: { teacherId; weekdays?: DayOfWeek[] }[]; legacy teacherIds?: string[] remains accepted; the two fields are mutually exclusive teachers[] gains effective weekdays: DayOfWeek[]
PATCH /timetables/:id/activities/:activityId existing teachers, when present, full-replaces assignments; omitted child weekdays means all effective activity weekdays. teacherIds retains legacy full-replace/all-weekdays semantics. Existing mutation envelope, widened activity response
GET /timetables/:id/lessons existing faceted read No query delta breaks[] and activities[] carry the widened responses
GET /attendance/teacher-day and activity register surfaces existing attendance guards No request delta; requested date supplies the weekday Duty audience gains TRACK/STUDENT; activity cards and cohort access use only supervisors covering that weekday

Teacher assignment normalization rules:

  • Duplicate teacherId entries, duplicate weekdays, an empty explicit weekday list, a weekday outside the effective activity repeat-set, or both teachers and teacherIds produce 400 VALIDATION_FAILED.
  • The response always emits the effective weekday array, including for a stored empty/global assignment, so clients never have to interpret the storage sentinel.
  • A teacher facet keeps an activity when that teacher covers at least one occurrence; the response's per-teacher weekdays tell the weekly board which occurrences belong to that teacher.

Weekday-aware consumers:

  • Diagnostics toSnapshotActivities: filter teacherIds/names per expanded weekday; teacher conflict, availability and budget behavior then follows without category changes.
  • Expected attendance day: carry only supervisors whose assignment covers the requested weekday. The frozen day shape therefore stores the correct supervisor ids/names.
  • Activity cohort authorization: assertTeacherCohortRead already receives the requested date; its supervisor query requires weekdays isEmpty OR weekdays has weekday(date).
  • Teacher day: derives from the corrected expected slots and needs no independent supervisor rule.
  • Timetable copy/revision/archive: copy teacher weekdays and break trackId/studentId exactly.
  • Timetable publish notifications still include every distinct activity supervisor assigned on at least one weekday; notifications are weekly-version announcements, not day cards.

Swagger considerations

  • Document teachers vs legacy teacherIds mutual exclusivity and full-replace semantics.
  • Document that response teacher weekdays are effective values and that omitting assignment weekdays means the activity's whole repeat-set.
  • Extend break audience descriptions and teacher-day duty audience enum/docs to TRACK/STUDENT.
  • No new error examples are required because VALIDATION_FAILED is existing behavior.

6. RBAC seed plan

Seed file Delta
PermissionScope (rbac-catalogue.ts) none
PermissionAction (rbac-catalogue.ts) none
ScopeFieldMapping (rbac-catalogue.ts) none
Role grants (roles.ts) none
*_SCOPES runtime constant none

7. Divergence ledger

Pattern We diverge by Reason Tradeoff accepted
Original activity design: one undifferentiated teacher row per activity Add weekday coverage to the existing row Supervisors genuinely vary by weekday while the activity identity and other fields stay shared Every teacher-sensitive occurrence consumer must select by weekday
Frontend suggestion: nullable scalar weekday and widened (activityId, teacherId, weekday) unique Store one non-null weekday array per (activityId, teacherId), with empty as global Preserves the existing unique identity, represents “all days” without N rows, and avoids PostgreSQL nullable-unique/global-plus-specific ambiguity Subset-to-parent is an app invariant rather than a declarative FK/CHECK
Activity audience uses explicit kind Break audience continues inferring kind from exactly one non-null ref Break has no ref-less EVERYONE case, so a kind column would duplicate derivable state and force an unnecessary backfill Break and activity storage remain intentionally asymmetric while the product capability is symmetric
Break create fans out one row per weekday Activity remains one row with weekdays[] Stable activity/attendance identity and shared later edits are explicit product requirements Only teacher coverage may vary inside the repeat-set

8. Pushback log

US says Conflicts with Proposed instead Status
Add a break kind column, or widen the nullable-ref shape A kind column is redundant for a selector that always has exactly one ref Widen the existing one-of shape to six refs and keep the CHECK/index as the source of truth Resolved — approved 2026-08-26
Add nullable scalar weekday to each activity-teacher row The existing relation has one row per teacher; nullable composite uniqueness permits awkward duplicate/global-plus-specific states Add non-null weekdays[] to that one row, with empty = all activity days Resolved — approved amendment 2026-08-26
teacherIds becomes a new object list Immediate replacement would break deployed clients Add teachers and retain teacherIds as a mutually exclusive compatibility input Resolved — approved 2026-08-26
Frontend may fan out N single-day activities Breaks stable identity, attendance cohort identity and later edits Keep server-owned single activity identity; no client fan-out Resolved — rejected as requested

9. Deferrals

  • Audience studentCount / audience-resolution endpoint — lower-priority observability enhancement; no request in this iteration — follow-up: future timetable roster iteration.
  • Granularity-preserving cascade picker contract — current collapsed rows remain the persisted truth — follow-up: future audience request-shape design.
  • Per-weekday activity time/room/audience/name — requires either occurrence overrides or a series/occurrence model, neither needed for supervisor variation — follow-up: new design if product asks.

10. Open questions

  • Approve the widened six-ref break audience without adding a break kind column.
  • Approve one non-null weekdays[] per activity teacher (empty = all days) plus the additive teachers request field and legacy teacherIds compatibility.
  • Confirm the audience count endpoint and write-time-collapse redesign remain deferred.

11. Verification plan

  • Unit specs:
  • src/timetables/timetables.service.spec.ts: break selector validation/copy projection; activity teacher normalization, legacy compatibility, invalid subsets, full replacement and weekday-shrink reconciliation.
  • src/timetables/timetables.queries.spec.ts: TRACK/STUDENT break union resolution; weekday-specific activity snapshot teachers; copied child columns; teacher-view inclusion.
  • src/attendance/attendance-day.queries.spec.ts: duty TRACK/STUDENT names and activity supervisor filtering for the requested weekday.
  • src/attendance/attendance-access.service.spec.ts: same-day supervisor admitted, different-day supervisor refused, global assignment admitted.
  • Existing diagnostics and expected-schedule/expected-attendance specs: conflict, availability, budget and teacher-day behavior use only the weekday's supervisors.
  • E2E specs:
  • test/timetables.e2e-spec.ts: create/read/update a student/track break audience; create a two-day activity with different supervisors; duplicate/revision round-trip; weekday-specific diagnostics.
  • test/attendance-activities.e2e-spec.ts: each supervisor sees/opens only their assigned weekday's activity register; a supervisor assigned on another weekday receives 403.
  • test/db-constraints.e2e-spec.ts: widened break CHECK/index rejects malformed direct rows and the activity-teacher empty default preserves legacy/global assignments.
  • Manual verification: frontend two-step dialog creates one two-day activity, edits Tuesday supervision independently, reopens with the same selection, and shows the two teachers on their respective teacher-day agendas.

Per repository policy, implementation verification commands run only when explicitly requested.


12. Sign-off

  • Approved by: Fabio Barbieri
  • Date: 2026-08-26
  • Chat reference: “go on implementing” in chat after review of the draft design and ordered implementation plan.
  • Approved amendment: Fabio Barbieri approved the Prisma-compatible empty-array global sentinel in chat on 2026-08-26 after implementation review established that Prisma scalar lists cannot be optional. This replaces only the internal NULL = global representation; the public contract and all other approved decisions are unchanged.

Until this section is filled, no implementation code is written. When it is filled, flip frontmatter status: to Approved in the same edit.