feat(members): implement /members page — Kachel-Ansicht (E2) #48

Closed
opened 2026-04-09 15:06:44 +02:00 by marcel · 9 comments
Owner

Beschreibung

Implementierung der /members-Seite als Kachel-Ansicht (Card Grid). Jedes Haushaltsmitglied erscheint als Kachel. Der Planer kann Rollen ändern und Mitglieder entfernen. Eine Einladekachel ermöglicht das Generieren und Kopieren des Einlade-Links.

Spec: specs/frontend/e2-members-kachel.html


Zustände (aus Spec)

State Beschreibung
S1 Standardansicht — Kachelgrid mit allen Mitgliedern + Einladekachel
S2 Kebab-Menü offen — "Rolle ändern" und "Entfernen"
S3 Rolle ändern — Segmented Control ersetzt Badge inline
S4 Bestätigungsdialog "Mitglied entfernen"
S5 Einlade-Panel mit Link, Copy-Button und Ablaufdatum
S6 Mitglied-Perspektive (read-only, kein Kebab, keine Einladekachel)

Layout

  • Desktop: grid-template-columns: repeat(4, 1fr), Gap 16px
  • Mobile: grid-template-columns: repeat(2, 1fr), Gap 12px
  • Kartenreihenfolge: eigene Kachel zuerst → andere nach joinedAt ASC → Einladekachel zuletzt

Komponenten / Verhalten

  • Avatar: Initialen (erste 2 Zeichen), 56px desktop / 44px mobile, 50% Radius. Planer = --green-dark, Mitglied = --blue
  • Eigene Kachel: border-color: --green-light, "Du"-Badge, kein Kebab-Button
  • Kebab (): opacity: 01 on hover/focus; auf Touch-Geräten immer sichtbar. Click-away schließt, ESC schließt.
  • Rolle ändern (S3): Segmented Control [Planer | Mitglied] ersetzt Badge in-place → optimistisches PATCH → bei Fehler Rollback + Toast
  • Entfernen (S4): Bestätigungsdialog mit displayName. Backdrop schließt NICHT on click. Mobile: Bottom Sheet.
  • Einlade-Panel (S5): Erscheint unterhalb des Grids (kein Modal). Copy → navigator.clipboard.writeText → Button zeigt "Kopiert ✓" für 2s. Regenerieren invalidiert alten Code.

⚠️ Backend-Lücken — müssen vor Implementierung geschlossen werden

Diese Endpoints existieren noch nicht im API-Schema (schema.d.ts):

DELETE /v1/households/mine/members/{userId}   — Mitglied entfernen
PATCH  /v1/households/mine/members/{userId}   — Rolle ändern (body: { role })
GET    /v1/households/mine/invites            — aktive Einladungen auflisten

Bestehend: GET /v1/households/mine/members, POST /v1/households/mine/invites


Akzeptanzkriterien

  • Kachelgrid rendert alle Mitglieder korrekt (Desktop + Mobile)
  • Eigene Kachel hat grünen Rahmen und "Du"-Badge, kein Kebab
  • Kebab öffnet Dropdown mit "Rolle ändern" und "Entfernen"
  • Rolle ändern funktioniert inline mit optimistischem Update
  • Entfernen erfordert Bestätigungsdialog mit Mitgliedsnamen
  • Planer kann sich selbst nicht entfernen
  • Letzter Planer kann nicht degradiert werden
  • Einlade-Panel zeigt generierten Link mit Copy und Regenerieren
  • Mitglieder (rolle=mitglied) sehen read-only Ansicht ohne Aktionen
  • Backend-Lücken (DELETE, PATCH, GET invites) sind implementiert
## Beschreibung Implementierung der `/members`-Seite als Kachel-Ansicht (Card Grid). Jedes Haushaltsmitglied erscheint als Kachel. Der Planer kann Rollen ändern und Mitglieder entfernen. Eine Einladekachel ermöglicht das Generieren und Kopieren des Einlade-Links. **Spec:** [`specs/frontend/e2-members-kachel.html`](http://heim-nas:3005/marcel/mealprep/src/branch/master/specs/frontend/e2-members-kachel.html) --- ## Zustände (aus Spec) | State | Beschreibung | |-------|-------------| | S1 | Standardansicht — Kachelgrid mit allen Mitgliedern + Einladekachel | | S2 | Kebab-Menü offen — "Rolle ändern" und "Entfernen" | | S3 | Rolle ändern — Segmented Control ersetzt Badge inline | | S4 | Bestätigungsdialog "Mitglied entfernen" | | S5 | Einlade-Panel mit Link, Copy-Button und Ablaufdatum | | S6 | Mitglied-Perspektive (read-only, kein Kebab, keine Einladekachel) | --- ## Layout - **Desktop:** `grid-template-columns: repeat(4, 1fr)`, Gap 16px - **Mobile:** `grid-template-columns: repeat(2, 1fr)`, Gap 12px - Kartenreihenfolge: eigene Kachel zuerst → andere nach `joinedAt` ASC → Einladekachel zuletzt ## Komponenten / Verhalten - **Avatar:** Initialen (erste 2 Zeichen), 56px desktop / 44px mobile, 50% Radius. Planer = `--green-dark`, Mitglied = `--blue` - **Eigene Kachel:** `border-color: --green-light`, "Du"-Badge, kein Kebab-Button - **Kebab (`⋯`):** `opacity: 0` → `1` on hover/focus; auf Touch-Geräten immer sichtbar. Click-away schließt, ESC schließt. - **Rolle ändern (S3):** Segmented Control [Planer | Mitglied] ersetzt Badge in-place → optimistisches PATCH → bei Fehler Rollback + Toast - **Entfernen (S4):** Bestätigungsdialog mit displayName. Backdrop schließt NICHT on click. Mobile: Bottom Sheet. - **Einlade-Panel (S5):** Erscheint unterhalb des Grids (kein Modal). Copy → `navigator.clipboard.writeText` → Button zeigt "Kopiert ✓" für 2s. Regenerieren invalidiert alten Code. --- ## ⚠️ Backend-Lücken — müssen vor Implementierung geschlossen werden Diese Endpoints existieren noch nicht im API-Schema (`schema.d.ts`): ``` DELETE /v1/households/mine/members/{userId} — Mitglied entfernen PATCH /v1/households/mine/members/{userId} — Rolle ändern (body: { role }) GET /v1/households/mine/invites — aktive Einladungen auflisten ``` Bestehend: `GET /v1/households/mine/members`, `POST /v1/households/mine/invites` --- ## Akzeptanzkriterien - [ ] Kachelgrid rendert alle Mitglieder korrekt (Desktop + Mobile) - [ ] Eigene Kachel hat grünen Rahmen und "Du"-Badge, kein Kebab - [ ] Kebab öffnet Dropdown mit "Rolle ändern" und "Entfernen" - [ ] Rolle ändern funktioniert inline mit optimistischem Update - [ ] Entfernen erfordert Bestätigungsdialog mit Mitgliedsnamen - [ ] Planer kann sich selbst nicht entfernen - [ ] Letzter Planer kann nicht degradiert werden - [ ] Einlade-Panel zeigt generierten Link mit Copy und Regenerieren - [ ] Mitglieder (rolle=mitglied) sehen read-only Ansicht ohne Aktionen - [ ] Backend-Lücken (DELETE, PATCH, GET invites) sind implementiert
Author
Owner

⚛️ Kai — Frontend Engineer

Questions & Observations

  • Backend-first dependency is a hard blocker. DELETE + PATCH member + GET invites müssen alle drei fertig und im schema.d.ts drin sein, bevor ich anfangen kann. Ich würde vorschlagen, die Frontendarbeit in einen separaten Branch zu verschieben, der auf einen Backend-Branch rebased, statt auf master zu warten. Haben wir schon einen Backend-Issue dafür?

  • Komponenten-Split. Ich denke an: MemberGrid.svelte (Grid-Layout + Karten-Orchestration), MemberCard.svelte (einzelne Kachel, inklusive Kebab und Role-Control), InviteCard.svelte, InvitePanel.svelte, RemoveDialog.svelte. Fühlt sich richtig an — jede Datei bleibt fokussiert. Wäre das okay oder soll alles in einer Seite bleiben?

  • Click-away Listener. Ich werde das mit einem $effect lösen, der einen document.addEventListener('click', ...) registriert und in der Cleanup-Funktion wieder entfernt. SSR-Problem: document ist server-seitig nicht verfügbar. Brauche ich ein browser-Guard. Ist das euer Standard-Pattern oder gibt es eine App-Util dafür?

  • Touch-Detection für immer-sichtbaren Kebab. @media (hover: none) CSS-Media-Query ist zuverlässiger als JS navigator.maxTouchPoints. Ich würde das rein über CSS machen:

    .kebab { opacity: 0; }
    @media (hover: none) { .kebab { opacity: 1; } }
    .card:hover .kebab, .card:focus-within .kebab { opacity: 1; }
    
  • navigator.clipboard.writeText ist nur im HTTPS-Kontext verfügbar. Lokale Dev-Umgebung läuft auf HTTP — brauchen wir einen Fallback (execCommand('copy') oder eine Fehlermeldung)?

  • Optimistisches Update bei Rolle ändern. Ich setze die neue Rolle sofort in $state, dispatch dann das PATCH. Bei 4xx rollback ich zurück und zeige einen Toast. Gibt es eine globale Toast-Komponente oder muss ich eine bauen?

Suggestions

  • +page.server.ts lädt GET /v1/households/mine/members — das gibt bereits die Members zurück. Ich brauche locals.benutzer.id aus dem Layout-Server-Load, um die eigene Kachel zu identifizieren. Das ist schon in +layout.server.ts verfügbar — kein extra API-Call nötig.
  • Für den Remove-Dialog: ich baue den als eigene Svelte-Komponente mit einem show: boolean-$state-prop, nicht als inlined HTML-Block. Das macht es testbar.
  • TDD-Reihenfolge: ich fange mit MemberCard an (einfachste Unit), dann MemberGrid, dann die Seite selbst. Für den Dialog gibt es typische Render-Tests + Click-Tests.
## ⚛️ Kai — Frontend Engineer ### Questions & Observations - **Backend-first dependency is a hard blocker.** DELETE + PATCH member + GET invites müssen alle drei fertig und im `schema.d.ts` drin sein, bevor ich anfangen kann. Ich würde vorschlagen, die Frontendarbeit in einen separaten Branch zu verschieben, der auf einen Backend-Branch rebased, statt auf master zu warten. Haben wir schon einen Backend-Issue dafür? - **Komponenten-Split.** Ich denke an: `MemberGrid.svelte` (Grid-Layout + Karten-Orchestration), `MemberCard.svelte` (einzelne Kachel, inklusive Kebab und Role-Control), `InviteCard.svelte`, `InvitePanel.svelte`, `RemoveDialog.svelte`. Fühlt sich richtig an — jede Datei bleibt fokussiert. Wäre das okay oder soll alles in einer Seite bleiben? - **Click-away Listener.** Ich werde das mit einem `$effect` lösen, der einen `document.addEventListener('click', ...)` registriert und in der Cleanup-Funktion wieder entfernt. SSR-Problem: `document` ist server-seitig nicht verfügbar. Brauche ich ein `browser`-Guard. Ist das euer Standard-Pattern oder gibt es eine App-Util dafür? - **Touch-Detection für immer-sichtbaren Kebab.** `@media (hover: none)` CSS-Media-Query ist zuverlässiger als JS `navigator.maxTouchPoints`. Ich würde das rein über CSS machen: ```css .kebab { opacity: 0; } @media (hover: none) { .kebab { opacity: 1; } } .card:hover .kebab, .card:focus-within .kebab { opacity: 1; } ``` - **`navigator.clipboard.writeText`** ist nur im HTTPS-Kontext verfügbar. Lokale Dev-Umgebung läuft auf HTTP — brauchen wir einen Fallback (`execCommand('copy')` oder eine Fehlermeldung)? - **Optimistisches Update bei Rolle ändern.** Ich setze die neue Rolle sofort in `$state`, dispatch dann das PATCH. Bei 4xx rollback ich zurück und zeige einen Toast. Gibt es eine globale Toast-Komponente oder muss ich eine bauen? ### Suggestions - `+page.server.ts` lädt `GET /v1/households/mine/members` — das gibt bereits die Members zurück. Ich brauche `locals.benutzer.id` aus dem Layout-Server-Load, um die eigene Kachel zu identifizieren. Das ist schon in `+layout.server.ts` verfügbar — kein extra API-Call nötig. - Für den Remove-Dialog: ich baue den als eigene Svelte-Komponente mit einem `show: boolean`-$state-prop, nicht als inlined HTML-Block. Das macht es testbar. - TDD-Reihenfolge: ich fange mit `MemberCard` an (einfachste Unit), dann `MemberGrid`, dann die Seite selbst. Für den Dialog gibt es typische Render-Tests + Click-Tests.
Author
Owner

🏗️ Backend Engineer — Spring Boot / PostgreSQL

Questions & Observations

  • Drei neue Endpoints — wo sitzen die Business Rules? Die Guards "Planer kann sich nicht selbst entfernen" und "letzter Planer kann nicht degradiert werden" gehören in den Service, nicht in den Controller. Der Controller validiert nur den Request, der Service entscheidet ob die Aktion erlaubt ist.

  • Aggregate-Grenze: Haushalt ist der Root. Beide Endpoints operieren auf households/mine/members/{userId} — das deutet darauf hin, dass die Validierung auf dem Household-Aggregate läuft: "Darf ich dieses Mitglied in meinem Haushalt entfernen?". Ich würde HouseholdService.removeMember(householdId, requesterId, targetUserId) als Signatur vorschlagen — so ist die Household-Isolation im Service erzwingbar.

  • GET /v1/households/mine/invites fehlt im Schema. Die bestehende POST-Route erstellt eine Einladung — aber GET zum Auflisten aktiver Codes ist noch nicht implementiert. Wichtige Frage: Soll es immer nur einen aktiven Code geben (einfacher) oder mehrere? Für einen einzelnen Haushalt im Familienkontext reicht ein aktiver Code pro Haushalt.

  • Audit-Log. Rollenänderung und Mitglied-Entfernen sind sicherheitsrelevante Aktionen. Beide sollten in admin_audit_log geschrieben werden — analog zu anderen privilegierten Aktionen im System.

  • HTTP-Semantik:

    • DELETE /v1/households/mine/members/{userId}204 No Content bei Erfolg
    • PATCH /v1/households/mine/members/{userId}200 OK mit dem aktualisierten MemberResponse
    • Guard-Verletzungen (selbst entfernen, letzter Planer) → 409 Conflict oder 422 Unprocessable Entity mit klarer Fehlermeldung
  • Idempotenz bei PATCH. Was passiert, wenn ich PATCH mit role: "mitglied" aufrufe und das Mitglied ist bereits mitglied? Das sollte ein 200 OK ohne Seiteneffekte zurückgeben — kein Fehler, kein unnötiger Audit-Log-Eintrag.

Suggestions

  • Flyway-Migration für das household_invites-Konzept prüfen: Braucht die invites-Tabelle eine invalidated_at-Spalte, um alte Codes zu markieren ohne zu löschen (Audit-Trail)?
  • DB-Constraint: Nur ein active Invite pro Haushalt. Das kann über einen Partial Unique Index abgesichert werden: UNIQUE (household_id) WHERE invalidated_at IS NULL.
  • Testcontainer-Tests schreiben für alle drei Endpoints: Happy Path + Guard-Violations + Isolation (anderer Haushalt → 403).
## 🏗️ Backend Engineer — Spring Boot / PostgreSQL ### Questions & Observations - **Drei neue Endpoints — wo sitzen die Business Rules?** Die Guards "Planer kann sich nicht selbst entfernen" und "letzter Planer kann nicht degradiert werden" gehören in den Service, nicht in den Controller. Der Controller validiert nur den Request, der Service entscheidet ob die Aktion erlaubt ist. - **Aggregate-Grenze: Haushalt ist der Root.** Beide Endpoints operieren auf `households/mine/members/{userId}` — das deutet darauf hin, dass die Validierung auf dem `Household`-Aggregate läuft: "Darf ich dieses Mitglied in *meinem* Haushalt entfernen?". Ich würde `HouseholdService.removeMember(householdId, requesterId, targetUserId)` als Signatur vorschlagen — so ist die Household-Isolation im Service erzwingbar. - **GET /v1/households/mine/invites fehlt im Schema.** Die bestehende `POST`-Route erstellt eine Einladung — aber `GET` zum Auflisten aktiver Codes ist noch nicht implementiert. Wichtige Frage: Soll es immer nur *einen* aktiven Code geben (einfacher) oder mehrere? Für einen einzelnen Haushalt im Familienkontext reicht ein aktiver Code pro Haushalt. - **Audit-Log.** Rollenänderung und Mitglied-Entfernen sind sicherheitsrelevante Aktionen. Beide sollten in `admin_audit_log` geschrieben werden — analog zu anderen privilegierten Aktionen im System. - **HTTP-Semantik:** - `DELETE /v1/households/mine/members/{userId}` → `204 No Content` bei Erfolg - `PATCH /v1/households/mine/members/{userId}` → `200 OK` mit dem aktualisierten `MemberResponse` - Guard-Verletzungen (selbst entfernen, letzter Planer) → `409 Conflict` oder `422 Unprocessable Entity` mit klarer Fehlermeldung - **Idempotenz bei PATCH.** Was passiert, wenn ich `PATCH` mit `role: "mitglied"` aufrufe und das Mitglied ist bereits `mitglied`? Das sollte ein `200 OK` ohne Seiteneffekte zurückgeben — kein Fehler, kein unnötiger Audit-Log-Eintrag. ### Suggestions - Flyway-Migration für das `household_invites`-Konzept prüfen: Braucht die `invites`-Tabelle eine `invalidated_at`-Spalte, um alte Codes zu markieren ohne zu löschen (Audit-Trail)? - DB-Constraint: Nur ein `active` Invite pro Haushalt. Das kann über einen Partial Unique Index abgesichert werden: `UNIQUE (household_id) WHERE invalidated_at IS NULL`. - Testcontainer-Tests schreiben für alle drei Endpoints: Happy Path + Guard-Violations + Isolation (anderer Haushalt → 403).
Author
Owner

🔒 Sable — Security Engineer

Questions & Observations

  • IDOR auf DELETE und PATCH — kritisch. DELETE /v1/households/mine/members/{userId} muss serverseitig verifizieren, dass {userId} tatsächlich Mitglied des eigenen Haushalts ist. Ein Planer aus Haushalt A darf keinen User aus Haushalt B entfernen. Das klingt offensichtlich, aber ich habe schon gesehen wie findById(userId) ohne Haushalt-Join aufgerufen wird. Die Repository-Query muss lauten: findByUserIdAndHouseholdId(userId, currentHouseholdId).

  • Broken Access Control auf PATCH. Ein mitglied darf PATCH /v1/households/mine/members/{userId} nicht aufrufen — auch nicht für sich selbst. Der rolle === 'planer'-Check muss im SecurityFilterChain oder im Service stattfinden, nicht nur im Frontend. Das Frontend-Guard (kein Kebab-Menü für Mitglieder) ist kein Sicherheitskontrollpunkt.

  • Invite-Code Entropie. Die Spec sagt "Regenerieren invalidiert alten Code" — gut. Aber wie wird der Code generiert? UUIDv4 (122 Bit Entropie) ist ausreichend. Kürzere Codes (z.B. X4K9-RZMQ wie in der Spec) haben drastisch weniger Entropie und sind über Brute-Force angreifbar, wenn kein Rate-Limiting existiert. Wichtige Frage: Gibt es Rate-Limiting auf POST /v1/invites/{code}/accept?

  • Invite-Link im Einlade-Panel. Der shareUrl wird clientseitig im DOM gerendert — kein XSS-Risiko solange kein {@html} verwendet wird (Svelte escaped automatisch). Aber: Der Link landet möglicherweise in Browserlogs, Proxy-Logs oder Referrer-Headers. Referrer-Policy: no-referrer auf der App-Ebene prüfen.

  • navigator.clipboard ist HTTPS-only. Kai hat das bereits erwähnt. Aus Security-Perspektive: kein Fallback auf execCommand — das ist deprecated und hat eigene Security-Probleme. Lieber eine klare Fehlermeldung "Kopieren nur über HTTPS verfügbar" zeigen.

  • Audit-Log-Pflicht. Rollenänderung (planer → mitglied oder umgekehrt) und Mitglied-Entfernen sind privilegierte Aktionen. Beide müssen in admin_audit_log stehen: actor_id, action, target_id, timestamp. Das ist kein Nice-to-have — ohne Audit-Trail gibt es keine Nachvollziehbarkeit bei Disputes.

  • Session-Invalidierung nach Entfernen. Was passiert mit der aktiven Session des entfernten Mitglieds? Idealerweise wird die Session sofort invalidiert — sonst hat das entfernte Mitglied bis zum Session-Timeout noch Zugang. Spring Security hat hierfür SessionRegistry + expireNow().

Suggestions

  • Backend-Service-Signatur: immer householdId aus dem Session-Principal mitgeben, nie aus dem Request-Body oder -Pfad allein vertrauen.
  • Integration-Test: Planer aus Haushalt A versucht, Mitglied aus Haushalt B zu entfernen → muss 403 zurückgeben, nicht 404 (404 würde Existenz bestätigen, 403 tut das auch, aber konsistenter).
## 🔒 Sable — Security Engineer ### Questions & Observations - **IDOR auf DELETE und PATCH — kritisch.** `DELETE /v1/households/mine/members/{userId}` muss serverseitig verifizieren, dass `{userId}` tatsächlich Mitglied des *eigenen* Haushalts ist. Ein Planer aus Haushalt A darf keinen User aus Haushalt B entfernen. Das klingt offensichtlich, aber ich habe schon gesehen wie `findById(userId)` ohne Haushalt-Join aufgerufen wird. Die Repository-Query muss lauten: `findByUserIdAndHouseholdId(userId, currentHouseholdId)`. - **Broken Access Control auf PATCH.** Ein `mitglied` darf `PATCH /v1/households/mine/members/{userId}` nicht aufrufen — auch nicht für sich selbst. Der `rolle === 'planer'`-Check muss im `SecurityFilterChain` oder im Service stattfinden, nicht nur im Frontend. **Das Frontend-Guard (kein Kebab-Menü für Mitglieder) ist kein Sicherheitskontrollpunkt.** - **Invite-Code Entropie.** Die Spec sagt "Regenerieren invalidiert alten Code" — gut. Aber wie wird der Code generiert? `UUIDv4` (122 Bit Entropie) ist ausreichend. Kürzere Codes (z.B. `X4K9-RZMQ` wie in der Spec) haben drastisch weniger Entropie und sind über Brute-Force angreifbar, wenn kein Rate-Limiting existiert. Wichtige Frage: Gibt es Rate-Limiting auf `POST /v1/invites/{code}/accept`? - **Invite-Link im Einlade-Panel.** Der `shareUrl` wird clientseitig im DOM gerendert — kein XSS-Risiko solange kein `{@html}` verwendet wird (Svelte escaped automatisch). Aber: Der Link landet möglicherweise in Browserlogs, Proxy-Logs oder Referrer-Headers. `Referrer-Policy: no-referrer` auf der App-Ebene prüfen. - **navigator.clipboard ist HTTPS-only.** Kai hat das bereits erwähnt. Aus Security-Perspektive: kein Fallback auf `execCommand` — das ist deprecated und hat eigene Security-Probleme. Lieber eine klare Fehlermeldung "Kopieren nur über HTTPS verfügbar" zeigen. - **Audit-Log-Pflicht.** Rollenänderung (`planer → mitglied` oder umgekehrt) und Mitglied-Entfernen sind privilegierte Aktionen. Beide müssen in `admin_audit_log` stehen: `actor_id`, `action`, `target_id`, `timestamp`. Das ist kein Nice-to-have — ohne Audit-Trail gibt es keine Nachvollziehbarkeit bei Disputes. - **Session-Invalidierung nach Entfernen.** Was passiert mit der aktiven Session des entfernten Mitglieds? Idealerweise wird die Session sofort invalidiert — sonst hat das entfernte Mitglied bis zum Session-Timeout noch Zugang. Spring Security hat hierfür `SessionRegistry` + `expireNow()`. ### Suggestions - Backend-Service-Signatur: immer `householdId` aus dem Session-Principal mitgeben, nie aus dem Request-Body oder -Pfad allein vertrauen. - Integration-Test: Planer aus Haushalt A versucht, Mitglied aus Haushalt B zu entfernen → muss `403` zurückgeben, nicht `404` (404 würde Existenz bestätigen, 403 tut das auch, aber konsistenter).
Author
Owner

🧪 QA Engineer

Questions & Observations

Fehlende Akzeptanzkriterien für wichtige Edge Cases:

  • Was passiert, wenn der Planer versucht sich selbst zu degradieren, aber noch ein anderer Planer existiert? Das sollte erlaubt sein — ist aber nirgends erwähnt. Nur wenn er der letzte Planer ist, soll es geblockt werden.
  • Gleichzeitige Rollenänderung. Zwei Planer ändern gleichzeitig die Rolle desselben Mitglieds — was gewinnt? Backend muss das definieren.
  • Member ist bereits entfernt wenn Bestätigung kommt. Race condition: zwei Planer wollen dasselbe Mitglied entfernen. Second DELETE sollte 404 oder 409 zurückgeben, nicht 500.
  • Invite abgelaufen. Das Panel zeigt expiresAt — aber was passiert wenn ein User einen abgelaufenen Link benutzt? Wird das Frontend informiert? Gibt es eine Fehlermeldung im POST /v1/invites/{code}/accept Response?
  • Haushalt hat 1 Mitglied (nur Planer). Kann der letzte Planer den Haushalt durch Selbst-Entfernen auflösen? Das ist kein definierter Zustand in der Spec.

Test-Coverage, die ich aufbauen würde:

Szenario Ebene
Grid rendert 3 Mitglieder + Invite-Kachel korrekt Component
Eigene Kachel zeigt "Du"-Badge, kein Kebab Component
Kebab-Dropdown öffnet/schließt (click-away, ESC) Component
Rolle ändern: optimistisches Update + Rollback bei Fehler Component
Entfernen-Dialog: öffnet, zeigt displayName, bestätigt Component
Backdrop-Klick schließt Dialog NICHT Component
Mitglied-Perspektive: keine Kebabs, keine Invite-Kachel Component
DELETE Member → 204 + Isolation (anderer Haushalt → 403) Integration
PATCH Role → 200 + letzter-Planer-Guard → 409/422 Integration
GET Invites → aktiver Code zurückgegeben Integration
J7 Journey: Einladen → Link kopieren → Mitglied entfernen E2E

Fehlende data-testid-Attribute in der Spec. Ich brauche testbare Selektoren für: member-grid, member-card, invite-card, kebab-btn, role-control, remove-dialog, confirm-remove-btn. Die Spec sollte diese benennen.

Suggestions

  • Akzeptanzkriterium ergänzen: "Letzter Planer kann seinen eigenen Planer-Status behalten aber nicht abgeben, solange kein anderer Planer existiert"
  • Akzeptanzkriterium ergänzen: "Bei netzwerkfehler während Entfernen: Dialog bleibt offen, Fehlermeldung erscheint, Kachel bleibt im Grid"
  • Playwright E2E für J7 anlegen — j7-manage-members.spec.ts — mindestens Happy Path: Einladen + Rolle ändern + Entfernen.
## 🧪 QA Engineer ### Questions & Observations **Fehlende Akzeptanzkriterien für wichtige Edge Cases:** - **Was passiert, wenn der Planer versucht sich selbst zu degradieren, aber noch ein anderer Planer existiert?** Das sollte erlaubt sein — ist aber nirgends erwähnt. Nur wenn er der *letzte* Planer ist, soll es geblockt werden. - **Gleichzeitige Rollenänderung.** Zwei Planer ändern gleichzeitig die Rolle desselben Mitglieds — was gewinnt? Backend muss das definieren. - **Member ist bereits entfernt wenn Bestätigung kommt.** Race condition: zwei Planer wollen dasselbe Mitglied entfernen. Second DELETE sollte `404` oder `409` zurückgeben, nicht `500`. - **Invite abgelaufen.** Das Panel zeigt `expiresAt` — aber was passiert wenn ein User einen abgelaufenen Link benutzt? Wird das Frontend informiert? Gibt es eine Fehlermeldung im `POST /v1/invites/{code}/accept` Response? - **Haushalt hat 1 Mitglied (nur Planer).** Kann der letzte Planer den Haushalt durch Selbst-Entfernen auflösen? Das ist kein definierter Zustand in der Spec. **Test-Coverage, die ich aufbauen würde:** | Szenario | Ebene | |---|---| | Grid rendert 3 Mitglieder + Invite-Kachel korrekt | Component | | Eigene Kachel zeigt "Du"-Badge, kein Kebab | Component | | Kebab-Dropdown öffnet/schließt (click-away, ESC) | Component | | Rolle ändern: optimistisches Update + Rollback bei Fehler | Component | | Entfernen-Dialog: öffnet, zeigt displayName, bestätigt | Component | | Backdrop-Klick schließt Dialog NICHT | Component | | Mitglied-Perspektive: keine Kebabs, keine Invite-Kachel | Component | | DELETE Member → 204 + Isolation (anderer Haushalt → 403) | Integration | | PATCH Role → 200 + letzter-Planer-Guard → 409/422 | Integration | | GET Invites → aktiver Code zurückgegeben | Integration | | J7 Journey: Einladen → Link kopieren → Mitglied entfernen | E2E | **Fehlende `data-testid`-Attribute in der Spec.** Ich brauche testbare Selektoren für: `member-grid`, `member-card`, `invite-card`, `kebab-btn`, `role-control`, `remove-dialog`, `confirm-remove-btn`. Die Spec sollte diese benennen. ### Suggestions - Akzeptanzkriterium ergänzen: "Letzter Planer kann seinen eigenen Planer-Status *behalten* aber nicht abgeben, solange kein anderer Planer existiert" - Akzeptanzkriterium ergänzen: "Bei netzwerkfehler während Entfernen: Dialog bleibt offen, Fehlermeldung erscheint, Kachel bleibt im Grid" - Playwright E2E für J7 anlegen — `j7-manage-members.spec.ts` — mindestens Happy Path: Einladen + Rolle ändern + Entfernen.
Author
Owner

🎨 Atlas — UI/UX Design

Questions & Observations

  • Typografie-Konsistenz. Button-Text im Dropdown und im Dialog muss exakt sein: font-size: 13px, font-weight: 500, letter-spacing: 0.04em, font-family: DM Sans. Das gilt für "Rolle ändern", "Entfernen" im Dropdown und "Abbrechen"/"Entfernen" im Dialog-Footer. Bitte nicht Tailwind's text-sm (14px) verwenden — hier ist 13px spezifiziert.

  • Touch-Target auf dem Kebab-Button. Der Kebab-Button ist mit top: 12px; right: 12px positioniert und muss mindestens 44×44px Tap-Fläche haben (WCAG 2.2 AA). Die visuelle Größe kann kleiner sein, aber das ::after-Pseudo-Element oder ein transparentes Padding muss den Tap-Bereich auf 44px ausdehnen.

  • Focus-Indicator. Alle interaktiven Elemente (Kacheln, Kebab, Dropdown-Items, Segmented-Control-Buttons, Dialog-Buttons) brauchen sichtbare Fokus-Indikatoren: outline: 2px solid var(--green-dark); outline-offset: 2px. Bitte nicht outline: none setzen. Keyboard-Navigation durch das Grid muss funktionieren (Tab → Kacheln, Enter → Kebab, Pfeiltasten im Dropdown).

  • Role-Badge Kontrastverhältnis. Die "Du"-Badge (--green-tint bg, --green-dark text): #E8F5EA auf #2E6E39 — das ist ~7:1, WCAG AA ✓. Das Mitglied-Badge (--blue-tint bg, --blue-dark text): #E6F1FB auf #0C447C — ca. 6.5:1, ✓. Beides passt.

  • Mobile Bottom Sheet Höhe. Der Dialog als Bottom Sheet auf Mobile: die Spezifikation sagt border-radius nur oben, kein max-width. Ich würde ergänzen: padding-bottom: env(safe-area-inset-bottom) damit der Inhalt auf Geräten mit Home-Indicator (iPhone) nicht abgeschnitten wird.

  • Einladekachel im leeren Grid. Was passiert wenn der Haushalt nur 1 Mitglied hat (der eingeloggte Planer) und sonst niemanden? Dann ist das Grid [eigene Kachel] [Einladekachel] — 2 von 4 Spalten gefüllt. Sollte die Einladekachel dann prominenter hervorgehoben werden (z.B. grid-column: span 2)?

  • Animation/Transition beim Entfernen. Die Spec sagt "fade-out" beim Entfernen einer Kachel. Ich empfehle opacity 200ms ease + transform: scale(0.95) 200ms ease kombiniert — reiner Opacity-Fade ohne Scale wirkt flach. Der Grid-Reflow danach soll ohne Transition passieren (CSS grid reflow ist nicht animierbar ohne Hacks).

Suggestions

  • --shadow-raised für das offene Dropdown ist richtig. Sicherstellen, dass z-index auf der Dropdown-Karte höher liegt als auf den benachbarten Kacheln — sonst wird das Dropdown von der nächsten Kachel überschnitten.
  • Segmented Control im Role-Edit: border-radius: --radius-md (6px) für den Container, die inneren Buttons haben border-radius: 0 (overflow: hidden on container macht das). Nicht --radius-full verwenden — das wäre eine Pill-Form, die nicht in das System passt.
  • Invite-Panel border-radius: --radius-xl (16px) passt zu den Kacheln. Der Abstand zwischen Grid und Panel: margin-top: 16px wie in der Spec. ✓
## 🎨 Atlas — UI/UX Design ### Questions & Observations - **Typografie-Konsistenz.** Button-Text im Dropdown und im Dialog muss exakt sein: `font-size: 13px`, `font-weight: 500`, `letter-spacing: 0.04em`, `font-family: DM Sans`. Das gilt für "Rolle ändern", "Entfernen" im Dropdown und "Abbrechen"/"Entfernen" im Dialog-Footer. Bitte nicht Tailwind's `text-sm` (14px) verwenden — hier ist 13px spezifiziert. - **Touch-Target auf dem Kebab-Button.** Der Kebab-Button ist mit `top: 12px; right: 12px` positioniert und muss mindestens 44×44px Tap-Fläche haben (WCAG 2.2 AA). Die visuelle Größe kann kleiner sein, aber das `::after`-Pseudo-Element oder ein transparentes Padding muss den Tap-Bereich auf 44px ausdehnen. - **Focus-Indicator.** Alle interaktiven Elemente (Kacheln, Kebab, Dropdown-Items, Segmented-Control-Buttons, Dialog-Buttons) brauchen sichtbare Fokus-Indikatoren: `outline: 2px solid var(--green-dark); outline-offset: 2px`. Bitte nicht `outline: none` setzen. Keyboard-Navigation durch das Grid muss funktionieren (Tab → Kacheln, Enter → Kebab, Pfeiltasten im Dropdown). - **Role-Badge Kontrastverhältnis.** Die "Du"-Badge (`--green-tint` bg, `--green-dark` text): `#E8F5EA` auf `#2E6E39` — das ist ~7:1, WCAG AA ✓. Das Mitglied-Badge (`--blue-tint` bg, `--blue-dark` text): `#E6F1FB` auf `#0C447C` — ca. 6.5:1, ✓. Beides passt. - **Mobile Bottom Sheet Höhe.** Der Dialog als Bottom Sheet auf Mobile: die Spezifikation sagt `border-radius` nur oben, kein `max-width`. Ich würde ergänzen: `padding-bottom: env(safe-area-inset-bottom)` damit der Inhalt auf Geräten mit Home-Indicator (iPhone) nicht abgeschnitten wird. - **Einladekachel im leeren Grid.** Was passiert wenn der Haushalt nur 1 Mitglied hat (der eingeloggte Planer) und sonst niemanden? Dann ist das Grid `[eigene Kachel] [Einladekachel]` — 2 von 4 Spalten gefüllt. Sollte die Einladekachel dann prominenter hervorgehoben werden (z.B. `grid-column: span 2`)? - **Animation/Transition beim Entfernen.** Die Spec sagt "fade-out" beim Entfernen einer Kachel. Ich empfehle `opacity 200ms ease` + `transform: scale(0.95) 200ms ease` kombiniert — reiner Opacity-Fade ohne Scale wirkt flach. Der Grid-Reflow danach soll ohne Transition passieren (CSS `grid` reflow ist nicht animierbar ohne Hacks). ### Suggestions - `--shadow-raised` für das offene Dropdown ist richtig. Sicherstellen, dass `z-index` auf der Dropdown-Karte höher liegt als auf den benachbarten Kacheln — sonst wird das Dropdown von der nächsten Kachel überschnitten. - Segmented Control im Role-Edit: `border-radius: --radius-md` (6px) für den Container, die inneren Buttons haben `border-radius: 0` (overflow: hidden on container macht das). Nicht `--radius-full` verwenden — das wäre eine Pill-Form, die nicht in das System passt. - Invite-Panel `border-radius: --radius-xl` (16px) passt zu den Kacheln. Der Abstand zwischen Grid und Panel: `margin-top: 16px` wie in der Spec. ✓
Author
Owner

🎨 Atlas — UI/UX Design · Designentscheidungen aus der Diskussion

Resolved

  1. Einladekachel span — Bleibt 1fr wie alle anderen Kacheln. Kein grid-column: span 2, auch wenn das Grid nur dünn besetzt ist.

  2. Avatar-Farbe der eigenen Kachel — Immer --green-dark, unabhängig von der Rolle des eingeloggten Users. Der grüne Rahmen (--green-light border) und das „Du"-Badge definieren die eigene Kachel — das Avatar-Grün gehört dazu, auch wenn die Rolle mitglied ist.

  3. BottomSheet — Wird als neue, wiederverwendbare BottomSheet.svelte-Systemkomponente gebaut. Nicht inline in den Remove-Dialog. Spezifikation sollte sie als eigenständiges Design-System-Element dokumentieren.

  4. Transition-Token für Card-Exit--transition-card-exit wird als offizieller Token im Design System angelegt (empfohlener Wert: opacity 200ms ease, transform 200ms ease). Kein ad-hoc CSS in der Komponente.

  5. SegmentedControl — Wird neu gebaut als SegmentedControl.svelte. Erstmalige Systemkomponente, entstanden in diesem Feature.


Insgesamt ist das Issue designseitig gut spezifiziert. Die fünf Punkte oben sind die einzigen Lücken, die vor der Implementierung entschieden werden mussten.

## 🎨 Atlas — UI/UX Design · Designentscheidungen aus der Diskussion ### Resolved 1. **Einladekachel span** — Bleibt `1fr` wie alle anderen Kacheln. Kein `grid-column: span 2`, auch wenn das Grid nur dünn besetzt ist. 2. **Avatar-Farbe der eigenen Kachel** — Immer `--green-dark`, unabhängig von der Rolle des eingeloggten Users. Der grüne Rahmen (`--green-light` border) und das „Du"-Badge definieren die eigene Kachel — das Avatar-Grün gehört dazu, auch wenn die Rolle `mitglied` ist. 3. **BottomSheet** — Wird als neue, wiederverwendbare `BottomSheet.svelte`-Systemkomponente gebaut. Nicht inline in den Remove-Dialog. Spezifikation sollte sie als eigenständiges Design-System-Element dokumentieren. 4. **Transition-Token für Card-Exit** — `--transition-card-exit` wird als offizieller Token im Design System angelegt (empfohlener Wert: `opacity 200ms ease, transform 200ms ease`). Kein ad-hoc CSS in der Komponente. 5. **SegmentedControl** — Wird neu gebaut als `SegmentedControl.svelte`. Erstmalige Systemkomponente, entstanden in diesem Feature. --- Insgesamt ist das Issue designseitig gut spezifiziert. Die fünf Punkte oben sind die einzigen Lücken, die vor der Implementierung entschieden werden mussten.
Author
Owner

⚛️ Kai — Frontend Engineer · Implementierungsentscheidungen aus der Diskussion

Resolved

  1. Backend-Strategie — Backend (DELETE, PATCH, GET invites) und Frontend werden im selben Ticket implementiert. Kein separater Branch, kein Warten.

  2. Toast-Komponente — Generisches Toast.svelte wird neu gebaut in src/lib/components/. API-Design berücksichtigt, dass UndoBar.svelte später einfach darauf aufsetzen kann (message + optionaler action-Prop via Snippet).

  3. Click-away browser-Guard — Kein App-Util vorhanden, kein Precedent im Codebase. Standard-Pattern: import { browser } from '$app/environment' + if (browser) inline im $effect der Kebab-Komponente.

  4. BottomSheet Props-API — Neue BottomSheet.svelte Systemkomponente (Atlas-Entscheidung) bekommt folgende API:

    let { open, onclose, title?, children }: {
      open: boolean;
      onclose: () => void;
      title?: string;
      children: Snippet;
    } = $props();
    
  5. PATCH/DELETE routing — Optimistische API-Calls (Rolle ändern, Mitglied entfernen) laufen über SvelteKit +server.ts als Proxy. Kein direkter Client-zu-Backend Call.


Technisch ist das Issue gut durchdacht. Die offenen Punkte oben sind die Vorentscheidungen, die ich vor dem ersten Commit brauchte.

## ⚛️ Kai — Frontend Engineer · Implementierungsentscheidungen aus der Diskussion ### Resolved 1. **Backend-Strategie** — Backend (DELETE, PATCH, GET invites) und Frontend werden im selben Ticket implementiert. Kein separater Branch, kein Warten. 2. **Toast-Komponente** — Generisches `Toast.svelte` wird neu gebaut in `src/lib/components/`. API-Design berücksichtigt, dass `UndoBar.svelte` später einfach darauf aufsetzen kann (message + optionaler action-Prop via Snippet). 3. **Click-away `browser`-Guard** — Kein App-Util vorhanden, kein Precedent im Codebase. Standard-Pattern: `import { browser } from '$app/environment'` + `if (browser)` inline im `$effect` der Kebab-Komponente. 4. **BottomSheet Props-API** — Neue `BottomSheet.svelte` Systemkomponente (Atlas-Entscheidung) bekommt folgende API: ```ts let { open, onclose, title?, children }: { open: boolean; onclose: () => void; title?: string; children: Snippet; } = $props(); ``` 5. **PATCH/DELETE routing** — Optimistische API-Calls (Rolle ändern, Mitglied entfernen) laufen über SvelteKit `+server.ts` als Proxy. Kein direkter Client-zu-Backend Call. --- Technisch ist das Issue gut durchdacht. Die offenen Punkte oben sind die Vorentscheidungen, die ich vor dem ersten Commit brauchte.
Author
Owner

🏗️ Backend Engineer — Spring Boot / PostgreSQL · Implementierungsentscheidungen aus der Diskussion

Resolved

  1. Einziger aktiver Invite-Code — Ein aktiver Code pro Haushalt. Abgesichert via Partial Unique Index: UNIQUE (household_id) WHERE invalidated_at IS NULL. Regenerieren setzt invalidated_at auf den alten Code und erstellt einen neuen.

  2. Guard-Response-Code409 Conflict für alle Business-Rule-Verletzungen: Planer versucht sich selbst zu entfernen, letzter Planer versucht sich zu degradieren. Klare Fehlermeldung im Response-Body.

  3. Session-Invalidierung nach Entfernen — Out of scope für dieses Ticket. Separates Issue angelegt: #56.

  4. Audit-Log — Out of scope für dieses Ticket. Separates Issue angelegt: #57. Beide Aktionen (Rollenänderung + Entfernen) werden dort nachgezogen.

  5. schema.d.ts Update — Backend schreibt die TypeScript-Typen für die drei neuen Endpoints (DELETE, PATCH, GET invites) als Teil der Implementierung in diesem Ticket. Kai bekommt die fertigen Typen, bevor er mit der Frontend-Arbeit beginnt.


Das Issue ist backend-seitig gut durchdacht. Die offenen Punkte oben sind die Vorentscheidungen für eine saubere Implementierung ohne Scope-Creep.

## 🏗️ Backend Engineer — Spring Boot / PostgreSQL · Implementierungsentscheidungen aus der Diskussion ### Resolved 1. **Einziger aktiver Invite-Code** — Ein aktiver Code pro Haushalt. Abgesichert via Partial Unique Index: `UNIQUE (household_id) WHERE invalidated_at IS NULL`. Regenerieren setzt `invalidated_at` auf den alten Code und erstellt einen neuen. 2. **Guard-Response-Code** — `409 Conflict` für alle Business-Rule-Verletzungen: Planer versucht sich selbst zu entfernen, letzter Planer versucht sich zu degradieren. Klare Fehlermeldung im Response-Body. 3. **Session-Invalidierung nach Entfernen** — Out of scope für dieses Ticket. Separates Issue angelegt: #56. 4. **Audit-Log** — Out of scope für dieses Ticket. Separates Issue angelegt: #57. Beide Aktionen (Rollenänderung + Entfernen) werden dort nachgezogen. 5. **`schema.d.ts` Update** — Backend schreibt die TypeScript-Typen für die drei neuen Endpoints (DELETE, PATCH, GET invites) als Teil der Implementierung in diesem Ticket. Kai bekommt die fertigen Typen, bevor er mit der Frontend-Arbeit beginnt. --- Das Issue ist backend-seitig gut durchdacht. Die offenen Punkte oben sind die Vorentscheidungen für eine saubere Implementierung ohne Scope-Creep.
Author
Owner

Implementation complete — branch feat/issue-48-members-kachel

All 21 tasks done. Red/green/refactor TDD throughout.


Backend (commits d1e4b6c, b04f2c5)

Task Details
Migration V026 invalidated_at timestamptz + partial unique index UNIQUE (household_id) WHERE invalidated_at IS NULL on household_invite
Entity + Repos HouseholdInvite.invalidatedAt, findByHouseholdIdAndInvalidatedAtIsNull, findByHouseholdIdAndUserId, countByHouseholdIdAndRole
ChangeRoleRequest DTO role field with @Pattern(planner|member) validation
getActiveInvite() Returns non-expired, non-invalidated invite or empty
createInvite() Invalidates existing active invite before creating new (regenerate)
removeMember() Guard: can't remove self (409), IDOR check (404 if not in household)
changeMemberRole() Guard: last-planner-cannot-degrade (409), idempotent if role unchanged
Controller GET /v1/households/mine/invites, DELETE /v1/households/mine/members/{userId}, PATCH /v1/households/mine/members/{userId}
schema.d.ts All 3 new endpoints typed with paths + operations

Backend: 328 tests


Frontend (commit 1b5704c)

Component / File Details
Toast.svelte New system component — message + dismiss
SegmentedControl.svelte New system component — options array, value, onchange
members/+page.server.ts Parallel load: members + active invite, exposes currentUserId
members/[userId]/+server.ts DELETE + PATCH proxy to backend
members/invites/+server.ts POST proxy (regenerate invite)
MemberCard.svelte Tile with avatar initials, "Du"-badge, kebab menu, inline SegmentedControl for role edit, click-away + ESC close
RemoveDialog.svelte Desktop modal + mobile BottomSheet — backdrop does NOT close
InviteCard.svelte Dashed invite tile (planner-only)
InvitePanel.svelte Invite link + copy button ("Kopiert ✓" 2s) + regenerate
MemberGrid.svelte 4-col desktop / 2-col mobile, own card first → joinedAt ASC → invite card last
members/+page.svelte Optimistic role update (rollback + toast on error), optimistic remove (rollback + toast on error)

Frontend: 78 test files, 768 tests | Type check: 0 new errors


Acceptance criteria

  • Kachelgrid rendert alle Mitglieder korrekt (Desktop + Mobile)
  • Eigene Kachel hat grünen Rahmen und "Du"-Badge, kein Kebab
  • Kebab öffnet Dropdown mit "Rolle ändern" und "Entfernen"
  • Rolle ändern funktioniert inline mit optimistischem Update
  • Entfernen erfordert Bestätigungsdialog mit Mitgliedsnamen
  • Planer kann sich selbst nicht entfernen (409 Conflict backend guard)
  • Letzter Planer kann nicht degradiert werden (409 Conflict backend guard)
  • Einlade-Panel zeigt generierten Link mit Copy und Regenerieren
  • Mitglieder (rolle=mitglied) sehen read-only Ansicht ohne Aktionen
  • Backend-Lücken (DELETE, PATCH, GET invites) sind implementiert

Next step: /review-pr or open PR from feat/issue-48-members-kachelmaster

## ✅ Implementation complete — branch `feat/issue-48-members-kachel` All 21 tasks done. Red/green/refactor TDD throughout. --- ### Backend (commits `d1e4b6c`, `b04f2c5`) | Task | Details | |---|---| | Migration V026 | `invalidated_at timestamptz` + partial unique index `UNIQUE (household_id) WHERE invalidated_at IS NULL` on `household_invite` | | Entity + Repos | `HouseholdInvite.invalidatedAt`, `findByHouseholdIdAndInvalidatedAtIsNull`, `findByHouseholdIdAndUserId`, `countByHouseholdIdAndRole` | | `ChangeRoleRequest` DTO | `role` field with `@Pattern(planner\|member)` validation | | `getActiveInvite()` | Returns non-expired, non-invalidated invite or empty | | `createInvite()` | Invalidates existing active invite before creating new (regenerate) | | `removeMember()` | Guard: can't remove self (409), IDOR check (404 if not in household) | | `changeMemberRole()` | Guard: last-planner-cannot-degrade (409), idempotent if role unchanged | | Controller | `GET /v1/households/mine/invites`, `DELETE /v1/households/mine/members/{userId}`, `PATCH /v1/households/mine/members/{userId}` | | `schema.d.ts` | All 3 new endpoints typed with paths + operations | **Backend: 328 tests ✅** --- ### Frontend (commit `1b5704c`) | Component / File | Details | |---|---| | `Toast.svelte` | New system component — message + dismiss | | `SegmentedControl.svelte` | New system component — options array, value, onchange | | `members/+page.server.ts` | Parallel load: members + active invite, exposes `currentUserId` | | `members/[userId]/+server.ts` | DELETE + PATCH proxy to backend | | `members/invites/+server.ts` | POST proxy (regenerate invite) | | `MemberCard.svelte` | Tile with avatar initials, "Du"-badge, kebab menu, inline `SegmentedControl` for role edit, click-away + ESC close | | `RemoveDialog.svelte` | Desktop modal + mobile `BottomSheet` — backdrop does NOT close | | `InviteCard.svelte` | Dashed invite tile (planner-only) | | `InvitePanel.svelte` | Invite link + copy button ("Kopiert ✓" 2s) + regenerate | | `MemberGrid.svelte` | 4-col desktop / 2-col mobile, own card first → joinedAt ASC → invite card last | | `members/+page.svelte` | Optimistic role update (rollback + toast on error), optimistic remove (rollback + toast on error) | **Frontend: 78 test files, 768 tests ✅ | Type check: 0 new errors ✅** --- ### Acceptance criteria - [x] Kachelgrid rendert alle Mitglieder korrekt (Desktop + Mobile) - [x] Eigene Kachel hat grünen Rahmen und "Du"-Badge, kein Kebab - [x] Kebab öffnet Dropdown mit "Rolle ändern" und "Entfernen" - [x] Rolle ändern funktioniert inline mit optimistischem Update - [x] Entfernen erfordert Bestätigungsdialog mit Mitgliedsnamen - [x] Planer kann sich selbst nicht entfernen (409 Conflict backend guard) - [x] Letzter Planer kann nicht degradiert werden (409 Conflict backend guard) - [x] Einlade-Panel zeigt generierten Link mit Copy und Regenerieren - [x] Mitglieder (rolle=mitglied) sehen read-only Ansicht ohne Aktionen - [x] Backend-Lücken (DELETE, PATCH, GET invites) sind implementiert **Next step:** `/review-pr` or open PR from `feat/issue-48-members-kachel` → `master`
Sign in to join this conversation.