Re-route the Coaching page onto the post-T-079 nested Sessions location + re-home the 46 orphaned legacy sessions
β Attestation¶
Read AGENTS.md; checked the board by scope (closest: T-079 the migration that caused this, and
T-052 stableId anchoring β neither covers re-pointing the coaching page at the moved data, so this
is not a duplicate). Tracking under this task, kept current.
Source: Coaching (Diagnostic) Β· https://claude.ai/code/session_01WfT2gXvbegVjgztHbHuGej
Why (owner, 2026-07-01 β verbatim)¶
"look into the Coaching page and the information that page fetch and write on Firestore, and see why some information are not being shown (I have previously migrated and restructured coaching related firestore docs, and apparently, some of the things didn't get re-write or re-routed afterwards)."
Escalated from ticket I-025 (the symptom). Approved scope (owner, via the diagnostic hand-off question, 2026-07-01): "Fix code + clean up data" β re-route the page reads/writes AND consolidate the orphaned sessions into one location (a backup-first Firestore migration).
Plain-language summary¶
Coaching data was moved by T-079 from one big Sessions/ collection into each student
(Students/{abbr}/Sessions), but the coaching page was never told β it kept reading the old, now
near-empty place, so migrated students showed blank/partial sessions, wrong balances, and no
vouchers/invoices. This makes the page read the new place (with a safe fallback to the old one), moves the
leftover sessions into the new place too, and repairs the cached per-student counts.
Root cause (live-data audit, tebs-mel, 2026-07-01)¶
- 67 sessions live at the canonical
Students/{abbr}/Sessions/{id}(post-T-079 shape: inlineorigStartTimestamp/origEndTimestamp,invoicemap field,payment/invoiceUpdateLogssubcolls). - 46 legacy sessions were still orphaned in the top-level
Sessions/collection (old shape: timestamps in anappointmentHistorysubcoll;payment/rateCharged/sessionVouchersubcolls). The T-079 migration script skips docs lackingabbr/studentAbbr, which these all did. - 0 id-overlap between the two sets; per-student the true total = old + new (root
totalSessionsconfirmed it for most). The coaching page read only the top-level orphans β missing data for the 6 migrated students (Geet/KT/MT/Nan/RYu/TC); the 5 never-migrated (Asa/Ceh/ET/ML/OJ) looked fine. - Accounting/records consumers were unaffected β T-079 had already moved them to
collectionGroup.
What changed¶
Code β every coaching-page session reader/writer now goes through one source¶
- New
lib/coaching/sessionsSource.tsβgetStudentSessionDocs(abbr, account)reads the canonical nested location plus a legacy top-level fallback (merged by id, nested wins);resolveSessionPathfor session-scoped writes. This is the single choke-point so a future move is a one-file edit. - Re-routed readers:
components/coaching/CoachingSessionsApp.tsx,components/StudentDialog/SessionsTab.tsx,lib/billing/compute.ts(buildContext),lib/coaching/useBillingInfo.ts(fetchVoucherInfo),components/StudentDialog/RetainersTab.tsx,lib/sessions.ts(computeSessionStartgained asessionPathparam). New-shape awareness added: inline-timestamp fallback when there's noappointmentHistory; invoice detected via the map field. - Re-routed writers:
components/StudentDialog/SessionDetail.tsx(voucher) andcomponents/StudentDialog/RateModal.tsx(rate) now write to the session's canonical nested path (resolved), not the old top-level path. (This also fixes a latent write bug: those writes were landing where nothing reads.) - The destructive summary write-back in
SessionsTab(it rewrote the student's totals from the old partial set) self-corrects now that it reads the full set. firestore.tebs-mel.rulesβ added an explicit rule for the nested session sub-subcollections (Students/{abbr}/Sessions/{id}/{appointmentHistory|payment|rateCharged|sessionVoucher|invoiceUpdateLogs}); the existing one-levelStudents/{id}/{sub}/{doc}rule can't reach that depth. Writes includebookkeeper-mel(billing edits), mirroring the legacy top-level Sessions rule.
Data (tebs-mel, backup-first, verified)¶
scripts/rehome-orphan-coaching-sessions.tsβ re-homed all 46 orphans intoStudents/{abbr}/Sessions(mapped bysessionNameβaccount; the 1 nameless doc mapped via itsappointmentHistory.client= Nancy Kwai), converting to the new shape (derived inline timestamps from the latest history entry; carried all subcollections), then deleted the source. Backup written toscripts/migration-backups/(gitignored) before any write.scripts/backfill-student-session-summaries.tsβ recomputed the cachedtotalSessions/proceeded/cancelled/jointDate/lastSessionon eachStudents/{abbr}root doc from the now-complete nested data (fixed Nan 21β22, TC 1β12; all others already correct).
Verification¶
- Post-migration
tebs-mel: top-levelSessions/= 0;collectionGroup('Sessions')= 113 (unchanged β no loss, no dupes); per-student nested counts all match the true totals (KT 50, Nan 22, MT 11, TC 12, RYu 3, OJ 4, ML 7, Asa/Ceh/ET/Geet 1). Spot-checked a re-homed cancelled session (inline times derived,sessionVoucher/appointmentHistorypreserved) and a re-homed session withrateCharged+payment. npx tsc --noEmitclean; newsessionsSource.tslints clean. (Pre-existing repo-wideno-explicit-any/ unused-var lint noise on the touched files was not introduced here and is out of scope.)- Not yet browser-verified (cloud agent can't log in) β owner to confirm on
/coaching.
Required follow-up (owner-gated)¶
- Deploy the tebs-mel Firestore rules:
firebase deploy --only firestore:rules(firebase.json mapstebs-melβfirestore.tebs-mel.rules), or via the repo'sdeploy-firestore-rulesworkflow. Until then, non-super-admin coaching users (e.g.bookkeeper-mel) will be denied the nested session sub-subcollection reads (rate/voucher/payment/history) β the catch-all rule still lets admin/super-admin through, so a super-admin owner sees everything immediately. The session list + balances that read the session doc itself work without the deploy. - App deploy is manual (main π‘π‘ = Vercel) β
curl -X POST "$VERCEL_DEPLOY_HOOK"when ready.
Blast radius (for other agents)¶
- Touches the coaching page surface only.
lib/billing/compute.ts+lib/coaching/*+ theStudentDialogtabs now readStudents/{abbr}/Sessions. Accounting/records (collectionGroup +findSessionRef) are unchanged and unaffected β they already matched both locations. - Firestore: top-level
Sessions/intebs-melis now empty (all sessions nested). Any new top-level reader would break β usegetStudentSessionDocs(client) orcollectionGroup('Sessions')/findSessionRef(server). - Dead legacy readers left in place (no callers, so harmless):
lib/billing/balance.tscomputeBalanceDueandlib/sessionStats.tsscanSessionsAndUpdateStudentsβ flagged for a future cleanup pass. - Related note:
pages/api/accounting/matchable-coaching-payments.tsreadssessionData.date(a field the new shape doesn't have) for ordinal sorting β pre-existing, accounting-side; not fixed here.
Log¶
- 2026-07-01 created from I-025; audited live
tebs-mel; shipped code re-route +sessionsSource.ts+ rules; ranrehome-orphan-coaching-sessions.ts --apply(46 re-homed, verified) andbackfill-student-session-summaries.ts --apply(Nan/TC fixed). Status doing pending rules deploy + owner browser-verification. Commit(s) on branchclaude/coaching-missing-data-4756ct. - 2026-07-01 MERGED (PR #830, squash
465fbab) + deployed to Vercel prod (READY) + rules auto-deployed viadeploy-firestore-rules.yml(run success). AGENTS.md documented the rules-deploy pipeline (PR #831). - 2026-07-01 orphaned session-subcollection tail (owner: Jake/
Geetshowed a false "amount due" though his only session has a voucher). Root cause: a subtler leftover of the same migration β the Apps Script calendar sync (apps-script/SessionSync.js) writes sessions to the top-levelSessions/{id}doc +Sessions/{id}/appointmentHistory; T-079 phase B moved the DOCS nested but only carriedpayment/invoice/invoiceUpdateLogsβ notappointmentHistoryβ and the pre-fix coaching UI wrotesessionVoucherto the top-level path. Deleting the top-level doc left those under phantom parents thatcollection('Sessions').get()doesn't return, so the earlier re-home missed them. The coaching page reads subcollections from the nested path, so Geet's orphaned voucher read as unpaid β false balance. Fixed withscripts/merge-orphaned-session-subcollections.ts --apply(67 sessions, 154 docs incl. Geet's voucher + 153 appointmentHistory) β nested; top-levelSessions/now fully empty (0 docs, 0 phantom subcolls). Verified: Geet voucherUsed=true β excluded from balance. Live-effective immediately (no deploy needed β the deployed reader already uses the nested path). Voucher writes won't re-orphan (UI now writes nested). Durability follow-up:SessionSync.jsstill writes top-level; make it write nested (needs aclaspdeploy by the owner) or re-run the merge script after scans β elseappointmentHistoryre-orphans for changed sessions (cosmetic: the detail-view timeline; does not affect billing). - 2026-07-01 UI (owner): the overdue red balance line expanded the card height.
StudentCard's balanceProgressbar is now always rendered (transparent when the balance is zero/unknown, red for amount due, green for credit) so every card is the same height with or without the line. Needs an app deploy to show.
2026-07-01 β β DONE (closing verdict)¶
- β
Attestation: read
AGENTS.md; closing this task per the owner's policy that a task isdoneonce merged tomain(deploy is not a prerequisite β decoupled/manual). Source: Coaching (Diagnostic) Β· https://claude.ai/code/session_01WfT2gXvbegVjgztHbHuGej - Outcome vs. plan: delivered in full β coaching page reads the canonical nested location via the shared
lib/coaching/sessionsSource.ts; all sessions consolidated underStudents/{abbr}/Sessions(top-levelSessions/fully empty,collectionGroupcount unchanged); stale summaries backfilled; the false-"amount-due" voucher tail (Jake/Geet) fixed by re-homing orphaned phantom subcollections; card layout stabilised;tebs-melrules extended (auto-deployed) + rules-deploy pipeline documented. - Finishing commit:
a5484f55(squash merge of PR #831 β orphaned-voucher/history recovery + card-height fix + rules-deploy docs). Prior landing commit:465fbab(PR #830 β the core re-route + re-home + rules). Data migrations were applied to prodtebs-meland verified (backups inscripts/migration-backups/, gitignored). - Deploy state at close: the data fixes are live (Firestore writes take effect immediately) and the
tebs-melrules auto-deployed; the code (page re-route + card fix) is merged tomainbut awaits a manual Vercel deploy β owner chose to hold the deploy (2026-07-01). Markingdoneregardless, per the merged-β -deployed policy now recorded in AGENTS.md. Residual owner steps: fire the deploy when ready + browser-verify/coaching. - Blast radius: coaching-page surface (
lib/coaching/*,lib/billing/*,StudentDialogtabs,CoachingSessionsApp) +tebs-melsession data (now single-source nested) +firestore.tebs-mel.rules. Accounting/records (collectionGroup +findSessionRef) unaffected. Durability follow-up (not blocking done):apps-script/SessionSync.jsstill writes sessions to the top-level path β a futureclasp-deployed change (or periodic re-run ofscripts/merge-orphaned-session-subcollections.ts) keepsappointmentHistoryfrom re-orphaning for changed sessions (cosmetic timeline only; never affects billing). - 2026-07-02 post-done UI rework (owner): the earlier card-height fix reserved vertical space in every
card so the overdue red line wouldn't change size β but that grew all cards. Reversed per owner: cards
keep their original compact size and the overdue/credit indicator is now a thin accent line
absolutely positioned at the card's bottom edge (zero height impact; red = due, green = credit).
ec1c8ec(#842), unfilteredtscclean. Merged tomain, not deployed (owner holding) β ships with the next Vercel deploy alongside the earlier card fix. Task staysdone(minor UI refinement; SHA appended to the index per the "record every related SHA" policy). - 2026-07-02 durability follow-up implemented (owner: "deploy that clasp-deployed fix"): rewrote the
calendar-sync Apps Script
apps-script/SessionSync.jsto write sessions to the canonical nested location. It now builds a per-run idβpath map via aSessionscollection-group scan (buildSessionPathCache_) and updates each session where it already lives β creating new ones atStudents/{abbr}/Sessions/{id}(falling back to top-level only when the abbr can't be resolved) and appendingappointmentHistoryunder that same path. This stops the re-orphaning at the source (a changed, previously-migrated session no longer spawns a duplicate top-level doc + orphaned history). Conservative fallbacks; the web-app dual-read +merge-orphaned-session-subcollections.tsstill cover any legacy stub. Deploy handoff: Apps Script deploys viaclasp push, which needs the script owner's Google login β I cannot do it from the cloud sandbox (no clasp CLI/.clasprc; env, GCP Secret Manager, and CI carry no clasp credential). The source is merged tomain; the owner must runclasp pushfromapps-script/on their machine, then test (Apps Script editor β runauditAllEventsβ confirm a session lands nested + a rescheduled event appends nested history). Not in the Next.jstscscope (ES5 Apps Script). - 2026-07-02 DEPLOYED + VERIFIED (owner ran
clasp push+auditAllEvents): the updated Apps Script is live and a full audit run wrote only to the nested location β post-runtebs-mel: top-levelSessions/= 0 docs / 0 phantom parents,collectionGroup('Sessions')still 113 (no dupes), per-student nested counts unchanged, 0 nested docs missingabbr, and existing sessions'updatedAtbumped to the run time (updated in place). Re-orphaning is now fixed at the source. T-146 fully closed β all items done, deployed, and verified. - 2026-07-03 independent end-to-end reconciliation (owner connected the live Google Calendar via a
connector; "go above and beyond and do further checking"): cross-checked the live Coaching calendar
(
c_cf5e78β¦fce6d) against the nestedtebs-melsessions, calendar-side rather than script-side. Result is a clean partition β 104 live confirmed calendar events in the sync window (2022-12β2026-06) each map by event-id to exactly oneStudents/{abbr}/Sessions/{id}doc (104/104, no misses), and every event's titleβabbr routing is correct (Kiri TβKT, Nancy Kwai[/(FaceTime)]βNan, Jake NgaiβGeet, β¦). The 9 nested sessions with no live event are precisely the deleted ones: eachget_eventreturns "entity not found", each carriessessionType:"Cancelled"(billing zeroes it βcompute.tstype==='cancelled') and atype:"Deleted"appointmentHistoryentry. Counts close exactly: 104 live + 9 deleted = 113 =collectionGroup('Sessions'); 0 top-levelSessions/, 0 phantom parents, 0 live event mis-flagged Cancelled, 0 ghost session billed. Confirms the deployed sync writes correctly nested, end-to-end. (Cosmetic-only observation, not fixed:sessionTypemixes case βPhysicalΓ38 /physicalΓ64; harmless because every reader lowercases before comparing.)
Commit index (backfilled 2026-07-01, best-effort Β· Coaching (Diagnostic))¶
Candidate related commits, auto-backfilled from git on main: commits whose message references this task's UID or a PR number it cites. Not verified β this squash-merged history can't yield a precise per-task list, so rows tagged (mentions only) name the task in passing (may be tangential) and untagged work commits may be missing. Treat as a starting point: verify, prune tangential rows, and append any real ones per the AGENTS.md "record every related SHA" policy.
465fbab2026-07-01 β fix(coaching): re-route Coaching page onto nested Sessions + re-home orphaned legacy sessions (#830)a5484f52026-07-01 β coaching: orphaned session vouchers/history recovery + card-height fix + rules-deploy docs (#831)4569b202026-07-01 β docs(agents,tasks): 'done' = merged-not-deployed policy; close T-146 (#832)ec1c8ec2026-07-02 β fix(coaching): overdue indicator as zero-height overlay, keep cards compact (T-146) (#842) (verified; post-done UI rework)81141a12026-07-02 β fix(apps-script): calendar sync writes sessions to nested Students/{abbr}/Sessions (T-146) (durability follow-up; deploy is a manualclasp pushby the owner)59899802026-07-02 β Merge pull request #849 (apps-script nested-write fix)