360 Fitness — Architektur

C4-Modell, abgeleitet aus dem Code: 237 Swift-Dateien in Fitness360/ und Fitness360Widgets/ plus das Backend /home/marc/docker/api-gateway. Quelldateien im Repo: docs/architecture/c4-diagrams.md (Mermaid), workspace.dsl (Structurizr).

Person System Container Komponente Extern — — durchgezogen: Aufruf · gestrichelt: Rückkanal

Ebene 1System-Kontext

Wer und was mit dem System spricht.

flowchart TB
    marc["Marc
Einziger Nutzer"]:::person subgraph koerper["Datenquellen am Koerper"] direction LR renpho["Renpho-Waage
Gewicht, Koerperfett"]:::ext watch["Apple Watch
Workouts, Puls, Schlaf"]:::ext end health["Apple Health
HealthKit
Geraete-Drehscheibe"]:::ext f360["360 Fitness
Ernaehrungserfassung, HealthKit-Export,
KI-Coach, Auswertung"]:::system subgraph apis["Externe APIs"] direction LR anthropic["Anthropic API
Claude Vision und Chat"]:::ext usda["USDA FoodData Central
Naehrwerte unverpackt"]:::ext off["Open Food Facts
Naehrwerte per Barcode"]:::ext tg["Telegram Bot API
Report und Alerts"]:::ext end marc -->|"Foto, Tap, Chat"| f360 renpho --> health watch --> health f360 -->|"schreibt Ernaehrung und Alkohol"| health health -.->|"liest v1-Metriken"| f360 f360 -->|"Foto-Analyse, Coach, Report"| anthropic f360 -->|"Naehrwert-Suche"| usda f360 -->|"Barcode-Lookup"| off f360 -->|"Report und Alerts"| tg tg -.->|"benachrichtigt"| marc classDef person fill:#08427b,stroke:#052e56,color:#fff classDef system fill:#1168bd,stroke:#0b4884,color:#fff classDef ext fill:#8a8a8a,stroke:#6b6b6b,color:#fff style koerper fill:none,stroke:#b9bec7,stroke-dasharray:4 4 style apis fill:none,stroke:#b9bec7,stroke-dasharray:4 4
Kerninvariante: Das VLM liefert ausschliesslich Lebensmittel + Menge. Kalorien und Makros kommen immer aus USDA FDC / Open Food Facts — nie aus dem Modellgedächtnis.

Ebene 2Container

Die Laufzeit-Bausteine: Gerät und ServGreen.

flowchart TB
    marc["Marc"]:::person

    subgraph phone["iPhone"]
        direction LR
        ios["iOS App
Swift 6, SwiftUI, MVVM
Erfassung, Anzeige, Coach-Chat,
Export-Koordinator"]:::container widget["Widget-Extension
WidgetKit, AppIntents
3x Alkohol-Intent, 3x Deep-Link"]:::container keychain[("Keychain
Shared Access Group
Gateway-Token")]:::store health["Apple Health
HealthKit-Store"]:::ext end subgraph servgreen["ServGreen"] direction LR traefik["Traefik
Docker, Lets Encrypt
TLS, gateway.servgreen.de"]:::container gw["API-Gateway
FastAPI, asyncpg
13 Endpunkte,
haelt alle Vendor-Keys"]:::container jobs["Jobs-Scheduler
APScheduler
Aggregate 03:00, Alerts 07:00,
Report So 18:00"]:::container pg[("Postgres 16
12 Tabellen
internes Docker-Netz")]:::store end anthropic["Anthropic API"]:::ext food["USDA FDC /
Open Food Facts"]:::ext tg["Telegram Bot API"]:::ext marc -->|"erfasst und prueft"| ios marc -->|"1 Tap ohne App-Start"| widget widget -.->|"Deep-Link fitness360://"| ios ios -->|"schreibt Ernaehrung,
liest Tageswerte"| health widget -->|"schreibt Alkohol"| health ios --> keychain widget --> keychain ios -->|"Bearer-Token, JSON, HTTPS"| traefik widget -->|"Alkohol-Log, best effort"| traefik traefik -->|"HTTP 8000"| gw gw -->|"asyncpg"| pg jobs -->|"asyncpg"| pg gw -->|"Vision und Coach"| anthropic gw -->|"Lookup"| food jobs -->|"Wochenreport"| anthropic jobs -->|"Report und Alerts"| tg tg -.-> marc classDef person fill:#08427b,stroke:#052e56,color:#fff classDef container fill:#438dd5,stroke:#2e6295,color:#fff classDef store fill:#3c7fbf,stroke:#2a5c8a,color:#fff classDef ext fill:#8a8a8a,stroke:#6b6b6b,color:#fff style phone fill:none,stroke:#b9bec7,stroke-dasharray:4 4 style servgreen fill:none,stroke:#b9bec7,stroke-dasharray:4 4
Der Export-Rückweg ist geräteseitig. HealthExportCoordinator horcht per HKObserverQuery und schiebt Deltas per HKAnchoredObjectQuery an POST /fitness360/health-export. Health Auto Export ist nicht im Datenpfad.

Ebene 3aKomponenten der iOS-App

MVVM-Schichtung. Alle Geräte-Abhängigkeiten liegen hinter Protokollen — deshalb laufen 42 Testsuites ohne Simulator.

flowchart TB
    subgraph praes["Praesentation"]
        direction LR
        views["Views
Home, Trends, Camera mit Modus
Essen-Barcode, Review, Alkohol-Grid,
Logs, Settings"]:::comp ds["Design-System
DesignTokens, GlassStyle,
Typography, Sparkline"]:::comp end vms["ViewModels — 15x
@Observable, @MainActor"]:::comp subgraph geraet["Geraete- und Domaenen-Services"] direction LR cam["AVFoundationCameraService
Session, Foto plus LiDAR-Tiefe"]:::comp vsup["Vision-Support
ImageEncoder, DepthSampler,
SceneScaleEstimator, ScaleHintBuilder"]:::comp hkw["HealthKitService
Autorisierung, food-Korrelation,
Alkohol-Samples"]:::comp hkr["HealthKitReader /
DayLogService

Tageswerte, Journal, Loeschen"]:::comp exp["HealthExportCoordinator
ObserverQuery, AnchoredQuery,
Anchor je Metrik"]:::comp end subgraph rein["Reiner Kern und lokale Stores"] direction LR map["Mapping-Kern
HealthMetricMapping,
MealSampleBuilder"]:::comp fav["FavoritesStore
UserDefaults, geraetelokal"]:::comp tok["GatewayTokenStore
Keychain, geteilte Gruppe"]:::comp end proxy["Proxy-Clients — 10x
URLSession, Bearer-Auth
Vision, Nutrition, Barcode, Coach,
Trends, Export, Alkohol, Labs, Training"]:::comp health["Apple Health
HealthKit"]:::ext gw["API-Gateway"]:::ext views --> vms views --> ds vms --> cam vms --> proxy vms --> hkw vms --> hkr vms --> fav cam -->|"Tiefenkarte"| vsup proxy -->|"JPEG-base64,
cm-Massstab"| vsup proxy --> tok hkw --> map exp --> map exp -->|"POST health-export"| proxy hkw --> health hkr --> health exp -->|"beobachtet"| health proxy -->|"Bearer, JSON, HTTPS"| gw classDef comp fill:#85bbf0,stroke:#5d82a8,color:#0b1620 classDef ext fill:#8a8a8a,stroke:#6b6b6b,color:#fff style praes fill:none,stroke:#b9bec7,stroke-dasharray:4 4 style geraet fill:none,stroke:#b9bec7,stroke-dasharray:4 4 style rein fill:none,stroke:#b9bec7,stroke-dasharray:4 4

Ebene 3bKomponenten des Gateways

Auth und Rate-Limit hängen als Router-Dependency, /health bleibt offen.

flowchart TB
    subgraph kern["Generischer Kern"]
        direction LR
        auth["auth
Bearer zu Principal
401 unbekannt, 403 falscher Scope"]:::comp rl["ratelimit
Drosselung je Token"]:::comp end routes["fitness360/routes
vision, nutrition, barcode, health-export,
training-log, labs, trends, alcohol, entry-delete"]:::comp croutes["coach/routes
chat, history,
pending-entries, ack"]:::comp subgraph fmod["Fachmodule"] direction LR vision["vision
System-Prompt plus JSON-Schema
liefert nur Items und Menge"]:::comp nut["nutrition
USDA FDC mit Datatype-Praeferenz,
Fallback Open Food Facts"]:::comp bc["barcode
EAN-Pruefung 8-12-13,
OFF v2, per-100g"]:::comp expm["health_export
UPSERT health_samples,
UNIQUE als Idempotenz"]:::comp labs["labs
Laborpanel"]:::comp end subgraph cmod["Coach"] direction LR cllm["coach/llm
Claude-Tool-Loop,
Historie-Fenster 30"]:::comp cctx["coach/context
Wochen-Kennzahlen
plus Zielwerte"]:::comp cread["coach/tools
describe_schema, query_database
Rolle coach_readonly"]:::comp cwrite["coach/write_tools
log_meal in Staging,
log_training direkt"]:::comp end pool["core/db
asyncpg-Pool plus Schema-Migration"]:::comp pg[("Postgres")]:::store anthropic["Anthropic API"]:::ext food["USDA FDC /
Open Food Facts"]:::ext routes --> auth routes --> rl croutes --> auth routes --> vision routes --> nut routes --> bc routes --> expm routes --> labs routes -->|"Trends, Alkohol,
Training, Loeschung"| pool croutes --> cllm cllm --> cctx cllm --> cread cllm --> cwrite cwrite -->|"Naehrwerte fuer Nachtrag"| nut vision --> anthropic cllm --> anthropic nut --> food bc --> food expm --> pool labs --> pool cctx --> pool cread -->|"SELECT readonly"| pool cwrite --> pool pool --> pg classDef comp fill:#85bbf0,stroke:#5d82a8,color:#0b1620 classDef store fill:#3c7fbf,stroke:#2a5c8a,color:#fff classDef ext fill:#8a8a8a,stroke:#6b6b6b,color:#fff style kern fill:none,stroke:#b9bec7,stroke-dasharray:4 4 style fmod fill:none,stroke:#b9bec7,stroke-dasharray:4 4 style cmod fill:none,stroke:#b9bec7,stroke-dasharray:4 4

Ablauf 1Foto → Mahlzeit

Kein Auto-Save: zwischen Schätzung und HealthKit-Write liegt immer der Review-Screen. Die Portionsschätzung hat 15–25 % Fehler, deshalb ist der Schritt nicht optional.

sequenceDiagram
    autonumber
    actor M as Marc
    participant V as CameraView / Review
    participant P as Proxy-Clients
    participant G as Gateway
    participant A as Anthropic
    participant F as USDA / OFF
    participant H as HealthKit
    participant E as ExportCoordinator
    participant DB as Postgres

    M->>V: Foto plus Kurztext
    V->>P: MealCapture inkl. LiDAR-Tiefe
    P->>P: JPEG max 1568px, base64, Massstab in cm
    P->>G: POST /fitness360/vision
    G->>A: messages mit Bild und JSON-Schema
    A-->>G: Items plus Menge, keine Naehrwerte
    G-->>P: FoodEstimate
    P-->>V: Vorschlag
    V->>M: Pflicht-Review, editierbar
    M->>V: bestaetigt
    V->>P: ReviewedMeal
    P->>G: POST /fitness360/nutrition
    G->>F: Naehrwert-Lookup
    F-->>G: kcal und Makros je 100g
    G-->>P: NourishedMeal
    P->>H: food-Korrelation schreiben
    H-->>E: ObserverQuery feuert
    E->>H: AnchoredQuery, nur Delta
    E->>G: POST /fitness360/health-export
    G->>DB: UPSERT health_samples
  

Ablauf 2Coach trägt eine Mahlzeit nach

Der serverseitige Coach hat keinen HealthKit-Zugriff. Die Staging-Tabelle verhindert Doppelzählung strukturell — nur der Geräte-Pfad erzeugt health_samples-Zeilen.

sequenceDiagram
    autonumber
    actor M as Marc
    participant C as CoachChatView
    participant G as Gateway /coach/chat
    participant A as Anthropic
    participant DB as Postgres
    participant App as HomeView beim Start
    participant H as HealthKit

    M->>C: "Hatte mittags 200g Reis"
    C->>G: POST /coach/chat
    G->>A: History plus Wochenkontext plus Tools
    A-->>G: tool_use log_meal
    G->>DB: INSERT coach_pending_entries
    A-->>G: Antworttext
    G-->>C: Antwort
    Note over App,H: naechster App-Start
    App->>G: GET /coach/pending-entries
    G-->>App: offene Eintraege
    App->>H: HealthKitService.save, gleicher Pfad wie Review
    App->>G: POST /coach/pending-entries/id/ack
  

Ablauf 3Nachtjobs

Der jobs-Container ersetzt n8n — geplante, getestete Python-Jobs.

flowchart LR
    subgraph jobs["Jobs-Container, APScheduler"]
        direction TB
        A["aggregates
03:00"]:::comp AL["alerts
07:00"]:::comp R["weekly_report
So 18:00"]:::comp end DB[("Postgres")]:::store CL["Anthropic"]:::ext TG["Telegram"]:::ext DB --> A A -->|"7-Tage-Rollmittel"| DB DB --> AL AL -->|"nur bei Schwellenverletzung"| TG DB --> R R --> CL CL -->|"Report-Text"| R R --> TG classDef comp fill:#85bbf0,stroke:#5d82a8,color:#0b1620 classDef store fill:#3c7fbf,stroke:#2a5c8a,color:#fff classDef ext fill:#8a8a8a,stroke:#6b6b6b,color:#fff style jobs fill:none,stroke:#b9bec7,stroke-dasharray:4 4

Doku-Drift: 6 Widersprüche zum Code

Die Diagramme bilden den Code ab. Folgende Dokumente widersprechen ihm — alles Doku-Drift, keine Code-Fehler.

DokumentAussageRealität im Code
CLAUDE.md „Aktueller Repo-Zustand"reines Konzept-Repo, noch kein Code, kein Xcode-Projekt237 Swift-Dateien, XcodeGen-project.yml, App + Widget + Test-Target
CLAUDE.md „Phasen-Disziplin"aktuell nur Phase 1, kein n8n, keine DB, kein CGMPhasen 1–4 live, dazu der KI-Coach (im Phasenplan nicht vorgesehen)
vision.md 5 + 6.4n8n als Integrations-Backendersetzt durch FastAPI-Gateway + APScheduler-Jobs (scheduler.py: „Ersetzt n8n")
vision.md 6.5 + 8Speicherziel noch nicht entschiedenPostgres 16 in Betrieb, 12 Tabellen
vision.md 5 (ASCII)Health Auto Export als Fallbacknicht im Datenpfad — die App exportiert selbst
vision.md 7CGM via LibreLinkUp direkt, an HealthKit vorbeiHealthMetricMapping mappt bloodGlucose bereits über HealthKit

Strukturelle Beobachtungen

  • Widget-Target kompiliert die komplette Service-Schicht. project.yml gibt der Extension Models + Services + Support als Quellen. Jede neue Service-Datei landet automatisch im Widget-Binary, auch wenn nur LogDrinkIntent sie braucht — die Kopplung wächst unbemerkt mit.
  • Zwei getrennte HealthKit-Autorisierungspfade. HealthKitService fordert Share+Read, HealthExportCoordinator nur Read. Absichtlich getrennt, erklärt die zwei Typlisten HealthDataCatalog vs. HealthMetricMapping.
  • Der Coach ist der einzige Schreibpfad ohne Review — sauber gelöst über die Staging-Tabelle statt über Prompt-Disziplin.
  • query_database ist doppelt abgesichert: DB-Rolle coach_readonly mit GRANT SELECT auf fünf Allowlist-Tabellen, plus app-seitige SELECT-/LIMIT-Prüfung.