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).
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
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
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.
| Dokument | Aussage | Realität im Code |
|---|---|---|
CLAUDE.md „Aktueller Repo-Zustand" | reines Konzept-Repo, noch kein Code, kein Xcode-Projekt | 237 Swift-Dateien, XcodeGen-project.yml, App + Widget + Test-Target |
CLAUDE.md „Phasen-Disziplin" | aktuell nur Phase 1, kein n8n, keine DB, kein CGM | Phasen 1–4 live, dazu der KI-Coach (im Phasenplan nicht vorgesehen) |
vision.md 5 + 6.4 | n8n als Integrations-Backend | ersetzt durch FastAPI-Gateway + APScheduler-Jobs (scheduler.py: „Ersetzt n8n") |
vision.md 6.5 + 8 | Speicherziel noch nicht entschieden | Postgres 16 in Betrieb, 12 Tabellen |
vision.md 5 (ASCII) | Health Auto Export als Fallback | nicht im Datenpfad — die App exportiert selbst |
vision.md 7 | CGM via LibreLinkUp direkt, an HealthKit vorbei | HealthMetricMapping mappt bloodGlucose bereits über HealthKit |
Strukturelle Beobachtungen
- Widget-Target kompiliert die komplette Service-Schicht.
project.ymlgibt der ExtensionModels+Services+Supportals Quellen. Jede neue Service-Datei landet automatisch im Widget-Binary, auch wenn nurLogDrinkIntentsie braucht — die Kopplung wächst unbemerkt mit. - Zwei getrennte HealthKit-Autorisierungspfade.
HealthKitServicefordert Share+Read,HealthExportCoordinatornur Read. Absichtlich getrennt, erklärt die zwei TyplistenHealthDataCatalogvs.HealthMetricMapping. - Der Coach ist der einzige Schreibpfad ohne Review — sauber gelöst über die Staging-Tabelle statt über Prompt-Disziplin.
query_databaseist doppelt abgesichert: DB-Rollecoach_readonlymitGRANT SELECTauf fünf Allowlist-Tabellen, plus app-seitige SELECT-/LIMIT-Prüfung.