Skip to content

fix(webapp): a bogus project deep link says so instead of swapping projects (BEA-83) - #131

Open
ssowonny wants to merge 1 commit into
mainfrom
bea-83-ph-scan-bug-a-bogus-project-deep-link-silently-renders-a
Open

fix(webapp): a bogus project deep link says so instead of swapping projects (BEA-83)#131
ssowonny wants to merge 1 commit into
mainfrom
bea-83-ph-scan-bug-a-bogus-project-deep-link-silently-renders-a

Conversation

@ssowonny

@ssowonny ssowonny commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

TL;DR

  • A link to a project you can't see used to open a different project and throw the file path away — nothing on screen said so.
  • Now it says "Project not found" at the URL you typed, sidebar still usable, "Back to " to recover.
  • The fallback chain is untouched: / still lands on the remembered project (BEA-75 unchanged).
  • Two deviations from the plan, both below: it's four redirects to gate, not three, and one existing e2e test pinned the old behavior and had to be rewritten.
  • Known gap (accepted in the spec): a project created on another device is absent from a stale projects list and reads as not-found until the next refresh.

One if was serving two cases that want opposite answers:

flowchart TD
    R["route.project"]
    R -->|"'' — landing on /"| A["redirect to the remembered project<br/>✅ correct, this is BEA-75"]
    R -->|"an id not in your list"| B["redirect to the fallback project<br/>❌ wrong project, path dropped"]
    B -.->|this PR| C["render 'Project not found'<br/>at the URL as typed"]
Loading

Reproduced on origin/main before touching anything — /no-such-project-xyz/some/file.md ended up at /30d8fa88-… showing the wiki home.

What changed

internal/webapp/frontend/src/apps/HubApp.tsx, ~20 lines:

  1. A flag, sibling to the existing orgMissing: projectMissing = !!route.project && !projects.some(p => p.id === route.project).
  2. A not-found panel, a copy of the org-not-found body two blocks up — same panel slot, so shell, sidebar and account bar stay mounted for free. Wired in ahead of routePage so /bad-id/settings renders not-found rather than the fallback project's settings.
  3. The redirects gated behind !projectMissing.

The copy deliberately does not distinguish "doesn't exist" from "you're not a member" — /api/projects only ever returns your orgs' projects, so the SPA genuinely can't tell, and a page that could would answer "does this id exist on this hub?" for anyone who asked. Same sentence as the org page.

Deviations from the reviewed plan

Both are small, but the plan said otherwise, so they're called out rather than buried:

Plan said Reality
Gate three redirects (:265, :272, :280) There are four below the flag — queryTarget (/bad-id/history?path=x) also rewrites off current.id. All four now sit inside one if (!projectMissing) block, so a fifth can't be added without inheriting the guard.
Only hub.spec.ts:207 and :220 need checking hub.spec.ts:25"unknown project id falls back to a real project" — asserted the exact behavior this issue calls a bug. Rewritten to the new expectation (URL preserved, not-found shown, sidebar still on a real project). The plan didn't mention it.

Screenshots

Same request in both: GET /no-such-project-xyz/some/file.md, signed in as e2e@example.com.

Before — the wiki home, URL rewritten to /30d8fa88-…, path gone After — URL stays /no-such-project-xyz/some/file.md
before after

(Playwright screenshots are viewport-only, so the address bar isn't in frame — the URL is asserted in the specs instead.)

What can't break

  • / still redirects. route.project === "" makes projectMissing false, so BEA-75's landing behavior is bit-for-bit unchanged. Its two specs pass untouched.
  • The fallback chain is not modified. current resolves to exactly the project it did before; it just stops being shown silently under someone else's URL.
  • /orgs/<id> and /billing are exempt, as before — neither is project-scoped.
  • rememberProject unchanged. It writes current.id, and the id it writes is always one the viewer is a member of.

What was run

  • go build ./..., go vet ./..., go test ./... — all packages pass.
  • npm run e2e154 passed, 1 skipped, 0 failed, including the new case and the two BEA-75 specs.
  • npm test (frontend units) — 57 pass.
  • npm run build + check-dist.shinternal/webapp/static committed and fresh.
  • New e2e case in browse.spec.ts covers the bullets plus a reload (no bounce, no loop) and the /insights and /notes/ rewrites that would otherwise undo the fix.
  • UI checked by driving the real hub, not by reading the diff — that's what the pair above is.

The one thing left open

A project created on another device sits outside a stale projects list, so its URL reads as not-found until the list refreshes. The spec accepts this rather than paying a server round-trip on every unknown id — projects refreshes on navigation and the sidebar recovers it. Worth a second look if anyone hits it in practice; it's the only false positive this change can produce.

Closes BEA-83.

Build session

cd $(git worktree list | grep bea-83 | awk '{print $1}') && claude --resume 05ee2a02-272b-4efa-803b-8c5242b2490b

(only works on the machine that ran the build)

…ojects (BEA-83)

/no-such-project-xyz/some/file.md used to redirect to whichever project the
fallback chain picked, dropping the path — nothing on screen distinguished
"that link is wrong" from "you opened your project."

An unknown route.project now renders a "Project not found" panel at the URL
as typed, mirroring the org-not-found page two blocks up: shell, sidebar and
account bar stay mounted, and "Back to <name>" points at the same project the
fallback picks today. The fallback chain itself is unchanged, and "/" still
redirects to the remembered project (BEA-75).

All four redirects below the flag rewrite the address bar off current.id, so
they are gated together — /bad-id/insights and /bad-id/notes/ would otherwise
undo the fix on their own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ssowonny
ssowonny requested a review from thefron August 5, 2026 14:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant