feat(settings): implement /settings page — Kachel-Ansicht (E1) #49

Closed
opened 2026-04-09 15:07:10 +02:00 by marcel · 8 comments
Owner

Beschreibung

Implementierung der /settings-Seite als Kachel-Hub (Card Sections). Drei Kacheln navigieren zu den jeweiligen Unterbereichen: Vorräte (D3), Mitglieder (E2) und Profil. Die Vorräte-Kachel zeigt die aktive Zutatenanzahl als Display-Font-Zahl. D3 (/household/staples?ctx=settings) verwendet die bestehende StaplesManager-Komponente — keine neue Komponente nötig.

Spec: specs/frontend/e1-settings-kachel.html


Zustände (aus Spec)

State Beschreibung
S1 Hub-Ansicht — drei Kacheln (Vorräte, Mitglieder, Profil)
S2 D3 Vorräte-Seite — StaplesManager mit Breadcrumb zurück
S3 Hover-Zustand der Kacheln
S4 Leerer Zustand — kein Vorrat gesetzt (0 aktive Vorräte)

Layout

  • Desktop: grid-template-columns: 2fr 1fr (obere Reihe) + 1fr 1fr (untere Reihe), Gap 16px
  • Mobile: Single column, volle Breite, Gap 12px
  • Alle Kacheln sind <a>-Tags

Kacheln

Vorräte (primär)

  • border-left: 3px solid --green-dark als Akzentstreifen
  • Stat-Zahl: Fraunces, 36px, --green-dark — Anzahl isStaple === true aus GET /v1/ingredient-categories
  • Leerer Zustand (stapleCount === 0): Stat-Zahl weglassen, "Noch keine Vorräte eingerichtet" + CTA "Jetzt einrichten →"
  • href: /household/staples?ctx=settings

Mitglieder

  • Meta: {memberCount} Mitglieder
  • href: /members

Profil

  • Meta: locals.benutzer.name
  • href: /profile (noch nicht implementiert — als disabled rendern oder Placeholder)

D3 — /household/staples?ctx=settings

  • Bestehende Seite unter src/routes/household/staples/+page.svelte existiert bereits
  • Breadcrumb ← Einstellungen über dem Seitentitel, verlinkt auf /settings
  • "Einstellungen" bleibt in der Sidebar aktiv (kein eigener Nav-Eintrag für Vorräte)
  • Hinweistext unter dem Chip-Grid: "Änderungen werden automatisch gespeichert. Gilt ab der nächsten Einkaufsliste."
  • Kein Speichern-Button — StaplesManager speichert debounced (300ms PATCH), nicht nochmal wrappen

Hover & Fokus

  • Hover: box-shadow: --shadow-raised, border-color: #C0BFB8
  • Transition: box-shadow 150ms ease, border-color 150ms ease
  • Focus-visible: outline: 2px solid --green-dark; outline-offset: 2px

Data Loading (+page.server.ts für /settings)

// stapleCount: Anzahl isStaple=true
const { data: categories } = await api.GET('/v1/ingredient-categories');
const stapleCount = categories?.flatMap(c => c.ingredients)
  .filter(i => i.isStaple).length ?? 0;

// memberCount: aus layout data oder separatem Call
// profile name: locals.benutzer.name

Akzeptanzkriterien

  • Hub zeigt drei Kacheln im 2-spaltig/1-spaltig Grid
  • Vorräte-Kachel zeigt korrekte Anzahl aktiver Vorräte
  • Leerer Zustand (0 Vorräte) zeigt alternativen Text und CTA
  • Hover-Zustand: Shadow + dunklerer Border, Transition
  • Alle Kacheln sind <a>-Tags mit korrektem href
  • D3-Seite hat Breadcrumb zurück zu /settings
  • "Einstellungen" aktiv in Sidebar auf D3-Seite
  • Hinweistext unter dem Chip-Grid auf D3
  • Mobile: Kacheln stacken vertikal, volle Breite
## Beschreibung Implementierung der `/settings`-Seite als Kachel-Hub (Card Sections). Drei Kacheln navigieren zu den jeweiligen Unterbereichen: Vorräte (D3), Mitglieder (E2) und Profil. Die Vorräte-Kachel zeigt die aktive Zutatenanzahl als Display-Font-Zahl. D3 (`/household/staples?ctx=settings`) verwendet die bestehende `StaplesManager`-Komponente — keine neue Komponente nötig. **Spec:** [`specs/frontend/e1-settings-kachel.html`](http://heim-nas:3005/marcel/mealprep/src/branch/master/specs/frontend/e1-settings-kachel.html) --- ## Zustände (aus Spec) | State | Beschreibung | |-------|-------------| | S1 | Hub-Ansicht — drei Kacheln (Vorräte, Mitglieder, Profil) | | S2 | D3 Vorräte-Seite — StaplesManager mit Breadcrumb zurück | | S3 | Hover-Zustand der Kacheln | | S4 | Leerer Zustand — kein Vorrat gesetzt (0 aktive Vorräte) | --- ## Layout - **Desktop:** `grid-template-columns: 2fr 1fr` (obere Reihe) + `1fr 1fr` (untere Reihe), Gap 16px - **Mobile:** Single column, volle Breite, Gap 12px - Alle Kacheln sind `<a>`-Tags ## Kacheln ### Vorräte (primär) - `border-left: 3px solid --green-dark` als Akzentstreifen - Stat-Zahl: Fraunces, 36px, `--green-dark` — Anzahl `isStaple === true` aus `GET /v1/ingredient-categories` - Leerer Zustand (`stapleCount === 0`): Stat-Zahl weglassen, "Noch keine Vorräte eingerichtet" + CTA "Jetzt einrichten →" - href: `/household/staples?ctx=settings` ### Mitglieder - Meta: `{memberCount} Mitglieder` - href: `/members` ### Profil - Meta: `locals.benutzer.name` - href: `/profile` (noch nicht implementiert — als disabled rendern oder Placeholder) --- ## D3 — `/household/staples?ctx=settings` - Bestehende Seite unter `src/routes/household/staples/+page.svelte` existiert bereits - Breadcrumb `← Einstellungen` über dem Seitentitel, verlinkt auf `/settings` - "Einstellungen" bleibt in der Sidebar aktiv (kein eigener Nav-Eintrag für Vorräte) - Hinweistext unter dem Chip-Grid: "Änderungen werden automatisch gespeichert. Gilt ab der nächsten Einkaufsliste." - **Kein Speichern-Button** — StaplesManager speichert debounced (300ms PATCH), nicht nochmal wrappen --- ## Hover & Fokus - Hover: `box-shadow: --shadow-raised`, `border-color: #C0BFB8` - Transition: `box-shadow 150ms ease, border-color 150ms ease` - Focus-visible: `outline: 2px solid --green-dark; outline-offset: 2px` --- ## Data Loading (`+page.server.ts` für `/settings`) ```ts // stapleCount: Anzahl isStaple=true const { data: categories } = await api.GET('/v1/ingredient-categories'); const stapleCount = categories?.flatMap(c => c.ingredients) .filter(i => i.isStaple).length ?? 0; // memberCount: aus layout data oder separatem Call // profile name: locals.benutzer.name ``` --- ## Akzeptanzkriterien - [ ] Hub zeigt drei Kacheln im 2-spaltig/1-spaltig Grid - [ ] Vorräte-Kachel zeigt korrekte Anzahl aktiver Vorräte - [ ] Leerer Zustand (0 Vorräte) zeigt alternativen Text und CTA - [ ] Hover-Zustand: Shadow + dunklerer Border, Transition - [ ] Alle Kacheln sind `<a>`-Tags mit korrektem href - [ ] D3-Seite hat Breadcrumb zurück zu /settings - [ ] "Einstellungen" aktiv in Sidebar auf D3-Seite - [ ] Hinweistext unter dem Chip-Grid auf D3 - [ ] Mobile: Kacheln stacken vertikal, volle Breite
Author
Owner

⚛️ Kai — Frontend Engineer

Questions & Observations

  • memberCount: woher? Das Issue sagt "aus layout data oder separatem Call". +layout.server.ts gibt aktuell benutzer und haushalt zurück — aber keine Mitgliederzahl. GET /v1/households/mine liefert laut Schema ein Haushalt-Objekt mit members[]. Ich würde das in +page.server.ts für /settings parallel zum Categories-Call laden: Promise.all([api.GET('/v1/ingredient-categories'), api.GET('/v1/households/mine')]). Kein separater Layout-Call nötig.

  • Profil-Karte: disabled Rendering. Die Spec sagt "disabled rendern oder Placeholder". Mein Vorschlag: <a href="/profile" aria-disabled="true" tabindex="-1"> mit pointer-events: none; opacity: 0.5 via Tailwind pointer-events-none opacity-50. Das behält die <a>-Semantik bei und ist zugänglich. Alternativ: <div role="link" aria-disabled="true"> — aber echtes <a> mit aria-disabled ist sauberer.

  • D3 Breadcrumb: existing page ändern. src/routes/household/staples/+page.svelte existiert bereits mit Onboarding-Logik (isOnboarding-Check). Ich füge den Breadcrumb nur hinzu wenn !isOnboarding && ctx === 'settings' — das ist die gleiche ctx-Logik die schon drin ist. Kein Breaking Change.

  • stapleCount SSR. Der API-Call in +page.server.ts läuft server-seitig — gut. Kein $effect nötig. Wenn der Call fehlschlägt, sollte stapleCount auf 0 defaulten (leerer Zustand) statt einen Error-State zu zeigen — das ist eine reine Info-Zahl, kein kritischer Fehler.

  • Component split. Die Settings-Seite ist eine einfache Hub-Seite — ich brauche keine separaten Komponenten dafür. Die Kacheln sind direkt in +page.svelte als <a>-Tags. Kein $state nötig, keine Interaktivität. Sauberste Lösung: reines Server-Rendering, nulljseitige Reaktivität.

Suggestions

  • +page.server.ts für /settings:
    export const load = async ({ fetch, locals }) => {
      const api = apiClient(fetch);
      const [categoriesRes, householdRes] = await Promise.all([
        api.GET('/v1/ingredient-categories'),
        api.GET('/v1/households/mine')
      ]);
      const stapleCount = categoriesRes.data
        ?.flatMap(c => c.ingredients ?? [])
        .filter(i => i.isStaple).length ?? 0;
      const memberCount = householdRes.data?.members?.length ?? 0;
      return { stapleCount, memberCount, userName: locals.benutzer!.name };
    };
    
  • Test: Vitest snapshot oder RTL-Test mit stapleCount: 0 → prüfen ob "Jetzt einrichten →" erscheint. Mit stapleCount: 14 → prüfen ob "14" und "Vorräte bearbeiten →" erscheint.
## ⚛️ Kai — Frontend Engineer ### Questions & Observations - **memberCount: woher?** Das Issue sagt "aus layout data oder separatem Call". `+layout.server.ts` gibt aktuell `benutzer` und `haushalt` zurück — aber keine Mitgliederzahl. `GET /v1/households/mine` liefert laut Schema ein Haushalt-Objekt mit `members[]`. Ich würde das in `+page.server.ts` für `/settings` parallel zum Categories-Call laden: `Promise.all([api.GET('/v1/ingredient-categories'), api.GET('/v1/households/mine')])`. Kein separater Layout-Call nötig. - **Profil-Karte: disabled Rendering.** Die Spec sagt "disabled rendern oder Placeholder". Mein Vorschlag: `<a href="/profile" aria-disabled="true" tabindex="-1">` mit `pointer-events: none; opacity: 0.5` via Tailwind `pointer-events-none opacity-50`. Das behält die `<a>`-Semantik bei und ist zugänglich. Alternativ: `<div role="link" aria-disabled="true">` — aber echtes `<a>` mit `aria-disabled` ist sauberer. - **D3 Breadcrumb: existing page ändern.** `src/routes/household/staples/+page.svelte` existiert bereits mit Onboarding-Logik (`isOnboarding`-Check). Ich füge den Breadcrumb nur hinzu wenn `!isOnboarding && ctx === 'settings'` — das ist die gleiche `ctx`-Logik die schon drin ist. Kein Breaking Change. - **stapleCount SSR.** Der API-Call in `+page.server.ts` läuft server-seitig — gut. Kein `$effect` nötig. Wenn der Call fehlschlägt, sollte `stapleCount` auf `0` defaulten (leerer Zustand) statt einen Error-State zu zeigen — das ist eine reine Info-Zahl, kein kritischer Fehler. - **Component split.** Die Settings-Seite ist eine einfache Hub-Seite — ich brauche keine separaten Komponenten dafür. Die Kacheln sind direkt in `+page.svelte` als `<a>`-Tags. Kein `$state` nötig, keine Interaktivität. Sauberste Lösung: reines Server-Rendering, nulljseitige Reaktivität. ### Suggestions - `+page.server.ts` für `/settings`: ```ts export const load = async ({ fetch, locals }) => { const api = apiClient(fetch); const [categoriesRes, householdRes] = await Promise.all([ api.GET('/v1/ingredient-categories'), api.GET('/v1/households/mine') ]); const stapleCount = categoriesRes.data ?.flatMap(c => c.ingredients ?? []) .filter(i => i.isStaple).length ?? 0; const memberCount = householdRes.data?.members?.length ?? 0; return { stapleCount, memberCount, userName: locals.benutzer!.name }; }; ``` - Test: Vitest snapshot oder RTL-Test mit `stapleCount: 0` → prüfen ob "Jetzt einrichten →" erscheint. Mit `stapleCount: 14` → prüfen ob "14" und "Vorräte bearbeiten →" erscheint.
Author
Owner

🏗️ Backend Engineer — Spring Boot / PostgreSQL

Questions & Observations

  • Keine neuen Backend-Endpoints nötig — aber einen Blick wert. E1 selbst braucht nur GET /v1/ingredient-categories (existiert) und GET /v1/households/mine (existiert). Das ist gut — dieser Issue ist rein frontend-seitig umsetzbar ohne Backend-Arbeit. Trotzdem zwei Punkte:

  • GET /v1/ingredient-categories liefert alle Zutaten aller Kategorien? Laut Schema gibt es listCategories — aber ist da auch isStaple pro Zutat dabei? Das hängt von der aktuellen API-Implementierung ab. Wenn isStaple nicht Teil der Response ist, muss der Backend-Endpoint erweitert werden. Bitte vor der Implementierung prüfen, was der Endpoint tatsächlich zurückgibt.

  • GET /v1/households/mine mit members[] — N+1 Risiko. Wenn der Endpoint die Member-Liste als embedded Array zurückgibt, macht Spring Data möglicherweise einen zusätzlichen Query pro Member (LazyLoading). Das ist für 2-4 Personen irrelevant, aber es lohnt sich zu prüfen ob die Query einen JOIN FETCH hat oder lazy ist. Ein @Query("SELECT h FROM Household h LEFT JOIN FETCH h.members WHERE h.id = :id") wäre die saubere Lösung.

  • Profil-Seite existiert noch nicht. Das Issue sagt "als disabled rendern". Aus Backend-Sicht: kein Endpoint nötig, also keine Bedenken. Nur anmerken: wenn die Profil-Seite irgendwann kommt, braucht sie GET/PATCH /v1/users/me — nicht Teil dieses Issues.

  • stapleCount in +page.server.ts berechnet — das ist die richtige Schicht. Nicht als Query-Parameter an den Backend schicken, nicht im Client berechnen. Server-seitig aus dem API-Response ableiten. ✓

Suggestions

  • Kurz verifizieren ob GET /v1/ingredient-categories tatsächlich isStaple: boolean pro Ingredient zurückgibt. Falls nicht → entweder den Endpoint erweitern oder eine separate Methode bauen. Das sollte vor der Frontend-Implementierung klar sein.
  • Wenn GET /v1/households/mine noch keinen expliziten JOIN FETCH hat: kurze Ergänzung in der Repository-Query eintragen — kein grosser Aufwand, verhindert zukünftige LazyInitializationExceptions ausserhalb der Transaktion.
## 🏗️ Backend Engineer — Spring Boot / PostgreSQL ### Questions & Observations - **Keine neuen Backend-Endpoints nötig — aber einen Blick wert.** E1 selbst braucht nur `GET /v1/ingredient-categories` (existiert) und `GET /v1/households/mine` (existiert). Das ist gut — dieser Issue ist rein frontend-seitig umsetzbar ohne Backend-Arbeit. Trotzdem zwei Punkte: - **`GET /v1/ingredient-categories` liefert alle Zutaten aller Kategorien?** Laut Schema gibt es `listCategories` — aber ist da auch `isStaple` pro Zutat dabei? Das hängt von der aktuellen API-Implementierung ab. Wenn `isStaple` nicht Teil der Response ist, muss der Backend-Endpoint erweitert werden. Bitte vor der Implementierung prüfen, was der Endpoint tatsächlich zurückgibt. - **`GET /v1/households/mine` mit `members[]` — N+1 Risiko.** Wenn der Endpoint die Member-Liste als embedded Array zurückgibt, macht Spring Data möglicherweise einen zusätzlichen Query pro Member (LazyLoading). Das ist für 2-4 Personen irrelevant, aber es lohnt sich zu prüfen ob die Query einen `JOIN FETCH` hat oder lazy ist. Ein `@Query("SELECT h FROM Household h LEFT JOIN FETCH h.members WHERE h.id = :id")` wäre die saubere Lösung. - **Profil-Seite existiert noch nicht.** Das Issue sagt "als disabled rendern". Aus Backend-Sicht: kein Endpoint nötig, also keine Bedenken. Nur anmerken: wenn die Profil-Seite irgendwann kommt, braucht sie `GET/PATCH /v1/users/me` — nicht Teil dieses Issues. - **stapleCount in `+page.server.ts` berechnet — das ist die richtige Schicht.** Nicht als Query-Parameter an den Backend schicken, nicht im Client berechnen. Server-seitig aus dem API-Response ableiten. ✓ ### Suggestions - Kurz verifizieren ob `GET /v1/ingredient-categories` tatsächlich `isStaple: boolean` pro Ingredient zurückgibt. Falls nicht → entweder den Endpoint erweitern oder eine separate Methode bauen. Das sollte vor der Frontend-Implementierung klar sein. - Wenn `GET /v1/households/mine` noch keinen expliziten `JOIN FETCH` hat: kurze Ergänzung in der Repository-Query eintragen — kein grosser Aufwand, verhindert zukünftige LazyInitializationExceptions ausserhalb der Transaktion.
Author
Owner

🔒 Sable — Security Engineer

Questions & Observations

  • Zugriffskontrolle auf /settings. Die Spec erwähnt keine Rollen-Einschränkung für E1. Sollte /settings für mitglied zugänglich sein? Wenn ja, was sieht ein Mitglied?

    • Vorräte-Kachel: Mitglieder sollten Vorräte sehen dürfen (und ggf. bearbeiten — D3 ist schon zugänglich)
    • Mitglieder-Kachel: navigiert zu /members (E2) — Mitglieder sehen E2 read-only gemäss #48
    • Profil-Kachel: eigenes Profil bearbeiten — das ist role-neutral, jeder darf sein eigenes Profil sehen
    • Fazit: /settings scheint für beide Rollen passend. Aber das sollte explizit dokumentiert werden, damit hooks.server.ts nicht versehentlich /settings auf planer-only beschränkt.
  • locals.benutzer.name in der Profil-Kachel. Der Name kommt aus der server-seitigen Session — kein direktes XSS-Risiko, da Svelte serverseitig escaped. Trotzdem: wenn displayName bei der Registrierung keinen Längen-Check hatte, könnte ein sehr langer Name das Card-Layout brechen (CSS-Overflow-Problem, kein Security-Problem). Eher ein robustness-Hinweis.

  • /v1/ingredient-categories — gibt das haushaltsspezifische Daten zurück? Wenn Zutaten global sind (nicht haushaltsspezifisch), ist das kein Multi-Tenancy-Problem. Aber wenn Stapel-Markierungen haushaltsspezifisch sind, muss der Endpoint sicherstellen dass er nur die Markierungen des eigenen Haushalts zurückgibt. Das ist kein neues Problem — D3 (StaplesManager) hat das gleiche Muster — aber es lohnt sich, es hier explizit zu bestätigen.

  • Profil-Card als aria-disabled Link. Kai's Vorschlag (<a href="/profile" aria-disabled="true">) ist aus Security-Sicht i.O. — /profile existiert noch nicht und gibt entweder 404 zurück oder wird über hooks.server.ts abgefangen. Kein Risiko.

  • Kein Formular auf E1. Die gesamte Settings-Hub-Seite hat keine Formulare und keinen User-Input (ausser Navigation). Kein CSRF-Risiko, keine Injection-Fläche. ✓

Suggestions

  • In der Spec oder im Issue klarstellen: E1 ist für planer und mitglied zugänglich. Beide sehen alle drei Kacheln. Die Kacheln navigieren zu Seiten, die dann rollenbasiert ihre eigene Zugangskontrolle haben.
  • hooks.server.ts prüfen: falls dort /household/* auf planer-only beschränkt ist, müsste /settings explizit ausgenommen werden.
## 🔒 Sable — Security Engineer ### Questions & Observations - **Zugriffskontrolle auf `/settings`.** Die Spec erwähnt keine Rollen-Einschränkung für E1. Sollte `/settings` für `mitglied` zugänglich sein? Wenn ja, was sieht ein Mitglied? - Vorräte-Kachel: Mitglieder sollten Vorräte sehen dürfen (und ggf. bearbeiten — D3 ist schon zugänglich) - Mitglieder-Kachel: navigiert zu `/members` (E2) — Mitglieder sehen E2 read-only gemäss #48 ✓ - Profil-Kachel: eigenes Profil bearbeiten — das ist role-neutral, jeder darf sein eigenes Profil sehen - Fazit: `/settings` scheint für beide Rollen passend. Aber das sollte explizit dokumentiert werden, damit `hooks.server.ts` nicht versehentlich `/settings` auf `planer-only` beschränkt. - **`locals.benutzer.name` in der Profil-Kachel.** Der Name kommt aus der server-seitigen Session — kein direktes XSS-Risiko, da Svelte serverseitig escaped. Trotzdem: wenn `displayName` bei der Registrierung keinen Längen-Check hatte, könnte ein sehr langer Name das Card-Layout brechen (CSS-Overflow-Problem, kein Security-Problem). Eher ein robustness-Hinweis. - **`/v1/ingredient-categories` — gibt das haushaltsspezifische Daten zurück?** Wenn Zutaten global sind (nicht haushaltsspezifisch), ist das kein Multi-Tenancy-Problem. Aber wenn Stapel-Markierungen haushaltsspezifisch sind, muss der Endpoint sicherstellen dass er nur die Markierungen des eigenen Haushalts zurückgibt. Das ist kein neues Problem — D3 (StaplesManager) hat das gleiche Muster — aber es lohnt sich, es hier explizit zu bestätigen. - **Profil-Card als `aria-disabled` Link.** Kai's Vorschlag (`<a href="/profile" aria-disabled="true">`) ist aus Security-Sicht i.O. — `/profile` existiert noch nicht und gibt entweder 404 zurück oder wird über `hooks.server.ts` abgefangen. Kein Risiko. - **Kein Formular auf E1.** Die gesamte Settings-Hub-Seite hat keine Formulare und keinen User-Input (ausser Navigation). Kein CSRF-Risiko, keine Injection-Fläche. ✓ ### Suggestions - In der Spec oder im Issue klarstellen: E1 ist für `planer` **und** `mitglied` zugänglich. Beide sehen alle drei Kacheln. Die Kacheln navigieren zu Seiten, die dann rollenbasiert ihre eigene Zugangskontrolle haben. - `hooks.server.ts` prüfen: falls dort `/household/*` auf `planer-only` beschränkt ist, müsste `/settings` explizit ausgenommen werden.
Author
Owner

🧪 QA Engineer

Questions & Observations

Fehlende Akzeptanzkriterien:

  • Was passiert wenn GET /v1/ingredient-categories fehlschlägt? Die Issue-Beschreibung sagt ?? 0 als Fallback — gut. Aber zeigt die Seite trotzdem? Oder bricht sie? Das sollte explizit im AK stehen: "Wenn der API-Call fehlschlägt, zeigt die Vorräte-Kachel den Leer-Zustand (0), die Seite bricht nicht."
  • Was wenn GET /v1/households/mine fehlschlägt? Dann wäre memberCount undefined. Wird "0 Mitglieder" oder ein Fehler angezeigt? Muss definiert sein.
  • Profil-Kachel disabled: welches visuelle Feedback? Das AK sagt nichts darüber. Cursor not-allowed? opacity: 50%? Click führt zu... nichts? 404? Das muss klar sein um testbar zu sein.
  • Breadcrumb auf D3: welche URL? Verlinkung auf /settings — aber wenn ich direkt auf /household/staples?ctx=settings navigiere (nicht via Hub), zeigt der Breadcrumb auch? Das ist ein wichtiger Rendering-Test.
  • Sidebar "Einstellungen" aktiv auf D3. Das AK erwähnt dies — aber wie wird das technisch sichergestellt? Der Sidebar-Link prüft isActiveRoute(href, pathname)/settings ist nicht der aktive Pfad wenn wir auf /household/staples sind. Das könnte ein Bug sein der aufgedeckt werden muss.

Test-Coverage:

Szenario Ebene
Hub rendert 3 Kacheln (Vorräte, Mitglieder, Profil) Component/Page
Vorräte-Kachel zeigt stapleCount=14 als Fraunces-Zahl Component/Page
Vorräte-Kachel mit stapleCount=0 zeigt Leer-Zustand Component/Page
memberCount korrekt in Mitglieder-Kachel Component/Page
Profil-Kachel zeigt benutzer.name Component/Page
Profil-Kachel ist als disabled markiert (aria-disabled) Component/Page
API-Fehler: Seite bricht nicht, zeigt Fallback Component/Page
D3: Breadcrumb "← Einstellungen" visible + link korrekt Component/Page
D3: Hinweistext unterhalb der Chips Component/Page
D3: Sidebar "Einstellungen" active (könnte failing sein!) Integration/E2E
Mobile: Kacheln stacken vertikal Playwright viewport test

Suggestions

  • Akzeptanzkriterien um API-Fehlerfall ergänzen für beide Calls.
  • Den Sidebar-aktiv-State auf D3 explizit als manuell zu prüfenden Punkt markieren — es könnte sein dass isActiveRoute angepasst werden muss um /household/staples als "Einstellungen"-aktiv zu erkennen (z.B. via prefix-matching auf /household).
  • data-testid="staple-count" und data-testid="member-count" ergänzen damit Tests robust auf die Zahlen zeigen können.
## 🧪 QA Engineer ### Questions & Observations **Fehlende Akzeptanzkriterien:** - **Was passiert wenn `GET /v1/ingredient-categories` fehlschlägt?** Die Issue-Beschreibung sagt `?? 0` als Fallback — gut. Aber zeigt die Seite trotzdem? Oder bricht sie? Das sollte explizit im AK stehen: "Wenn der API-Call fehlschlägt, zeigt die Vorräte-Kachel den Leer-Zustand (0), die Seite bricht nicht." - **Was wenn `GET /v1/households/mine` fehlschlägt?** Dann wäre `memberCount` undefined. Wird "0 Mitglieder" oder ein Fehler angezeigt? Muss definiert sein. - **Profil-Kachel disabled: welches visuelle Feedback?** Das AK sagt nichts darüber. Cursor `not-allowed`? `opacity: 50%`? Click führt zu... nichts? 404? Das muss klar sein um testbar zu sein. - **Breadcrumb auf D3: welche URL?** Verlinkung auf `/settings` — aber wenn ich direkt auf `/household/staples?ctx=settings` navigiere (nicht via Hub), zeigt der Breadcrumb auch? Das ist ein wichtiger Rendering-Test. - **Sidebar "Einstellungen" aktiv auf D3.** Das AK erwähnt dies — aber wie wird das technisch sichergestellt? Der Sidebar-Link prüft `isActiveRoute(href, pathname)` — `/settings` ist nicht der aktive Pfad wenn wir auf `/household/staples` sind. Das könnte ein Bug sein der aufgedeckt werden muss. **Test-Coverage:** | Szenario | Ebene | |---|---| | Hub rendert 3 Kacheln (Vorräte, Mitglieder, Profil) | Component/Page | | Vorräte-Kachel zeigt stapleCount=14 als Fraunces-Zahl | Component/Page | | Vorräte-Kachel mit stapleCount=0 zeigt Leer-Zustand | Component/Page | | memberCount korrekt in Mitglieder-Kachel | Component/Page | | Profil-Kachel zeigt benutzer.name | Component/Page | | Profil-Kachel ist als disabled markiert (aria-disabled) | Component/Page | | API-Fehler: Seite bricht nicht, zeigt Fallback | Component/Page | | D3: Breadcrumb "← Einstellungen" visible + link korrekt | Component/Page | | D3: Hinweistext unterhalb der Chips | Component/Page | | D3: Sidebar "Einstellungen" active (könnte failing sein!) | Integration/E2E | | Mobile: Kacheln stacken vertikal | Playwright viewport test | ### Suggestions - Akzeptanzkriterien um API-Fehlerfall ergänzen für beide Calls. - Den Sidebar-aktiv-State auf D3 explizit als manuell zu prüfenden Punkt markieren — es könnte sein dass `isActiveRoute` angepasst werden muss um `/household/staples` als "Einstellungen"-aktiv zu erkennen (z.B. via prefix-matching auf `/household`). - `data-testid="staple-count"` und `data-testid="member-count"` ergänzen damit Tests robust auf die Zahlen zeigen können.
Author
Owner

🎨 Atlas — UI/UX Design

Questions & Observations

  • 2fr 1fr Grid — Proportionen in der Praxis. Bei einer typischen Desktop-Breite von ~900px Content-Bereich ergibt 2fr 1fr ca. 590px + 295px. Die Vorräte-Kachel hat dann viel horizontalen Raum für eine simple Stat-Zahl + Text. Das ist okay, aber prüfen ob der Inhalt nicht zu dünn wirkt. Falls nötig: die große Fraunces-Zahl (36px) anchors die Kachel gut nach oben.

  • Profil-Kachel disabled State — visuell klären. Die Spec zeigt die Kachel mit einem Placeholder "Weitere Einstellungen folgen". Ich würde die Profil-Kachel trotzdem rendern aber opacity: 0.5 + cursor: not-allowed setzen. Kein pointer-events: none auf dem <a> selbst — das bricht Keyboard-Navigation. Stattdessen: hover + focus-visible Styles deaktivieren via :is([aria-disabled=true]) { opacity: .5; cursor: not-allowed; } und im Click-Handler ein preventDefault().

  • Breadcrumb-Typografie. Der Breadcrumb "← Einstellungen / Vorräte" auf D3 soll font-size: 12px, color: var(--color-text-muted) sein. Das "←" ist ein Unicode-Zeichen (), kein Icon — korrekt so, keine externe Dependency nötig. Der Separator "/" dazwischen: font-size: 10px oder gleiche Größe, leicht gedimmt.

  • Hinweistext auf D3. Die Spec hat font-style: italic für den Hinweistext "Änderungen werden automatisch gespeichert." Laut Design-System: italic ist nur in Fraunces erlaubt. DM Sans italic sollte nicht verwendet werden. Stattdessen: normales DM Sans, font-size: 11px, color: var(--color-text-muted) ohne italic. Das ist konform mit dem System.

  • border-left: 3px solid --green-dark auf der Vorräte-Kachel. Das ist ein 3px Akzent — prüfen ob das mit dem normalen 1px solid --color-border der Kachel sauber zusammenkommt. Der Left-Border überschreibt den Default-Border — es müssen also beide Seiten stimmen: border: 1px solid var(--color-border); border-left: 3px solid var(--green-dark). In dieser Reihenfolge, sonst überschreibt der generelle Border den Left-Akzent.

  • Vorräte-Kachel CTA-Text. "Vorräte bearbeiten →" und "Jetzt einrichten →" — das Pfeil-Zeichen ist (Unicode). Kein > verwenden. font-size: 12px, font-weight: 500, color: var(--green-dark).

Suggestions

  • font-style: italic im Hinweistext auf D3 entfernen — DM Sans italic ist im Design-System nicht erlaubt. Einfach color: var(--color-text-muted); font-size: 11px reicht.
  • Overflow-Handling für locals.benutzer.name in der Profil-Kachel: text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 100% — lange Namen sollen nicht die Kachel aufbrechen.
  • Die Stat-Zahl (36px Fraunces) soll line-height: 1 haben — sonst gibt es ungewollten vertikalen Freiraum über und unter der Zahl.
## 🎨 Atlas — UI/UX Design ### Questions & Observations - **`2fr 1fr` Grid — Proportionen in der Praxis.** Bei einer typischen Desktop-Breite von ~900px Content-Bereich ergibt `2fr 1fr` ca. 590px + 295px. Die Vorräte-Kachel hat dann viel horizontalen Raum für eine simple Stat-Zahl + Text. Das ist okay, aber prüfen ob der Inhalt nicht zu dünn wirkt. Falls nötig: die große Fraunces-Zahl (36px) anchors die Kachel gut nach oben. - **Profil-Kachel disabled State — visuell klären.** Die Spec zeigt die Kachel mit einem Placeholder "Weitere Einstellungen folgen". Ich würde die Profil-Kachel trotzdem rendern aber `opacity: 0.5` + `cursor: not-allowed` setzen. Kein `pointer-events: none` auf dem `<a>` selbst — das bricht Keyboard-Navigation. Stattdessen: hover + focus-visible Styles deaktivieren via `:is([aria-disabled=true]) { opacity: .5; cursor: not-allowed; }` und im Click-Handler ein `preventDefault()`. - **Breadcrumb-Typografie.** Der Breadcrumb "← Einstellungen / Vorräte" auf D3 soll `font-size: 12px`, `color: var(--color-text-muted)` sein. Das "←" ist ein Unicode-Zeichen (`←`), kein Icon — korrekt so, keine externe Dependency nötig. Der Separator "/" dazwischen: `font-size: 10px` oder gleiche Größe, leicht gedimmt. - **Hinweistext auf D3.** Die Spec hat `font-style: italic` für den Hinweistext "Änderungen werden automatisch gespeichert." Laut Design-System: *italic ist nur in Fraunces erlaubt*. DM Sans italic sollte nicht verwendet werden. Stattdessen: normales DM Sans, `font-size: 11px`, `color: var(--color-text-muted)` ohne italic. Das ist konform mit dem System. - **`border-left: 3px solid --green-dark` auf der Vorräte-Kachel.** Das ist ein `3px` Akzent — prüfen ob das mit dem normalen `1px solid --color-border` der Kachel sauber zusammenkommt. Der Left-Border überschreibt den Default-Border — es müssen also beide Seiten stimmen: `border: 1px solid var(--color-border); border-left: 3px solid var(--green-dark)`. In dieser Reihenfolge, sonst überschreibt der generelle Border den Left-Akzent. - **Vorräte-Kachel CTA-Text.** "Vorräte bearbeiten →" und "Jetzt einrichten →" — das Pfeil-Zeichen ist `→` (Unicode). Kein `>` verwenden. `font-size: 12px`, `font-weight: 500`, `color: var(--green-dark)`. ### Suggestions - `font-style: italic` im Hinweistext auf D3 entfernen — DM Sans italic ist im Design-System nicht erlaubt. Einfach `color: var(--color-text-muted); font-size: 11px` reicht. - Overflow-Handling für `locals.benutzer.name` in der Profil-Kachel: `text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 100%` — lange Namen sollen nicht die Kachel aufbrechen. - Die Stat-Zahl (`36px Fraunces`) soll `line-height: 1` haben — sonst gibt es ungewollten vertikalen Freiraum über und unter der Zahl.
Author
Owner

🎨 Atlas — UI/UX Designer · Design-Entscheide aus der Diskussion

Entschieden

  • Grid-Layoutgrid-template-columns: repeat(2, 1fr), auto-flow. Alle drei Kacheln gleich breit, nach 2 Kacheln automatischer Umbruch. Die Spec-Variante mit zwei separaten Grids (2fr 1fr / 1fr 1fr) ist Overkill und wird verworfen.

  • Italic Hinweistext auf D3font-style: italic aus der Spec entfernen. DM Sans italic ist im Design-System nicht erlaubt. Stattdessen: color: var(--color-text-muted); font-size: 11px ohne italic.

  • Profil-Kachel — kein disabled-State. Die Kachel linkt direkt zu /profile. Sobald die Seite existiert, funktioniert der Link. Kein aria-disabled, kein pointer-events: none.

  • Border-Reihenfolge Vorräte-Kachel — Implementierungshinweis: border muss vor border-left stehen, sonst überschreibt die Kurzform den grünen Akzentstreifen:

    border: 1px solid var(--color-border);
    border-left: 3px solid var(--green-dark);
    
  • line-height: 1 Stat-Zahl — bereits korrekt in der Spec dokumentiert, kein Handlungsbedarf.

Nicht besprochen / offen

  • Overflow-Handling für langen benutzer.name in der Profil-Kachel (text-overflow: ellipsis) — kein expliziter Entscheid, aber empfohlen.

Insgesamt ein sauberer, einfacher Hub. Das vereinfachte Grid macht die Implementierung deutlich geradliniger.

## 🎨 Atlas — UI/UX Designer · Design-Entscheide aus der Diskussion ### Entschieden - **Grid-Layout** — `grid-template-columns: repeat(2, 1fr)`, auto-flow. Alle drei Kacheln gleich breit, nach 2 Kacheln automatischer Umbruch. Die Spec-Variante mit zwei separaten Grids (`2fr 1fr` / `1fr 1fr`) ist Overkill und wird verworfen. - **Italic Hinweistext auf D3** — `font-style: italic` aus der Spec entfernen. DM Sans italic ist im Design-System nicht erlaubt. Stattdessen: `color: var(--color-text-muted); font-size: 11px` ohne italic. - **Profil-Kachel** — kein disabled-State. Die Kachel linkt direkt zu `/profile`. Sobald die Seite existiert, funktioniert der Link. Kein `aria-disabled`, kein `pointer-events: none`. - **Border-Reihenfolge Vorräte-Kachel** — Implementierungshinweis: `border` muss vor `border-left` stehen, sonst überschreibt die Kurzform den grünen Akzentstreifen: ```css border: 1px solid var(--color-border); border-left: 3px solid var(--green-dark); ``` - **`line-height: 1` Stat-Zahl** — bereits korrekt in der Spec dokumentiert, kein Handlungsbedarf. ### Nicht besprochen / offen - Overflow-Handling für langen `benutzer.name` in der Profil-Kachel (`text-overflow: ellipsis`) — kein expliziter Entscheid, aber empfohlen. --- Insgesamt ein sauberer, einfacher Hub. Das vereinfachte Grid macht die Implementierung deutlich geradliniger.
Author
Owner

⚛️ Kai — Frontend Engineer · Implementierungsentscheide

Entschieden

  • Tile-Name — Die zweite Kachel heißt Haushalt (nicht Mitglieder). href bleibt /members.

  • Sidebar — Wird im Rahmen dieses Issues vereinfacht: nur „Einstellungen" als Einstiegspunkt. Das Breadcrumb-/Active-State-Problem auf D3 entfällt damit. Das Breadcrumb auf D3 ($page.url.searchParams.get('ctx')) ist nicht nötig.

  • Back-Navigation auf D3 — Kein Breadcrumb-Markup, kein Breadcrumb-Komponent. Stattdessen ein einfacher „← Einstellungen"-Back-Link.

  • Gridgrid-cols-2 gap-4 (Tailwind), grid-cols-1 auf Mobile. Kein custom CSS. Entspricht dem Atlas-Entscheid repeat(2, 1fr).

  • Component SplitSettingsCard.svelte als eigene Komponente. Props: title, href, cta, meta? (für Zahl/Name-Zeile), accent? (grüner Linksstreifen bei Vorräte).

Offen / nicht besprochen

  • Test-Strategie (Vitest): QA hat eine Test-Matrix skizziert — noch kein expliziter Entscheid welche Tests Pflicht sind für diesen Issue.

Saubere, SSR-only Page. Kein $state, kein $effect — pures Server-Rendering mit einer schlanken Presenter-Komponente.

## ⚛️ Kai — Frontend Engineer · Implementierungsentscheide ### Entschieden - **Tile-Name** — Die zweite Kachel heißt **Haushalt** (nicht Mitglieder). `href` bleibt `/members`. - **Sidebar** — Wird im Rahmen dieses Issues vereinfacht: nur „Einstellungen" als Einstiegspunkt. Das Breadcrumb-/Active-State-Problem auf D3 entfällt damit. Das Breadcrumb auf D3 (`$page.url.searchParams.get('ctx')`) ist nicht nötig. - **Back-Navigation auf D3** — Kein Breadcrumb-Markup, kein Breadcrumb-Komponent. Stattdessen ein einfacher „← Einstellungen"-Back-Link. - **Grid** — `grid-cols-2 gap-4` (Tailwind), `grid-cols-1` auf Mobile. Kein custom CSS. Entspricht dem Atlas-Entscheid `repeat(2, 1fr)`. - **Component Split** — `SettingsCard.svelte` als eigene Komponente. Props: `title`, `href`, `cta`, `meta?` (für Zahl/Name-Zeile), `accent?` (grüner Linksstreifen bei Vorräte). ### Offen / nicht besprochen - Test-Strategie (Vitest): QA hat eine Test-Matrix skizziert — noch kein expliziter Entscheid welche Tests Pflicht sind für diesen Issue. --- Saubere, SSR-only Page. Kein `$state`, kein `$effect` — pures Server-Rendering mit einer schlanken Presenter-Komponente.
Author
Owner

Implementierung abgeschlossen

Branch: feat/issue-49-settings-kachel-hub

Commits

  1. feat(settings): add +page.server.ts loading stapleCount, memberCount, userName (33cccd3)

    • GET /v1/ingredients → zählt isStaple=truestapleCount
    • GET /v1/households/minemembers.lengthmemberCount
    • locals.benutzer.nameuserName
    • Fallback 0 bei API-Fehler (beide Calls)
  2. feat(settings): add SettingsCard component with title, href, cta, meta, accent props (3f9fb90)

    • $lib/components/SettingsCard.svelte
    • Props: title, href, cta, meta?, accent?
    • Hover-Shadow, Focus-visible, Akzentstreifen via data-accent
  3. feat(settings): implement settings hub page with three cards (109b41b)

    • Grid grid-cols-1 sm:grid-cols-2 gap-4
    • Vorräte-Kachel: Fraunces-Zahl + Leer-Zustand (0 Vorräte)
    • Haushalt-Kachel mit data-testid="member-count"
    • Profil-Kachel → /profile
  4. feat(settings): add ← Einstellungen back-link on D3 staples page when ctx=settings (0b3d062)

  5. feat(settings): add autosave hint text below StaplesManager on D3 when ctx=settings (48802a0)

    • „Änderungen werden automatisch gespeichert. Gilt ab der nächsten Einkaufsliste."
    • font-size: 11px, color: var(--color-text-muted), kein italic

Tests: 727/727 (69 Dateien)

Akzeptanzkriterien

  • Hub zeigt drei Kacheln im repeat(2, 1fr) Grid
  • Vorräte-Kachel zeigt korrekte Anzahl aktiver Vorräte
  • Leerer Zustand (0 Vorräte) zeigt alternativen Text und CTA
  • Hover-Zustand: Shadow + dunklerer Border, Transition
  • Alle Kacheln sind <a>-Tags mit korrektem href
  • D3-Seite hat Back-Link zurück zu /settings
  • Hinweistext unter dem Chip-Grid auf D3
  • Mobile: Kacheln stacken vertikal (grid-cols-1)

→ Nächster Schritt: /review-pr oder PR öffnen auf master

## ✅ Implementierung abgeschlossen Branch: `feat/issue-49-settings-kachel-hub` ### Commits 1. **feat(settings): add +page.server.ts loading stapleCount, memberCount, userName** (`33cccd3`) - `GET /v1/ingredients` → zählt `isStaple=true` → `stapleCount` - `GET /v1/households/mine` → `members.length` → `memberCount` - `locals.benutzer.name` → `userName` - Fallback 0 bei API-Fehler (beide Calls) 2. **feat(settings): add SettingsCard component with title, href, cta, meta, accent props** (`3f9fb90`) - `$lib/components/SettingsCard.svelte` - Props: `title`, `href`, `cta`, `meta?`, `accent?` - Hover-Shadow, Focus-visible, Akzentstreifen via `data-accent` 3. **feat(settings): implement settings hub page with three cards** (`109b41b`) - Grid `grid-cols-1 sm:grid-cols-2 gap-4` - Vorräte-Kachel: Fraunces-Zahl + Leer-Zustand (0 Vorräte) - Haushalt-Kachel mit `data-testid="member-count"` - Profil-Kachel → `/profile` 4. **feat(settings): add ← Einstellungen back-link on D3 staples page when ctx=settings** (`0b3d062`) 5. **feat(settings): add autosave hint text below StaplesManager on D3 when ctx=settings** (`48802a0`) - „Änderungen werden automatisch gespeichert. Gilt ab der nächsten Einkaufsliste." - `font-size: 11px`, `color: var(--color-text-muted)`, kein italic ### Tests: 727/727 ✅ (69 Dateien) ### Akzeptanzkriterien - [x] Hub zeigt drei Kacheln im `repeat(2, 1fr)` Grid - [x] Vorräte-Kachel zeigt korrekte Anzahl aktiver Vorräte - [x] Leerer Zustand (0 Vorräte) zeigt alternativen Text und CTA - [x] Hover-Zustand: Shadow + dunklerer Border, Transition - [x] Alle Kacheln sind `<a>`-Tags mit korrektem href - [x] D3-Seite hat Back-Link zurück zu /settings - [x] Hinweistext unter dem Chip-Grid auf D3 - [x] Mobile: Kacheln stacken vertikal (`grid-cols-1`) → Nächster Schritt: `/review-pr` oder PR öffnen auf `master`
Sign in to join this conversation.