Skip to content

Let an anonymous reader open the report card's track in the analysis map (#666) - #667

Merged
pokle merged 1 commit into
masterfrom
claude/github-issue-666-74c2a6
Aug 23, 2026
Merged

Let an anonymous reader open the report card's track in the analysis map (#666)#667
pokle merged 1 commit into
masterfrom
claude/github-issue-666-74c2a6

Conversation

@pokle

@pokle pokle commented Aug 23, 2026

Copy link
Copy Markdown
Owner

Closes #666.

Preview: https://claude-github-issue-666-74c2.glidecomp.pages.dev

The gap

The report card is public and server-rendered, and under its map it offers "Open full track in the analysis map" — the one place a reader gets the per-track tooling (Events, Glides, Climbs, Sinks) the card itself has no room for. That button was drawn for signed-in visitors only, because the analysis viewer redirected everyone else to /u/me/. So the reader most likely to follow it — the pilot who was sent the link, their club, a meet director on someone else's laptop — was shown a page that explains the score and then no way to look at the flight.

What changed

?compId=…&taskId=…&pilotId=… — and only that shape — now opens with no session. The gate is one function, isPublicCompLink() in web/frontend/src/analysis/public-deep-link.ts, and it is a whitelist rather than a blocklist, so a param it has never heard of cannot quietly widen it. The bare page, the personal library (storedTrack, storedTask, u), the bundled samples (track, task, sampleComp) and the share target all still redirect.

The user != null wrapper is gone from the report card, so the button is now static markup — identical on the server and after hydration, rather than appearing once /api/auth/me answers.

Security — each of the issue's points, confirmed

  • No new data exposure. The four endpoints the deep link uses are already optionalAuth and public, and the report card already downloads the very same IGC anonymously to draw its own map.
  • Hidden test comps stay hidden. All four routes 404 for a non-admin. The viewer now answers a 404 with "Competition task not found", and that branch returns before appendCompBreadcrumbs(), so nothing names the comp or the task. Asserted, including document.title.
  • No mutation path opens. Every write still goes through analysis/storage.ts, gated by storage.isAvailable() (session?.signedIn) on the client and requireAuth on the server.
  • Client-side only. web/workers/ is untouched — no route moved off requireAuth, no new endpoint.
  • Onboarding untouched. needsOnboarding still fires for a session that has one; "no session" and "not onboarded" stay different answers.
  • A failure to ask is not an answer (E2E: comp-detail tests flake ~1 run in 5, only in a full-suite run #481). The gate reads getCurrentUserOnce(), which retries a dropped request or a 5xx before answering, so a blip cannot read as "anonymous".

One deviation from the issue, on the owner's call

The issue proposed hiding the file/import tooling from anonymous readers. The owner's direction was the opposite: dropping in an IGC file of your own is fine — the only thing an anonymous visitor can't reach is the account library, which was already gated. So menu-open-igc, the drop zone, and the XContest/AirScore importers are all left alone (they're in-memory or external-fetch, and mutate nothing).

What is hidden is the sample loaders: they reload the page with ?track= / ?sampleComp=, which the gate refuses, so an anonymous reader clicking one would be thrown out to sign-in. They're marked data-requires-account in the markup and hidden in one sweep, so a new sample loader is covered by the attribute it's written with.

Coverage

  • public-deep-link.test.ts — 7 unit tests over the whitelist.
  • A signed out block in e2e/report-card.spec.ts:
    • the button is offered, its href is exactly the deep link, and following it loads the pilot's track and the task with all six single-track tabs (and every data-requires-account control is hidden);
    • twelve other URLs — bare, each personal-library param, each sample param, half a comp link, and a comp link with a smuggled storedTrack/track — all still land on /u/me/;
    • a deep link into a hidden test comp says not-found and names nothing.

Run green: bun run test (1580), web/frontend vitest (724), test:e2e on report-card, analysis-map (signed-in, unchanged), lazy-map-in-view, explain-affordance, non-route-links, and the full test:e2e:ssr (38, including a clean hydration check on :pilot).

🤖 Generated with Claude Code

…map (#666)

The report card is public and server-rendered, and under its map it offers
"Open full track in the analysis map" — the one place a reader gets the
per-track tooling (Events, Glides, Climbs, Sinks) the card itself has no room
for. That button was drawn for signed-in visitors only, because the analysis
viewer redirected everyone else to sign in. So the reader most likely to
follow it — the pilot who was sent the link, their club, a meet director on
somebody else's laptop — was shown a page that explains the score and then no
way to look at the flight.

The gate itself dates from March, when the viewer WAS the product and gating
it was the sign-up funnel. Neither that nor the personal library it grew into
covers the case that exists now.

So `?compId=…&taskId=…&pilotId=…` — and only that shape — now opens with no
session. `isPublicCompLink()` is a whitelist rather than a blocklist, so a
param it has never heard of cannot quietly widen it; the bare page, the
personal library, the bundled samples and the share target all still redirect.

The deep link moves no byte a curl could not already fetch: its four endpoints
are `optionalAuth` and public, and the report card already downloads the very
same IGC anonymously to draw its own map. Nothing here relaxes the server —
no route moved off `requireAuth`, no endpoint was added, and no worker file is
touched. A hidden `test` comp still 404s on all four routes, and the viewer now
answers that with "Competition task not found", built before any breadcrumb
that could have named the competition.

The sample loaders reload the page with `?track=` / `?sampleComp=`, which the
gate refuses, so they are marked `data-requires-account` and hidden from an
anonymous reader rather than left to bounce them out of the page. Dropping in a
file of one's own is unchanged and needs no account; only the account library
is out of reach, and it was already gated by `storage.isAvailable()`.

Coverage: `isPublicCompLink` unit tests, and a "signed out" block in
e2e/report-card.spec.ts — the button is offered and its link loads the pilot's
track with all six tabs; twelve other URLs still ask for a sign-in; a deep link
into a hidden test comp finds nothing and names nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Preview Deployment
https://df703e8a.glidecomp.pages.dev
Commit: e88f3c6

@pokle
pokle merged commit 1bcce03 into master Aug 23, 2026
6 checks passed
@pokle
pokle deleted the claude/github-issue-666-74c2a6 branch August 23, 2026 11:11
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.

Let anonymous readers of the report card open the analysis map for that one track

1 participant