Skip to content

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: inline origStartTimestamp/origEndTimestamp, invoice map field, payment/invoiceUpdateLogs subcolls).
  • 46 legacy sessions were still orphaned in the top-level Sessions/ collection (old shape: timestamps in an appointmentHistory subcoll; payment/rateCharged/sessionVoucher subcolls). The T-079 migration script skips docs lacking abbr/studentAbbr, which these all did.
  • 0 id-overlap between the two sets; per-student the true total = old + new (root totalSessions confirmed 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); resolveSessionPath for 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 (computeSessionStart gained a sessionPath param). New-shape awareness added: inline-timestamp fallback when there's no appointmentHistory; invoice detected via the map field.
  • Re-routed writers: components/StudentDialog/SessionDetail.tsx (voucher) and components/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-level Students/{id}/{sub}/{doc} rule can't reach that depth. Writes include bookkeeper-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 into Students/{abbr}/Sessions (mapped by sessionNameβ†’account; the 1 nameless doc mapped via its appointmentHistory.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 to scripts/migration-backups/ (gitignored) before any write.
  • scripts/backfill-student-session-summaries.ts β€” recomputed the cached totalSessions/proceeded/cancelled/jointDate/lastSession on each Students/{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-level Sessions/ = 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/appointmentHistory preserved) and a re-homed session with rateCharged+payment.
  • npx tsc --noEmit clean; new sessionsSource.ts lints clean. (Pre-existing repo-wide no-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 maps tebs-mel β†’ firestore.tebs-mel.rules), or via the repo's deploy-firestore-rules workflow. 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/* + the StudentDialog tabs now read Students/{abbr}/Sessions. Accounting/records (collectionGroup + findSessionRef) are unchanged and unaffected β€” they already matched both locations.
  • Firestore: top-level Sessions/ in tebs-mel is now empty (all sessions nested). Any new top-level reader would break β€” use getStudentSessionDocs (client) or collectionGroup('Sessions') / findSessionRef (server).
  • Dead legacy readers left in place (no callers, so harmless): lib/billing/balance.ts computeBalanceDue and lib/sessionStats.ts scanSessionsAndUpdateStudents β€” flagged for a future cleanup pass.
  • Related note: pages/api/accounting/matchable-coaching-payments.ts reads sessionData.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; ran rehome-orphan-coaching-sessions.ts --apply (46 re-homed, verified) and backfill-student-session-summaries.ts --apply (Nan/TC fixed). Status doing pending rules deploy + owner browser-verification. Commit(s) on branch claude/coaching-missing-data-4756ct.
  • 2026-07-01 MERGED (PR #830, squash 465fbab) + deployed to Vercel prod (READY) + rules auto-deployed via deploy-firestore-rules.yml (run success). AGENTS.md documented the rules-deploy pipeline (PR #831).
  • 2026-07-01 orphaned session-subcollection tail (owner: Jake/Geet showed 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-level Sessions/{id} doc + Sessions/{id}/appointmentHistory; T-079 phase B moved the DOCS nested but only carried payment/invoice/invoiceUpdateLogs β€” not appointmentHistory β€” and the pre-fix coaching UI wrote sessionVoucher to the top-level path. Deleting the top-level doc left those under phantom parents that collection('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 with scripts/merge-orphaned-session-subcollections.ts --apply (67 sessions, 154 docs incl. Geet's voucher + 153 appointmentHistory) β†’ nested; top-level Sessions/ 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.js still writes top-level; make it write nested (needs a clasp deploy by the owner) or re-run the merge script after scans β€” else appointmentHistory re-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 balance Progress bar 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 is done once merged to main (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 under Students/{abbr}/Sessions (top-level Sessions/ fully empty, collectionGroup count unchanged); stale summaries backfilled; the false-"amount-due" voucher tail (Jake/Geet) fixed by re-homing orphaned phantom subcollections; card layout stabilised; tebs-mel rules 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 prod tebs-mel and verified (backups in scripts/migration-backups/, gitignored).
  • Deploy state at close: the data fixes are live (Firestore writes take effect immediately) and the tebs-mel rules auto-deployed; the code (page re-route + card fix) is merged to main but awaits a manual Vercel deploy β€” owner chose to hold the deploy (2026-07-01). Marking done regardless, 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/*, StudentDialog tabs, CoachingSessionsApp) + tebs-mel session data (now single-source nested) + firestore.tebs-mel.rules. Accounting/records (collectionGroup + findSessionRef) unaffected. Durability follow-up (not blocking done): apps-script/SessionSync.js still writes sessions to the top-level path β€” a future clasp-deployed change (or periodic re-run of scripts/merge-orphaned-session-subcollections.ts) keeps appointmentHistory from 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), unfiltered tsc clean. Merged to main, not deployed (owner holding) β€” ships with the next Vercel deploy alongside the earlier card fix. Task stays done (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.js to write sessions to the canonical nested location. It now builds a per-run idβ†’path map via a Sessions collection-group scan (buildSessionPathCache_) and updates each session where it already lives β€” creating new ones at Students/{abbr}/Sessions/{id} (falling back to top-level only when the abbr can't be resolved) and appending appointmentHistory under 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.ts still cover any legacy stub. Deploy handoff: Apps Script deploys via clasp 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 to main; the owner must run clasp push from apps-script/ on their machine, then test (Apps Script editor β†’ run auditAllEvents β†’ confirm a session lands nested + a rescheduled event appends nested history). Not in the Next.js tsc scope (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-run tebs-mel: top-level Sessions/ = 0 docs / 0 phantom parents, collectionGroup('Sessions') still 113 (no dupes), per-student nested counts unchanged, 0 nested docs missing abbr, and existing sessions' updatedAt bumped 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 nested tebs-mel sessions, 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 one Students/{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: each get_event returns "entity not found", each carries sessionType:"Cancelled" (billing zeroes it β€” compute.ts type==='cancelled') and a type:"Deleted" appointmentHistory entry. Counts close exactly: 104 live + 9 deleted = 113 = collectionGroup('Sessions'); 0 top-level Sessions/, 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: sessionType mixes 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.

  • 465fbab 2026-07-01 β€” fix(coaching): re-route Coaching page onto nested Sessions + re-home orphaned legacy sessions (#830)
  • a5484f5 2026-07-01 β€” coaching: orphaned session vouchers/history recovery + card-height fix + rules-deploy docs (#831)
  • 4569b20 2026-07-01 β€” docs(agents,tasks): 'done' = merged-not-deployed policy; close T-146 (#832)
  • ec1c8ec 2026-07-02 β€” fix(coaching): overdue indicator as zero-height overlay, keep cards compact (T-146) (#842) (verified; post-done UI rework)
  • 81141a1 2026-07-02 β€” fix(apps-script): calendar sync writes sessions to nested Students/{abbr}/Sessions (T-146) (durability follow-up; deploy is a manual clasp push by the owner)
  • 5989980 2026-07-02 β€” Merge pull request #849 (apps-script nested-write fix)