Web app route audit (2026-06-15): 404/400/403 + slug-vs-ID consistency

Seen in 1 project by 1 person

About

Full review of apps/web on develop. Method: authenticated Playwright crawl (140 pages, [email protected]) + anonymous public-surface crawl (120 pages) + static link inventory, every runtime finding verified directly with curl/REST before listing. Reusable crawlers left at scripts/crawl-audit.mjs and scripts/crawl-anon.mjs.

== VERIFIED BUGS (ordered by severity) ==

[HIGH] Subject pages link projects with NO handle segment.

  • (open) s/[slug]/page.tsx:120 -> href /app/projects/u/${p.slug ?? p.id} and (public) s/[slug]/page.tsx:318 -> /app/projects/u/${project.slug}.
  • Route is /app/projects/u/[handle]/[...path]. These emit /app/projects/u/<slug> (slug sits in the handle position, no path) -> 404. Every "Listed in" project link on a subject page is broken.

[HIGH] Options + Search aggregators link a project by its IMMEDIATE slug (flat path, missing parent chain).

  • (public) options/page.tsx:69 -> /app/projects/u/${r.owner_handle}/${r.project_slug}; (public) search/page.tsx:130 -> /app/projects/u/${r.owner_handle}/${r.slug}.
  • For a nested child project the canonical path needs the ancestor chain (/u/<handle>/<parent>/<child>). Flat path 404s.
  • Confirmed live (anon crawl): /app/projects/u/yancou800/wines, /berat, /valbona-guesthouse, /albanie-04-25 all 404. All four are children of the public umbrella albania-may-2026, and all have public_state=null.
  • DOUBT: these children are non-public, so the aggregator may also be leaking links to private sub-projects onto a public page. Decide: link only to public roots, or build the full canonical chain (buildCanonicalProjectPath) and respect visibility.

[MED] /decisions returns 404 while /lists and /plans return 200.

  • (public) [kind]/page.tsx:18 has a local KIND_BY_URL map with only lists+plans; missing 'decisions' -> notFound(). It reimplements kind-mapping instead of reusing the shared kindFromUrlSegment() (lib/public-section/kind-label.ts), which DOES map decisions->decision. The sibling detail route [kind]/[slug] uses the shared helper, so /decisions/<slug> would resolve but /decisions list 404s. Classic reuse-before-build drift.
  • Not linked in-app (direct-URL / SEO only). DOUBT: is the 'decision' kind intentionally dropped from public discovery? If yes, the detail route should reject it too for consistency. If no, swap the local map for kindFromUrlSegment.

[MED] IDs instead of slugs in URLs (the systemic complaint).

  • Canonical project URL is /app/projects/u/<handle>/<chain> (group form /g/<group>/...), built by buildCanonicalProjectPath, which falls back to UUID per chain segment when any slug is null. projects.slug is nullable (e.g. "Child Leg A" has slug=null).
  • Raw UUID leaks into the address bar via:
    • slug ?? id fallbacks: Rail.tsx:54, ProjectListClient.tsx:389/645, stream/ObjectBlock.tsx:80/93, ProjectSummaryBlock.tsx:165, api/jump-index/route.ts:34.
    • raw-id links: ContextChip.tsx:32 (focus.projectId is always a UUID); definitions/[defId] and options/[optionId]/edit routes are UUID-keyed by design.
    • NULL-slug projects (no slug exists, UUID is the only option).
  • The [projectId] layout 308-redirects SLUG-form URLs to canonical but deliberately does NOT redirect UUID-form (used as an internal hop). DOUBT/observation: direct navigation to /app/projects/<slug> served 200 WITHOUT self-healing to the /u/<handle>/ form in my fetch test (the layout slug->canonical 308 did not fire). May be x-pathname-header dependent. Needs a proper root-cause: should slug URLs canonicalize on direct hit?

[LOW] /app/brand showcase fires real DB queries with fake option IDs -> 400.

  • The brand gallery renders reaction components with demo IDs (gallery-hike-1, gallery-lodging-1, gallery-general-1). useReactions then queries GET /rest/v1/property_reactions?option_id=eq.gallery-hike-1 against a uuid column -> 400 (x3) + console errors. Internal showcase page; give the gallery a mock/no-fetch mode or real seed IDs.

[LOW] /api/participants?kind=persona returns 403 (Admin only) for every non-admin.

  • components/forms/ConfigureScene.tsx:212 fetches it unconditionally on the project settings/view scene. The 403 is swallowed (.catch -> []), so it is functionally harmless, but every non-admin gets a 403 console error on those pages. Gate the fetch behind isAdmin.

[LOW] Anon shell fires authed-only endpoints -> 401 console noise on public pages.

  • GET /api/jump-index (command bar index) 401s on /login and other anon pages; GET /api/activity 401s on ~12 public pages (profiles, option pages). Pages still render 200. Skip these fetches when unauthenticated.

== VERIFIED NON-ISSUES (do not re-chase) ==

  • The authed crawl reported ~60 project pages as errors; these were CRAWLER timeout artifacts (Next dev on-demand route compilation under sequential load, 25s cap). Sampled 4 directly -> all 200. Not bugs.
  • /lists/<slug>/<handle> (3-segment) is NOT a 404: middleware 308-redirects it to /app/projects/u/<handle>/<slug> and lands 200 (intentional mirror URL). The href shape in [kind]/page.tsx:104 is by design.
  • ConfigureScene passing the definition UUID to /definitions/<defId> is correct: that route resolves by d.id.

== DATA HYGIENE (dev DB only, not code) ==

  • subjects table is full of tmp-fk-* throwaway slugs; many owned-public-06-13-* test projects; some projects have null slug. Worth a dev-data cleanup but not a code defect.

Suggest splitting the two HIGH items into their own tasks if you want them tracked/closed independently.

Links

No links shared yet.

Listed in

Bookmarked in

Not in any public bookmark categories yet.