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/:idadmit them viaAttendancePolicy+ Y-set) but have no read surface: every aggregate endpoint isAttendanceBoardPolicy= 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_CLASScohorts or every teacher batch tripsATTENDANCE_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.ts — isSchoolWideWriter |
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_uniquerectangle 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) |
AttendanceWriteResponseDto — group 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-dayJSDoc is FE-facing copy: the agenda contract, flag-vs-completeness distinction, echo-the-anchor instruction.- Widened
cohortTypeenums documented on both read filter and write DTOs; 403 example for the new code on/rows,/inconsistencies. - Envelope
groupcard 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.
lastMetOnon other-group cards — only if the UX proves to need it — follow-up: revisit after FE integration.- Write-envelope
followUpsgating for callers withoutmanage_communications— tables are empty until the comms-loop ships; gate then — follow-up: comms-loop spec. - Student/referent self surface —
/attendance/mename reserved for it — follow-up: its own spec (ch19 §5 footnote already parks it). - Admin
?teacherId=inspection onteacher-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;startTickordering; 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);recordedper mode;otherGroupsminus 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, outsiderATTENDANCE_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:loadCohortRosterIdsSUBJECT_GROUP/COMBINED_CLASS branches (as-of-D membership, union, NOT_FOUND).attendance.policy.spec.ts:AttendanceTeacherPolicy(teacher admitted, others fail closed),AttendanceCohortReadPolicybranches.- 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; followsfeedback_e2e_isolation_patterns+ the temporal fixture discipline — future-relative dates, self-provisioned students, name-prefix sweeps inafterAll): - teacher-day happy path: ordered slots, combined card, flags in a PERIOD and a DAILY department, counters before/after a take,
otherGroupscontent, 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.