Cleanup map (revised v2) · SHOT Clubhouse · feat/perform-redesign

What's actually legacy — and what we mistook for it

Stevie pushed back I never interact with /assess as a coach — is it superadmin? /clubs/X 404s for me. Do proper in-depth research with ast-grep, find all call hierarchy + routes + everything possible, update this with the revised understanding.
Revision note · 2026-05-18 · v3 — FLAT vs NESTED routing dimension found v1 (first pass) called features/assess/ legacy — wrong; it's live. v2 (route-guard audit) made it SHARED — half wrong. v3 (this version): you pushed back with real URLs (/perform/events/:id/draft) and revealed the missing dimension. Two parallel /perform event URL trees exist: FLAT /perform/events/:id/* mounts EventDetailPage (the canonical coach page); NESTED /perform/clubs/.../events/.../ mounts AssessComponents.* (effectively orphan — only reached via internal cascades). Coaches universally use FLAT. So 4 more pages move into the retireable column: DraftEvent, PublishedEvent, SessionManagement, ReviewEvent. Real shared set shrinks to just CreateEvent + CoachEvaluation + 3 pre-eval pages.
routed /assess routes
12
11 × role:superadmin · 1 × role:coach
external pushers
0
zero history.push into /assess from outside features/assess/
retireable pages
10 of 15
SHARED 2 · ORPHAN/LEGACY 10 · ATHLETE-FACING 3
features/assess external usage
59
services 30 · types 10 · components 9 · utils 5 · etc.
00

Method — how this revised picture was derived

Tool inventory + reproducible commands, so the next reader can verify or extend the audit.

What ran (read-only)

All searches scoped to src/ on feat/perform-redesign at HEAD. Backed by ast-grep 0.39.5, ripgrep 14.1.1, fd 10.3.0. No code changes; outputs captured to .agents/scratch/structure-research/ (gitignored).

  • Route inventoryast-grep --lang ts -p '{ path: $PATH, $$$ }' across all src/routes/*.ts, then grep'd path:/guard:/component: triplets to flatten into a table.
  • Component barrel introspection — read src/routes/components.ts for AssessComponents/CoachComponents/V2Components/TeamComponents; each lazy-import resolves a page to a route mount.
  • Route × component matrix — for each AssessComponents.<Name>, counted occurrences in coachRoutes.ts (legacy), v2Routes.ts (new perform), protectedRoutes.ts (athlete-facing). Three counts > 0 = shared; one tier only = legacy or athlete-facing.
  • External-nav detectionast-grep --lang ts -p 'history.push(`$_/assess/$_$$$`)' + history.replace + <Link to=> grep, all filtered to exclude src/features/assess/ + src/routes/. Result: zero external nav into /assess.
  • Dead-code per page — for each features/assess/pages/*.tsx: count route mounts (grep'd routes/*.ts) + external importers (grep filtered out features/assess/). Result: every page is routed.
  • Submodule fan-out — per features/assess/<sub>/ directory: count external importers via grep -rln "from '@/features/assess/<sub>. Result: services/ 30, types/ 10, others ≤9.
  • pages/section/Coach live-vs-dead — 217 .tsx files; only 57 referenced in routes/components.ts barrels. ~160 are sub-components or orphans.
01

The three layers — corrected

First pass had features/assess/ in the LEGACY tier. The route × importer evidence puts it in SHARED instead — it's actively serving both coach and superadmin route trees, plus the athlete-facing pre-eval flow.

Canonical new work

"v2 layer"

Where any new Perform-area feature should land. Page under pages/v2/<area>/; components from components/v2/<area>/.

  • pages/v2/perform/ (route targets)
  • components/v2/perform/coach/
  • components/v2/perform/player/
  • components/v2/perform/parent/
  • components/v2/perform/shared/
  • components/v2/perform/report/

The Perform Redesign shipped entirely here.

SHARED live, dual-mounted

"event + eval data layer"

What I previously called "legacy assess." Actually the live event + evaluations module — its pages are mounted at BOTH /perform (coach) AND /assess (superadmin inspect), and its services are imported by 30 external files.

  • features/assess/services/ — 30 importers (events/, team/, perform v2)
  • features/assess/types/ — 10 importers (shared interfaces)
  • features/assess/pages/* 6 SHARED + 3 athlete pre-eval
  • features/assess/components/ — 9 importers

Cannot delete. Could RENAME for clarity.

Truly LEGACY retireable

"the actually retireable bit"

11 superadmin-only /assess routes + 6 pages that ONLY those routes mount. Zero coach exposure. Deleting them removes a parallel inspect surface and ~6 page files of orphan code.

  • AssessDashboard, PreEvaluationResponses
  • PlayerAnalytics, ParentDashboard
  • InvitePlayers, ManagePlayers
  • + components/shadow/ (102 files) — separate retirement
  • + components/v2/coach/ orphans (5 of 6 files)
  • + components/v2/player/ orphans (10 of 12)
  • + ~160 unrouted pages/section/Coach/ files

Can delete. Most of pages/section/Coach needs per-file audit.

02

Route × Component matrix — with effective coach reachability

v2 of this matrix added a column you can't get from route tables alone: effective reachability — does any code path in the coach flow actually navigate to that route? A route mounted but never pushed-to is effectively orphan even when role-gated to coach.

The missing dimension: the /perform tree has BOTH a FLAT URL shape /perform/events/:id/* (mounts EventDetailPage from pages/v2/perform/events/ — the canonical coach event surface delivered in the redesign) AND a NESTED URL shape /perform/clubs/:clubId/teams/:teamId/events/:eventId/* (mounts AssessComponents.*). Every coach-flow caller uses FLAT. The NESTED routes are dual-mounted but unreachable except via DraftEventPage's internal cascades.

Component (page) /assess (superadmin) /perform NESTED /perform FLAT Coach reaches? Status
CreateEventsuperadmin × 3coach × 1YES — only mountSHARED · live
CoachEvaluationcoach × 2coach × 1YES — coach clicks EvaluateSHARED · live
EventDetailPage
(pages/v2/perform/events/)
coach × 2 (draft + published)YES — canonical coach event pageCANONICAL
DraftEventsuperadmin × 1coach × 1NO — FLAT preemptsEFFECTIVELY ORPHAN
PublishedEventsuperadmin × 1coach × 1NO — FLAT preemptsEFFECTIVELY ORPHAN
SessionManagementsuperadmin × 1coach × 1NO — only via DraftEvent cascadeEFFECTIVELY ORPHAN
ReviewEventsuperadmin × 1coach × 1NO — only via DraftEvent cascadeEFFECTIVELY ORPHAN
AssessDashboardsuperadmin × 1NOLEGACY
PreEvaluationResponsessuperadmin × 1NOLEGACY
PlayerAnalyticssuperadmin × 1NOLEGACY
ParentDashboardsuperadmin × 1NOLEGACY
InvitePlayerssuperadmin × 1NOLEGACY
ManagePlayerssuperadmin × 1NOLEGACY
PreEvaluationLanding/pre-eval × 1YES (athlete)ATHLETE-FACING
PreEvaluationQuestions/pre-eval × 1YES (athlete)ATHLETE-FACING
PreEvaluationComplete/pre-eval × 1YES (athlete)ATHLETE-FACING

How the FLAT preemption works: Stevie's actual coach flow shows the redesign settled on FLAT URLs (/perform/events/:id/draft). Every navigation in the codebase pushes to FLAT — EventList, EventsSectionContainer, CompactEventCard, LinkedEventCard, CreateEventPage's post-create redirect, even features/assess pages push to FLAT. The NESTED route shape (/perform/clubs/.../events/.../*) exists in v2Routes.ts but has no callers outside DraftEventPage's internal state-cascade. Only CreateEvent (mounted at NESTED /events/create) and CoachEvaluation (mounted at NESTED /events/.../evaluate) are still on the coach hot path.

Confirmed via grep: 8+ callers push to /perform/events/:id/draft (FLAT) — EventList, EventsSectionContainer, CompactEventCard, LinkedEventCard, MemberEventDetails, CoachEvaluationPage, CreateEventPage post-create redirect. Only DraftEventPage itself pushes to NESTED paths (and only internally between session/evaluate/review states it'll never reach if nothing lands on it).

03

Where each file goes — diagram (revised)

Top-down stack with the corrected tiering. features/assess/ moves UP from legacy into a new SHARED tier — it's the live event+eval module, dual-mounted by superadmin /assess and coach /perform routes.

CANONICAL new perform redesign work SHARED dual-mounted event + eval data LEGACY retireable superadmin / orphan v2Routes.ts 26 role:coach routes pages/v2/perform/ CoachPerform · PlayerPerform · ParentPerform components/v2/perform/ coach · player · parent · report · shared hooks/ useTeamFrameworkGaps · useAthleteIDPData foundation/ auth · premium · communication features/assess/services/ 30 external importers event + eval data layer features/assess/pages/ 6 SHARED · 3 athlete-facing mounted by both routes features/assess/types/ 10 external importers shared interface contract features/assess/{components, hooks, utils, config}/ 9 + 2 + 5 + 3 external importers · 14 #shame:blocked markers (FK-embed family — Rank 2) /perform/clubs/.../events/* (coach) /pre-evaluation/* (athlete) 11 × /assess routes role:superadmin only parallel inspect surface 6 LEGACY-only pages AssessDashboard, Analytics … delete with their routes components/shadow/ 102 files · 9 consumers web-components experiment components/v2/coach/ 5 of 6 orphan pre-redesign attempt components/v2/player/ 10 of 12 orphan pre-redesign attempt pages/section/Coach/ 217 files · 57 routed ~160 unrouted (audit) /club/.../assess/* (superadmin inspect) 0 external nav · 11 routes · safe to retire canonical → shared (imports) legacy route → shared page (dual mount) page → mount point

The middle yellow tier is what changed. Previously placed in LEGACY (red); evidence shows it's actually dual-mounted live code. The dashed clay arrow shows the parallel-inspect dependency — superadmin /assess routes point at the same pages the coach /perform routes do.

04

Cleanup ranked (revised)

Same six ranks, but Rank 6's scope shrank dramatically once the route-component matrix surfaced that features/assess/ isn't retireable. Ranks 1–5 unchanged; Rank 6 split into a small "retire superadmin inspect surface" + a separate per-file audit of pages/section/Coach/.

~15 min LOW IMPACT zero risk
1

Delete #shame:deprecated items

Two pieces of dead code marked deprecated; today they're just clutter.

What's degraded

Nothing for users. Devexp only — grep false-positives + reading past dead code.

Fix

git rm two functions; verify zero hits; commit.

#shame:deprecated × 2 features/assess/utils/eventParticipants.ts
~6–12 hr HIGH IMPACT user-visible perf
2

Resolve the public_profiles FK-embed family

⬆ Bumped in urgency — these markers live in features/assess/services/ which has 30 external importers. The N+1 is paid TODAY by every coach + athlete that hits an event detail page, the Pulse feed, attendance tracking, or invites.

14 of 20 #shame:blocked markers are the same problem: public_profiles view doesn't carry FK metadata, so PostgREST FK-embed fails and every name+avatar join becomes a two-step query.

User-visible perf hit (per surface)
  • Attendance tracking — ~400 ms vs ~200 ms
  • Pulse comment likers sheet — wasted 4xx + ~100 ms
  • TeamManagement — ~150 ms → ~450 ms TTFB on 30-athlete teams
  • Invite code redemption — 2 extra queries per code
  • Parent dashboard — linear scaling with # of kids
Root cause

PR #2471 dropped blanket SELECT policy + introduced public_profiles view. PostgREST FK-embed only resolves on physical FKs, not view-projected. Code falls back to two-step + JS stitch.

Fix path

A. SECURITY DEFINER RPC per query — BR1/BR2 pattern.

B. Lift PostgREST FK metadata via grants.

C. useProfileEmbed hook abstraction (hides ugliness, no perf gain).

Dependencies

None for A or C. Ships per rls-stack-sequenced-merge rule with staging soak.

#shame:blocked × 14 features/assess/services/ 30 importers depend on this
~2–3 hr MED IMPACT visual consistency
3

Migrate 4 Alex Tailwind→CSS-var inconsistencies

Alex AI assistant — 4 spots using raw Tailwind / inline hex instead of --shot-* tokens. No visible bug today; breaks on theme switch or rebrand.

Where the 4 are
  • AlexHeader.tsx:142 — IconButton Tailwind
  • AlexTabs.tsx:46 — inactive pill Tailwind
  • HelpStepsCard.tsx:41 — inline hex
  • quickChips.ts:90 — unused priorityRank
Fix

Read surrounding pattern; swap to var(--shot-...). For quickChips: wire or drop.

#shame:debt × 4features/alex/
~15–25 hr HIGH IMPACT 102 files reclaimed
4

Retire src/components/shadow/ entirely

102 files of Shadow-DOM web-components. Two API styles for the same idea — documented foot-gun. Only 9 files import from it.

Tiered consumers

Tier 1 (user-facing): Login, PerformPageLayout (every /perform render!), ChildManagement.

Tier 2 (admin/showcase): ShotsArticleEditor, 2 DesignSystem sections.

Tier 3 (legacy coach): EditTeamSettings, TeamFlat, TeamFrameworkSettings.

Fix path

Build v2/ui equivalents first; migrate Tier 1 with auth-flow regression tests; Tier 2 then 3; delete shadow/ only after all 9 consumers verified gone.

102 files9 live consumers
~2–3 hr MED IMPACT bundle + grep cleanup
5

Audit + delete orphan v2/coach + v2/player files

Older v2 attempt (pre-Perform-Redesign). Verified zero-importer files via grep audit.

The orphans (verified)
  • v2/coach: 5 of 6 zero-importer
  • v2/player: 10 of 12 zero-importer
  • v2/coach/CoachQuickActions — 1 importer (verify before delete)
Fix

Per-file knip audit, delete, run vitest + vite build.

15+ orphan files
~3–4 hr MED IMPACT REVISED ↓ from 20–40 hr
6

Retire the superadmin inspect surface + the orphan NESTED tree

⬇ Down from 20–40 hr (v1) ⬆ Up from 3–4 hr (v2) → now ~5–6 hr (v3). Bigger than v2 because the orphan NESTED /perform/clubs/.../events/.../* routes can also retire — they mount AssessComponents pages no coach ever lands on.

What goes (10 pages + ~16 routes)
  • 6 LEGACY pages + their 6 superadmin /assess routes — AssessDashboard, PreEvaluationResponses, PlayerAnalytics, ParentDashboard, InvitePlayers, ManagePlayers
  • 4 EFFECTIVELY ORPHAN pages + their 4 NESTED /perform/clubs/.../events/ routes + 4 legacy /assess/events/ routes — DraftEvent, PublishedEvent, SessionManagement, ReviewEvent (coaches use the FLAT URL which mounts EventDetailPage)
  • Internal history.push'es inside these pages — wire any that still want to land on FLAT to /perform/events/:id/* instead of nested
  • The base /club/:clubId/team/:teamId/assess superadmin entry — retire alongside AssessDashboard
What stays
  • features/assess/services/ — 30 importers, live event+eval data layer
  • features/assess/types/ — 10 importers, shared interface contract
  • CreateEvent — only mount, coaches hit it
  • CoachEvaluation — only evaluation surface, coaches reach via PublishedEventPage's evaluate button
  • EventDetailPage at FLAT URL — the canonical coach event page
  • 3 athlete-facing pre-eval pages
Risk

The 4 orphan pages CAN still be reached by direct URL entry — if any superadmin has a bookmark, it breaks. The 6 LEGACY pages are clearly built for a workflow (superadmin inspect tools) — confirm none are still actively used before deletion.

Verification before deletion: grep any production routing analytics for hits on /assess/* and /perform/clubs/.../events/.../{draft,published,session,review} — if hits are zero, safe to retire.

Companion audit

pages/section/Coach/: 217 files, 57 routed via barrels. The other ~160 are sub-components or orphans — separate per-file audit, ~5–10 hr.

10 pages · ~16 routes coachRoutes.ts + v2Routes.ts nested followup: pages/section/Coach audit
~2–3 hr LOW IMPACT naming clarity
7

Rename features/assess/features/event-editing/ (or similar)

★ New rank discovered during the audit. features/assess/ is the event-editing + evaluations module, not the "old assess world." The name misleads every reader who's not already in the team's head.

Why it matters

New contributor reads features/assess/services/coach.ts, assumes it's pre-Perform-Redesign and skips it. Misses that 30 other files depend on it. Sees #shame:blocked markers and assumes "doomed legacy"; doesn't realise it's hot path.

Options

A. Rename to features/event-editing/ (one big move + 59 imports to update).

B. Split: services + types into features/events/ (already exists); pages stay where they are; deprecate /assess/* paths.

C. Leave alone + add a top-of-folder README explaining "this is the event editing flow, not legacy assess."

discovered during audit pure rename · zero behaviour change
05

The #shame:blocked pattern — concrete example

All 14 FK-embed shame markers share this shape. Left: today's two-step query (slow). Right: SECURITY DEFINER RPC (proven BR1/BR2 pattern).

features/assess/hooks/useAttendanceTracking.ts today (two-step)
// #shame:blocked - FK embed `profiles!user_id` cannot
// resolve through public_profiles (PostgREST limit).

const { data } = await supabase
  .from('team_members')
  .select(`
    user_id,
    profiles!user_id(id, full_name, avatar_url)
  `)
  .eq('team_id', teamId);

// → fallback fires SECOND query:
const { data: profiles } = await supabase
  .from('public_profiles')
  .select('id, full_name, avatar_url')
  .in('id', userIds);

return teamMembers.map((tm) => ({
  ...tm,
  profile: profiles.find((p) => p.id === tm.user_id),
}));
supabase/migrations/.../get_team_roster_lite.sql fixed pattern (BR1)
CREATE OR REPLACE FUNCTION
  public.get_team_roster_lite(p_team_id uuid)
RETURNS TABLE (
  user_id uuid, full_name text,
  avatar_url text, is_coach boolean
)
LANGUAGE sql STABLE
SECURITY DEFINER      -- bypasses RLS,
                       -- so JOIN works
AS $$
  SELECT
    p.id, p.full_name, p.avatar_url,
    (tc.user_id IS NOT NULL) AS is_coach
  FROM public.team_members tm
  JOIN public.profiles p ON p.id = tm.user_id
  LEFT JOIN public.team_coaches tc ON ...
  WHERE tm.team_id = p_team_id
    AND tm.status = 'active'
    AND (...access check inline...);
$$;

-- Client side becomes one round trip:
const { data } = await supabase.rpc(
  'get_team_roster_lite', { p_team_id: teamId });

Each #shame:blocked surface gets its own RPC. Access check moves INTO the RPC. One round trip replaces N+1.

06

Risks & mitigations

Top five things that can go wrong during the cleanup itself.

Risk
Sev
Mitigation
Login page ShadowTextInput swap breaks auth — every user affected.
HIGH
Playwright auth-lane regression + 48h staging soak with synthetic-user health check. Don't confuse the two ShadowTextInput variants.
Deleting a "legacy-only" /assess page (Rank 6) turns out to break a workflow nobody flagged.
MED
↓ Revised down — the 6 candidate pages are SUPERADMIN-ONLY (verified). Worst case: a debug/inspect workflow needs the page back; cheap to restore from git.
FK-embed RPC migration changes RLS posture — a bug inside the SECURITY DEFINER leaks data RLS would've caught.
HIGH
Every new SECURITY DEFINER RPC ships with positive + negative tests. rls-stack-sequenced-merge: one PR at a time, staging soak between.
Mass shadow-component cleanup creates merge conflicts.
MED
Tier 1 → 2 → 3 ordering keeps each batch small. Nothing else currently touches components/shadow/.
Renaming features/assess/ (Rank 7) breaks an import the grep missed.
LOW
vite build catches every TypeScript import. Run vitest run after; revert cleanly if anything fails.
07

Decisions Stevie needs to make

Rank 2 — pick the FK-embed fix option
A = RPC per query (proven, ~14 RPCs), B = lift PostgREST FK metadata (1 config, brittle), C = client-side hook abstraction (hides ugliness, no perf gain). Recommendation: A for hot-path, C for cold-path.
Decide before: starting rank 2
Rank 6 — keep 6 legacy-only pages around as superadmin tools, or delete?
AssessDashboard, PlayerAnalytics, ParentDashboard, InvitePlayers, ManagePlayers, PreEvaluationResponses — each was clearly built for a superadmin workflow. Confirm none are still actively used before deletion.
Decide before: starting rank 6
Rank 7 — rename features/assess/, split it, or just add a README?
Rename is purest but biggest churn (59 imports). Split into features/events/ is most logical but breaks the file-tree mental model. README is cheapest, least helpful.
Decide before: starting rank 7
File the 7 ranks as beads, or keep this doc as the working artefact?
Beads give per-rank PR threading; the doc gives the holistic view. Could do both.
Decide before: closing this thread