Rewrite AGENTS.md to match the corrected pr-iphone-deploy.yml:
§5 Workflow:
- Clean step: 'GARDE build/spm-cache' (not ../build/spm-cache), explicit
warning never to 'rm -rf build/', note that Xcode global cache is spared.
- New step 5: write Secrets.xcconfig from Gitea secrets before xcodegen.
- Scheme lists 4 targets (TabataGo, TabataGoWidget, TabataGoWatch,
TabataGoWatchWidget) — clarify TabataGoWidget is iOS, not watchOS.
- wait-approval: re-fetches all comments each cycle, regex \bLGTM\b/\bKO\b
anywhere in body, squash-merge.
- Document concurrency block and timeouts.
§5 Secrets table:
- Replace false 'Injectés via Config/Secrets.xcconfig → Info.plist' with
the real flow (CI writes file from secrets, Info.plist reads $(VAR)).
- Add historical-exposure warning: Secrets.xcconfig was committed before
the gitignore rule; key rotation recommended.
§5 Pitfalls + §8 anti-patterns:
- 'rm -rf build/' (not just 'rm -rf cache SPM').
- Don't nuke ~/Library/Caches/com.apple.dt.Xcode.
- permissions: block is intentionally absent (no-op under Gitea API).
- Don't remove concurrency block.
- Don't anchor LGTM grep to body start.
- {"Do":"squash"} not {"Do":"merge"}.
- Scheme must list all 4 targets.
§7 rule #9: expanded with the CI-writes-file flow.
287 lines
17 KiB
Markdown
287 lines
17 KiB
Markdown
# 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é). `timeout-minutes: 30` (build-deploy), `120` (wait-approval).
|
||
**Concurrency** : `group: pr-iphone-deploy-<PR#>`, `cancel-in-progress: true` → un push sur une PR existante annule le pipeline précédent (pas de double-merge, pas de runner gaspillé). Différentes PRs tournent en parallèle.
|
||
**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/derived`, `TabataGo.xcodeproj`, `Package.resolved`, DerivedData, ModuleCache.
|
||
- **GARDE le cache SPM** (`build/spm-cache` — c'est le chemin passé à `-clonedSourcePackagesDirPath`) — RevenueCat ≈ 1.1 GiB, re-cloner = 5+ min et échecs aléatoires du sandbox. **Ne jamais faire `rm -rf build/`** : ça détruirait ce cache. Le clean step ne touche que `build/derived`.
|
||
- Ne touche **pas** à `~/Library/Caches/com.apple.dt.Xcode` (cache Xcode global — le nuke ralentit tous les builds suivants).
|
||
4. **Install tools** : `brew install xcodegen node ios-deploy` (sans `|| true` — si brew down, on fail ici, pas deux steps plus loin).
|
||
5. **Write `Config/Secrets.xcconfig`** depuis les secrets Gitea (`SUPABASE_URL`, `SUPABASE_ANON_KEY`, `REVENUECAT_API_KEY`, `POSTHOG_API_KEY`). Preflight : fail loud si un secret requis manque. Le fichier est gitigné (règle #9) ; CI l'écrase à chaque run. `Info.plist` lit les valeurs via `$(SUPABASE_URL)` etc., et `project.yml` wire le xcconfig en Debug+Release — aucun autre changement nécessaire côté build.
|
||
6. **xcodegen generate** → `xcodebuild -resolvePackageDependencies` → **build** (`-scheme TabataGo`, Debug, auto-provisioning, team `2MJF39L8VY`). Le scheme build les 4 targets : `TabataGo` (app iOS), `TabataGoWidget` (widget iOS, vrai target — ne pas confondre avec `TabataGoWatchWidget` qui est watchOS), `TabataGoWatch` (app watchOS), `TabataGoWatchWidget` (complication watchOS).
|
||
7. **Deploy iPhone** UDID `00008120-000925CE3672201E` : `devicectl` WiFi d'abord, fallback `ios-deploy` USB.
|
||
8. **Post comment** "Prêt à tester" sur la PR.
|
||
9. **Job `wait-approval`** : poll (30s, max 240 = 2h). **Re-fetche TOUS les comments** chaque cycle (pas `since_id`) pour attraper aussi les edits (un reviewer passant de "KO" à "LGTM"). Match LGTM/KO n'importe où dans le body (regex `\bLGTM\b` / `\bKO\b`, case-insensitive) — "Tested, LGTM!" compte ; "KOM" ne compte pas. `LGTM` → **squash-merge** (`{"Do":"squash"}`). `KO` → blocage. Timeout → fail.
|
||
|
||
### Secrets
|
||
|
||
| Secret | Usage |
|
||
|--------|-------|
|
||
| `PR_API_TOKEN` | Checkout + API Gitea (comment, merge). **JAMAIS `GITEA_TOKEN`/`GITHUB_TOKEN`**. Tout passe par l'API Gitea avec ce token — un bloc `permissions:` natif serait no-op, c'est pourquoi il n'y en a pas. |
|
||
| `SUPABASE_URL`, `SUPABASE_ANON_KEY`, `REVENUECAT_API_KEY`, `POSTHOG_API_KEY` | Un step CI écrit `Config/Secrets.xcconfig` depuis ces 4 secrets **avant** `xcodegen generate`. `Info.plist` lit les valeurs via `$(SUPABASE_URL)` etc. ; `project.yml` wire le xcconfig en Debug+Release. `POSTHOG_API_KEY` peut être vide (PostHog pas encore câblé en SPM). |
|
||
|
||
> ⚠️ **Historique** : `Config/Secrets.xcconfig` a été committé par erreur dans le passé (avant la règle gitignore) avec des valeurs réelles. Le fichier est maintenant gitigné et écrasé par CI, mais l'historique git contient encore l'ancienne version — **rotation des clés Supabase/RevenueCat recommandée côté ops**.
|
||
|
||
### ⚠️ 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). Il liste **4 targets** : `TabataGo`, `TabataGoWidget`, `TabataGoWatch`, `TabataGoWatchWidget`.
|
||
- **Ne jamais `rm -rf build/` ni `rm -rf build/spm-cache`** dans le CI — RevenueCat y vit. Le clean step ne touche que `build/derived`.
|
||
- `SWIFT_ENABLE_EXPLICIT_MODULES=NO` au build — requis sinon segfault linker sur `.pcm` stale.
|
||
- `-skipPackagePluginValidation -allowProvisioningUpdates`.
|
||
- **Concurrency** : le bloc `concurrency:` est volontaire — ne pas le retirer, sinon les pushes successifs empilent des pipelines et tentent des double-merges.
|
||
- **LGTM/KO regex** : matche n'importe où dans le body avec `\b...\b` (word boundary). Ne pas revenir à un `grep '"body": *"LGTM"'` ancré — il raterait "Tested, LGTM!".
|
||
- **Squash-merge** (`{"Do":"squash"}`) — pas merge commit ni rebase. Garde l'historique `main` linéaire.
|
||
- **Comment edits** : `wait-approval` re-fetche tous les comments chaque cycle (pas `since_id`) pour attraper les edits.
|
||
|
||
---
|
||
|
||
## 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`. En CI, un step écrit ce fichier depuis les secrets Gitea (`SUPABASE_URL`, `SUPABASE_ANON_KEY`, `REVENUECAT_API_KEY`, `POSTHOG_API_KEY`) **avant** `xcodegen generate` ; `Info.plist` lit les valeurs via `$(VAR)` et `project.yml` wire le xcconfig en Debug+Release.
|
||
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 build/` ou `rm -rf build/spm-cache` en CI | Garder `build/spm-cache` ; ne cleaner que `build/derived` |
|
||
| `rm -rf ~/Library/Caches/com.apple.dt.Xcode` | Laisser le cache Xcode global intact (sinon tous les builds ralentissent) |
|
||
| Utiliser `GITEA_TOKEN` en CI | `PR_API_TOKEN` uniquement |
|
||
| Compter sur un bloc `permissions:` natif Gitea Actions | Tout passe par l'API Gitea avec `PR_API_TOKEN` ; le bloc natif serait no-op |
|
||
| Retirer le bloc `concurrency:` | Permet double-merge et pipelines empilés |
|
||
| Ancre le grep LGTM au début du body (`'"body": *"LGTM"'`) | Regex `\bLGTM\b` n'importe où dans le body (attrape "Tested, LGTM!") |
|
||
| `{"Do":"merge"}` (merge commit) | `{"Do":"squash"}` pour un `main` linéaire |
|
||
| Supprimer le scheme explicite dans `project.yml` | xcodegen 2.45.4 n'en crée pas — scheme doit lister `TabataGo`/`TabataGoWidget`/`TabataGoWatch`/`TabataGoWatchWidget` |
|
||
| 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
|
||
```
|