Build Admin Reservierungen — overview grouped by person #12
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Task 12 — Plan reference:
docs/superpowers/plans/2026-05-05-erbstuecke-wannsee.mdUser story (US-ADM-007):
As an admin, I see all reservations grouped by person, with article thumbnails, so I can quickly see who has reserved what.
Acceptance criteria
bg-admin-bg text-white) with their display name and reservation count/uploads/route; fallback placeholder shown if no photo existsSQL approach
Query all codes, then JOIN reservierungen → artikel → artikel_fotos for thumbnails. Group in server-side JS.
Files to create
src/routes/admin/reservierungen/+page.sveltesrc/routes/admin/reservierungen/+page.server.tsDepends on: #8 | Size: M | Spec: reservierung-design §5.4, views spec View 08
👤 Markus Keller — Application Architect
Observations
+page.server.tsdoes two separate round-trips: oneSELECTfor all codes, then oneSELECTfor all reservierungen. This is correct and idiomatic for better-sqlite3 — prepared statements run synchronously, so no async overhead. The in-process JS grouping viafilter()is fine for the expected data volume (dozens of codes, hundreds of reservierungen at most).(SELECT filename FROM artikel_fotos WHERE artikel_id = a.id ORDER BY position LIMIT 1) AS thumbnailis correct. It selects the primary photo without a second round-trip or an N+1 problem. This is the right pattern.+layout.server.tsunder/admin/— no per-route check needed, and the issue correctly does not add one. This matches the single-enforcement-point principle.{ byPerson }shaped data — a flat, typed array. No client-side fetch, no API route. Correct.r.created_at DESC. This means within each person's list the most recently reserved article appears first. The spec (§5.4 and View 07 mockup) does not specify an ordering within person groups — the current choice is reasonable but worth noting as a decision.{#if data.byPerson.length === 0}handles the empty-codes case cleanly. The zero-reservation per-person case is handled with the{:else}branch. Both edge cases are covered.thumbnailcolumn is derived fromartikel_fotos.filename(relative path). The template renders it as/uploads/{r.thumbnail}. This is consistent with how other views in the plan handle photo URLs — the uploads route serves fromUPLOAD_DIR.Recommendations
db.ts, not inline insideload(). The pattern across all other tasks in the plan is: prepare once at module load, import and call in the route. Keeping inlinedb.prepare(...)calls inside load functions creates statements that are re-parsed on every request. Extract:getDb()call inside the load function is correct only ifgetDb()returns the singleton. Verify it does — the plan showsgetDb()as a factory wrapper, but the singleton is the expected return value after first initialization.byPersongrouping is done in JS after fetching all rows. For the expected scale this is fine. Do not prematurely optimize with a GROUP BY — the JS grouping is more readable and the data set is tiny.Open Decisions (omit if none)
r.created_at DESC(most recent first). The spec does not prescribe an order. Alternative: alphabetical by article title. Low cost either way, but the choice should be explicit before implementation starts.👤 Felix Brandt — Fullstack Developer
Observations
+page.svelteplan uses$props()with the modern Svelte 5 runes syntax (let { data }: { data: PageData } = $props()). Correct — no legacyexport let data.{#each data.byPerson as person}loop lacks a key expression. Each person has anidfield — it must be keyed:{#each data.byPerson as person (person.id)}. Same for the inner loop:{#each person.reservierungen as r (r.artikel_id)}. Without keys, Svelte falls back to position-based reconciliation.<div>vs<img>is a simple{#if r.thumbnail}branch — no complex derived state needed here since this is a static read-only view.r.titel ?? '—'correctly handles null article titles. Matches the acceptance criteria ("title or '—'").+page.server.tsdoes not import prepared statements fromdb.ts— instead it callsdb.prepare(...)inline. Per project convention, prepared statements belong inlib/db.ts, prepared once at module load, exported, and imported into the route. Fix this before implementation.locals— this page is admin-only but auth is enforced by the layout guard, so no redundant check is needed. The plan correctly omits it.bg-canvas px-1.5 py-0.5 rounded— it renders as a subtle canvas-colored chip. The spec mockup (View 07) shows the category as a small all-caps label below the article name, not a badge. The plan's implementation diverges slightly from the mockup here.use:enhance, noonMount, no client-side fetch — this is a pure SSR load-function page. Correct.Recommendations
{#each}loops immediately:db.prepare(...)calls out of the load function intolib/db.tsas module-level exported prepared statements. This is both a performance improvement and a consistency requirement across the codebase.text-ink-muted,text-[8.5px] font-extrabold uppercase tracking-[.4px]— not a colored badge chip. Revise the category element in the template to:data: PageDatatype annotation on$props()is correct — keep it. Do not useany.Open Decisions (omit if none)
bg-canvas px-1.5 py-0.5 rounded) but the View 07 mockup shows plain all-caps text. The badge approach is defensible on its own but it is not spec-compliant. (Raised by: Felix Brandt)👤 Nora "NullX" Steiner — Application Security Engineer
Observations
/uploads/{r.thumbnail}. Thethumbnailcolumn value comes directly fromartikel_fotos.filename, which is stored server-side as a UUID-based relative path (e.g.,1/uuid.webp) — not user-supplied. This is safe: the value originates from the server's ownrandomUUID()call at upload time, never from user input./uploads/[...path]/+server.ts) has a path traversal guard (path.resolve()bounds check). Even if the database were compromised and contained a traversal payload, the serve route would reject it with 400. Defense in depth is present.+layout.server.ts— this page has no direct auth check, which is correct. The layout guard redirects to/admin/loginfor unauthenticated requests. This matches the "redirect, don't 403" pattern.thumbnailvalue is used in an<img src="...">attribute. Since the value comes from the server database (not reflected user input), there is no XSS risk here. SvelteKit's template engine escapes attribute values by default.alt=""on thumbnail images is appropriate: these are decorative thumbnails. Screen readers skip them. The article title adjacent to the image provides the accessible label.Recommendations
altattribute on thumbnails is correctly empty (alt=""). Keep it — do not add article titles as alt text for these decorative thumbnails, as the title text is already rendered adjacent in the DOM. Redundant alt text degrades screen reader UX.artikel_fotos.filenameis always stored as a relative path (e.g.,1/uuid.webp) and never as an absolute path or URL. If the upload logic ever writes an absolute path to the database, the/uploads/{r.thumbnail}URL construction would silently produce a broken URL rather than a traversal risk — but it should be validated at write time regardless.(No open decisions — security posture on this read-only page is sound as specified.)
👤 Sara Holt — QA Engineer & Test Strategist
Observations
byPersonwith an empty array. This depends on thecodes.map(...)JS grouping logic — it is not protected by a database constraint and must be explicitly tested.<div>whenr.thumbnailis null) is a rendering branch that needs a component test, not just a load function test.byPersonand the Svelte component must not containonMountorfetch. This is a static code check but should be part of the code review checklist.Recommendations
Add an integration test step before the implementation step:
Add a component test using
vitest-browser-svelteto verify the empty-state renders the italic "Keine Reservierungen" text and that the person header is visible even for zero-reservation codes.Verify the
{#each}key expressions are present before testing — missing keys can cause incorrect DOM reconciliation in tests.The integration test should import the actual load function (not mock it) and call it with a real
:memory:database. Do not mockgetDb().Open Decisions (omit if none)
src/lib/but route load function tests have no established location yet. Propose:src/routes/admin/reservierungen/+page.server.test.tsalongside the implementation. Confirm this is acceptable before implementation starts. (Raised by: Sara Holt)👤 Leonie Voss — UI/UX Design Lead
Observations
.res-thumb { width:26px; height:26px }. The plan's Svelte template usesw-[26px] h-[26px]. So 26px is the implemented spec value — but the acceptance criterion referencing "thumbnail 26×26px" is inconsistent with the functional spec paragraph which says 48px. One of these is wrong and needs to be authoritative.opacity:.4on both the dark header row and the res-body for zero-reservation people. The plan's Svelte template does not apply any opacity or visual graying to the header row for zero-reservation codes — only the body's empty-state text usestext-ink-muted italic. This is a significant spec deviation. The header should also be visually dimmed.text-ink-muted italic(color:#6B6050). The spec mockup usescolor: rgba(255,255,255,.35)on a white background body — which renders as near-invisible light text. The plan's approach (text-ink-muted) is readable on the white surface, but may not match the intended "ausgegraut" feel. The spec mockup's white text on white background at 35% opacity seems intentional for the subdued style.bg-admin-bg text-white px-2.5 py-1.5 text-[9.5px] font-bold rounded-t. The spec mockup uses.res-head { background:var(--ab); color:#fff; padding:6px 9px; font-size:9.5px; font-weight:700; border-radius:4px 4px 0 0 }. These match well — padding and border-radius are equivalent.({person.reservierungen.length})only when count > 0. The spec mockup shows "Markus — 3 Artikel" format with an em dash separator, not parentheses. Spec:{person.displayName} — {count} Artikel(with " — 0 Artikel" even for empty, to be consistent).font-bold(Inter 700). The page titlefont-serif text-[14px] font-boldcorrectly uses Lora. Design tokens used correctly — no hardcoded hex values in the template.bg-admin-bg,bg-surface,bg-canvas,text-ink,text-ink-muted— all correct token names from the Tailwind config.Recommendations
opacity-40to the entire person block (both header and body) whenperson.reservierungen.length === 0:opacity:.4on the combined block.{person.displayName} — {person.reservierungen.length} Artikel. Show "0 Artikel" for empty persons rather than hiding the count.<div class="... bg-[#C4B8A8]">uses a hardcoded hex value. Replace with the established canvas placeholder color. The spec uses#C4B8A8as a placeholder tone — add it as a CSS tokenplaceholderor usebg-canvasas a reasonable fallback that stays within the token system.Open Decisions (omit if none)
👤 Tobias Wendt (@tobiwendt) — DevOps & Platform Engineer
Observations
src/routes/admin/reservierungen/. No new infrastructure, environment variables, volumes, or Docker changes required. The deployment footprint is zero — the feature rides entirely on the existing container./uploads/{r.thumbnail}URL in the template is served by the existing uploads route (src/routes/uploads/[...path]/+server.ts). No new file-serving configuration needed.getDb()call returns the module-level singleton. The singleton is initialized once when the Node process starts. This is stable across requests — no per-request database connection overhead.DATABASE_PATHandUPLOAD_DIRcover all requirements for this page./health) is not affected by this change.Recommendations
/admin/reservierungenloads without a 500 error. Add it to the deploy checklist:db.ts) will be re-prepared on every module import during SSR. This is not a reliability risk but it is unnecessary overhead. Support the move to module-level prepared statements that the architect and developer are recommending.(No open decisions on the infrastructure side.)
👤 Elicit — Requirements Engineer
Observations
/admin/reservierungen). View 08 in the spec is the Übersicht dashboard (/admin/uebersicht). This is a documentation error in the issue — the correct reference is View 07. Low practical risk since the implementation target is unambiguous, but the mismatch should be corrected to avoid confusion.reservierung-designstates "Thumbnail (48 px)". These are contradictory. The views spec CSS mockup uses 26px (but states all mockups are at ~55% scale, which would put 48px at full scale as ~26px displayed). The issue criterion "26×26px" propagates the unresolved ambiguity from the spec into the acceptance criteria.codestable must be seeded with data. This is a prerequisite for the page to display anything meaningful.Recommendations
Open Decisions (omit if none)
🗳️ Decision Queue — Action Required
6 decisions need your input before implementation starts.
Spec Accuracy
Visual Design
Thumbnail size: 26px vs 48px — The functional spec §5.4 says "Thumbnail (48 px)". The views spec CSS mockup, the plan, and the issue acceptance criteria all say 26×26px. The mockup is stated to be at ~55% scale, so 26px displayed ≈ 47px full-size, consistent with 48px. Recommended decision: use 26×26px as specified in the implementation plan. Needs explicit product owner confirmation. (Raised by: Leonie Voss, Elicit)
"Ausgegraut" scope for zero-reservation persons — The spec mockup applies
opacity: 0.4to the entire person block (dark header row + white body). The implementation plan only shows the body's empty-state text as italic+muted — neither the header nor the body block is visually dimmed. Options: (A) dim the entire person block at opacity-40 (matches spec mockup), (B) dim only the body text (plan's current approach, easier to implement). (Raised by: Leonie Voss, Elicit)Header count format — The spec mockup shows "Markus — 3 Artikel" with an em dash. The plan shows "(3)" in parentheses, hidden when count is 0. Options: (A) use em dash format "Markus — 3 Artikel" including "0 Artikel" for empty (spec-compliant), (B) keep parenthetical with count hidden for zero (plan's current approach, less visual noise). (Raised by: Leonie Voss)
Category display style — The plan renders the category as a canvas-colored rounded chip (
bg-canvas px-1.5 py-0.5 rounded). The spec mockup shows plain all-caps text intext-ink-mutedstyling. Options: (A) plain all-caps label matching spec mockup, (B) keep badge chip (not spec-compliant but visually distinguishable). (Raised by: Felix Brandt)Architecture / Code Convention
r.created_at DESC(newest first). The spec does not prescribe an order. Alternative: alphabetical by article title (more predictable for admin scanning). Low effort to change before implementation but high effort to change after. (Raised by: Markus Keller)Testing
src/routes/admin/reservierungen/+page.server.test.ts(co-located with implementation). Confirm this location is acceptable, then add tests before writing the implementation. (Raised by: Sara Holt)