Skip to content

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:

  1. WOPC short URL → "Application error: a client-side exception has occurred while loading eop.theestablishers.com (see the browser console for more information)."
  2. 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 pdfUrl={props.pdfUrl} ref={props.ref} title={} />

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)getCoachingInvoiceForSessionfindSessionRef(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.

Storing studentAbbr on the short-link record would have removed the need for the slow-path scan, but:

  1. 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.
  2. 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 findSessionRef correctness 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 without studentAbbr).
  • Source: Records (Infrastructure) · https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD
  • Owner direction (verbatim, 2026-06-25): quoted above.
  • Tests + tsc: npx tsc --noEmit clean. 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 via SharedCoachingInvoiceView (PaymentRequest template).
  • Blast radius: prop rename touches only the SharedWopcView component + its two call sites; findSessionRef change is additive (extra fallback after the existing fast paths). The four other callers of findSessionRef all pass studentAbbr so 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.

  • bc1af24 2026-06-25 — fix(records): service-invoice ZIP UX (T-122) + share viewer WOPC/coaching (T-123)
  • 9be22b9 2026-06-25 — Merge: T-122 service-invoice ZIP UX + T-123 share viewer WOPC/coaching fixes
  • a98745b 2026-06-25 — Merge main + renumber T-122 → T-126 (renumber-on-merge, parallel-agent T-122/123/124/125 collision) (mentions only)
  • b9eef83 2026-06-29 — fix(viewer): rename ref→documentRef in ViewProps to prevent React prop extraction (T-123 follow-up)
  • 9c916db 2026-06-30 — Merge PR #819: WOPC viewer fix + unified dates + batch-ZIP CORS + IR56M status mirror (mentions only)