Share viewer — WOPC client-side exception + coaching invoice "Document not found"
Why (owner, 2026-06-25)¶
"The current rendered short URL for coaching invoices and WOPCs do not work (Project invoice link in WOPC register works fine tho)"
Owner-supplied error messages:
- WOPC short URL → "Application error: a client-side exception has occurred while loading eop.theestablishers.com (see the browser console for more information)."
- Coaching invoice short URL → "Shared Document (page title) / Document not found / The referenced document no longer exists. Please contact the sender."
Project-invoice short URLs render fine on the same viewer.
Diagnosis¶
WOPC — SharedWopcView collided with React's reserved ref prop¶
The viewer renders:
SharedWopcView is a plain function component (not wrapped in
React.forwardRef). React intercepts the ref prop on EVERY element
as a forwarded-ref handler; the prop never reaches the component's
parameter list, leaving ref === undefined inside. The component then
passes that undefined straight into <iframe title={…}> AND surfaces
React's dev-mode warning ("Function components cannot be given refs").
The same component was used by the LEGACY long-URL viewer
(pages/view/document/[token].tsx) — both viewers carry the same
broken prop pattern.
Coaching — findSessionRef collection-group fallback missed nested sessions¶
fetchSharedCoachingInvoice(sessionId) → getCoachingInvoiceForSession
→ findSessionRef(db, sessionId) (no studentAbbr, because the short-
link record stores only sessionId). The fallback was:
coachingDb
.collectionGroup('Sessions')
.where(FieldPath.documentId(), '==', sessionId)
.limit(1)
.get()
For collection-group queries on documentId(), Firestore compares
against the FULL relative path (Students/{abbr}/Sessions/{id}), not
the trailing doc ID alone. Post-T-079, every active session lives at
Students/{abbr}/Sessions/{id} — so the query silently returned empty
for every nested session, the fetcher returned null, and the viewer
fell through to NotFoundView. Top-level legacy Sessions/{id} would
have matched (their relative path IS the doc id), but the coaching
sub-tab the user shares from is post-migration.
What shipped¶
components/share/SharedDocumentView.tsx¶
SharedWopcView's ref: string prop is renamed to documentRef:
string. Both internal usages (<HeaderBar reference={documentRef} />
and <iframe title={…documentRef} />) updated to match. Inline doc
comment explains the React reserved-prop trap.
pages/v/[id].tsx, pages/view/document/[token].tsx¶
Both viewers now spread documentRef={props.ref} to SharedWopcView
(was ref={props.ref}). The page-level props.ref name stays — that
field is the document reference string in the page's own props shape,
not a React ref — only the prop name on the child element changed.
lib/accounting/coachingSessions.ts¶
findSessionRef gains a manual-scan fallback after the documentId()
query returns empty:
const allSessions = await coachingDb.collectionGroup('Sessions').get()
for (const doc of allSessions.docs) {
if (doc.id === sessionId) return doc.ref
}
The fast path still runs first (so top-level legacy sessions resolve in
one query); only when both the studentAbbr path AND the
documentId() filter come up empty do we pay for the full scan.
Cost: the coaching-invoice no-login viewer is a low-rate path (one open per share-link click) and the manual scan reads the same collection group the matchable-coaching-invoices endpoint already walks — same shape of cost, well within budget for a viewer click.
Why this and not "just store studentAbbr in the short link"¶
Storing studentAbbr on the short-link record would have removed the
need for the slow-path scan, but:
- It wouldn't fix any of the 90-day-expiry short links already minted
without
studentAbbr— those are live in user CSVs right now and would keep failing until rotation. - It would mean a one-off mint-side change that's irrelevant once
every short-link record carries the field — a 90-day temporary
bandaid for what's actually a
findSessionRefcorrectness bug.
Fixing findSessionRef is correct everywhere it's called (1 viewer
path + 4 other consumers), past + future records included.
Decision log¶
2026-06-25 — T-123 opened + shipped¶
- ✅ Attestation (Records (Infrastructure)): read
AGENTS.md; no open task covered either symptom. WOPC viewer rendering is mine (items 1+2 / T-114). Coaching-session helpers are shared but the failure mode is viewer-specific (only this path calls withoutstudentAbbr). - Source: Records (Infrastructure) · https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD
- Owner direction (verbatim, 2026-06-25): quoted above.
- Tests + tsc:
npx tsc --noEmitclean. Live verification left to the owner — re-open both URL kinds. Expect a WOPC PDF iframe with a title + reference subtitle, and a coaching invoice rendered viaSharedCoachingInvoiceView(PaymentRequesttemplate). - Blast radius: prop rename touches only the
SharedWopcViewcomponent + its two call sites;findSessionRefchange is additive (extra fallback after the existing fast paths). The four other callers offindSessionRefall passstudentAbbrso their behaviour is unchanged.
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.
bc1af242026-06-25 — fix(records): service-invoice ZIP UX (T-122) + share viewer WOPC/coaching (T-123)9be22b92026-06-25 — Merge: T-122 service-invoice ZIP UX + T-123 share viewer WOPC/coaching fixesa98745b2026-06-25 — Merge main + renumber T-122 → T-126 (renumber-on-merge, parallel-agent T-122/123/124/125 collision) (mentions only)b9eef832026-06-29 — fix(viewer): rename ref→documentRef in ViewProps to prevent React prop extraction (T-123 follow-up)9c916db2026-06-30 — Merge PR #819: WOPC viewer fix + unified dates + batch-ZIP CORS + IR56M status mirror (mentions only)