Files
tabatago/AGENTS.md
millianlmx 6937a36ccd
All checks were successful
Admin Web Docker / Docker Build Validation (push) Has been skipped
Admin Web Docker / Admin Web Tests (push) Successful in 33s
CI / Detect Changes (push) Successful in 3s
Admin Web Docker / Semantic Release (push) Successful in 11s
CI / YouTube Worker (push) Has been skipped
Admin Web Docker / Build & Push Docker Image (push) Successful in 22s
CI / Deploy (push) Has been skipped
fix(ci): pr-iphone-deploy — cache, secrets, LGTM regex, concurrency, watch build, scope (#12)
2026-07-19 20:59:33 +02:00

17 KiB
Raw Permalink Blame History

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/001006. 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 XcodeGenproject.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/)

TabataGoWatchAppWatchRootView → é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_workoutsCREATE 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 : arch -arm64 brew install xcodegen (sans || true — si brew down, on fail ici, pas deux steps plus loin). arch -arm64 requis car le shell act_runner tourne sous Rosetta 2 sur Apple Silicon, mais Homebrew est dans le prefix ARM /opt/homebrew — sans ça, brew refuse d'installer/upgrader. Seul xcodegen est nécessaire : node n'est pas utilisé par ce workflow, et l'installer déclenche le "installed dependents check" de brew qui essaie de rebuilder des formulae sans rapport (ex. gemini-cli) et fail le build.
  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 change­ment nécessaire côté build.
  6. xcodegen generatexcodebuild -resolvePackageDependenciesbuild (-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 : xcrun devicectl device install app uniquement. devicectl gère nativement la découverte WiFi (réseau) et filaire (USB-C/Thunderbolt) — pas de fallback ios-deploy.
  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. LGTMsquash-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

# 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