mitbringsl/AGENTS.md
Tronax a00db14cba
Phase F: NetworkMonitor, offline banner, README & architecture docs
- NetworkMonitor: ConnectivityState Flow using ConnectivityManager.NetworkCallback
- ListsScreen & ViewModel: live offline indicator banner when disconnected
- Documentation:
  - docs/ARCHITECTURE.md: system design & tech stack overview
  - docs/SYNC.md: HLC timestamping, op_log outbox & LWW projection specification
  - docs/API.md: REST API endpoint specification
  - README.md: quickstart guide for backend, docker compose & Android app
- Verification: backend & android test suites 100% green 
2026-08-05 20:14:04 +02:00

231 lines
14 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=`.
- **MVP-Scope:** Single-Owner-Listen (`list_members` existiert, wird in Phase 2 für
geteilte Listen genutzt); 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
│ │ │ ├── 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
│ │ │ ├── itemstore.go # GetItems
│ │ │ └── suggeststore.go # Search (pg_trgm fuzzy)
│ │ └── httpapi/
│ │ ├── api.go # API-Objekt + Router (alle Routen aktiv)
│ │ ├── 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
│ │ ├── lists.go # GET/POST /api/lists, GET /api/lists/{id}
│ │ ├── ops.go # POST/GET /api/lists/{id}/ops (Push/Pull)
│ │ └── 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
│ ├── 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/ # NOCH LEER (folgt Phase F)
└── android/ # NOCH LEER (folgt Phase D)
```
---
## 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).
### Wo genau weitermachen?
### Wo genau weitermachen?
**Alle Phasen (Phase A bis F) sind vollständig abgeschlossen und verifiziert ✅.**
Erledigt:
- ✅ Phase A: Backend-Fundament, Migrationen, Docker, Caddy Setup.
- ✅ Phase B: Argon2id Password Auth, Sessions, OIDC (Google/Generic) Verification.
- ✅ Phase C: HLC Sync Engine, idempotent `op_log` append, LWW Projections, REST Endpoints & Suggestions.
- ✅ Phase D: Android Architecture (Kotlin 2.x, Compose M3, Room LWW DAOs, Retrofit API, Hilt DI).
- ✅ Phase E: Local-First `ShoppingRepository`, WorkManager `SyncWorker` & `SyncManager`, Full UI (Auth, Lists, Detail mit Autocomplete).
- ✅ Phase F: `NetworkMonitor` Offline-Indicator, Dokumentation (`ARCHITECTURE.md`, `SYNC.md`, `API.md`, `README.md`).
---
## 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 ./...
# 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
# Migrationen manuell gegen bestehende DB anwenden
docker run --rm --network <net> \
-e DATABASE_URL="postgres://app:PW@<db-host>:5432/appdb?sslmode=disable" \
mitbringsl-backend:test /app/migrate up
```
## Git-Status
- Repo initialisiert, Branch `main`. Remote ist konfiguriert (`origin`).
- Phase A + Phase B + Phase C + Phase D + Phase E + Phase F committed und gepusht.
- **Projekt Mitbringsl MVP ist vollständig abgeschlossen ✅.**