# TabataGo — Référence canonique pour agents IA > **Une seule source de vérité.** Ce document décrit le projet *réel*. Si un autre > fichier (CLAUDE.md, schema.sql) contredit, ce document gagne. --- ## 0. ⚠️ Fichiers obsolètes — NE PAS LIRE | Fichier | Statut | Pourquoi | |---------|--------|----------| | `CLAUDE.md` (racine) | ❌ OBSOLÈTE | Décrit **TabataFit**, un concept Expo/React Native abandonné | | `supabase/schema.sql` | ❌ OBSOLÈTE | Header littéral "TabataFit" — décrit l'ancien schéma | | `supabase/seed.sql`, `supabase/setup-admin.sql` | ⚠️ Vérifier | Peuvent être périmés ; préférer `supabase/migrations/` | | `AGENTS.md` (ancien, racine) | 🔄 Remplacé par celui-ci | Mentionne PostHog 3.x comme actif (faux, voir §3) | **Source de vérité du schéma : `supabase/migrations/001` → `006`.** Jamais `schema.sql`. --- ## 1. Identité produit | | | |---|---| | **Nom** | TabataGo | | **Bundle** | `fr.millianlmx.tabatago` | | **Team** | `2MJF39L8VY` | | **But** | App d'entraînement Tabata (intervalles 20s travail / 10s repos) sur iOS + watchOS, avec programmes par zone corporelle, HealthKit, musique, et abonnements RevenueCat | | **Version** | 1.0 (build 2) — non publiée App Store | | **Propriétaire** | Millian LMX (CEO) | | **Gitea** | `https://gitea.1000co.fr/millianlmx/tabatago` | --- ## 2. Stack technique | Couche | Réalité vérifiée | |--------|------------------| | Langage | **Swift 6.0**, concurrence stricte (`SWIFT_STRICT_CONCURRENCY: complete`) | | UI | **SwiftUI** uniquement | | iOS | **26.0+** | | watchOS | **11.0+** | | Build | **XcodeGen** — `project.yml` est la source, `.xcodeproj` est généré | | SPM | **Supabase** `2.5.0+`, **RevenueCat** `5.0.0+` | | ❌ NON installé | **PostHog** — absent de `project.yml` packages. Voir §3. | | Architecture | **MVVM + `@Observable`**, `AppState.shared` central | | Backend | **Supabase** (PostgreSQL, Auth, Storage, Edge Functions Deno) | | Local cache | **SwiftData** (`TabataGoSchema.container`) | | Santé | **HealthKit** (fréquence cardiaque, calories, workouts) | | CI/CD | **Gitea Actions** sur runner macOS auto-hébergé (label `macos`) | --- ## 3. Architecture iOS ### Cibles (`tabatago-swift/project.yml`) | Cible | Type | Plateforme | Bundle ID | |-------|------|------------|-----------| | `TabataGo` | application | iOS 26.0 | `fr.millianlmx.tabatago` | | `TabataGoWatch` | application | watchOS 11.0 | `fr.millianlmx.tabatago.watchkitapp` | | `TabataGoWatchWidget` | app-extension | watchOS 11.0 | `fr.millianlmx.tabatago.watchkitapp.widget` | | `TabataGoTests` | bundle.unit-test | iOS | `.tests` | | `TabataGoUITests` | bundle.ui-testing | iOS | `.uitests` | **Dépendances d'intégration** : `TabataGo` embed `TabataGoWatch` qui embed `TabataGoWatchWidget`. Fichier partagé entre iOS et watch : `TabataGo/Services/WatchConnectivityTypes.swift` (compilé dans les deux targets via `group: TabataGoWatch/Services`). ### Bootstrap & data flow ``` TabataGoApp (@main) └─ RootView() ├─ .environment(AppState.shared) // @Observable singleton ├─ .modelContainer(TabataGoSchema.container) // SwiftData └─ .task { await AppState.shared.bootstrap() } ``` `AppState.bootstrap()` (MainActor, idempotent, skip en preview) : 1. `PurchaseService.shared.initialize()` (RevenueCat) 2. `AnalyticsService.shared.initialize()` — ⚠️ **no-op par défaut** car PostHog n'est pas dans SPM : tout est gardé par `#if canImport(PostHog)` qui est toujours faux. La taxonomie d'events existe mais n'émet rien tant que la dépendance n'est pas ajoutée. ### Navigation (PAS de NavigationStack) `MainTabView` = **`TabView` + `Tab(value:)`** (Liquid Glass, iOS 26), 4 onglets dans cet ordre exact du code : ``` home → programs → activity → profile ``` Modals full-screen via `.sheet`/`fullScreenCover` au-dessus : `PlayerView`, `CompletionView`, `PaywallView`, `OnboardingView`. ### Services (`TabataGo/Services/`) | Service | Rôle | |---------|------| | `SupabaseService` | Client Supabase (Auth, DB, Storage) | | `HealthKitService` | Lecture/écriture HealthKit | | `PurchaseService` | RevenueCat IAP, état d'abonnement | | `MusicService` | Apple Music + intégration piste YouTube | | `AudioService` | Playback audio (coachs sonores, alerts) | | `AnalyticsService` | **Stub PostHog** (voir ci-dessus) | | `PhoneConnectivityManager` | Côté iOS — WatchConnectivity (échange avec la montre) | | `WatchConnectivityTypes` | Protocoles/messages partagés iOS↔Watch (compilé dans les 2 targets) | > Le miroir côté watch est `TabataGoWatch/Services/WatchConnectivityManager.swift` (cible watchOS, pas iOS). ### ViewModels (`@Observable`) `HomeViewModel`, `HealthViewModel`, `PlayerViewModel`, `PurchaseViewModel`, `MusicPlayerViewModel`. ### Models (`TabataGo/Models/`) `WorkoutProgram`, `WorkoutSession`, `UserProfile`, `HealthSnapshot`, `MusicTrack`, `WorkoutActivityAttributes`, `MusicActivityAttributes` (Live Activities), `TabataGoSchema` (SwiftData), `PreviewData`/`MockPrograms` (previews). ### Watch (`TabataGoWatch/`) `TabataGoWatchApp` → `WatchRootView` → états `WatchIdleView` / `WatchActivityView` / `WatchPlayerView`. Moteur : `WatchPlayerEngine`. Connectivité : `WatchConnectivityManager` (coté watch). Complications : `TabataGoComplication`. --- ## 4. Base de données ### Supabase — source = `supabase/migrations/` (001→006) | # | Fichier | Contenu | |---|---------|---------| | 001 | `001_initial_schema.sql` | `trainers`, `workouts`, `collections`, `achievements`, `admin_users` | | 002 | `002_download_jobs.sql` | `download_jobs`, `download_items` (jobs d'import playlist YouTube) | | 003 | `003_music_genre.sql` | `music_genre` | | 004 | `004_download_items_public_read.sql` | RLS : lecture publique de `download_items` | | 005 | `005_workout_programs.sql` | **DROP** `programs`/`program_workouts` → **CREATE** `workout_programs` (body_zone enum `upper-body`/`lower-body`/`full-body` + level `Beginner`/`Intermediate`/`Advanced`) et `program_tabatas` (3 tabatas/program, 2 exercices chacun, 8 rounds × 20s/10s) + RLS public read + admin all | | 006 | `006_seed_workout_programs.sql` | **18 programmes seedés** (12 free : 4 par zone en 2B+1I+1A ; 6 premium) + **54 tabatas** (18×3), UUIDs stables | **Tables actives post-migration** : `trainers`, `workouts`, `collections`, `achievements`, `admin_users`, `download_jobs`, `download_items`, `music_genre`, `workout_programs`, `program_tabatas`. **Tables supprimées (ne pas recréer)** : `programs`, `program_workouts` (DROP en 005). **RLS** : lecture publique sur `workout_programs`/`program_tabatas`/`download_items` ; écriture restreinte aux `admin_users` (`EXISTS (SELECT 1 FROM admin_users WHERE id = auth.uid())`). ### SwiftData — cache local `TabataGoSchema.container` (et `.previewContainer` pour les `#Preview`). Persistance offline des sessions/programmes côté app. Ne pas confondre avec Supabase. --- ## 5. CI/CD ### Workflow : `.github/workflows/pr-iphone-deploy.yml` **Trigger** : PR ouverte/synchronize/reopened sur `main`. **Runner** : label `macos` (auto-hébergé). **Déroulé** : 1. **Checkout** (shallow, branche head) via `PR_API_TOKEN`. 2. **Setup PATH** : `echo "/opt/homebrew/bin" >> $GITHUB_PATH` — requis car Rosetta ne le voit pas. 3. **Clean ciblé** : - Supprime build artifacts, `TabataGo.xcodeproj`, `Package.resolved`, DerivedData, ModuleCache. - **GARDE le cache SPM** (`../build/spm-cache`) — RevenueCat ≈ 1.1 GiB, re-cloner = 5+ min et échecs aléatoires du sandbox. 4. **Install tools** : `brew install xcodegen node ios-deploy`. 5. **xcodegen generate** → `xcodebuild -resolvePackageDependencies` → **build** (`-scheme TabataGo`, Debug, auto-provisioning, team `2MJF39L8VY`). 6. **Deploy iPhone** UDID `00008120-000925CE3672201E` : `devicectl` WiFi d'abord, fallback `ios-deploy` USB. 7. **Post comment** "Prêt à tester" sur la PR. 8. **Job `wait-approval`** : poll (30s, max 240 = 2h) les commentaires de la PR. `LGTM` → auto-merge. `KO` → blocage. Timeout → fail. ### Secrets | Secret | Usage | |--------|-------| | `PR_API_TOKEN` | Checkout + API Gitea (comment, merge). **JAMAIS `GITEA_TOKEN`/`GITHUB_TOKEN`** | | `SUPABASE_URL`, `SUPABASE_ANON_KEY`, `REVENUECAT_API_KEY`, `POSTHOG_API_KEY` | Injectés via `Config/Secrets.xcconfig` → Info.plist | ### ⚠️ Pitfalls CI - `/opt/homebrew/bin` **hors PATH par défaut** sous Rosetta → toujours l'ajouter. - **xcodegen 2.45.4 ne génère pas de schemes auto** → scheme `TabataGo` défini explicitement dans `project.yml` (ne pas le supprimer). - **Ne jamais `rm -rf` le cache SPM** dans le CI (c'est l'inverse des build artifacts). - `SWIFT_ENABLE_EXPLICIT_MODULES=NO` au build — requis sinon segfault linker sur `.pcm` stale. - `-skipPackagePluginValidation -allowProvisioningUpdates`. --- ## 6. Sous-projets ### `admin-web/` — Dashboard admin Next.js 15 App Router, shadcn/ui. Gestion workouts / trainers / collections / programs. Auth Supabase → table `admin_users`. Stack : TypeScript, `middleware.ts`, tests Playwright (`e2e/`) + Vitest. ### `youtube-worker/` — Worker YouTube Node.js (`server.js`, `package.json`, `Dockerfile`). Télécharge l'audio de playlists YouTube → **Supabase Storage**. Piloté par les Edge Functions ci-dessous. ### `supabase/functions/` — Edge Functions (Deno) | Function | Rôle | |----------|------| | `youtube-playlist/` | Crée un `download_job`, liste les vidéos | | `youtube-process/` | Orchestre le téléchargement (via youtube-worker) | | `youtube-status/` | Statut d'un job | | `youtube-classify/` | Classification genre musical | | `main/` | Auth JWT (helper `jose`) | | `_shared/` | `auth.ts`, `cors.ts`, `supabase-client.ts`, `youtube-client.ts` | > Le pipeline musical complet : Edge Functions orchestrent → `youtube-worker` (Node.js) télécharge via **yt-dlp** + **Innertube** → audio dans Supabase Storage (`workout-audio` bucket) → classification automatique par **Gemini** (`gemini-3.1-flash-lite-preview`) → métadonnées dans `download_jobs`/`download_items` → genre dans `music_genre`. --- ## 7. Règles d'or pour les agents 1. **Source de vérité = `project.yml` + `supabase/migrations/`.** Tout le reste est dérivé ou obsolète. 2. **SwiftUI only.** Pas d'UIKit sauf nécessité absolue démontrée. 3. **`@Observable`** (macro Observation), **jamais `@ObservableObject`/`@Published`**. 4. **Concurrency stricte.** `async/await` partout, `@MainActor` sur l'UI, pas de completion handlers. 5. **Pas de force-unwrap.** `guard let`/`if let`, jamais `!`. 6. **Navigation = TabView + sheet/fullScreenCover.** **Pas de `NavigationStack`.** 7. **Tout en français** côté user-facing (`Localizable.xcstrings` / `L10n`). Codes/types restent en anglais. 8. **XcodeGen.** Éditer `project.yml`, pas `.xcodeproj`. Régénérer avec `xcodegen generate`. 9. **Secrets.** Jamais committer `Config/Secrets.xcconfig`. Template = `.example`. 10. **Lazy d'abord.** Avant d'ajouter une dépendance : stdlib Apple, puis ce qui existe déjà dans le repo, puis SPM déjà installé. PostHog n'est pas installé — ne pas l'ajouter sans justification. --- ## 8. Anti-patterns / pièges documentés | ❌ Ne pas faire | ✅ Faire | |-----------------|---------| | Lire `CLAUDE.md` ou `schema.sql` pour le schéma | Lire `supabase/migrations/001→006` | | Importer PostHog comme si c'était actif | Savoir que `AnalyticsService` est un **no-op** (PostHog pas en SPM) | | Utiliser `NavigationStack` | `TabView` + modals | | `@ObservableObject` / `@Published` | `@Observable` | | Force-unwrap `!` | `guard let` | | Strings UI en anglais | Françaises dans `L10n`/`Localizable.xcstrings` | | `rm -rf` le cache SPM en CI | Garder `build/spm-cache` | | Utiliser `GITEA_TOKEN` en CI | `PR_API_TOKEN` uniquement | | Supprimer le scheme explicite dans `project.yml` | xcodegen 2.45.4 n'en crée pas | | Compter sur `admin-web/` pour la app iOS | Dashboard admin séparé, communique via Supabase uniquement | | Recréer `programs`/`program_workouts` | Remplacés par `workout_programs`/`program_tabatas` (migration 005) | --- ## 9. Contexte projet & skills - **Repo Gitea** : `https://gitea.1000co.fr/millianlmx/tabatago` (branche intégration : `main`). - **Équipe** : "Millian Team" — équipe d'agents IA. Skills à loader selon la tâche : - `senior-ios` — SwiftUI, Xcode, iOS/watchOS - `senior-backend` — Supabase, SQL, Edge Functions - `senior-devops` — CI/CD Gitea Actions, deploy - `po-pm` — vision produit, specs, validation - **Team skill TabataGo** : documente les anti-patterns ci-dessus ; aligne tous les agents sur les mêmes conventions. - **Hermes profile actif** : `default`. Les skills vivent dans le profile sous `skills/`. - **Docs utiles** : `docs/ci-cd-setup.md`, `docs/app-store-submission.md`, `docs/maestro-e2e-testing-strategy.md`, `docs/ui-feature-brief.md`. - **Scripts** : `scripts/ci-status.py`, `scripts/deploy-functions.sh`. ### Commandes usuelles ```bash # Générer le projet Xcode cd tabatago-swift && xcodegen generate # Ouvrir open tabatago-swift/TabataGo.xcodeproj # Déployer les Edge Functions bash scripts/deploy-functions.sh # Statut CI python3 scripts/ci-status.py ```