Skip to content

Teacher attendance surface — slot-grouped day board + narrowed write authority


1. Problem distillation

  • Teachers already hold full write mechanics on the register (POST /records, PATCH /records/:id admit them via AttendancePolicy + Y-set) but have no read surface: every aggregate endpoint is AttendanceBoardPolicy = admin/secretary/principal only. A teacher cannot even obtain the BE-minted anchors a write requires.
  • The admin board's unit (homeroom / grade-group roster cards) is wrong for teachers. Their day is an agenda of teaching slots: "these are the groups you have today, in order; these carry the register duty; and here is everything else you teach."
  • The duty flag is an obligation marker derived from Department.attendanceMode, not a completeness meter: PERIOD → every lesson slot; DAILY → only the slot that opens the day for a student. Completeness ("taken / N missing") is a separate, coexisting signal.
  • Acting on cells must be identical to the admin surface — same rows view, same write endpoints, same envelope — which forces one real gap closed: SG rosters span homerooms, so the write path needs SUBJECT_GROUP/COMBINED_CLASS cohorts or every teacher batch trips ATTENDANCE_ENTRIES_SPAN_GROUPS.
  • Product re-ruling on write authority (Fabio, 2026-08-02): a teacher may write only cells of slots they teach, plus their homeroom class as tutor. The Y-set's third clause (teaches-the-student-same-day) is deleted.

Success criteria (observable behavior that proves this works): - A teacher calls GET /attendance/teacher-day and receives their lesson slots for the date ordered by startTick — combined meetings as one card — each carrying needsAttendance, rosterSize/recorded, and a cohort selector; plus every other SG they teach on that date. - In a PERIOD department every slot of theirs is flagged; in a DAILY department only slots that open the day for ≥1 rostered student are flagged, regardless of what has been recorded. - Opening a card = GET /attendance/rows?cohortType=SUBJECT_GROUP|COMBINED_CLASS returning the admin-identical rows DTO (full day strips + register[]); a teacher opening a group they do not teach on that date gets 403 ATTENDANCE_NOT_AUTHORIZED_FOR_COHORT; admins can use the new cohorts tenant-wide. - A teacher batch write declaring their SG/combined cohort passes the group fence and returns the write envelope with the group card recounted over that cohort. - A teacher writing a cell of a slot they do not teach (and are not tutor for) is refused 403 — including the previously-allowed same-day-teacher case. - Admin surfaces (/groups, /groups/pending, follow-ups) are byte-identical for admin callers; teachers remain excluded from them.

Non-goals (in-scope-shaped things this iteration is explicitly not doing): - Activity slot cards on the teacher day board (deferred; duty attribution still accounts for activity geometry). - A homeroom-tutor aggregate view (HOMEROOM-cohort rows for the tutor). - Teacher access to follow-ups / manage_communications. - The student/referent self attendance surface (/attendance/me namespace deliberately left free for it). - lastMetOn on other-group cards; admin ?teacherId= inspection on teacher-day.


2. Patterns survey

Analogous module/spec What we'd borrow What doesn't fit
src/attendance/attendance-board.service.ts Single-assembly rule: resolve the date once via ExpectedAttendanceResolver, project every widget from it; the day-records read that feeds counters; clock + AY resolution Its unit of answer is a roster-group card built from loadGroupCatalog; the teacher's unit is a slot, so the assembly is a sibling service, not a board mode
src/attendance/attendance.policy.ts definePolicy grant-shaped role branches; the documented reasoning for splitting record vs aggregate policies AttendanceBoardPolicy must stay admin-only for /groups+pending; the two new policies are siblings, not edits to it
src/attendance/attendance.service.tsisSchoolWideWriter Provenance-based school-wide check (recompile from non-teacher, non-parameterized roles), incl. the parameterized-role fail-closed stance It answers WRITE; the rows/inconsistencies gate needs the same question for READ — generalize the helper, don't duplicate it
src/grades/grades.controller.ts /grades/me, src/teachers/teachers.controller.ts /teachers/me The codebase's me convention = "the calling person's own record" Exactly why the teacher agenda is not /attendance/me/... — that name belongs to the future student/referent self surface; route is GET /attendance/teacher-day
docs/superpowers/specs/2026-07-25-attendance-admin-day-register-design.md Cohort selector (cohortType+cohortId) as the universal group key; the write envelope contract ({ records, group, inconsistencies, followUps }) Cohort enum was HOMEROOM/GRADE_GROUP only; this spec widens it
src/attendance/expected/expected-attendance.resolver.ts As-of-date slots with teacherIds (C3 episodes) — "my slots today" is a pure filter over it; byStudent gives first-slot-of-day for DAILY duty attribution DaySlotRow carries neither the slot's department (→ mode) nor a combined slot's member-SG ids — filled by one SG-meta query, not by widening the resolver contract

3. Architecture mapping

Primitive Apply? How Justify
Tenant scope yes Every new query tenant-filtered; routes under @Controller('attendance') + @ProtectedResource(); no new tables so RLS coverage unchanged Standard
Academic-year scope yes Date → AY resolution identical to the board; teaching episodes and rosters are AY-bound by their own tables Standard
RBAC entity key existing attendance No delta to entity-keys.ts Reads gate on attendance.read, writes on register:write + take, all existing
Scopes existing register No new scopes Teacher preset already holds it
Actions none take reused as-is No new verbs
Service base custom New AttendanceTeacherService (aggregate read, no CRUD base) beside AttendanceBoardService/AttendanceInsightsService — module's established shape Aggregate module, not entity CRUD
queries.ts shape named functions New: loadTeachingEpisodesOn (SG ids taught by teacher on D), loadSubjectGroupDayMeta (dept id + attendanceMode + labels for slot SG ids), loadCombinedMemberSgIdsOn; extended: loadCohortRosterIds gains SUBJECT_GROUP/COMBINED_CLASS branches (as-of-D, memberships∩placed, union across combined members) Mandatory queries.ts convention
Error codes one new ATTENDANCE_NOT_AUTHORIZED_FOR_COHORT — 403, params: { cohortType, cohortId }, messages { en_US, it_IT } Distinct from NOT_FOUND (cohort exists; caller lacks it)
DTO conventions new + widened New dto/teacher-day-response.dto.ts (+ query DTO with optional @IsDateOnly date); DayFilterQueryDto.cohortType and TakeAttendanceDto.cohortType widened to the 4-value enum; UpdateAttendanceCellDto gains optional cohortType+cohortId echo (both-or-neither) Contract-only JSDoc (Swagger rule)
File-backed sub-resources n/a No files
Custom fields no Aggregate DTOs, no person-scope payloads n/a
Profile completeness no n/a

4. Data model plan

Schema deltas

  • None. No new tables, columns, or enums.

Migration shape

  • No migration.

Indexes and uniqueness

  • None. All new reads ride existing indexes (attendance_cell_unique rectangle reads, episode tables' interval indexes).

5. API surface

Verb Path Decorators Request DTO Response DTO
GET /attendance/teacher-day @RequireScopes(ATTENDANCE, 'read'), @AppliesPolicy(AttendanceTeacherPolicy), @AggregateResponse() TeacherDayQueryDto { date? } (default school-today) TeacherDayResponseDto
GET /attendance/rows unchanged except @AppliesPolicy(AttendanceCohortReadPolicy) DayFilterQueryDto (cohortType widened) AttendanceGroupRowsDto (unchanged shape)
GET /attendance/inconsistencies unchanged except @AppliesPolicy(AttendanceCohortReadPolicy) DayFilterQueryDto (widened) InconsistenciesResponseDto (unchanged)
POST /attendance/records unchanged TakeAttendanceDto (cohortType widened) AttendanceWriteResponseDtogroup card may now be SG/CC-shaped
PATCH /attendance/records/:id unchanged UpdateAttendanceCellDto + optional cohort echo AttendanceWriteResponseDto

TeacherDayResponseDto{ date, today: TeacherDaySlotDto[], otherGroups: TeacherTaughtGroupDto[] }; today ordered by startTick then label, otherGroups by name.

TeacherDaySlotDto{ anchor { subjectGroupId? ⊕ combinedClassId?, startTick } (BE-minted), startTime, endTime, durationMinutes, label, roomId, roomName, teacherIds, teacherNames, cohort { cohortType: 'SUBJECT_GROUP'|'COMBINED_CLASS', cohortId }, attendanceMode, needsAttendance, rosterSize, recorded }.

TeacherTaughtGroupDto{ subjectGroupId, name, subjectName, subjectCode, gradeName, departmentName, rosterCount, cohort }.

Pinned semantics: - needsAttendance — static duty marker, records never consulted. PERIOD dept → true on every lesson slot. DAILY dept → true iff this slot is the first slot of the day for ≥1 rostered student, first-slot computed over ALL slot kinds including activities (an activity opening a student's day carries the day-opener duty even though activity cards are deferred). - recorded — completeness signal, independent of the flag. PERIOD: roster students with a stored cell at this slot's tick. DAILY: roster students with ≥1 authored cell anywhere that date. - Date navigation — any date renders (past = agenda as it was, resolved on-D; future = preview; writes still refused by the future-date gate). Reads never 409; an uncovered date yields empty today (and otherGroups still answers from episodes). - No teacher profile in the AY → { today: [], otherGroups: [] } (fail-soft). - Combined dedup — a member SG whose meeting surfaced today as a combined card does not reappear in otherGroups. - Own-group gate (rows + inconsistencies, non-school-wide readers only): cohort must be SUBJECT_GROUP/COMBINED_CLASS and the caller holds a teaching episode effective on the queried date for the SG (any member, for a combined cohort); else 403 ATTENDANCE_NOT_AUTHORIZED_FOR_COHORT. School-wide readers (provenance-checked, READ-generalization of isSchoolWideWriter) bypass; parameterized-role holders do not (fail-closed, same §7 ch19 reasoning). - PATCH cohort echo — both-or-neither; the cell's student must belong to the declared cohort on the record's date (else 422 ATTENDANCE_ENTRIES_SPAN_GROUPS); omitted → existing snapshot-derived group.

Swagger considerations

  • teacher-day JSDoc is FE-facing copy: the agenda contract, flag-vs-completeness distinction, echo-the-anchor instruction.
  • Widened cohortType enums documented on both read filter and write DTOs; 403 example for the new code on /rows, /inconsistencies.
  • Envelope group card notes the SG/CC-shaped variant.

6. RBAC seed plan

Seed file Delta
PermissionScope (rbac-catalogue.ts) none
PermissionAction (rbac-catalogue.ts) none
ScopeFieldMapping (rbac-catalogue.ts) none — all new/changed routes are @AggregateResponse()
Role grants (roles.ts) none — teacher already holds attendance.register + take
*_SCOPES runtime constant none

No reseed required in any environment.


7. Divergence ledger

Pattern We diverge by Reason Tradeoff accepted
ch19 §8: aggregate reads are admin-only; "scoped reads come back with an access-scope resolver" Admitting teachers to /rows + /inconsistencies via a new policy + in-service own-group assertion rather than a policy where The surfaces are aggregate — there is no record query for a where to narrow; the anticipated "resolver" lands as a cohort-level gate Authorization logic lives in the service (mirrors the Y-set precedent); pinned by policy + service specs
ch19 §7 ruling: "write authority is broader than read visibility" (3-clause Y-set) Deleting clause 3 (teaches-the-student-same-day); teachers keep slot-teacher (as-of-D) + homeroom tutor (live identity) Product re-ruling by Fabio 2026-08-02: a same-day teacher must not modify other slots' cells Witnessed-event back-fill by a same-day colleague is lost; the slot's own teacher, the tutor, or a school-wide writer must record it. admin_override audit verb fires more often, correctly
Board DTO family (/groups cards) New sibling endpoint with a slot-agenda DTO instead of a board groupBy mode Unit of answer differs (slot vs roster group); folding would fork AttendanceGroupBoardDto semantics per caller One more endpoint + DTO family to maintain
toRegister (live, elapsed-only, cell-counting) needsAttendance is static geometry; completeness is a separate recorded counter Fabio ruling: duty ("you MUST take care of this group today") ≠ completeness ("not complete, but not yours to take") FE renders two signals; neither substitutes for the other

8. Pushback log

US says Conflicts with Proposed instead Status
"Groups should be flagged as needs-to-take-attendance" (ambiguous live-vs-static) Live completeness would duplicate toRegister and turn the duty flag off when a colleague takes it Static mode-derived duty flag + separate recorded counter Resolved (chat 2026-08-02)
"To act on cell the surface should be identical to admins" SG rosters span homerooms → teacher batches trip the HOMEROOM/GRADE_GROUP group fence Widen cohortType on write to SUBJECT_GROUP/COMBINED_CLASS; envelope recounts over the declared cohort Resolved
"Teacher cannot write others' cells, only their own" Existing Y-set clause 3 deliberately allowed same-day teachers to write any cell of a taught student BE narrowing to clauses 1+2 (not an FE-only affordance) Resolved — explicit ruling
Activities initially in scope ("all three in") Fabio reversed: activities deferred from the day board; duty attribution still respects activity geometry Resolved

9. Deferrals

  • Activity cards on teacher-day — supervisors currently have no surface for their own activity registers — follow-up: next iteration of this spec.
  • Homeroom-tutor aggregate view (HOMEROOM-cohort rows for the tutor; they keep per-cell write authority) — follow-up: revisit with the tutor/pastoral surface.
  • lastMetOn on other-group cards — only if the UX proves to need it — follow-up: revisit after FE integration.
  • Write-envelope followUps gating for callers without manage_communications — tables are empty until the comms-loop ships; gate then — follow-up: comms-loop spec.
  • Student/referent self surface/attendance/me name reserved for it — follow-up: its own spec (ch19 §5 footnote already parks it).
  • Admin ?teacherId= inspection on teacher-day — route name chosen so this extends without a rename — follow-up: on demand.

10. Open questions

None — all resolved in chat 2026-08-02 (Q1–Q9 ledger in §1/§7/§8).


11. Verification plan

  • Unit specs:
  • attendance-teacher.service.spec.ts (new): slot filter by caller's as-of-D teacher id; startTick ordering; combined meeting as one card; PERIOD flag always-true; DAILY first-slot attribution incl. a student whose day opens with an activity (my lesson NOT flagged) and one whose day opens with my slot (flagged); recorded per mode; otherGroups minus today's SGs incl. combined members; empty-profile fail-soft; date default = school-today.
  • attendance-authority.spec.ts: re-pinned to the two-clause Y-set; the same-day-teacher case now denied.
  • attendance.service.spec.ts: SG/CC cohort fence (member ok, outsider ATTENDANCE_ENTRIES_SPAN_GROUPS); envelope group card shaped by declared cohort; PATCH echo validation (both-or-neither, membership check); teaches-map no longer loaded on either write path.
  • attendance.queries.spec.ts: loadCohortRosterIds SUBJECT_GROUP/COMBINED_CLASS branches (as-of-D membership, union, NOT_FOUND).
  • attendance.policy.spec.ts: AttendanceTeacherPolicy (teacher admitted, others fail closed), AttendanceCohortReadPolicy branches.
  • Board/insights service specs: own-group gate — teacher + own SG passes, foreign SG 403, school-wide bypass, parameterized-role fail-closed.
  • E2E specs (test/attendance-teacher.e2e-spec.ts, new; follows feedback_e2e_isolation_patterns + the temporal fixture discipline — future-relative dates, self-provisioned students, name-prefix sweeps in afterAll):
  • teacher-day happy path: ordered slots, combined card, flags in a PERIOD and a DAILY department, counters before/after a take, otherGroups content, date navigation to a past lesson day.
  • rows: teacher on own SG cohort (admin-identical shape), foreign SG → 403 new code, admin on SG cohort tenant-wide.
  • write: SG-cohort batch → envelope group card over the SG; combined-cohort batch over the union roster.
  • authority: teacher writing another slot's cell same day → 403 (the deleted clause-3 case); tutor writing own class's cell → allowed.
  • Manual verification: none beyond E2E; gates (npm test, npm run test:e2e) run by Fabio.

12. Sign-off

  • Approved by: Fabio Barbieri
  • Date: 2026-08-02
  • Chat reference: "go on with the plan" after section-by-section walkthrough (Q1–Q9 ledger) in chat 2026-08-02

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