mitbringsl/AGENTS.md
Tronax 37cbb816e5
Feature: Bring-style emoji & category icons for items
Every shopping item now gets a visual like in Bring: a product emoji on
a softly tinted category tile.

- ItemVisuals.kt: pure client-side classifier. German dictionary
  (~180 entries) maps item names to emoji + one of 13 categories
  (produce, drinks, bakery, dairy, meat&fish, frozen, pantry, sweets,
  household, drugstore, baby, pet, other), each with an accent color.
  Longest-keyword-first matching so compounds resolve to their most
  specific entry (Wassermelone is produce, not a drink); juice
  compounds (Apfelsaft, Orangensaft, ...) are listed explicitly because
  their fruit prefix is longer than 'saft'. Unknown items fall back to
  the generic cart on neutral grey. Works fully offline, nothing leaves
  the device.
- ListDetailScreen: emoji tile in every item row, emoji in the
  autocomplete suggestions dropdown, and a live emoji preview as the
  text field's leading icon while typing.
- ItemVisualsTest: longest-match priority, case/whitespace
  insensitivity, fallbacks, category coverage.

Verified: assembleDebug + unit tests green (JDK 21).
2026-08-22 10:16:37 +02:00

331 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# AGENTS.md Kontext & Fortschritt für KI-Agenten
> Diese Datei hält den Projekt-Stand und die Architekturentscheidungen fest, damit
> ein Agent (z.B. zuhause) sofort weiterarbeiten kann. Sie ist Teil des Repos.
## Projekt: Mitbringsl
Local-First Einkaufslisten-App (Bring-Alternative, werbefrei).
**Backend:** Go (net/http, pgx, golang-migrate, go-oidc) als Docker-Container.
**App:** Kotlin + Jetpack Compose, Room, Hilt, WorkManager, offline-first.
**Deployment:** docker-compose (Caddy + backend + migrate + postgres:16), auto-HTTPS.
Vollständiger Plan liegt als genehmigtem Plan zugrunde (siehe Abschnitt "Roadmap").
---
## Getroffene Architekturentscheidungen (verbindlich)
- **Backend-Sprache:** Go (nicht Rust).
- **DB:** PostgreSQL (`postgres:16-alpine`), Extensions `pgcrypto` + `pg_trgm`.
- **Sync:** Local-First, **keine Datenverluste** Append-only `op_log` (SOURCE OF TRUTH),
`items`/`lists` sind Projektionen; Konfliktlösung per LWW-Register `(hlc_ts, client_id)`
+ Tombstones (`deleted_at`). HLC = Hybrid Logical Clock.
- Push: `POST /api/lists/{id}/ops` (idempotent via `UNIQUE(client_id, client_seq)`).
- Pull: `GET /api/lists/{id}/ops?since={seq}`.
- **Auth:**
- Eigene User: E-Mail/Passwort, **Argon2id** (PHC-Format).
- OIDC: **Google** + **Generic OIDC**. Die **App macht den Code+PKCE-Flow selbst**
und schickt nur das `id_token` ans Backend (`POST /auth/oidc`).
Backend verifiziert via `github.com/coreos/go-oidc/v3` (Signatur gegen JWKS,
iss/aud/exp) und stellt **eigene opaque Session-Tokens** aus
(`sessions`-Tabelle, SHA-256-Hash gespeichert).
- **AppAuth-Android bewusst NICHT verwendet** (seit 2021 verwaist). Stattdessen:
Google via **Credential Manager**, Generic OIDC via **Custom Tabs + eigenes PKCE**.
- **Vorschläge:** aggregiert aus Item-Namen aller User (`item_names`-Tabelle,
pg_trgm fuzzy search). Endpoint `GET /api/suggestions?q=`.
- **Post-MVP (umgesetzt):**
- **Geteilte Listen per Invite-Code:** `lists.invite_code` (8 Zeichen, UNIQUE,
Migration 000002). `POST /api/lists/{id}/invite` (Owner/Member lesen Code,
wird lazy generiert), `POST /api/lists/join` (beitreten als `member`).
`GET /api/lists` + `GET /api/lists/{id}` + Push/Pull-Ops prüfen Membership
(owner ODER `list_members`-Eintrag). Kein Leave/Revoke-Endpoint, kein
Rate-Limiting auf join (bekannt, bewusst offengehalten).
- **Account optional / Local-Only-Mode:** App startet ohne Login direkt in die
Listen (`local_user`-Fallback im `SessionManager`); Sync/Account ist optionaler
Einstieg. Teilen/Beitreten erfordert aktive Server-Verbindung (App-gated).
- **Self-Hosted Server-URL:** konfigurierbar in der App (Auth-Screen), umgesetzt
via `DynamicBaseUrlInterceptor` (schreibt scheme/host/port pro Request um).
- **Server-getriebene Auth-Discovery:** Öffentlicher Endpoint `GET /api/config`
verrät der App vor dem Login die verfügbaren Methoden (`password_enabled`,
OIDC-Provider mit `display_name`, z.B. „Authentik"). `AUTH_PASSWORD_ENABLED=false`
→ Register/Login per Passwort → 403 (OIDC-only-Enforcement). App-Login zeigt
nur noch die angebotenen Methoden (ConnectView → LoginView).
- **Profil + Settings-Screen:** `GET/PUT /api/me` (aktuell `display_name`),
`SettingsScreen` mit Account & Sync, Profil, Theme (System/Hell/Dunkel,
persistiert in SessionManager), „Lokale Daten zurücksetzen" (nur Room-Tabellen,
Serverdaten bleiben) und About.
- **Kritische Sync-/Datensicherheits-Fixes (Android):** SyncWorker pullt jetzt
**alle** getrackten Listen (nicht nur welche mit pending Outbox vorher kamen
Remote-Änderungen stiller Listen nie an); lokale HLC tickt mit jedem eingehenden
Server-Op (`tick(op.hlcTs)` → LWW korrekt bei Uhr-Drift); `list_create` in
`applyServerOpProjection` ergänzt; Projektions-Write + op_log-Insert in einer
Room-Transaktion; `clientId`/`clientSeq` persistent in SessionManager;
**kein** `fallbackToDestructiveMigration` mehr (Source of Truth darf nie
gewispt werden); ProGuard-Keep-Rules für kotlinx.serialization/Retrofit
(R8-Releases crashten sonst); `network_security_config` (Cleartext nur für
localhost/10.0.2.2); WorkManager-Default-Initializer deaktiviert (Hilt).
- **Authentik-OIDC:** läuft über den bestehenden Generic-OIDC-Provider
(`provider: "generic"`); die App erlaubt manuelle id_token-Eingabe.
- **MVP-Scope:** keine Echtzeit-Push (nur Periodic-Pull 15 min + Pull-on-Online);
keine Web-UI.
---
## Technologie-Stacks (final)
### Backend
| Bereich | Wahl |
|---|---|
| Go-Version | **1.26** (`go.mod` hat `go 1.26`; Docker-Image `golang:1.26-alpine`) |
| HTTP | stdlib `net/http` (Go 1.22+ Routing mit Methoden + Path-Vars) |
| DB-Driver | `github.com/jackc/pgx/v5` (pgxpool) |
| Migrationen | `github.com/golang-migrate/migrate/v4` + `source/iofs` (eingebettet via `embed.FS`) |
| OIDC | `github.com/coreos/go-oidc/v3` + `golang.org/x/oauth2` |
| Passwörter | Argon2id (`golang.org/x/crypto/argon2`) |
| Sessions | opaque Tokens (32 B base64url), `sessions`-Tabelle, SHA-256-Hash |
| Config | `github.com/caarlos0/env/v11` (struct-tag env) |
| Logging | `log/slog` mit `NewJSONHandler` → stdout |
| Docker | Multi-Stage, `CGO_ENABLED=0`, `gcr.io/distroless/static-debian12:nonroot` |
| Queries | **direkt mit pgx** (sqlc wurde aus Skalierbarkeit bewusst auf später verschoben) |
### Android (noch nicht begonnen)
| Bereich | Wahl |
|---|---|
| Build | Kotlin DSL + `libs.versions.toml`, Single-Module `:app` |
| Kotlin/AGP/Gradle | Kotlin 2.x (K2), AGP 8.9+, Gradle 8.11+, JDK 17 |
| UI | Compose BOM (2025.x) + Material 3 |
| Arch | MVVM + `ViewModel` + `StateFlow`, UDF |
| DB | Room (KSP), Source of Truth via `Flow` |
| Netzwerk | Retrofit + OkHttp + kotlinx.serialization |
| Sync | WorkManager `CoroutineWorker` (Outbox-Drain + Cursor-Pull) |
| DI | Hilt (KSP) + `hilt-navigation-compose` + `hilt-work` |
| IDs | Client-seitige UUIDs |
| Auth | Google: Credential Manager; Generic OIDC: Custom Tabs + PKCE selbst |
| Suche | `OutlinedTextField` + `DropdownMenu`, Room-FTS, Server-Fallback |
| SDK | minSdk 26, compile/target 36 |
---
## Repository-Struktur
```
mitbringsl/
├── AGENTS.md ← DIES DATEI
├── README.md
├── .gitignore
├── backend/
│ ├── cmd/
│ │ ├── server/main.go # HTTP-Server-Einstieg (verdrahtet cfg/logger/pool/api)
│ │ └── migrate/main.go # Migrations-Runner (iofs-embedded)
│ ├── internal/
│ │ ├── config/config.go # Config (caarlos0/env)
│ │ ├── logging/logging.go # slog JSON-Setup
│ │ ├── store/db.go # pgxpool-Setup
│ │ ├── auth/ # PHASE B Password + Session + OIDC
│ │ │ ├── password.go # Argon2id im PHC-Format (HashPassword/VerifyPassword)
│ │ │ ├── password_test.go # PHC-Roundtrip-Tests
│ │ │ ├── user.go # UserStore: CreateUser/GetByEmail/GetByID/GetByOIDCSubject/CreateOIDCUser/UpdateDisplayName
│ │ │ ├── session.go # SessionStore: opaque Tokens, SHA-256-Hash, Create/Lookup/Revoke
│ │ │ ├── oidc.go # OIDCService: id_token-Verifikation (JWKS/iss/aud/exp), Provider-Caching
│ │ │ └── pgcode.go # isUniqueViolation (SQLSTATE 23505)
│ │ ├── sync/ # PHASE C Hybrid Logical Clock
│ │ │ ├── hlc.go # HLC: Tick/Now/After, wall_ms<<16|counter, global mutex
│ │ │ └── hlc_test.go # HLC Unit-Tests (monoton, kausal, keine Duplikate)
│ │ ├── store/ # PHASE C Store-Schicht
│ │ │ ├── db.go # pgxpool-Setup
│ │ │ ├── opstore.go # AppendOps (idempotent, LWW-Projektion), PullOps (cursor)
│ │ │ ├── liststore.go # CreateList/GetLists/GetList/GetInviteCode/JoinByInviteCode
│ │ │ ├── liststore_test.go # Invite/Join/Membership-Integrationstests (brauchen TEST_DATABASE_URL)
│ │ │ ├── itemstore.go # GetItems
│ │ │ └── suggeststore.go # Search (pg_trgm fuzzy)
│ │ └── httpapi/
│ │ ├── api.go # API-Objekt + Router (alle Routen aktiv)
│ │ ├── api_test.go # E2E-HTTP-Tests: Share/Join/Sync + 401-Gating (TEST_DATABASE_URL)
│ │ ├── render.go # JSON-Render + Problem + Fehler-Sentinale + decodeJSON
│ │ ├── middleware.go # requestID/logging/recover/cors + Chain
│ │ ├── health.go # /healthz + /readyz
│ │ ├── auth.go # Register/Login/Logout/OIDC-Handler + RequireAuth-Middleware (403 wenn Passwort-Login deaktiviert)
│ │ ├── config.go # GET /api/config (öffentlich): verfügbare Auth-Methoden für die App
│ │ ├── me.go # GET/PUT /api/me (eigenes Profil, display_name)
│ │ ├── lists.go # GET/POST /api/lists, GET /api/lists/{id}, POST /invite, POST /join
│ │ ├── ops.go # POST/GET /api/lists/{id}/ops (Push/Pull, Membership-Check)
│ │ └── suggest.go # GET /api/suggestions?q=
│ ├── migrations/
│ │ ├── embed.go # //go:embed *.sql
│ │ ├── 000001_init_schema.up.sql # users/sessions/lists/list_members/items/op_log/item_names
│ │ ├── 000001_init_schema.down.sql
│ │ ├── 000002_add_invite_code.up.sql # lists.invite_code UNIQUE + Backfill owner→list_members
│ │ └── 000002_add_invite_code.down.sql
│ ├── Dockerfile # Multi-Stage, baut server + migrate
│ ├── .dockerignore
│ ├── go.mod / go.sum
├── deploy/
│ ├── docker-compose.yml # caddy + backend + migrate + db (CADDY_HTTP/HTTPS_PORT)
│ ├── Caddyfile # standalone: auto-HTTPS (Let's Encrypt)
│ ├── Caddyfile.behind-proxy # hinter externem Reverse Proxy: auto_https off, trusted_proxies
│ ├── .env.example # alle env-Vars dokumentiert inkl. Caddy-Ports
│ └── db/init/001_extensions.sql # CREATE EXTENSION pgcrypto, pg_trgm
├── docs/
│ ├── ARCHITECTURE.md
│ ├── SYNC.md # HLC/LWW/op_log
│ └── API.md # REST Specs (inkl. join/invite)
└── android/ # Single-Module :app (Compose M3, Room, Hilt)
└── app/src/main/java/com/example/mitbringsl/
├── data/auth/ # SessionManager (Local-User-Fallback, Server-URL, clientId/clientSeq, Theme-Pref)
├── data/local/ # Room DB (lists, items, op_log), DAOs (LWW upsert)
├── data/remote/ # Retrofit-API, DTOs, DynamicBaseUrlInterceptor, AuthInterceptor
├── data/repository/ # ShoppingRepository, AuthRepository
├── data/sync/ # HybridLogicalClock, SyncManager, SyncWorker (pullt ALLE Listen, HLC-Tick pro Server-Op)
├── di/ # Hilt Modules
├── ui/auth/ # AuthScreen (ConnectView: Server-URL + /api/config-Discovery, dann LoginView)
├── ui/lists/ # ListsScreen/ViewModel (Join-Dialog, Offline-Banner, Settings-Zahnrad)
├── ui/detail/ # ListDetailScreen/ViewModel (Share-Button, Autocomplete), ItemVisuals (Emoji-/Kategorie-Icons)
├── ui/settings/ # SettingsScreen/ViewModel (Account, Profil, Theme, Reset, About)
└── util/ # NetworkMonitor
```
---
## Roadmap / Fortschritt
Legende: ✅ erledigt · 🚧 in Arbeit · ⬜ offen
- ✅ Repo-Struktur + `.gitignore` + `README.md` + `git init` (Branch `main`)
-**Phase A Backend-Fundament:** Config, slog, pgxpool, `/healthz`+`/readyz`, Server-Main.
-**Phase A Migrationen:** vollständiges Init-Schema (up+down) + `migrate`-Binary (iofs).
-**Phase A Docker:** Dockerfile (Go 1.26 → distroless nonroot), docker-compose
(caddy/backend/migrate/db), Caddyfile, `.env.example`.
**Verifiziert:** Image baut, beide Binaries laufen im Container (Smoke-Test OK).
-**Phase B Auth (Password):** Argon2id im PHC-Format + `UserStore` (Create/GetByEmail/GetByID)
+ `SessionStore` (opaque Tokens, SHA-256-Hash, Create/Lookup/Revoke) + Handler
`Register`/`Login`/`Logout` + `RequireAuth`-Middleware.
**Verifiziert:** `go test ./internal/auth/...` grün, E2E-Smoke-Test gegen echtes
PostgreSQL via Docker (Register/Login/Logout/Duplicate/Short-PW/Wrong-PW alle korrekt).
-**Phase B OIDC:** `OIDCService` (go-oidc v3, JWKS-Signatur, iss/aud/exp, Provider-Caching)
+ `POST /auth/oidc` (find-or-create User via `(oidc_issuer, oidc_subject)`, reuses `issueSession`)
+ Config-Validierung (enabled → client_id/issuer Pflicht).
**Verifiziert:** E2E-Flow gegen lokalen Mock-IdP (Discovery → JWKS → signiertes id_token
→ User angelegt → 2. Login findet gleichen User → tampered Token → 401). Alle Fehlerpfade
geprüft (disabled→400, unknown provider→400, missing fields→400, invalid→401).
-**Phase C Sync-Kern:** HLC (`internal/sync/hlc.go`), `op_log`-Append idempotent mit
LWW-Projektion (items/lists) + Tombstones. `internal/store/opstore.go`.
**Verifiziert:** `go build/vet/test ./...` grün. HLC-Tests (monoton, kausal, keine Duplikate).
-**Phase C Endpoints:** `GET/POST /api/lists`, `GET /api/lists/{id}`,
`POST/GET /api/lists/{id}/ops` (Push idempotent + Pull mit Cursor `?since=`).
-**Phase C Suggestions:** `suggeststore.go` (pg_trgm fuzzy), `GET /api/suggestions?q=`,
`upsertItemName` in Projektion (innerhalb Push-Transaktion).
-**Phase C Caddy:** `Caddyfile.behind-proxy` (auto_https off, trusted_proxies),
`CADDY_HTTP_PORT`/`CADDY_HTTPS_PORT` in docker-compose.
-**Phase D Android-Fundament:** Gradle (Kotlin DSL, Version Catalog, Hilt/KSP, Compose BOM, Room, Retrofit, WorkManager), Theme, Nav.
**Verifiziert:** `./gradlew assembleDebug` und `./gradlew test` erfolgreich.
-**Phase D Repository + Retrofit-API + DTOs:** Room DB (`lists`, `items`, `op_log`), DAOs (LWW upsert), `MitbringslApi`, AuthInterceptor, Hilt Modules (`DatabaseModule`, `NetworkModule`, `RepositoryModule`).
-**Phase D Login-Screen:** `AuthScreen` (E-Mail/Passwort Login + Registrierung), `AuthViewModel`, `AuthRepository`, `SessionManager`.
-**Phase E SyncEngine:** `HybridLogicalClock` (client-seitig), `SyncWorker` (`CoroutineWorker` Outbox Drain + Server Cursor Pull), `SyncManager` (15 min periodisch + Sofort-Sync), `ShoppingRepository` (local-first mutations via Room + `op_log`).
-**Phase E Listen-Übersicht + Detail + AddItemBar:** `ListsScreen` & `ListsViewModel`, `ListDetailScreen` & `ListDetailViewModel` (Sectioning erledigt/offen, Autocomplete Suggestions dropdown), Compose Navigation 3.
-**Phase F Polish:** `NetworkMonitor` (ConnectivityManager StateFlow), Offline-Banner in `ListsScreen`, Material 3 Empty States.
-**Phase F README + docs:** `README.md` Quickstart, `docs/ARCHITECTURE.md`,
`docs/SYNC.md` (HLC/LWW/op_log), `docs/API.md` (REST Specs).
-**Post-MVP Account optional:** App startet ohne Login direkt in die Listen
(Local-Only-Default), Sync/Account optional.
-**Post-MVP Self-Hosted Server-URL + Authentik:** Server-URL in der App
konfigurierbar (`DynamicBaseUrlInterceptor`), Authentik über Generic-OIDC.
-**Post-MVP Geteilte Listen:** Invite-Codes (Migration 000002), Join/Invite-
Endpoints, Membership-Checks in `GetLists`/`GetList`/Push/Pull.
-**Post-MVP Tests für Share/Join:** `liststore_test.go` (Store-Level) +
`api_test.go` (E2E über HTTP): Invite/Join/Idempotenz/Rollen/Access-Control
(Fremde → 404 auf allen Listen-Endpoints), Cross-Member-Sync via op_log,
401-Gating aller geschützten Routen. Beide Dateien skippen ohne
`TEST_DATABASE_URL` (Docker-Rezept im Dateikopf).
-**Post-MVP Kritische Android-Fixes (Sync/Build/Data-Safety):** SyncWorker
pullt alle getrackten Listen, HLC-Tick pro Server-Op, Room-Transaktionen um
Projektion+Outbox, persistente clientId/clientSeq, kein destructive Migration,
ProGuard-Rules, Network-Security-Config, Hilt-WorkManager-Init.
-**Post-MVP Auth-Discovery + OIDC-only:** `GET /api/config` (öffentlich),
`AUTH_PASSWORD_ENABLED` (403-Enforcement), `OIDC_GENERIC_DISPLAY_NAME`,
App-Connect-Flow (Server-URL → Methoden-Discovery → LoginView).
-**Post-MVP Settings-Screen + Profil:** `GET/PUT /api/me`
(`UserStore.UpdateDisplayName`), `SettingsScreen`/`SettingsViewModel`
(Account&Sync, Profil, Theme, Reset, About), Theme-Pref in SessionManager.
-**Post-MVP Produkt-Icons (Bring-Style):** `ui/detail/ItemVisuals.kt`
rein clientseitiger Classifier: deutsches Wörterbuch (~180 Einträge) mappt
Item-Namen auf **Emoji + Kategorie** (13 Kategorien mit Akzentfarbe).
Longest-Keyword-Match („Apfelsaft" → Getränk, nicht Obst; Saft-Komposita
explizit gelistet), unbekannte Items → 🛒/Sonstiges. UI: Icon-Kachel in
ItemRow, Icons im Autocomplete-Dropdown, Live-Emoji im Eingabefeld
(`leadingIcon`). Unit-Tests in `ItemVisualsTest.kt`.
-**Post-MVP Build hinter restriktiven Proxies:** foojay-Resolver &
jvmToolchain auskommentiert, Gradle-Wrapper 9.6.1, `.ssl-work/` ignoriert.
### Wo genau weitermachen?
**MVP + Post-MVP-Features sind abgeschlossen.** Das Backend ist feature-complete
für den aktuellen Scope. Offene Punkte, geordnet nach Nutzen:
1. **Join-Rate-Limiting:** `POST /api/lists/join` hat kein Rate-Limit; 8-Zeichen-
Code ist brute-force-bar (62⁸ ≈ 2×10¹⁴, aber trotzdem). Z.B. pro Session/IP
drosseln oder fehlerhafte Joins verzögern.
2. **Leave-List / Member-Removal / Code-Revocation:** Es gibt keinen Endpoint,
eine Liste zu verlassen, Mitglieder zu entfernen oder einen Invite-Code zu
rotieren. `list_members`-Rollen (`owner`/`member`) werden bisher kaum genutzt.
3. **Invite-Code-Optimierung:** Codes sind UUID-Präfixe (`uuid[:8]`), nicht
kollisionsresistent geprüft (UNIQUE-Constraint fängt es, aber CreateList kann
dann fehlschlagen). Besser: kryptografisches Alphabet ohne Verwechslungsbuchstaben.
4. **Tests für neuere Endpoints:** `GET /api/config`, `GET/PUT /api/me` und das
`AUTH_PASSWORD_ENABLED=false`-Verhalten (403) haben noch keine Testabdeckung
das Test-Setup dafür steht in `api_test.go` bereit.
5. **Echtzeit-Push (langfristig):** Periodic-Pull 15 min ist MVP; SSE/WebSocket
für sofortige Updates wäre der nächste Schritt.
6. **Android-Tests:** UI/ViewModel-Tests fehlen fast komplett (nur 2 Stock-Tests).
---
## Wichtige technische Notizen / Fallstricke (bereits gelöst)
- **golang-migrate `iofs`-Pfad:** Der Import ist
`github.com/golang-migrate/migrate/v4/source/iofs` (MIT `/source/`).
Die Pfade ohne `/source/` gibt es nicht mehr → Build-Fehler.
- **Go-Version:** `pgx/v5 v5.10.0` braucht Go ≥ 1.25, deshalb Go 1.26
(lokal + Docker-Image `golang:1.26-alpine`). `go.mod` hat `go 1.26`.
- **Docker-Build auf Windows/Docker Desktop:** `--network host` im Build-Container
funktioniert NICHT (WSL2-NAT). Für Tests: echtes `docker network create` + Container-Namen nutzen.
- **Compose braucht zwingend `POSTGRES_PASSWORD`** (`.env` oder Env), da
`db.environment` mit `${POSTGRES_PASSWORD:?...}`-Assertion vor dem Build validiert wird.
- **Healthcheck im `backend`-Service wurde entfernt**, weil distroless/static kein
`wget`/`curl` enthält. Später: eigenes Binary, das `/healthz` per Go-HTTP prüft,
oder `healthcheck` über Caddy/extern.
- **SQL-Queries werden direkt mit pgx geschrieben** (kein sqlc im MVP), da sqlc lokal
nicht installiert ist und ein extra Code-Gen-Schritt nötig wäre. Bei Bedarf später
problemlos nachrüstbar.
---
## Build- & Test-Befehle
```bash
# Backend lokal bauen
cd backend && go build ./... && go vet ./...
# Unit-Tests (ohne DB)
cd backend && go test ./...
# Integrationstests (Store + E2E-HTTP) gegen Docker-PostgreSQL:
docker run -d --name mitbringsl-test-pg -e POSTGRES_USER=app \
-e POSTGRES_PASSWORD=testpw -e POSTGRES_DB=appdb -p 55432:5432 postgres:16-alpine
cd backend && TEST_DATABASE_URL="postgres://app:testpw@localhost:55432/appdb?sslmode=disable" \
go test ./...
docker rm -f mitbringsl-test-pg # danach aufräumen
# Backend-Image bauen
cd backend && docker build -t mitbringsl-backend:test .
# Komplettes Stack starten (braucht deploy/.env)
cd deploy && cp .env.example .env && docker compose up -d --build
# Android bauen
cd android && ./gradlew assembleDebug && ./gradlew test
```
## Git-Status
- Branch `main`, Remote `origin` konfiguriert (git.dietzlabs.net).
- MVP (Phase AF) + Post-MVP-Features (optionale Accounts, Server-URL-Config,
geteilte Listen) committed und gepusht.