ProjectR Docs
Audit 2026-09

Findings Catalog

The audit's concrete lists: unused components, dead code, false-success sites, flow verdicts, page scorecards, backend gaps.

Every item below was verified with file:line evidence in the lane files, which were removed from the repo on 2026-09-05 once the fix program landed (reachable through git history under apps/dashboard/docs/audit-2026-09). Finding IDs (F-xxx) are stable references.

Unused components (delete — zero imports repo-wide)

ui/aspect-ratio · ui/context-menu · ui/field · ui/input-otp · ui/menubar · ui/navigation-menu · ui/pagination · ui/resizable · ui/table/data-table-date-filter · ui/table/data-table-slider-filter (F-H2). field.tsx is the only adopt-later candidate (new shadcn form pattern, never adopted).

Dead code — safe to delete (verified repo-wide)

  • apps/dashboard/src/.next/ — an ESLint cache committed to git; also fix the unanchored /.next/ gitignore (F-H1).
  • src/lib/dal.ts — zero importers; its header claims to be "the REAL security boundary" (F-H3).
  • src/lib/format.ts, src/utils/collection-utils.ts — transitively dead (F-H5, F-H15).
  • 15 dead src/hooks/convex exports: isValidSlug, formatJobStatus, getJobStatusColor, useR2Upload, useAvatarUpload, useModerateRating, WEBHOOK_STATUS_OPTIONS + types SeasonalClaim, StampHistoryEntry, NotificationDoc, NotificationPrefs, TransformedOrder, UserWithAggregates, SupportStats (F-H6).
  • The stamps/shake chain: framer-motion ships to zero clients; stores/loyalty-store.ts (1,203 lines, the app's only zustand store) is a husk whose actions log "use Convex hooks instead"; its single consumer (stamp-collection-container.tsx, 845 L) is itself unreachable — and contains a latent false "Stamp awarded" toast (F-A1, F-G14).
  • Dependencies removable after build-verify: react-resizable-panels, input-otp, server-only, @convex-dev/r2, @projectr/backend, framer-motion (F-H8).
  • WARP.md — byte-identical duplicate of CLAUDE.md (F-H9). One console.log at ConvexClientProvider.tsx:57 (F-H13).

The false-success map (~35 sites)

The hook layer returns data only; a {success:false} denial becomes a success toast or an empty list. Modules and counts:

ModuleSitesWhat the user sees today
Ratings moderation9 + bulkdenial → success toast on every single moderate/feature/delete/report-resolve
Issued vouchers4revoke / bulk-revoke / extend / reissue denial → "Revoked N voucher(s) successfully"
Loyalty campaigns4create/update redirect as if saved; toggle + bulk activate/pause fabricate success counts
Voucher templates2 + bulkedits silently discarded with "updated successfully"
Products & groups8silent no-op saves; group create navigates to /products/groups/null
Notifications6queue process/retry/cancel + template create/save/delete → green "Admin access required"
Content campaigns3create/update discarded on duplicate slug; delete false-success
Duit Raya2campaign delete + destructive code reset false-success
Support / user edit3status changes + partial saves unchecked

Root cause F-B8 (hook contract), user impact F-F1 (false-empty). Fix = T1 in the backlog.

User-flow verdicts (all 16 traced)

FlowVerdictBlocking issue
Support inboxINTACT—
Ratings moderationBROKENpagination dead (rows past 20 unreachable) + false-success on every op
Voucher templatesFRAGILEdenial → success toast; journey "completes" without saving
Loyalty campaignsFRAGILEsame false-success on create/edit
ContentFRAGILEduplicate slug silently discards create/update
All others (13)DEGRADEDworks, with listed friction

Highlights: staff onboarding is broken end-to-end (/users/new delegates to public sign-up, which always dead-ends in AccessDenied — F-J3); notification compose 'all' mode promises sends >100 that the backend always rejects (MAX_BULK=100, F-I37); audit-log export only exports client-loaded rows (F-J2). Full traces: lane-j-flows.md.

Page scorecards (76 routes, all covered)

🔴 = has a P1 finding. Clusters:

ClusterRoutes🔴Headline
Ratings65no states, dead pagination, unconfirmed delete, false-success
Loyalty84public campaign query, false-success create/edit, fabricated KPIs
Vouchers104issued-page false-success ×4, template form false-success
Products & groups73silent no-op saves, /groups/null navigation, eternal-loading not-found
Orders32client/backend tier mismatch, unconfirmed force-expire, identical badges
Content92dup-slug false-success, dead pagination on campaigns listing
Users50model citizen (one 🟡: unchecked edit-form results)
Auth30sign-up dead-end + orphan access-denied page (🟡)
Raya71destructive code reset false-success
Overview, profile, webhooks, audit-log, support, notifications140mostly solid; notifications has 6 false-success sites

Plus 4 trivial routes (two redirects, two "coming soon" placeholders) — inventoried, no findings.

Backend (Convex) gaps

  • Zero audit logging in three destructive families: Shopify webhook admin (7 mutations incl. delete-all), the entire ratings/moderation surface, gamificationLoyalty deletes incl. the super_admin hard-delete cascade (F-D1/2/3/4).
  • Two public no-auth queries: gamificationLoyalty.getCampaignById (leaks reward tiers with exact probabilities — callers verified: 2 dashboard components, no mobile, no HTTP route), duitRaya.getCampaignById (campaign metadata + claim stats) (F-I2-1, F-I43).
  • Suspended-staff write path: triggerFullSync/syncSingleProduct check role but not isProfileAccessActive — banned staff with a live session can still write to the live Shopify store (F-D5).
  • Pagination is mostly fake: .take(500–10,000) + JS slice dominates (silent truncation); Convex paginate in only 4 files; exportCodes truncates at 10k of 100k (F-D8, F-D9).
  • Analytics cost: overview/trends collect every order + every profile twice per range while orderTotals/usersByRole aggregates have ~zero production reads (F-D6).
  • Response-shape variance (root cause of theme 1): 5 envelopes, 8 payload keys, 3 pagination styles across dashboard-facing functions (census in lane-d-convex.md).
  • Bonus: dead N+1 in dashboardProducts search (F-D14), unbounded bulkGenerateVouchers (F-D13), markAllAsRead mega-transaction (F-D11), HTTP export counts cancelled/refunded as revenue (F-D7).

UI / shadcn debt

  • components.json is stale (wrong css path, old aliases) — blocks all CLI refresh; fix first (F-E10).
  • button.tsx/card.tsx are pre-data-slot vintage; dialog.tsx/table.tsx are locally customized — never blind-refresh (F-E1/E2/E12).
  • 3 ui/ files import app components and Convex hooks: image-upload-field, video-upload-field, r2-image (F-E5); channel-multi-select hard-locks voucher domain config into the ui layer (F-E6).
  • tiptap-editor relies on prose classes but the typography plugin isn't installed — editor content renders unstyled (F-E3).
  • No --success/--warning/--info tokens; ~650 raw palette utilities escape the .theme-* runtime system — worst: shake-to-win-modal (55), stamp-collection-grid (46), stamp-collection-card (27); 108 redundant dark: variants (F-E11).
  • Consolidation: 5 hand-rolled delete dialogs vs an existing preset; 3 table archetypes; 9 date-entry implementations with two conflicting value contracts (F-E7/8/9).

Performance top items

Serial 3–4 round-trip auth chain before data (F-G1/G2) · overview full remount on every toggle (F-G5, live-confirmed) · orders double-subscription for tab counts (F-G3) · un-memoized transforms re-mapping up to 10k docs per render (F-G4) · closed media-picker holds a live 500-file subscription on every image-bearing form (F-G6) · header bell subscribes 20 docs on all routes (F-G7) · recharts statically imported on 5+ routes (F-G8) · fetch-all tables (F-G9) · media grid renders full-size R2 originals with exactly one loading="lazy" in the app (F-G10).

Next.js 16 items

No error.tsx at /dashboard — ~56 pages unwind to global-error (F-C1) · Dockerfile expects output:'standalone', never set (F-C2) · broken title-template chain → bare/duplicated titles (F-C3, live-confirmed) · CSP ships unsafe-eval/unsafe-inline despite "strict" comment (F-C4) · auth POST proxy: no upstream timeout, unbounded body, error-message leak (F-C5) · Sentry PII + 100% traces + replays on an admin tool (F-C7). Sessions live in localStorage, so a proxy-level session gate is impossible without an auth-model change — stated tradeoff, backend enforces (F-C8).

Docs drift (the audit also reconciled the docs)

TECH_DEBT.md: 3 of 4 "Pending" actives are already resolved; review 10 weeks overdue (F-H10) · README pins stale · BACKEND_API_CHECKLIST.md contradicts root CLAUDE.md on _generated handling (F-H11) · dashboard CLAUDE.md's feature-organization pattern is dead letter — the "deviant" vouchers/content layouts are the better structure; rewrite the doc (F-A9) · access-control.ts header describes a phantom MANAGER role.

On this page