mitbringsl/AGENTS.md
Tronax cdc0c785b9
Docs: catch up to auth discovery, settings screen and sync fixes
Document the four features that landed on main in b44bc8c..85c790e:

- AGENTS.md: post-MVP section now covers server-driven auth discovery
  (GET /api/config, AUTH_PASSWORD_ENABLED 403 enforcement,
  OIDC_GENERIC_DISPLAY_NAME), the settings screen with GET/PUT /api/me,
  and the critical Android sync/data-safety fixes (pull-all-lists,
  client HLC tick per server op, no destructive migration, ProGuard
  rules). Repo structure updated (httpapi config.go/me.go, ui/settings),
  roadmap entries added, open-points list extended with test backlog for
  the new endpoints.
- API.md: new sections for GET /api/config (public) and GET/PUT /api/me;
  403 responses documented for register/login when password auth is off.
- SYNC.md: client pull loop (every tracked list, op_log pruning) and the
  client HLC discipline (tick per incoming server op) that keeps LWW
  correct across devices with skewed clocks.
- README: highlights for auth discovery/OIDC-only mode and the settings
  screen.
2026-08-22 09:45:45 +02:00

21 KiB
Raw Blame History

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)
        ├── 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 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

# 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.