feat: jersey-public SvelteKit route with Keycloak gate (#243) #246

Merged
forgejo_admin merged 2 commits from 243-jersey-public-svelte-route into main 2026-04-10 23:45:02 +00:00
Contributor

Summary

Adds the System B public jersey intake route at /jersey-public inside the (app)/ route group. The route inherits the existing Keycloak auth guard from (app)/+layout.svelte — no new auth code, no oauth2-proxy, no ingress changes.

Auth Chain (per feedback_funnel_requires_auth.md)

  1. Visitor hits /jersey-public
  2. (app)/+layout.svelte runs initKeycloak() on mount (check-sso, PKCE)
  3. Its reactive $effect guard checks PUBLIC_APP_ROUTES/jersey-public is intentionally NOT in that list
  4. Unauthenticated visitors goto('/signin') which triggers Keycloak login (realm westside-basketball, client westside-spa)
  5. After login, user bounces back to /jersey-public with a valid session
  6. On mount the form reads preferred_username + email claims via getUserName() / getEmail() and prefills playerName + email (still editable so parents can submit for kids)
  7. Submit calls POST /api/jersey-public-orders with Authorization: Bearer <token> from getToken()

Changes

  • src/routes/(app)/jersey-public/+page.svelte (new, ~450 lines) — Svelte 5 runes, mobile-first, pure CSS. Ports the approved playground prototype (forgejo_admin/westside-playground#57) to SvelteKit. K/Q toggle swaps both jersey card images via $derived URLs. Tier radio wraps the whole .jersey-option card. Inline validation with jp-error banners. Status banner for submit result. Handles 404 gracefully — shows a "backend not yet deployed, text Marcus" message rather than a raw error since the API endpoint lands in a separate PR.
  • src/lib/keycloak.js — adds 3-line getEmail() helper matching the style of the existing getUserId() / getUserName() getters.

Acceptance Criteria

  • /jersey-public unauthenticated -> redirected to /signin (inherited from (app)/+layout.svelte guard)
  • Authenticated visitors land with name + email prefilled from JWT
  • K/Q toggle swaps both card images to correct MinIO URLs (4 URLs total)
  • Form blocks + shows inline errors on missing required fields
  • Valid submit POSTs to /api/jersey-public-orders with Authorization: Bearer <JWT>
  • 404 from missing backend shows a friendly "not yet deployed" message
  • Mobile-first layout (single column on phone, 2-col size grid at 640px+)
  • /jersey-public is NOT in PUBLIC_APP_ROUTES — verified by grep
  • Svelte 5 $state runes, no Tailwind, reuses existing .jersey-* classes from src/app.css

Verification Greps

# 1. PUBLIC_APP_ROUTES does NOT mention jersey-public
$ grep -n "jersey-public" src/routes/(app)/+layout.svelte
(no match — PASS)

# 2. No Tailwind in the new route
$ grep -rn "Tailwind|@apply| bg-[a-z]| text-[a-z]| flex-[a-z]" src/routes/(app)/jersey-public/
(no match — PASS)

# 3. Exactly 4 MinIO jersey image URLs
$ grep -c "minio-api.tail5b443a" src/routes/(app)/jersey-public/+page.svelte
4

# 4. getEmail() helper added
$ grep -n "export function getEmail" src/lib/keycloak.js
147:export function getEmail() {

Test Plan

  • npm run check -> 0 errors in new files (existing a11y warnings in admin/teams and checkout are pre-existing and unrelated)
  • npm run build -> builds cleanly, .svelte-kit/output/server/entries/pages/(app)/jersey-public/_page.svelte.js generated (11.62 kB)
  • Manual QA (follow-up):
    • npm run dev, visit /jersey-public unauthenticated -> redirects to /signin
    • After Keycloak login, land on /jersey-public with name + email prefilled
    • Toggle Kings/Queens — both tier card images swap
    • Submit blank form — 7 inline errors appear
    • Submit filled form — request to /api/jersey-public-orders with Authorization: Bearer header (DevTools Network tab); 404 shows friendly message
    • Mobile viewport (375px) — stacked layout; tablet (640px+) — 2-col size grid

Review Checklist

  • Route lives under (app)/ and is NOT added to PUBLIC_APP_ROUTES
  • Uses Svelte 5 $state / $derived runes
  • Reuses $lib/keycloak.js helpers (ready, getUserName, getEmail, getToken) — no new auth code
  • Pure CSS, no Tailwind utilities
  • Mobile-first layout
  • getEmail() helper added to $lib/keycloak.js
  • Prefill does not lock the field — still editable for parents
  • Graceful 404 handling while backend endpoint is in-flight
  • npm run check and npm run build both green
  • Hands off System A ((public)/jersey) and System C ((app)/checkout) per ticket scope

Follow-up Scope (not in this PR)

  • Backend endpoint POST /api/jersey-public-orders lands in a separate PR (#430 per the ticket)
  • Team select is hardcoded to marcus-kings / marcus-queens / other with a TODO comment — switch to /api/teams when backend wires up
  • Unit / component tests — westside-landing has no test runner configured (package.json scripts are only dev, build, preview, check). Adding Vitest is out of scope for this ticket and likely belongs in a separate infra ticket.

Closes #243

  • Story: story:WS-S31 — admin public jersey intake link
  • Arch: arch-jersey-intake — architecture doc
  • Board: board-westside-basketball item #946
  • Playground: forgejo_admin/westside-playground#57 (approved prototype)
  • Conventions: feedback_funnel_requires_auth.md, feedback_svelte_is_html.md, feedback_no_tailwind.md
## Summary Adds the System B public jersey intake route at `/jersey-public` inside the `(app)/` route group. The route inherits the existing Keycloak auth guard from `(app)/+layout.svelte` — no new auth code, no oauth2-proxy, no ingress changes. ## Auth Chain (per `feedback_funnel_requires_auth.md`) 1. Visitor hits `/jersey-public` 2. `(app)/+layout.svelte` runs `initKeycloak()` on mount (check-sso, PKCE) 3. Its reactive `$effect` guard checks `PUBLIC_APP_ROUTES` — `/jersey-public` is **intentionally NOT** in that list 4. Unauthenticated visitors `goto('/signin')` which triggers Keycloak login (realm `westside-basketball`, client `westside-spa`) 5. After login, user bounces back to `/jersey-public` with a valid session 6. On mount the form reads `preferred_username` + `email` claims via `getUserName()` / `getEmail()` and prefills `playerName` + `email` (still editable so parents can submit for kids) 7. Submit calls `POST /api/jersey-public-orders` with `Authorization: Bearer <token>` from `getToken()` ## Changes - `src/routes/(app)/jersey-public/+page.svelte` (new, ~450 lines) — Svelte 5 runes, mobile-first, pure CSS. Ports the approved playground prototype (`forgejo_admin/westside-playground#57`) to SvelteKit. K/Q toggle swaps both jersey card images via `$derived` URLs. Tier radio wraps the whole `.jersey-option` card. Inline validation with `jp-error` banners. Status banner for submit result. Handles 404 gracefully — shows a "backend not yet deployed, text Marcus" message rather than a raw error since the API endpoint lands in a separate PR. - `src/lib/keycloak.js` — adds 3-line `getEmail()` helper matching the style of the existing `getUserId()` / `getUserName()` getters. ## Acceptance Criteria - [x] `/jersey-public` unauthenticated -> redirected to `/signin` (inherited from `(app)/+layout.svelte` guard) - [x] Authenticated visitors land with name + email prefilled from JWT - [x] K/Q toggle swaps both card images to correct MinIO URLs (4 URLs total) - [x] Form blocks + shows inline errors on missing required fields - [x] Valid submit POSTs to `/api/jersey-public-orders` with `Authorization: Bearer <JWT>` - [x] 404 from missing backend shows a friendly "not yet deployed" message - [x] Mobile-first layout (single column on phone, 2-col size grid at 640px+) - [x] `/jersey-public` is NOT in `PUBLIC_APP_ROUTES` — verified by grep - [x] Svelte 5 `$state` runes, no Tailwind, reuses existing `.jersey-*` classes from `src/app.css` ## Verification Greps ``` # 1. PUBLIC_APP_ROUTES does NOT mention jersey-public $ grep -n "jersey-public" src/routes/(app)/+layout.svelte (no match — PASS) # 2. No Tailwind in the new route $ grep -rn "Tailwind|@apply| bg-[a-z]| text-[a-z]| flex-[a-z]" src/routes/(app)/jersey-public/ (no match — PASS) # 3. Exactly 4 MinIO jersey image URLs $ grep -c "minio-api.tail5b443a" src/routes/(app)/jersey-public/+page.svelte 4 # 4. getEmail() helper added $ grep -n "export function getEmail" src/lib/keycloak.js 147:export function getEmail() { ``` ## Test Plan - `npm run check` -> 0 errors in new files (existing a11y warnings in admin/teams and checkout are pre-existing and unrelated) - `npm run build` -> builds cleanly, `.svelte-kit/output/server/entries/pages/(app)/jersey-public/_page.svelte.js` generated (11.62 kB) - Manual QA (follow-up): - [ ] `npm run dev`, visit `/jersey-public` unauthenticated -> redirects to `/signin` - [ ] After Keycloak login, land on `/jersey-public` with name + email prefilled - [ ] Toggle Kings/Queens — both tier card images swap - [ ] Submit blank form — 7 inline errors appear - [ ] Submit filled form — request to `/api/jersey-public-orders` with `Authorization: Bearer` header (DevTools Network tab); 404 shows friendly message - [ ] Mobile viewport (375px) — stacked layout; tablet (640px+) — 2-col size grid ## Review Checklist - [x] Route lives under `(app)/` and is NOT added to `PUBLIC_APP_ROUTES` - [x] Uses Svelte 5 `$state` / `$derived` runes - [x] Reuses `$lib/keycloak.js` helpers (`ready`, `getUserName`, `getEmail`, `getToken`) — no new auth code - [x] Pure CSS, no Tailwind utilities - [x] Mobile-first layout - [x] `getEmail()` helper added to `$lib/keycloak.js` - [x] Prefill does not lock the field — still editable for parents - [x] Graceful 404 handling while backend endpoint is in-flight - [x] `npm run check` and `npm run build` both green - [x] Hands off System A (`(public)/jersey`) and System C (`(app)/checkout`) per ticket scope ## Follow-up Scope (not in this PR) - Backend endpoint `POST /api/jersey-public-orders` lands in a separate PR (#430 per the ticket) - Team select is hardcoded to `marcus-kings` / `marcus-queens` / `other` with a `TODO` comment — switch to `/api/teams` when backend wires up - Unit / component tests — westside-landing has no test runner configured (`package.json` scripts are only `dev`, `build`, `preview`, `check`). Adding Vitest is out of scope for this ticket and likely belongs in a separate infra ticket. ## Related Closes #243 ## Related Notes - Story: `story:WS-S31` — admin public jersey intake link - Arch: `arch-jersey-intake` — architecture doc - Board: board-westside-basketball item #946 - Playground: `forgejo_admin/westside-playground#57` (approved prototype) - Conventions: `feedback_funnel_requires_auth.md`, `feedback_svelte_is_html.md`, `feedback_no_tailwind.md`
feat: jersey-public SvelteKit route with Keycloak gate (#243)
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
52d6140870
Adds the System B public jersey intake route inside the (app)/ route
group so it inherits the existing Keycloak auth guard. Unauthenticated
visitors auto-redirect to /signin via the (app)/+layout.svelte $effect
guard, then bounce back after login. The route is intentionally NOT
added to PUBLIC_APP_ROUTES.

- New route: src/routes/(app)/jersey-public/+page.svelte
- Adds getEmail() helper to $lib/keycloak.js (reads tokenParsed.email)
- Prefills playerName + email from JWT on mount, stays editable so
  parents can submit on behalf of kids
- Svelte 5 $state runes for form state, $derived for K/Q image swap
- Pure CSS (no Tailwind), mobile-first, reuses .jersey-option and
  .jersey-form-group classes from src/app.css
- Form POSTs to /api/jersey-public-orders with Authorization: Bearer
  header. Handles 404 gracefully (backend lands in separate PR).

Story: WS-S31
Arch: arch-jersey-intake

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Contributor

PR #246 Review

DOMAIN REVIEW

Stack: SvelteKit 5 (runes: $state, $derived), pure CSS, Keycloak JS adapter. Single new route (app)/jersey-public/+page.svelte (539 lines) + 8-line getEmail() helper in src/lib/keycloak.js.

Auth chain verified:

  • src/routes/(app)/+layout.svelte:26PUBLIC_APP_ROUTES array checked against my local main. Contains /register, /signin, /jersey, /jersey/success, /jersey/cancel, /checkout, /checkout/success, /checkout/cancel, /forgot-password, /reset-password. Does NOT contain /jersey-public — confirmed. The reactive $effect guard at (app)/+layout.svelte:46-62 will goto('/signin') for unauthenticated visitors. Auth chain is sound and matches feedback_funnel_requires_auth.md.
  • PR body explicitly documents the 7-step auth chain. Compliant.

getEmail() helper:

export function getEmail() {
  return keycloak?.tokenParsed?.email || '';
}

Functionally correct. Minor style drift from the existing getUserName() / getUserId() pattern which uses an early if (!keycloak?.tokenParsed) return ''; guard before destructuring — but the optional-chaining one-liner is equivalent and arguably cleaner. Non-blocking nit.

getToken() usage: const token = await getToken(); then spreads Authorization: Bearer ${token} conditionally. Matches the Promise<string|null> signature at src/lib/keycloak.js:104. Token refresh (updateToken(30)) is handled inside getToken(). Correct.

System A / System C scope isolation: PR metadata shows changed_files: 2. Only src/lib/keycloak.js and the new (app)/jersey-public/+page.svelte. No modifications to (public)/jersey, (app)/jersey, (app)/checkout, or (app)/+layout.svelte. Clean scope.

Form validation (+page.svelte:58-78):

  • Required: playerName, email (w/ regex), team, kq, topSize, shortSize, tier
  • Optional: num1, num2, num3 — validated against /^(0|00|[1-9][0-9]?)$/ only when non-empty
  • Single errors.numbers flag for all three number fields
  • EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ — permissive but sufficient
  • trim() applied on compare and on payload
    Matches spec.

K/Q image swap (+page.svelte:14-23 + 43-44): 4 MinIO URLs (kings-home, kings-away-new, queens-home, queens-away-new) in JERSEY_IMAGES constant. $derived swaps both reversible and shooter images based on kq. Fallback ?? JERSEY_IMAGES.kings.reversible. Verified 4 URLs match minio-api.tail5b443a.ts.net/assets/westside/jerseys/.

Submit (+page.svelte:96-157):

  • 201 -> success + partial form reset (keeps name/email, clears team/kq/sizes/numbers/tier)
  • 404 -> friendly "backend not deployed" message w/ Marcus's phone (385) 450-9963 (number is used consistently across 7 other files in the repo)
  • Other statuses -> extracts body.message || body.detail, falls back to Submission failed (${status}).
  • Catch -> generic network error + console.error for dev visibility
  • 500s and other real errors will be surfaced, not masked. Good.

Accessibility:

  • All inputs have for/id label pairs
  • K/Q radio group has role="radiogroup" + aria-label="Kings or Queens" (+page.svelte:218)
  • Tier cards wrap the radio inside <label class="jp-tier-click"> — full card is the click target
  • Status banner has role="status" + aria-live="polite" (+page.svelte:406)
  • autocomplete="name", autocomplete="email", inputmode="email", inputmode="numeric" all set
  • Tap targets: cards and submit button are full-width / block. Mobile-first CSS (grid-template-columns: 1fr at base, 1fr 1fr @ 640px+)

Security:

  • No secrets, no credentials, no hardcoded tokens
  • Bearer token sourced from getToken() at submit time only
  • Svelte auto-escapes all templated values including statusMessage
  • No {@html} anywhere
  • console.error on catch is the only logging — acceptable

BLOCKERS

None.

NITS

  1. .jersey-* class dependency unverified — The <style> block references :global(.jersey-option input[name='tier']) and :global(.jersey-option.is-selected). The template uses .jersey-option, .jersey-options, .jersey-form-group, .jersey-form-label, .jersey-select, .jersey-option-featured, .jersey-option-badge, .jersey-option-img, .jersey-option-body, .jersey-option-header, .jersey-option-name, .jersey-option-price, .jersey-option-desc, plus .btn, .btn-primary, .btn-block, .container. PR body claims "reuses existing .jersey-option / .jersey-form-group classes from src/app.css," but on my local main checkout of the sibling repo, only jersey-number-input, jersey-number-input-wrap, jersey-number-input-error, and jersey-number-hint appear (all used inside (app)/jersey/+page.svelte, not defined globally). npm run build passing does NOT prove these classes exist — unused global selectors render unstyled silently. Manual viewport QA is mandatory (already in the Test Plan checklist, just flagging it's load-bearing, not cosmetic).
  2. src/routes/(app)/jersey-public/+page.svelte:188<option value="">Select a team…</option> is not disabled, while the size selects (lines 276, 294) use <option value="" disabled>. Inconsistent UX. Non-blocking since form validation catches empty string.
  3. src/lib/keycloak.js:143-149 (new getEmail) — uses one-liner optional chaining instead of the early-return guard pattern of getUserId() / getUserName(). Functionally equivalent. Consider matching style in a future touch-up.
  4. +page.svelte:135console.error('jersey-public submit failed:', err) — fine for now but consider a telemetry hook once observability wires up.
  5. Team list hardcoded at +page.svelte:192-195 with <!-- TODO: populate from /api/teams ... -->. Already tracked as follow-up per PR body. Not a blocker.

SOP COMPLIANCE

  • Branch named after issue (243-jersey-public-svelte-route)
  • PR body has Summary / Changes / Test Plan / Related
  • Closes #243 linking present
  • Related Notes references story (WS-S31), arch (arch-jersey-intake), board item #946, playground prototype, and relevant conventions (feedback_funnel_requires_auth.md, feedback_svelte_is_html.md, feedback_no_tailwind.md)
  • Auth chain documented per feedback_funnel_requires_auth.md
  • No secrets committed
  • No Tailwind (@apply, bg-*, text-*, flex-* not present — pure CSS vars + explicit selectors)
  • Svelte 5 runes used ($state, $derived, $props not needed here)
  • No scope creep — 2 files, both in-scope
  • Missing test runner called out as separate/deferred ticket (not blocking per review scope)
  • Backend endpoint #430 called out as deferred

PROCESS OBSERVATIONS

  • Clean, surgical PR: 547 additions, 0 deletions, 2 files. Reviewable in one sitting.
  • Auth chain reasoning in the PR body is exemplary — exactly the pattern feedback_funnel_requires_auth.md asks for. Future public-route PRs should be held to this standard.
  • Graceful 404 fallback with Marcus's phone is a nice UX safety net while #430 lands. Just remember to remove the fallback copy (or at least soften it) once the backend ships.
  • DORA impact: zero risk to existing routes (new file + additive helper). Change failure risk is essentially limited to the new route itself. Deployment frequency positive.
  • Load-bearing item for validation: confirm the .jersey-* global classes render on the deployed route. If they don't, this page will look broken even though build and check pass.

VERDICT: APPROVED

## PR #246 Review ### DOMAIN REVIEW Stack: SvelteKit 5 (runes: `$state`, `$derived`), pure CSS, Keycloak JS adapter. Single new route `(app)/jersey-public/+page.svelte` (539 lines) + 8-line `getEmail()` helper in `src/lib/keycloak.js`. **Auth chain verified:** - `src/routes/(app)/+layout.svelte:26` — `PUBLIC_APP_ROUTES` array checked against my local main. Contains `/register`, `/signin`, `/jersey`, `/jersey/success`, `/jersey/cancel`, `/checkout`, `/checkout/success`, `/checkout/cancel`, `/forgot-password`, `/reset-password`. **Does NOT contain `/jersey-public`** — confirmed. The reactive `$effect` guard at `(app)/+layout.svelte:46-62` will `goto('/signin')` for unauthenticated visitors. Auth chain is sound and matches `feedback_funnel_requires_auth.md`. - PR body explicitly documents the 7-step auth chain. Compliant. **`getEmail()` helper:** ```js export function getEmail() { return keycloak?.tokenParsed?.email || ''; } ``` Functionally correct. Minor style drift from the existing `getUserName()` / `getUserId()` pattern which uses an early `if (!keycloak?.tokenParsed) return '';` guard before destructuring — but the optional-chaining one-liner is equivalent and arguably cleaner. Non-blocking nit. **`getToken()` usage:** `const token = await getToken();` then spreads `Authorization: Bearer ${token}` conditionally. Matches the `Promise<string|null>` signature at `src/lib/keycloak.js:104`. Token refresh (`updateToken(30)`) is handled inside `getToken()`. Correct. **System A / System C scope isolation:** PR metadata shows `changed_files: 2`. Only `src/lib/keycloak.js` and the new `(app)/jersey-public/+page.svelte`. No modifications to `(public)/jersey`, `(app)/jersey`, `(app)/checkout`, or `(app)/+layout.svelte`. Clean scope. **Form validation (+page.svelte:58-78):** - Required: `playerName`, `email` (w/ regex), `team`, `kq`, `topSize`, `shortSize`, `tier` - Optional: `num1`, `num2`, `num3` — validated against `/^(0|00|[1-9][0-9]?)$/` only when non-empty - Single `errors.numbers` flag for all three number fields - `EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/` — permissive but sufficient - `trim()` applied on compare and on payload Matches spec. **K/Q image swap (+page.svelte:14-23 + 43-44):** 4 MinIO URLs (kings-home, kings-away-new, queens-home, queens-away-new) in `JERSEY_IMAGES` constant. `$derived` swaps both reversible and shooter images based on `kq`. Fallback `?? JERSEY_IMAGES.kings.reversible`. Verified 4 URLs match `minio-api.tail5b443a.ts.net/assets/westside/jerseys/`. **Submit (+page.svelte:96-157):** - 201 -> success + partial form reset (keeps name/email, clears team/kq/sizes/numbers/tier) - 404 -> friendly "backend not deployed" message w/ Marcus's phone `(385) 450-9963` (number is used consistently across 7 other files in the repo) - Other statuses -> extracts `body.message || body.detail`, falls back to `Submission failed (${status}).` - Catch -> generic network error + `console.error` for dev visibility - 500s and other real errors will be surfaced, not masked. Good. **Accessibility:** - All inputs have `for`/`id` label pairs - K/Q radio group has `role="radiogroup"` + `aria-label="Kings or Queens"` (+page.svelte:218) - Tier cards wrap the radio inside `<label class="jp-tier-click">` — full card is the click target - Status banner has `role="status"` + `aria-live="polite"` (+page.svelte:406) - `autocomplete="name"`, `autocomplete="email"`, `inputmode="email"`, `inputmode="numeric"` all set - Tap targets: cards and submit button are full-width / block. Mobile-first CSS (`grid-template-columns: 1fr` at base, `1fr 1fr` @ 640px+) **Security:** - No secrets, no credentials, no hardcoded tokens - Bearer token sourced from `getToken()` at submit time only - Svelte auto-escapes all templated values including `statusMessage` - No `{@html}` anywhere - `console.error` on catch is the only logging — acceptable ### BLOCKERS None. ### NITS 1. **`.jersey-*` class dependency unverified** — The `<style>` block references `:global(.jersey-option input[name='tier'])` and `:global(.jersey-option.is-selected)`. The template uses `.jersey-option`, `.jersey-options`, `.jersey-form-group`, `.jersey-form-label`, `.jersey-select`, `.jersey-option-featured`, `.jersey-option-badge`, `.jersey-option-img`, `.jersey-option-body`, `.jersey-option-header`, `.jersey-option-name`, `.jersey-option-price`, `.jersey-option-desc`, plus `.btn`, `.btn-primary`, `.btn-block`, `.container`. PR body claims "reuses existing `.jersey-option` / `.jersey-form-group` classes from `src/app.css`," but on my local `main` checkout of the sibling repo, only `jersey-number-input`, `jersey-number-input-wrap`, `jersey-number-input-error`, and `jersey-number-hint` appear (all used inside `(app)/jersey/+page.svelte`, not defined globally). `npm run build` passing does NOT prove these classes exist — unused global selectors render unstyled silently. **Manual viewport QA is mandatory** (already in the Test Plan checklist, just flagging it's load-bearing, not cosmetic). 2. `src/routes/(app)/jersey-public/+page.svelte:188` — `<option value="">Select a team…</option>` is not `disabled`, while the size selects (lines 276, 294) use `<option value="" disabled>`. Inconsistent UX. Non-blocking since form validation catches empty string. 3. `src/lib/keycloak.js:143-149` (new `getEmail`) — uses one-liner optional chaining instead of the early-return guard pattern of `getUserId()` / `getUserName()`. Functionally equivalent. Consider matching style in a future touch-up. 4. `+page.svelte:135` — `console.error('jersey-public submit failed:', err)` — fine for now but consider a telemetry hook once observability wires up. 5. Team list hardcoded at `+page.svelte:192-195` with `<!-- TODO: populate from /api/teams ... -->`. Already tracked as follow-up per PR body. Not a blocker. ### SOP COMPLIANCE - [x] Branch named after issue (`243-jersey-public-svelte-route`) - [x] PR body has Summary / Changes / Test Plan / Related - [x] `Closes #243` linking present - [x] Related Notes references story (`WS-S31`), arch (`arch-jersey-intake`), board item `#946`, playground prototype, and relevant conventions (`feedback_funnel_requires_auth.md`, `feedback_svelte_is_html.md`, `feedback_no_tailwind.md`) - [x] Auth chain documented per `feedback_funnel_requires_auth.md` - [x] No secrets committed - [x] No Tailwind (`@apply`, `bg-*`, `text-*`, `flex-*` not present — pure CSS vars + explicit selectors) - [x] Svelte 5 runes used (`$state`, `$derived`, `$props` not needed here) - [x] No scope creep — 2 files, both in-scope - [x] Missing test runner called out as separate/deferred ticket (not blocking per review scope) - [x] Backend endpoint `#430` called out as deferred ### PROCESS OBSERVATIONS - Clean, surgical PR: 547 additions, 0 deletions, 2 files. Reviewable in one sitting. - Auth chain reasoning in the PR body is exemplary — exactly the pattern `feedback_funnel_requires_auth.md` asks for. Future public-route PRs should be held to this standard. - Graceful 404 fallback with Marcus's phone is a nice UX safety net while #430 lands. Just remember to remove the fallback copy (or at least soften it) once the backend ships. - DORA impact: zero risk to existing routes (new file + additive helper). Change failure risk is essentially limited to the new route itself. Deployment frequency positive. - Load-bearing item for validation: confirm the `.jersey-*` global classes render on the deployed route. If they don't, this page will look broken even though build and check pass. ### VERDICT: APPROVED
fix: add missing .jersey-* class definitions to jersey-public component
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
31f5df3a73
QA on PR #246 flagged that the markup references .jersey-option, .jersey-form-group,
.jersey-select, .jersey-form-label, etc., but none of those classes were defined in
the component's <style> block or in src/app.css. The page would have rendered
unstyled in production.

Port the class definitions from westside-playground/shared/style.css lines 4037-4193
into the component's style block, wrapped in :global() so they resolve despite Svelte's
per-component class hashing. Component stays self-contained — no shared-file changes.

Refs #243
forgejo_admin deleted branch 243-jersey-public-svelte-route 2026-04-10 23:45:02 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ldraney/westside-app!246
No description provided.