feat(settings): implement /settings page — Kachel-Ansicht (E1) #49
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?
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 bestehendeStaplesManager-Komponente — keine neue Komponente nötig.Spec:
specs/frontend/e1-settings-kachel.htmlZustände (aus Spec)
Layout
grid-template-columns: 2fr 1fr(obere Reihe) +1fr 1fr(untere Reihe), Gap 16px<a>-TagsKacheln
Vorräte (primär)
border-left: 3px solid --green-darkals Akzentstreifen--green-dark— AnzahlisStaple === trueausGET /v1/ingredient-categoriesstapleCount === 0): Stat-Zahl weglassen, "Noch keine Vorräte eingerichtet" + CTA "Jetzt einrichten →"/household/staples?ctx=settingsMitglieder
{memberCount} Mitglieder/membersProfil
locals.benutzer.name/profile(noch nicht implementiert — als disabled rendern oder Placeholder)D3 —
/household/staples?ctx=settingssrc/routes/household/staples/+page.svelteexistiert bereits← Einstellungenüber dem Seitentitel, verlinkt auf/settingsHover & Fokus
box-shadow: --shadow-raised,border-color: #C0BFB8box-shadow 150ms ease, border-color 150ms easeoutline: 2px solid --green-dark; outline-offset: 2pxData Loading (
+page.server.tsfür/settings)Akzeptanzkriterien
<a>-Tags mit korrektem href⚛️ Kai — Frontend Engineer
Questions & Observations
memberCount: woher? Das Issue sagt "aus layout data oder separatem Call".
+layout.server.tsgibt aktuellbenutzerundhaushaltzurück — aber keine Mitgliederzahl.GET /v1/households/mineliefert laut Schema ein Haushalt-Objekt mitmembers[]. Ich würde das in+page.server.tsfür/settingsparallel 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">mitpointer-events: none; opacity: 0.5via Tailwindpointer-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>mitaria-disabledist sauberer.D3 Breadcrumb: existing page ändern.
src/routes/household/staples/+page.svelteexistiert bereits mit Onboarding-Logik (isOnboarding-Check). Ich füge den Breadcrumb nur hinzu wenn!isOnboarding && ctx === 'settings'— das ist die gleichectx-Logik die schon drin ist. Kein Breaking Change.stapleCount SSR. Der API-Call in
+page.server.tsläuft server-seitig — gut. Kein$effectnötig. Wenn der Call fehlschlägt, solltestapleCountauf0defaulten (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.svelteals<a>-Tags. Kein$statenötig, keine Interaktivität. Sauberste Lösung: reines Server-Rendering, nulljseitige Reaktivität.Suggestions
+page.server.tsfür/settings:stapleCount: 0→ prüfen ob "Jetzt einrichten →" erscheint. MitstapleCount: 14→ prüfen ob "14" und "Vorräte bearbeiten →" erscheint.🏗️ 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) undGET /v1/households/mine(existiert). Das ist gut — dieser Issue ist rein frontend-seitig umsetzbar ohne Backend-Arbeit. Trotzdem zwei Punkte:GET /v1/ingredient-categoriesliefert alle Zutaten aller Kategorien? Laut Schema gibt eslistCategories— aber ist da auchisStaplepro Zutat dabei? Das hängt von der aktuellen API-Implementierung ab. WennisStaplenicht 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/minemitmembers[]— 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 einenJOIN FETCHhat 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.tsberechnet — 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
GET /v1/ingredient-categoriestatsächlichisStaple: booleanpro Ingredient zurückgibt. Falls nicht → entweder den Endpoint erweitern oder eine separate Methode bauen. Das sollte vor der Frontend-Implementierung klar sein.GET /v1/households/minenoch keinen explizitenJOIN FETCHhat: kurze Ergänzung in der Repository-Query eintragen — kein grosser Aufwand, verhindert zukünftige LazyInitializationExceptions ausserhalb der Transaktion.🔒 Sable — Security Engineer
Questions & Observations
Zugriffskontrolle auf
/settings. Die Spec erwähnt keine Rollen-Einschränkung für E1. Sollte/settingsfürmitgliedzugänglich sein? Wenn ja, was sieht ein Mitglied?/members(E2) — Mitglieder sehen E2 read-only gemäss #48 ✓/settingsscheint für beide Rollen passend. Aber das sollte explizit dokumentiert werden, damithooks.server.tsnicht versehentlich/settingsaufplaner-onlybeschränkt.locals.benutzer.namein der Profil-Kachel. Der Name kommt aus der server-seitigen Session — kein direktes XSS-Risiko, da Svelte serverseitig escaped. Trotzdem: wenndisplayNamebei 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-disabledLink. Kai's Vorschlag (<a href="/profile" aria-disabled="true">) ist aus Security-Sicht i.O. —/profileexistiert noch nicht und gibt entweder 404 zurück oder wird überhooks.server.tsabgefangen. 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
planerundmitgliedzugänglich. Beide sehen alle drei Kacheln. Die Kacheln navigieren zu Seiten, die dann rollenbasiert ihre eigene Zugangskontrolle haben.hooks.server.tsprüfen: falls dort/household/*aufplaner-onlybeschränkt ist, müsste/settingsexplizit ausgenommen werden.🧪 QA Engineer
Questions & Observations
Fehlende Akzeptanzkriterien:
GET /v1/ingredient-categoriesfehlschlägt? Die Issue-Beschreibung sagt?? 0als 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."GET /v1/households/minefehlschlägt? Dann wärememberCountundefined. Wird "0 Mitglieder" oder ein Fehler angezeigt? Muss definiert sein.not-allowed?opacity: 50%? Click führt zu... nichts? 404? Das muss klar sein um testbar zu sein./settings— aber wenn ich direkt auf/household/staples?ctx=settingsnavigiere (nicht via Hub), zeigt der Breadcrumb auch? Das ist ein wichtiger Rendering-Test.isActiveRoute(href, pathname)—/settingsist nicht der aktive Pfad wenn wir auf/household/staplessind. Das könnte ein Bug sein der aufgedeckt werden muss.Test-Coverage:
Suggestions
isActiveRouteangepasst werden muss um/household/staplesals "Einstellungen"-aktiv zu erkennen (z.B. via prefix-matching auf/household).data-testid="staple-count"unddata-testid="member-count"ergänzen damit Tests robust auf die Zahlen zeigen können.🎨 Atlas — UI/UX Design
Questions & Observations
2fr 1frGrid — Proportionen in der Praxis. Bei einer typischen Desktop-Breite von ~900px Content-Bereich ergibt2fr 1frca. 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-allowedsetzen. Keinpointer-events: noneauf 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 einpreventDefault().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: 10pxoder gleiche Größe, leicht gedimmt.Hinweistext auf D3. Die Spec hat
font-style: italicfü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-darkauf der Vorräte-Kachel. Das ist ein3pxAkzent — prüfen ob das mit dem normalen1px solid --color-borderder 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: italicim Hinweistext auf D3 entfernen — DM Sans italic ist im Design-System nicht erlaubt. Einfachcolor: var(--color-text-muted); font-size: 11pxreicht.locals.benutzer.namein der Profil-Kachel:text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 100%— lange Namen sollen nicht die Kachel aufbrechen.36px Fraunces) sollline-height: 1haben — sonst gibt es ungewollten vertikalen Freiraum über und unter der Zahl.🎨 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: italicaus der Spec entfernen. DM Sans italic ist im Design-System nicht erlaubt. Stattdessen:color: var(--color-text-muted); font-size: 11pxohne italic.Profil-Kachel — kein disabled-State. Die Kachel linkt direkt zu
/profile. Sobald die Seite existiert, funktioniert der Link. Keinaria-disabled, keinpointer-events: none.Border-Reihenfolge Vorräte-Kachel — Implementierungshinweis:
bordermuss vorborder-leftstehen, sonst überschreibt die Kurzform den grünen Akzentstreifen:line-height: 1Stat-Zahl — bereits korrekt in der Spec dokumentiert, kein Handlungsbedarf.Nicht besprochen / offen
benutzer.namein der Profil-Kachel (text-overflow: ellipsis) — kein expliziter Entscheid, aber empfohlen.Insgesamt ein sauberer, einfacher Hub. Das vereinfachte Grid macht die Implementierung deutlich geradliniger.
⚛️ Kai — Frontend Engineer · Implementierungsentscheide
Entschieden
Tile-Name — Die zweite Kachel heißt Haushalt (nicht Mitglieder).
hrefbleibt/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-1auf Mobile. Kein custom CSS. Entspricht dem Atlas-Entscheidrepeat(2, 1fr).Component Split —
SettingsCard.svelteals eigene Komponente. Props:title,href,cta,meta?(für Zahl/Name-Zeile),accent?(grüner Linksstreifen bei Vorräte).Offen / nicht besprochen
Saubere, SSR-only Page. Kein
$state, kein$effect— pures Server-Rendering mit einer schlanken Presenter-Komponente.✅ Implementierung abgeschlossen
Branch:
feat/issue-49-settings-kachel-hubCommits
feat(settings): add +page.server.ts loading stapleCount, memberCount, userName (
33cccd3)GET /v1/ingredients→ zähltisStaple=true→stapleCountGET /v1/households/mine→members.length→memberCountlocals.benutzer.name→userNamefeat(settings): add SettingsCard component with title, href, cta, meta, accent props (
3f9fb90)$lib/components/SettingsCard.sveltetitle,href,cta,meta?,accent?data-accentfeat(settings): implement settings hub page with three cards (
109b41b)grid-cols-1 sm:grid-cols-2 gap-4data-testid="member-count"/profilefeat(settings): add ← Einstellungen back-link on D3 staples page when ctx=settings (
0b3d062)feat(settings): add autosave hint text below StaplesManager on D3 when ctx=settings (
48802a0)font-size: 11px,color: var(--color-text-muted), kein italicTests: 727/727 ✅ (69 Dateien)
Akzeptanzkriterien
repeat(2, 1fr)Grid<a>-Tags mit korrektem hrefgrid-cols-1)→ Nächster Schritt:
/review-proder PR öffnen aufmaster