Build Gate Screen — code entry and URL parameter flow #5

Open
opened 2026-05-05 10:57:13 +02:00 by marcel · 8 comments
Owner

Task 5 — Plan reference: docs/superpowers/plans/2026-05-05-erbstuecke-wannsee.md

User story (US-AUTH-001 + US-AUTH-002):

  • As a family member, I open a link with ?code=AB3K7MN2 and am immediately logged in and redirected to the gallery.
  • As a family member without a link, I can enter my code manually and reach the gallery.

Acceptance criteria

  • GET /?code=AB3K7MN2 validates code server-side, sets family_code HTTP-only cookie, redirects to /galerie (US-AUTH-001)
  • GET / with valid family_code cookie already set → immediately redirects to /galerie (no gate shown)
  • Manual form entry: correct code → cookie set, redirect to /galerie (US-AUTH-002)
  • Wrong code → error message: "Code nicht bekannt — bitte prüfe die Eingabe." with ⚠ icon (not color alone — WCAG)
  • Code input is font-mono, uppercase, tracking-[4px], centered, maxlength="8", inputmode="text", autocomplete="off"
  • Works without JavaScript (form submits to POST action)
  • Page title: "Erbstücke Wannsee"
  • Layout matches spec: centered card, max-w-sm, 🏡 icon, hint text at bottom

Design spec reference

View 01 in docs/superpowers/specs/2026-05-05-erbstuecke-wannsee-views.html

Files to create

  • src/routes/+page.svelte
  • src/routes/+page.server.ts

Depends on: #3 | Size: XS | Spec: reservierung-design §4.1, views spec View 01

## Task 5 — Plan reference: `docs/superpowers/plans/2026-05-05-erbstuecke-wannsee.md` **User story (US-AUTH-001 + US-AUTH-002):** - As a family member, I open a link with `?code=AB3K7MN2` and am immediately logged in and redirected to the gallery. - As a family member without a link, I can enter my code manually and reach the gallery. ### Acceptance criteria - [ ] `GET /?code=AB3K7MN2` validates code server-side, sets `family_code` HTTP-only cookie, redirects to `/galerie` (US-AUTH-001) - [ ] `GET /` with valid `family_code` cookie already set → immediately redirects to `/galerie` (no gate shown) - [ ] Manual form entry: correct code → cookie set, redirect to `/galerie` (US-AUTH-002) - [ ] Wrong code → error message: "Code nicht bekannt — bitte prüfe die Eingabe." with ⚠ icon (not color alone — WCAG) - [ ] Code input is `font-mono`, `uppercase`, `tracking-[4px]`, centered, `maxlength="8"`, `inputmode="text"`, `autocomplete="off"` - [ ] Works without JavaScript (form submits to POST action) - [ ] Page title: "Erbstücke Wannsee" - [ ] Layout matches spec: centered card, max-w-sm, `🏡` icon, hint text at bottom ### Design spec reference View 01 in `docs/superpowers/specs/2026-05-05-erbstuecke-wannsee-views.html` ### Files to create - `src/routes/+page.svelte` - `src/routes/+page.server.ts` **Depends on:** #3 | **Size:** XS | **Spec:** reservierung-design §4.1, views spec View 01
marcel added this to the v1.0 — MVP milestone 2026-05-05 10:57:13 +02:00
Author
Owner

👤 Markus Keller — Application Architect

Observations

  • The issue correctly places both the URL-parameter flow and the manual form in a single +page.server.ts. This is the right shape: one route, one file, two entry points (GET load + POST action).
  • The spec says GET /?code=AB3K7MN2 validates server-side, sets cookie, and redirects. This must live in the load function, not in a Form Action — correct, because it is triggered by a GET.
  • The spec also says "valid family_code cookie already set → immediately redirects to /galerie". This redirect belongs in hooks.server.ts or at the top of the load function, not scattered into middleware. Placing it in load is fine for a single-route gate; the hook approach is cleaner if other routes later need the same check.
  • The acceptance criterion "Works without JavaScript (form submits to POST action)" confirms the Form Action pattern for manual entry. The use:enhance layer is additive, not load-bearing.
  • locals.familyCode must be populated by hooks.server.ts before the load function runs. The issue depends on #3 (which presumably sets up the hooks.server.ts auth layer and the db.ts singleton). This dependency is correctly called out.
  • No REST API layer, no /api/ prefix, no onMount fetch — the route tree matches the architecture spec exactly.
  • The +page.server.ts for this screen needs: (1) a prepared statement against the codes table, (2) the cookie-set helper from lib/auth.ts, and (3) a redirect(303, '/galerie') on success. All three belong in lib/, not inlined in the route.

Recommendations

  • Define the load function to check locals.familyCode first (already set by hooks) and redirect immediately, before touching url.searchParams. This keeps the fast path at the top.
  • For the ?code= parameter validation in load, do not create a new DB query inside the load body — use the prepared statement exported from lib/db.ts (stmtCodeByValue). It must be prepared at module load time, not per-request.
  • The manual form POST action should return fail(401, { error: 'code_not_found' }) on invalid code — never expose the raw code value in the error body. The template maps the error key to the German string.
  • Do not add a pre-flight check (SELECT before INSERT/cookie-set). The DB lookup either returns a row or it doesn't — one round trip only.
  • redirect(303, '/galerie') after successful cookie-set in both paths (URL param and form). SvelteKit's redirect() throws, so no explicit return is needed afterward.
  • Export SCHEMA_SQL from lib/db.ts so integration tests can set up an identical :memory: database for this route's action.

Open Decisions (omit if none)

  • Cookie-redirect in load vs. hooks: The "valid cookie → redirect" check can live in hooks.server.ts (applies globally to /) or at the top of +page.svelte's load. In hooks.server.ts it is DRY if other unauthenticated routes are added later; in load it is more explicit and co-located with the gate logic. For a single-route gate, either works — but the choice should be made once and documented so it isn't revisited per-route.
## 👤 Markus Keller — Application Architect ### Observations - The issue correctly places both the URL-parameter flow and the manual form in a single `+page.server.ts`. This is the right shape: one route, one file, two entry points (GET load + POST action). - The spec says `GET /?code=AB3K7MN2` validates server-side, sets cookie, and redirects. This must live in the `load` function, not in a Form Action — correct, because it is triggered by a GET. - The spec also says "valid `family_code` cookie already set → immediately redirects to `/galerie`". This redirect belongs in `hooks.server.ts` or at the top of the `load` function, not scattered into middleware. Placing it in `load` is fine for a single-route gate; the hook approach is cleaner if other routes later need the same check. - The acceptance criterion "Works without JavaScript (form submits to POST action)" confirms the Form Action pattern for manual entry. The `use:enhance` layer is additive, not load-bearing. - `locals.familyCode` must be populated by `hooks.server.ts` before the load function runs. The issue depends on `#3` (which presumably sets up the `hooks.server.ts` auth layer and the `db.ts` singleton). This dependency is correctly called out. - No REST API layer, no `/api/` prefix, no `onMount` fetch — the route tree matches the architecture spec exactly. - The `+page.server.ts` for this screen needs: (1) a prepared statement against the `codes` table, (2) the cookie-set helper from `lib/auth.ts`, and (3) a `redirect(303, '/galerie')` on success. All three belong in `lib/`, not inlined in the route. ### Recommendations - Define the `load` function to check `locals.familyCode` first (already set by hooks) and redirect immediately, before touching `url.searchParams`. This keeps the fast path at the top. - For the `?code=` parameter validation in `load`, do **not** create a new DB query inside the load body — use the prepared statement exported from `lib/db.ts` (`stmtCodeByValue`). It must be prepared at module load time, not per-request. - The manual form POST action should return `fail(401, { error: 'code_not_found' })` on invalid code — never expose the raw code value in the error body. The template maps the error key to the German string. - Do not add a pre-flight check (SELECT before INSERT/cookie-set). The DB lookup either returns a row or it doesn't — one round trip only. - `redirect(303, '/galerie')` after successful cookie-set in both paths (URL param and form). SvelteKit's `redirect()` throws, so no explicit `return` is needed afterward. - Export `SCHEMA_SQL` from `lib/db.ts` so integration tests can set up an identical `:memory:` database for this route's action. ### Open Decisions _(omit if none)_ - **Cookie-redirect in load vs. hooks**: The "valid cookie → redirect" check can live in `hooks.server.ts` (applies globally to `/`) or at the top of `+page.svelte`'s `load`. In `hooks.server.ts` it is DRY if other unauthenticated routes are added later; in `load` it is more explicit and co-located with the gate logic. For a single-route gate, either works — but the choice should be made once and documented so it isn't revisited per-route.
Author
Owner

👤 Felix Brandt — Fullstack Developer

Observations

  • Two files to create: src/routes/+page.svelte and src/routes/+page.server.ts. The route shape is correct — gate screen lives at the root route.
  • The +page.server.ts needs two entry points: the load function (handles ?code= URL param) and a default Form Action (handles manual POST). The acceptance criteria map cleanly to these two.
  • The +page.svelte is a pure form component — no $state needed beyond maybe a submitting flag for the use:enhance loading state. No $derived logic is required here; all branching is server-side.
  • The error message "Code nicht bekannt — bitte prüfe die Eingabe." with ⚠ icon must come from form?.error returned by fail(401, { error: 'code_not_found' }). The template renders the German string, not the raw key.
  • Code input spec: font-mono, uppercase, tracking-[4px], centered, maxlength="8", inputmode="text", autocomplete="off". The uppercase CSS class on the input does not uppercase the submitted value server-side — the server must call .toUpperCase() on the form data value before DB lookup.
  • No onMount + fetch. No client-side validation. All validation in the Form Action. The component receives data (from load) and form (from action result).
  • The use:enhance directive makes the form work with JS but must not be required for the no-JS path (AC: "Works without JavaScript").

Recommendations

  • In the Form Action, extract the code with: const code = String(formData.get('code') ?? '').trim().toUpperCase(). Validate: if (!code || code.length !== 8) return fail(400, { error: 'code_invalid_format' }). Then look up the DB. This is the correct order: format check → DB lookup.
  • Use redirect(303, '/galerie') (imported from @sveltejs/kit) on success in both load and the action. In the action, it throws — do not return after calling it.
  • In +page.svelte, bind use:enhance to set submitting = $state(false) so the button shows a loading state when JS is available. The button text can switch to "Bitte warten…" while submitting — but the form must submit and work identically without JS.
  • The <input> element needs name="code" to match formData.get('code') on the server. This is the most common omission on gate screens.
  • Map form?.error values to user strings in the template — never render form?.error directly:
    {#if form?.error === 'code_not_found' || form?.error === 'code_invalid_format'}
      <p class="text-xs text-status-taken mt-2 flex items-center gap-1.5">
        <span aria-hidden="true"></span> Code nicht bekannt — bitte prüfe die Eingabe.
      </p>
    {/if}
    
  • The load function reads locals.familyCode (set by hooks.server.ts) and redirects to /galerie if already set. It then reads url.searchParams.get('code'), looks it up, sets the cookie, and redirects. If the param is absent or invalid, it returns {} (empty — renders the gate form).

Open Decisions (omit if none)

  • Uppercase enforcement on input: The spec says uppercase CSS class. This visually uppercases but does not uppercase the submitted value in all browsers consistently. The server must normalize regardless. The question is whether to also add a JS oninput handler that uppercases in real time for UX — this is a progressive enhancement decision (no impact on correctness, small UX improvement). Mark as a UI polish call.
## 👤 Felix Brandt — Fullstack Developer ### Observations - Two files to create: `src/routes/+page.svelte` and `src/routes/+page.server.ts`. The route shape is correct — gate screen lives at the root route. - The `+page.server.ts` needs two entry points: the `load` function (handles `?code=` URL param) and a `default` Form Action (handles manual POST). The acceptance criteria map cleanly to these two. - The `+page.svelte` is a pure form component — no `$state` needed beyond maybe a `submitting` flag for the `use:enhance` loading state. No `$derived` logic is required here; all branching is server-side. - The error message "Code nicht bekannt — bitte prüfe die Eingabe." with ⚠ icon must come from `form?.error` returned by `fail(401, { error: 'code_not_found' })`. The template renders the German string, not the raw key. - Code input spec: `font-mono`, `uppercase`, `tracking-[4px]`, centered, `maxlength="8"`, `inputmode="text"`, `autocomplete="off"`. The `uppercase` CSS class on the input does **not** uppercase the submitted value server-side — the server must call `.toUpperCase()` on the form data value before DB lookup. - No `onMount` + fetch. No client-side validation. All validation in the Form Action. The component receives `data` (from `load`) and `form` (from action result). - The `use:enhance` directive makes the form work with JS but must not be required for the no-JS path (AC: "Works without JavaScript"). ### Recommendations - In the Form Action, extract the code with: `const code = String(formData.get('code') ?? '').trim().toUpperCase()`. Validate: `if (!code || code.length !== 8) return fail(400, { error: 'code_invalid_format' })`. Then look up the DB. This is the correct order: format check → DB lookup. - Use `redirect(303, '/galerie')` (imported from `@sveltejs/kit`) on success in both `load` and the action. In the action, it throws — do not return after calling it. - In `+page.svelte`, bind `use:enhance` to set `submitting = $state(false)` so the button shows a loading state when JS is available. The button text can switch to "Bitte warten…" while submitting — but the form must submit and work identically without JS. - The `<input>` element needs `name="code"` to match `formData.get('code')` on the server. This is the most common omission on gate screens. - Map `form?.error` values to user strings in the template — never render `form?.error` directly: ```svelte {#if form?.error === 'code_not_found' || form?.error === 'code_invalid_format'} <p class="text-xs text-status-taken mt-2 flex items-center gap-1.5"> <span aria-hidden="true">⚠</span> Code nicht bekannt — bitte prüfe die Eingabe. </p> {/if} ``` - The `load` function reads `locals.familyCode` (set by `hooks.server.ts`) and redirects to `/galerie` if already set. It then reads `url.searchParams.get('code')`, looks it up, sets the cookie, and redirects. If the param is absent or invalid, it returns `{}` (empty — renders the gate form). ### Open Decisions _(omit if none)_ - **Uppercase enforcement on input**: The spec says `uppercase` CSS class. This visually uppercases but does not uppercase the submitted value in all browsers consistently. The server must normalize regardless. The question is whether to also add a JS `oninput` handler that uppercases in real time for UX — this is a progressive enhancement decision (no impact on correctness, small UX improvement). Mark as a UI polish call.
Author
Owner

👤 Nora "NullX" Steiner — Application Security Engineer

Observations

  • The gate screen is the only publicly accessible surface. Its attack surface: code brute-force via the form POST and via crafted URL parameters, and user enumeration via differing error messages.
  • The issue's error message spec is already correct: "Code nicht bekannt — bitte prüfe die Eingabe." — one generic message for all failure modes (wrong code, malformed code, missing code). This avoids the user-enumeration pattern of returning different errors for "no such code" vs. "code exists but…".
  • The spec says "⚠ icon + text — kein Farbe allein (WCAG)". This is also the correct security-UI pattern: the error communicates via both text and icon, not a red border alone.
  • The cookie must be set with httpOnly: true, sameSite: 'strict', secure: !dev, path: '/', and a meaningful maxAge. The issue does not specify maxAge — the system design implies 30 days for family members. This must be explicit in lib/auth.ts.
  • Code brute-force: 8 characters from the charset used in the project (ABCDEFGHJKLMNPQRSTUVWXYZ23456789 — 32 chars) = 32^8 ≈ 10^12 combinations. Not brute-forceable at normal web request rates. No rate limiting is required for MVP at this scale (family-only app, short-lived). However, SvelteKit has no built-in rate limiting — Caddy can add it at the proxy layer if needed.
  • Reflecting the code value: The spec error message does not include the submitted code. Confirmed correct — never echo input_code back in the error (CWE-209).
  • URL parameter handling: url.searchParams.get('code') in the load function. The value must be passed through .trim().toUpperCase() and length-checked before the DB query. A 10,000-character string in the URL parameter should not reach the prepared statement.
  • Cookie set in load vs. action: Setting the cookie from a load function is supported in SvelteKit via event.cookies.set(). The load function receives the full RequestEvent. This is correct for the GET ?code= flow.

Recommendations

  • In lib/auth.ts, define the cookie-set helper with all required options hardcoded — never leave sameSite, httpOnly, or secure to caller discretion:
    export function setFamilyCodeCookie(cookies: Cookies, codeStr: string) {
      // HttpOnly prevents XSS from reading the session token
      cookies.set('family_code', codeStr, {
        httpOnly: true,
        sameSite: 'strict',
        secure: !dev,
        path: '/',
        maxAge: 60 * 60 * 24 * 30,  // 30 days
      });
    }
    
  • Sanitize the URL parameter before DB lookup: const param = (url.searchParams.get('code') ?? '').trim().toUpperCase().slice(0, 8). The .slice(0, 8) bounds the input before it touches any query.
  • The Form Action must use the same sanitization. Extract into a shared sanitizeCode(raw: string): string helper in lib/auth.ts to avoid drift between the two paths.
  • Do not log the code value at any level — not to console, not to a hypothetical log sink. Log only the code's row ID after a successful lookup (non-sensitive identifier).
  • Verify the prepared statement in lib/db.ts is parameterized: db.prepare('SELECT * FROM codes WHERE code = ?') — never string-interpolated. This is CWE-89 (SQL injection) prevention.

Open Decisions (omit if none)

  • Rate limiting on the gate POST: At family scale (< 20 users), brute-force is theoretical. Caddy can add rate_limit via a plugin, but this adds operational complexity. The decision is: accept the theoretical risk for MVP (probability near zero, no PII at stake beyond family item preferences), or add Caddy-level rate limiting now. This is a risk-tolerance call for the project owner, not a technical constraint.
## 👤 Nora "NullX" Steiner — Application Security Engineer ### Observations - The gate screen is the only publicly accessible surface. Its attack surface: code brute-force via the form POST and via crafted URL parameters, and user enumeration via differing error messages. - The issue's error message spec is already correct: "Code nicht bekannt — bitte prüfe die Eingabe." — one generic message for all failure modes (wrong code, malformed code, missing code). This avoids the user-enumeration pattern of returning different errors for "no such code" vs. "code exists but…". - The spec says "⚠ icon + text — kein Farbe allein (WCAG)". This is also the correct security-UI pattern: the error communicates via both text and icon, not a red border alone. - The cookie must be set with `httpOnly: true`, `sameSite: 'strict'`, `secure: !dev`, `path: '/'`, and a meaningful `maxAge`. The issue does not specify `maxAge` — the system design implies 30 days for family members. This must be explicit in `lib/auth.ts`. - **Code brute-force**: 8 characters from the charset used in the project (`ABCDEFGHJKLMNPQRSTUVWXYZ23456789` — 32 chars) = 32^8 ≈ 10^12 combinations. Not brute-forceable at normal web request rates. No rate limiting is required for MVP at this scale (family-only app, short-lived). However, SvelteKit has no built-in rate limiting — Caddy can add it at the proxy layer if needed. - **Reflecting the code value**: The spec error message does not include the submitted code. Confirmed correct — never echo `input_code` back in the error (CWE-209). - **URL parameter handling**: `url.searchParams.get('code')` in the `load` function. The value must be passed through `.trim().toUpperCase()` and length-checked before the DB query. A 10,000-character string in the URL parameter should not reach the prepared statement. - **Cookie set in `load` vs. action**: Setting the cookie from a `load` function is supported in SvelteKit via `event.cookies.set()`. The `load` function receives the full `RequestEvent`. This is correct for the GET `?code=` flow. ### Recommendations - In `lib/auth.ts`, define the cookie-set helper with all required options hardcoded — never leave `sameSite`, `httpOnly`, or `secure` to caller discretion: ```typescript export function setFamilyCodeCookie(cookies: Cookies, codeStr: string) { // HttpOnly prevents XSS from reading the session token cookies.set('family_code', codeStr, { httpOnly: true, sameSite: 'strict', secure: !dev, path: '/', maxAge: 60 * 60 * 24 * 30, // 30 days }); } ``` - Sanitize the URL parameter before DB lookup: `const param = (url.searchParams.get('code') ?? '').trim().toUpperCase().slice(0, 8)`. The `.slice(0, 8)` bounds the input before it touches any query. - The Form Action must use the same sanitization. Extract into a shared `sanitizeCode(raw: string): string` helper in `lib/auth.ts` to avoid drift between the two paths. - Do **not** log the code value at any level — not to console, not to a hypothetical log sink. Log only the code's row ID after a successful lookup (non-sensitive identifier). - Verify the prepared statement in `lib/db.ts` is parameterized: `db.prepare('SELECT * FROM codes WHERE code = ?')` — never string-interpolated. This is CWE-89 (SQL injection) prevention. ### Open Decisions _(omit if none)_ - **Rate limiting on the gate POST**: At family scale (< 20 users), brute-force is theoretical. Caddy can add `rate_limit` via a plugin, but this adds operational complexity. The decision is: accept the theoretical risk for MVP (probability near zero, no PII at stake beyond family item preferences), or add Caddy-level rate limiting now. This is a risk-tolerance call for the project owner, not a technical constraint.
Author
Owner

👤 Sara Holt — QA Engineer & Test Strategist

Observations

  • This issue has 8 acceptance criteria, all of which are testable. The split between server behavior (AC 1–3) and UI spec (AC 4–8) maps cleanly to integration tests and component/E2E tests respectively.
  • There are no existing test files in the project yet (src/lib/db.test.ts, auth.test.ts are listed in the plan but not created). This is Issue #5 — the gate screen — and the plan marks it as depending on Issue #3 (project scaffold + hooks). The test infrastructure must exist before tests for this issue can run.
  • The two code-validation paths (URL param in load, manual entry in action) are distinct server behaviors that each need an integration test. They are not the same code path — do not cover them with a single test.
  • AC "Works without JavaScript" is a server-rendering guarantee. It is verified by submitting the form with fetch (no JS) or by disabling JS in Playwright and navigating to /?code=VALID.
  • The error message acceptance criterion specifies exact German text and a ⚠ icon. The test must assert on the exact rendered text, not the error key.

Recommendations

Unit tests (Vitest, lib/auth.ts):

it('sanitizeCode trims, uppercases, and slices to 8 chars', () => {
  expect(sanitizeCode('  ab3k7mn2extra  ')).toBe('AB3K7MN2');
});
it('sanitizeCode handles empty string', () => {
  expect(sanitizeCode('')).toBe('');
});

Integration tests (Vitest + :memory: SQLite, src/routes/+page.server.ts):

  • GET /?code=VALID8CHR → sets family_code cookie, redirects 303 to /galerie
  • GET /?code=BADCODE1 (not in DB) → renders gate form (no redirect, no cookie set)
  • POST / with valid code → sets cookie, redirects 303 to /galerie
  • POST / with unknown code → fail(401, { error: 'code_not_found' })
  • POST / with malformed code (length ≠ 8) → fail(400, { error: 'code_invalid_format' })
  • GET / with valid family_code cookie already set → redirects 303 to /galerie (no gate rendered)

E2E (Playwright):

test('URL code link logs in and redirects to gallery', async ({ page }) => {
  await page.goto(`/?code=${TEST_CODE}`);
  await expect(page).toHaveURL('/galerie');
});

test('manual code entry with wrong code shows German error message', async ({ page }) => {
  await page.goto('/');
  await page.getByRole('textbox').fill('WRONGCOD');
  await page.getByRole('button', { name: 'Weiter' }).click();
  await expect(page.getByText('Code nicht bekannt — bitte prüfe die Eingabe.')).toBeVisible();
  await expect(page.getByText('⚠')).toBeVisible();
});

test('gate works without JavaScript', async ({ browser }) => {
  const context = await browser.newContext({ javaScriptEnabled: false });
  const page = await context.newPage();
  await page.goto('/');
  await page.getByRole('textbox').fill(TEST_CODE);
  await page.getByRole('button', { name: 'Weiter' }).click();
  await expect(page).toHaveURL('/galerie');
  await context.close();
});

Accessibility (axe-core in Playwright):

test('gate screen passes WCAG 2.1 AA', async ({ page }) => {
  await page.goto('/');
  const results = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa']).analyze();
  expect(results.violations).toEqual([]);
});
  • All integration tests must use :memory: SQLite with SCHEMA_SQL from lib/db.ts. Never share a DB instance across tests.
  • Do not test the Form Action via Playwright for the error path — use a direct import of the action with a mock RequestEvent. Playwright E2E is for the happy path and the no-JS path.

Open Decisions (omit if none)

  • Test seed data strategy: Integration tests need a valid code row in the DB to exercise the happy path. The options are: (a) insert a known test code directly in beforeEach, or (b) expose a test-only createCode helper from lib/auth.ts. Option (a) is simpler and keeps test data co-located with the test. Option (b) is more reusable across multiple test files. For a project of this size, option (a) is sufficient — but decide once and apply consistently across all route tests. (Raised by: Sara Holt)
## 👤 Sara Holt — QA Engineer & Test Strategist ### Observations - This issue has 8 acceptance criteria, all of which are testable. The split between server behavior (AC 1–3) and UI spec (AC 4–8) maps cleanly to integration tests and component/E2E tests respectively. - There are no existing test files in the project yet (`src/lib/db.test.ts`, `auth.test.ts` are listed in the plan but not created). This is Issue #5 — the gate screen — and the plan marks it as depending on Issue #3 (project scaffold + hooks). The test infrastructure must exist before tests for this issue can run. - The two code-validation paths (URL param in `load`, manual entry in action) are distinct server behaviors that each need an integration test. They are not the same code path — do not cover them with a single test. - AC "Works without JavaScript" is a server-rendering guarantee. It is verified by submitting the form with `fetch` (no JS) or by disabling JS in Playwright and navigating to `/?code=VALID`. - The error message acceptance criterion specifies exact German text and a ⚠ icon. The test must assert on the exact rendered text, not the error key. ### Recommendations **Unit tests (Vitest, `lib/auth.ts`):** ```typescript it('sanitizeCode trims, uppercases, and slices to 8 chars', () => { expect(sanitizeCode(' ab3k7mn2extra ')).toBe('AB3K7MN2'); }); it('sanitizeCode handles empty string', () => { expect(sanitizeCode('')).toBe(''); }); ``` **Integration tests (Vitest + `:memory:` SQLite, `src/routes/+page.server.ts`):** - `GET /?code=VALID8CHR` → sets `family_code` cookie, redirects 303 to `/galerie` - `GET /?code=BADCODE1` (not in DB) → renders gate form (no redirect, no cookie set) - `POST /` with valid code → sets cookie, redirects 303 to `/galerie` - `POST /` with unknown code → `fail(401, { error: 'code_not_found' })` - `POST /` with malformed code (length ≠ 8) → `fail(400, { error: 'code_invalid_format' })` - `GET /` with valid `family_code` cookie already set → redirects 303 to `/galerie` (no gate rendered) **E2E (Playwright):** ```typescript test('URL code link logs in and redirects to gallery', async ({ page }) => { await page.goto(`/?code=${TEST_CODE}`); await expect(page).toHaveURL('/galerie'); }); test('manual code entry with wrong code shows German error message', async ({ page }) => { await page.goto('/'); await page.getByRole('textbox').fill('WRONGCOD'); await page.getByRole('button', { name: 'Weiter' }).click(); await expect(page.getByText('Code nicht bekannt — bitte prüfe die Eingabe.')).toBeVisible(); await expect(page.getByText('⚠')).toBeVisible(); }); test('gate works without JavaScript', async ({ browser }) => { const context = await browser.newContext({ javaScriptEnabled: false }); const page = await context.newPage(); await page.goto('/'); await page.getByRole('textbox').fill(TEST_CODE); await page.getByRole('button', { name: 'Weiter' }).click(); await expect(page).toHaveURL('/galerie'); await context.close(); }); ``` **Accessibility (axe-core in Playwright):** ```typescript test('gate screen passes WCAG 2.1 AA', async ({ page }) => { await page.goto('/'); const results = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa']).analyze(); expect(results.violations).toEqual([]); }); ``` - All integration tests must use `:memory:` SQLite with `SCHEMA_SQL` from `lib/db.ts`. Never share a DB instance across tests. - Do not test the Form Action via Playwright for the error path — use a direct import of the action with a mock `RequestEvent`. Playwright E2E is for the happy path and the no-JS path. ### Open Decisions _(omit if none)_ - **Test seed data strategy**: Integration tests need a valid code row in the DB to exercise the happy path. The options are: (a) insert a known test code directly in `beforeEach`, or (b) expose a test-only `createCode` helper from `lib/auth.ts`. Option (a) is simpler and keeps test data co-located with the test. Option (b) is more reusable across multiple test files. For a project of this size, option (a) is sufficient — but decide once and apply consistently across all route tests. _(Raised by: Sara Holt)_
Author
Owner

👤 Leonie Voss — UI/UX Design Lead & Accessibility Strategist

Observations

  • The spec (View 01) is detailed and implementation-ready. All Tailwind classes are specified in the impl-ref table. The mockup shows a centered card at all viewports — no responsive breakpoint variation needed for this screen.
  • The 🏡 icon is decorative (aria-hidden="true"). The card title "Erbstücke Wannsee" is the page's <h1> (Lora, 20px/700). The page <title> must also be "Erbstücke Wannsee" per AC.
  • The code input requires a <label> element. The spec shows no visible label (the subtext serves as the visual prompt). A sr-only label is mandatory for screen reader users — the input cannot be identified as "edit text" alone.
  • The error display requires icon + text — color alone is not sufficient (WCAG 1.4.1). The spec table confirms: "⚠ Icon + Text — kein Farbe allein". The error must also be announced by screen readers — use role="alert" or aria-live="assertive" on the error container.
  • The hint text at the bottom ("Noch kein Code? Wende dich an Marcel oder Tante.") is text-[10px] — the spec permits this as a below-minimum exception for hint text only. It is not body copy. Do not apply this font size anywhere else.
  • The "Weiter →" button is min-h-[48px] full-width — correct touch target (exceeds the 44px minimum). The spec shows min-height: 48px specifically for this button.
  • The code input height is h-12 (48px), with tracking-[4px], font-mono, uppercase, text-center. The spellcheck="false" attribute must be added to prevent browser spell-check from underlining the code characters.
  • No navigation bar on this screen — it is the gate, before authentication. Correct: the nav bar only appears on the gallery view.

Recommendations

  • Use a sr-only label for the code input:
    <label for="code-input" class="sr-only">Zugangscode eingeben</label>
    <input
      id="code-input"
      name="code"
      type="text"
      inputmode="text"
      autocomplete="off"
      spellcheck="false"
      maxlength="8"
      class="font-mono text-base font-semibold tracking-[4px] uppercase text-center w-full h-12 border border-line rounded-lg bg-[#FAFAF7] mb-3 focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-primary/30"
    />
    
  • Wrap the error paragraph in an aria-live region so screen readers announce the error after form submission without a page reload (when JS is active):
    <div aria-live="assertive" aria-atomic="true">
      {#if form?.error}
        <p class="text-xs text-status-taken mt-2 flex items-center gap-1.5">
          <span aria-hidden="true"></span>
          Code nicht bekannt — bitte prüfe die Eingabe.
        </p>
      {/if}
    </div>
    
  • Set the page <title> via SvelteKit's <svelte:head>:
    <svelte:head><title>Erbstücke Wannsee</title></svelte:head>
    
  • The 🏡 icon container uses bg-[#DFF0E6] — this is not a design token. It should either become a token (--color-free-bg or similar) or be accepted as a one-off. If the same shade appears elsewhere (the "Meine Reservierung" badge uses #E8F5EC), standardize on one value.
  • The "Weiter →" button label includes a directional arrow. For screen readers, the arrow character may be read as "right-pointing arrow" depending on the screen reader. Either use aria-label="Weiter" on the button and keep the arrow as aria-hidden, or use the word "Weiter" without the symbol.
  • Apply focus-visible:ring-2 focus-visible:ring-primary/30 focus-visible:border-primary on the submit button as well, to ensure keyboard users can see focus state. Never rely on browser defaults alone.

Open Decisions (omit if none)

  • The #DFF0E6 icon background color: This value appears in the spec as a one-off for the gate icon. It is close to but not identical to the "Meine Reservierung" badge color (#E8F5EC). Should this be consolidated into a single token, or documented as two intentionally distinct greens? This affects the design system's token count and must be decided before the design system tokens are finalized. (Raised by: Leonie Voss)
## 👤 Leonie Voss — UI/UX Design Lead & Accessibility Strategist ### Observations - The spec (View 01) is detailed and implementation-ready. All Tailwind classes are specified in the impl-ref table. The mockup shows a centered card at all viewports — no responsive breakpoint variation needed for this screen. - The 🏡 icon is decorative (`aria-hidden="true"`). The card title "Erbstücke Wannsee" is the page's `<h1>` (Lora, 20px/700). The page `<title>` must also be "Erbstücke Wannsee" per AC. - The code input requires a `<label>` element. The spec shows no visible label (the subtext serves as the visual prompt). A `sr-only` label is mandatory for screen reader users — the input cannot be identified as "edit text" alone. - The error display requires `⚠` icon + text — color alone is not sufficient (WCAG 1.4.1). The spec table confirms: "⚠ Icon + Text — kein Farbe allein". The error must also be announced by screen readers — use `role="alert"` or `aria-live="assertive"` on the error container. - The hint text at the bottom ("Noch kein Code? Wende dich an Marcel oder Tante.") is `text-[10px]` — the spec permits this as a below-minimum exception for hint text only. It is not body copy. Do not apply this font size anywhere else. - The "Weiter →" button is `min-h-[48px]` full-width — correct touch target (exceeds the 44px minimum). The spec shows `min-height: 48px` specifically for this button. - The code input height is `h-12` (48px), with `tracking-[4px]`, `font-mono`, `uppercase`, `text-center`. The `spellcheck="false"` attribute must be added to prevent browser spell-check from underlining the code characters. - No navigation bar on this screen — it is the gate, before authentication. Correct: the nav bar only appears on the gallery view. ### Recommendations - Use a `sr-only` label for the code input: ```svelte <label for="code-input" class="sr-only">Zugangscode eingeben</label> <input id="code-input" name="code" type="text" inputmode="text" autocomplete="off" spellcheck="false" maxlength="8" class="font-mono text-base font-semibold tracking-[4px] uppercase text-center w-full h-12 border border-line rounded-lg bg-[#FAFAF7] mb-3 focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-primary/30" /> ``` - Wrap the error paragraph in an `aria-live` region so screen readers announce the error after form submission without a page reload (when JS is active): ```svelte <div aria-live="assertive" aria-atomic="true"> {#if form?.error} <p class="text-xs text-status-taken mt-2 flex items-center gap-1.5"> <span aria-hidden="true">⚠</span> Code nicht bekannt — bitte prüfe die Eingabe. </p> {/if} </div> ``` - Set the page `<title>` via SvelteKit's `<svelte:head>`: ```svelte <svelte:head><title>Erbstücke Wannsee</title></svelte:head> ``` - The 🏡 icon container uses `bg-[#DFF0E6]` — this is not a design token. It should either become a token (`--color-free-bg` or similar) or be accepted as a one-off. If the same shade appears elsewhere (the "Meine Reservierung" badge uses `#E8F5EC`), standardize on one value. - The "Weiter →" button label includes a directional arrow. For screen readers, the arrow character `→` may be read as "right-pointing arrow" depending on the screen reader. Either use `aria-label="Weiter"` on the button and keep the arrow as `aria-hidden`, or use the word "Weiter" without the symbol. - Apply `focus-visible:ring-2 focus-visible:ring-primary/30 focus-visible:border-primary` on the submit button as well, to ensure keyboard users can see focus state. Never rely on browser defaults alone. ### Open Decisions _(omit if none)_ - **The `#DFF0E6` icon background color**: This value appears in the spec as a one-off for the gate icon. It is close to but not identical to the "Meine Reservierung" badge color (`#E8F5EC`). Should this be consolidated into a single token, or documented as two intentionally distinct greens? This affects the design system's token count and must be decided before the design system tokens are finalized. _(Raised by: Leonie Voss)_
Author
Owner

👤 Tobias Wendt — DevOps & Platform Engineer

Observations

  • Issue #5 creates src/routes/+page.svelte and src/routes/+page.server.ts — no infrastructure files are in scope. Nothing to review in Dockerfile, docker-compose.yml, or Caddyfile for this issue specifically.
  • However, the gate screen's load function sets an HTTP-only cookie. Whether secure: !dev evaluates correctly depends on the dev import from $app/environment — this works correctly in the SvelteKit Node adapter production build and in vite dev. No special container config is needed.
  • The deployment topology (Caddy on host, plain HTTP to container on port 3000) means secure: false in dev (local HTTP) and secure: true in production (HTTPS via Caddy). The !dev flag handles this correctly with no additional environment variable.
  • The /health endpoint referenced in the docker-compose.yml healthcheck (wget -qO- http://localhost:3000/health || exit 1) is not part of this issue — it must exist before the container healthcheck can pass. Confirm it is created in an earlier issue (likely Task 1 or Task 3). If not, flag it.
  • The SESSION_SECRET environment variable check (if (!process.env.SESSION_SECRET) throw new Error(...)) is in lib/db.ts or hooks.server.ts. This must fire at startup, not at cookie-set time. If it is missing, the container will start, pass the healthcheck, and silently fail on the first login attempt. That is a hidden failure mode.

Recommendations

  • Confirm the /health route (src/routes/health/+server.ts returning 200 OK) is implemented before this issue is marked done. The container healthcheck depends on it. If it is not in an earlier task, add it here — it is a 5-line file.
  • Verify the startup guard for SESSION_SECRET is in place before this issue ships:
    // lib/db.ts or hooks.server.ts — checked at module load, not per-request
    if (!process.env.SESSION_SECRET) {
      throw new Error('SESSION_SECRET environment variable is required');
    }
    
    Without this, a misconfigured .env on the server silently degrades security.
  • The DATABASE_PATH environment variable must also be checked at startup — new Database(undefined) will throw a cryptic error rather than a clear diagnostic. Add: if (!process.env.DATABASE_PATH) throw new Error('DATABASE_PATH is required').
  • No new environment variables are introduced by this issue. The .env.example does not need to be updated for this task.
  • Post-deploy smoke test for the gate screen is covered by the existing checklist item: curl https://erbstuecke.raddatz.cloud/ returns 200. No additional infra-level verification needed for this specific issue.
## 👤 Tobias Wendt — DevOps & Platform Engineer ### Observations - Issue #5 creates `src/routes/+page.svelte` and `src/routes/+page.server.ts` — no infrastructure files are in scope. Nothing to review in Dockerfile, docker-compose.yml, or Caddyfile for this issue specifically. - However, the gate screen's `load` function sets an HTTP-only cookie. Whether `secure: !dev` evaluates correctly depends on the `dev` import from `$app/environment` — this works correctly in the SvelteKit Node adapter production build and in `vite dev`. No special container config is needed. - The deployment topology (Caddy on host, plain HTTP to container on port 3000) means `secure: false` in dev (local HTTP) and `secure: true` in production (HTTPS via Caddy). The `!dev` flag handles this correctly with no additional environment variable. - The `/health` endpoint referenced in the `docker-compose.yml` healthcheck (`wget -qO- http://localhost:3000/health || exit 1`) is not part of this issue — it must exist before the container healthcheck can pass. Confirm it is created in an earlier issue (likely Task 1 or Task 3). If not, flag it. - The `SESSION_SECRET` environment variable check (`if (!process.env.SESSION_SECRET) throw new Error(...)`) is in `lib/db.ts` or `hooks.server.ts`. This must fire at startup, not at cookie-set time. If it is missing, the container will start, pass the healthcheck, and silently fail on the first login attempt. That is a hidden failure mode. ### Recommendations - Confirm the `/health` route (`src/routes/health/+server.ts` returning `200 OK`) is implemented before this issue is marked done. The container healthcheck depends on it. If it is not in an earlier task, add it here — it is a 5-line file. - Verify the startup guard for `SESSION_SECRET` is in place before this issue ships: ```typescript // lib/db.ts or hooks.server.ts — checked at module load, not per-request if (!process.env.SESSION_SECRET) { throw new Error('SESSION_SECRET environment variable is required'); } ``` Without this, a misconfigured `.env` on the server silently degrades security. - The `DATABASE_PATH` environment variable must also be checked at startup — `new Database(undefined)` will throw a cryptic error rather than a clear diagnostic. Add: `if (!process.env.DATABASE_PATH) throw new Error('DATABASE_PATH is required')`. - No new environment variables are introduced by this issue. The `.env.example` does not need to be updated for this task. - Post-deploy smoke test for the gate screen is covered by the existing checklist item: `curl https://erbstuecke.raddatz.cloud/ returns 200`. No additional infra-level verification needed for this specific issue.
Author
Owner

👤 Elicit — Requirements Engineer

Observations

  • The issue is well-structured: user stories (US-AUTH-001, US-AUTH-002) are explicit, acceptance criteria are testable, dependencies are declared, size is estimated (XS), and the design spec reference is precise. This passes the Definition of Ready checklist on 7 of 8 criteria.
  • The one gap: the issue has no labels in the tracker. Type, priority, and area labels are absent. For backlog health, this is a minor gap — the issue is small and well-understood, but the label taxonomy should be applied consistently.
  • US-AUTH-001 (?code= URL parameter flow) and US-AUTH-002 (manual entry form) are correctly separated as user stories. They share the same route but represent distinct user journeys: the family member who clicks a link vs. the one who types a code manually.
  • Missing edge case in AC: What happens when ?code= is present but the value is syntactically malformed (e.g., ?code= with no value, or ?code=X with 1 character)? The AC covers "wrong code → error message" but does not explicitly specify whether a malformed URL param silently falls through to the gate form or shows an error. The spec notes section says "Manuell falscher Code → Fehlermeldung" — this implies the URL param path may silently drop to the form without an error. This should be made explicit.
  • Missing AC for the case where ?code= param is valid but the gate form is already shown: If a user lands on /?code=VALID with an expired or mismatched cookie, the load function clears the old cookie and sets the new one. This path is not called out in the AC but should work by construction.
  • The page title AC ("Page title: 'Erbstücke Wannsee'") is correctly specified and testable. Browser tab title and <title> element are the same thing in SvelteKit via <svelte:head>.
  • NFR check: accessibility (WCAG AA) is addressed in the design spec. Performance is non-issue for a static gate page. Security is covered (HTTP-only cookie, no code reflection). No localization NFR (German only — correct, non-goal is multi-language). Privacy: no PII is collected at the gate — the code is a pseudonymous family identifier. No GDPR concern at this scale.

Recommendations

  • Add labels to this issue: feature, P1-high, area:auth (or equivalent from the project's taxonomy). Even for a small issue, consistent labeling keeps the backlog filterable.
  • Add one explicit AC for the malformed URL param case:

    GET /?code= (empty value) or GET /?code=X (too short) → gate form is shown without error message, URL param is silently ignored (not treated as a failed attempt).
    This is the least-surprising behavior and prevents a confusing error flash for users who manually truncate a link.

  • The dependency on #3 should be noted with the specific deliverables it must provide: hooks.server.ts (populates locals.familyCode), lib/db.ts (exports stmtCodeByValue), and lib/auth.ts (exports setFamilyCodeCookie). If #3 does not deliver these, #5 is blocked.
  • The "Depends on: #3" notation is correct but the issue tracker should use a formal "blocked by" link if Gitea supports it, so the dependency is machine-readable for planning.
## 👤 Elicit — Requirements Engineer ### Observations - The issue is well-structured: user stories (US-AUTH-001, US-AUTH-002) are explicit, acceptance criteria are testable, dependencies are declared, size is estimated (XS), and the design spec reference is precise. This passes the Definition of Ready checklist on 7 of 8 criteria. - The one gap: the issue has no labels in the tracker. Type, priority, and area labels are absent. For backlog health, this is a minor gap — the issue is small and well-understood, but the label taxonomy should be applied consistently. - US-AUTH-001 (`?code=` URL parameter flow) and US-AUTH-002 (manual entry form) are correctly separated as user stories. They share the same route but represent distinct user journeys: the family member who clicks a link vs. the one who types a code manually. - **Missing edge case in AC**: What happens when `?code=` is present but the value is syntactically malformed (e.g., `?code=` with no value, or `?code=X` with 1 character)? The AC covers "wrong code → error message" but does not explicitly specify whether a malformed URL param silently falls through to the gate form or shows an error. The spec notes section says "Manuell falscher Code → Fehlermeldung" — this implies the URL param path may silently drop to the form without an error. This should be made explicit. - **Missing AC for the case where `?code=` param is valid but the gate form is already shown**: If a user lands on `/?code=VALID` with an expired or mismatched cookie, the load function clears the old cookie and sets the new one. This path is not called out in the AC but should work by construction. - The page title AC ("Page title: 'Erbstücke Wannsee'") is correctly specified and testable. Browser tab title and `<title>` element are the same thing in SvelteKit via `<svelte:head>`. - NFR check: accessibility (WCAG AA) is addressed in the design spec. Performance is non-issue for a static gate page. Security is covered (HTTP-only cookie, no code reflection). No localization NFR (German only — correct, non-goal is multi-language). Privacy: no PII is collected at the gate — the code is a pseudonymous family identifier. No GDPR concern at this scale. ### Recommendations - Add labels to this issue: `feature`, `P1-high`, `area:auth` (or equivalent from the project's taxonomy). Even for a small issue, consistent labeling keeps the backlog filterable. - Add one explicit AC for the malformed URL param case: > `GET /?code=` (empty value) or `GET /?code=X` (too short) → gate form is shown without error message, URL param is silently ignored (not treated as a failed attempt). This is the least-surprising behavior and prevents a confusing error flash for users who manually truncate a link. - The dependency on `#3` should be noted with the specific deliverables it must provide: `hooks.server.ts` (populates `locals.familyCode`), `lib/db.ts` (exports `stmtCodeByValue`), and `lib/auth.ts` (exports `setFamilyCodeCookie`). If `#3` does not deliver these, `#5` is blocked. - The "Depends on: #3" notation is correct but the issue tracker should use a formal "blocked by" link if Gitea supports it, so the dependency is machine-readable for planning.
Author
Owner

🗳️ Decision Queue — Action Required

5 decisions need your input before implementation starts.

Architecture

  • Cookie-redirect placement: load vs. hooks — The "valid cookie → redirect to /galerie" check can live in hooks.server.ts (globally enforced, DRY if more unauthenticated routes appear later) or at the top of +page.server.ts's load (explicit, co-located with gate logic). Both are correct for a single-route gate. Cost of hooks: slightly less obvious where the redirect happens. Cost of load: must be manually repeated if a second public route is added. Decide once and document. (Raised by: Markus Keller)

Security

  • Rate limiting on the gate POST — 32^8 ≈ 10^12 code combinations means brute-force is not a realistic threat at family scale. However, Caddy supports rate limiting via a plugin. Options: (a) Accept the theoretical risk for MVP — no rate limiting, revisit if the app is ever exposed publicly. (b) Add rate_limit in the Caddyfile now — adds plugin dependency and operational complexity. For a short-lived family app with ~20 users, option (a) is defensible. (Raised by: Nora Steiner)

Testing

  • Test seed data strategy — Integration tests need a valid codes row in the :memory: DB. Options: (a) Insert directly in beforeEach — simple, self-contained. (b) Expose a createCode() helper from lib/auth.ts — reusable across all route tests but adds a test-only export. Option (a) is recommended for this project's size. Decide once and apply consistently to all test files. (Raised by: Sara Holt)

UI / Accessibility

  • "Weiter →" button arrow character — The symbol may be announced by screen readers as "right-pointing arrow." Options: (a) Keep "Weiter →" as-is and add aria-label="Weiter" to suppress the arrow for AT. (b) Remove the arrow entirely, use "Weiter" only. (c) Wrap the arrow in aria-hidden="true". Option (c) is the minimal change with full accessibility compliance. (Raised by: Leonie Voss)

Design System

  • #DFF0E6 gate icon background vs. #E8F5EC badge background — These are two similar but distinct greens used in different contexts (gate icon halo vs. "Meine Reservierung" badge). Options: (a) Keep both as one-off hardcoded values — simpler, no token bloat. (b) Consolidate into a single free-bg token — consistent, one value to update. (c) Define two named tokens (free-bg-icon and free-bg-badge) — semantically precise but adds two tokens for one visual area. This must be resolved before the design system tailwind.config.js is finalized. (Raised by: Leonie Voss)
## 🗳️ Decision Queue — Action Required _5 decisions need your input before implementation starts._ ### Architecture - **Cookie-redirect placement: load vs. hooks** — The "valid cookie → redirect to /galerie" check can live in `hooks.server.ts` (globally enforced, DRY if more unauthenticated routes appear later) or at the top of `+page.server.ts`'s `load` (explicit, co-located with gate logic). Both are correct for a single-route gate. Cost of hooks: slightly less obvious where the redirect happens. Cost of load: must be manually repeated if a second public route is added. Decide once and document. _(Raised by: Markus Keller)_ ### Security - **Rate limiting on the gate POST** — 32^8 ≈ 10^12 code combinations means brute-force is not a realistic threat at family scale. However, Caddy supports rate limiting via a plugin. Options: (a) Accept the theoretical risk for MVP — no rate limiting, revisit if the app is ever exposed publicly. (b) Add `rate_limit` in the Caddyfile now — adds plugin dependency and operational complexity. For a short-lived family app with ~20 users, option (a) is defensible. _(Raised by: Nora Steiner)_ ### Testing - **Test seed data strategy** — Integration tests need a valid `codes` row in the `:memory:` DB. Options: (a) Insert directly in `beforeEach` — simple, self-contained. (b) Expose a `createCode()` helper from `lib/auth.ts` — reusable across all route tests but adds a test-only export. Option (a) is recommended for this project's size. Decide once and apply consistently to all test files. _(Raised by: Sara Holt)_ ### UI / Accessibility - **"Weiter →" button arrow character** — The `→` symbol may be announced by screen readers as "right-pointing arrow." Options: (a) Keep "Weiter →" as-is and add `aria-label="Weiter"` to suppress the arrow for AT. (b) Remove the arrow entirely, use "Weiter" only. (c) Wrap the arrow in `aria-hidden="true"`. Option (c) is the minimal change with full accessibility compliance. _(Raised by: Leonie Voss)_ ### Design System - **`#DFF0E6` gate icon background vs. `#E8F5EC` badge background** — These are two similar but distinct greens used in different contexts (gate icon halo vs. "Meine Reservierung" badge). Options: (a) Keep both as one-off hardcoded values — simpler, no token bloat. (b) Consolidate into a single `free-bg` token — consistent, one value to update. (c) Define two named tokens (`free-bg-icon` and `free-bg-badge`) — semantically precise but adds two tokens for one visual area. This must be resolved before the design system `tailwind.config.js` is finalized. _(Raised by: Leonie Voss)_
Sign in to join this conversation.