Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6.6 KiB
StreakFit — Modèle de domaine (spec OSDD)
App de cross-training / suivi muscu. Concept central = le streak (régularité : jours d'entraînement
consécutifs, gamification à la Duolingo). Stack : Laravel 13 + OSDD (xefi/laravel-osdd) + Octane/FrankenPHP,
Filament 5 (admin), Sanctum (auth cookie SPA), lomkit REST API + access-control, MySQL (mysql-shared), RustFS/S3 (médias).
Règles d'architecture (NON négociables)
- Le front ne contient AUCUNE logique métier, même l'auth. Tout est calculé/validé côté back ; l'API renvoie du prêt-à-afficher (streak courant, PR, progression, prochaine séance, instructions dans la locale). Auth = cookie de session Sanctum, flow OIDC piloté 100% par le back.
- OSDD : une couche = un package
type: layersousfunctional/(métier) outechnical/(infra), autonome (src/,database/migrations|factories|seeders,routes/, providerextends LayerServiceProvider). Placer chaque fichier viaphp artisan osdd:<gen> --layer=<functional|technical>/<nom>. - i18n : fr, pt-BR, en. Instructions d'exos = en+fr (dataset), pt-BR retombe sur en/fr.
- FK toujours vers
users(id)(table possédée parfunctional/users). Ne pas recréer la table users. - Pattern de référence =
functional/exercises(déjà fait : model + migration + config + import command).
Couches & schéma
functional/users (existe — auth)
users: +locale(string, def 'fr'),unitsenum('metric','imperial') def 'metric',timezone(string, nullable),avatar_path(nullable),oidc_sub(string nullable, unique). Profil léger sur le user.
functional/exercises (FAIT — lecture seule, catalogue)
exercises: external_id(unique), name, slug, category, body_part, equipment, target, muscle_group, secondary_muscles(json), instructions(json {en:[],fr:[]}), image_path, gif_path, attribution.
functional/routines
Une routine = liste ordonnée d'exercices avec séries/reps/repos.
routines: user_id(fk), name, slug, description(text null), is_public(bool def false), estimated_minutes(int null).routine_exercises(pivot enrichi) : routine_id(fk), exercise_id(fk), position(int), sets(int), target_reps(string, ex "8-12"), rest_seconds(int null), tempo(string null), target_weight(decimal 8,2 null), notes(text null).- Relations : Routine belongsTo User ; Routine hasMany RoutineExercise (ordered by position) ; RoutineExercise belongsTo Exercise.
functional/programs
Un programme = planning de routines sur plusieurs semaines.
programs: user_id(fk), name, slug, description(text null), duration_weeks(int), days_per_week(int null), is_public(bool def false).program_slots: program_id(fk), week(int, 1..duration_weeks), weekday(tinyint 1..7), routine_id(fk null), label(string null).- Relations : Program belongsTo User ; Program hasMany ProgramSlot ; ProgramSlot belongsTo Routine.
functional/tracking
Séances réalisées + sets loggés + records + mensurations.
workout_sessions: user_id(fk), routine_id(fk null), program_id(fk null), title(string null), started_at(datetime), ended_at(datetime null), status enum('active','completed','abandoned') def 'active', notes(text null), total_volume(decimal 10,2 null).set_logs: workout_session_id(fk), exercise_id(fk), set_number(int), reps(int null), weight(decimal 8,2 null), rpe(decimal 3,1 null), duration_seconds(int null), distance_meters(decimal 8,2 null), is_warmup(bool def false), is_completed(bool def true), logged_at(datetime).personal_records: user_id(fk), exercise_id(fk), type enum('1rm','max_weight','max_reps','max_volume','best_time'), value(decimal 10,2), unit(string), achieved_at(datetime), workout_session_id(fk null). unique(user_id,exercise_id,type).body_metrics: user_id(fk), measured_on(date), weight(decimal 6,2 null), body_fat(decimal 5,2 null), notes(text null). unique(user_id,measured_on).- Événement : à la complétion d'une WorkoutSession, dispatch
WorkoutCompleted(session)(listener côté streaks).
functional/streaks (LE CONCEPT)
streaks(1 par user) : user_id(fk, unique), current_count(int def 0), longest_count(int def 0), last_active_date(date null), freezes_available(int def 0), freeze_last_granted_on(date null).streak_days: user_id(fk), date(date), source enum('workout','manual','freeze','rest'), workout_session_id(fk null). unique(user_id,date).goals: user_id(fk), type enum('weekly_sessions','weekly_minutes','weekly_volume'), target(int), period enum('week') def 'week', is_active(bool def true).achievements(badges) : user_id(fk), key(string, ex 'streak_7','streak_30','first_workout'), earned_at(datetime), meta(json null). unique(user_id,key).- Logique (service
StreakService, côté back) : listener surWorkoutCompleted→ enregistre unstreak_day(source=workout) pour la date → recalcule : si last_active_date == veille → current++ ; == aujourd'hui → no-op ; sinon si un freeze couvre le trou → consomme ; sinon reset à 1. Met à jour longest. Débloque les achievements aux paliers.
technical/integrations (EXTENSIBILITÉ Garmin/Strava)
Générique : aucune référence en dur à un provider. Garmin/Strava = futures couches functional/strava, functional/garmin
qui implémentent le contrat et s'enregistrent dans le registre.
integrations(comptes connectés) : user_id(fk), provider(string), external_user_id(string null), access_token(text null, cast encrypted), refresh_token(text null, cast encrypted), token_expires_at(datetime null), scopes(json null), status enum('connected','revoked','error') def 'connected', connected_at(datetime), last_synced_at(datetime null), meta(json null). unique(user_id,provider).external_activities: integration_id(fk), user_id(fk), provider(string), external_id(string), type(string), started_at(datetime), duration_seconds(int null), distance_meters(decimal 10,2 null), calories(int null), raw(json null). unique(provider,external_id).- Contrats (
src/Contracts/) : interfaceActivityProvider{ key():string; label():string; connectUrl(User):string; handleCallback(User,array):Integration; fetchActivities(Integration, ?Carbon $since):iterable; } — et un singletonProviderRegistry{ register(ActivityProvider), get(string):?ActivityProvider, all():array }. Enregistré dans le provider de la couche.
API (lomkit REST) — fait plus tard, couche par couche
Resources lomkit sous src/Rest/Resources, contrôlées par access-control (chaque user ne voit que ses données ;
exercises = public en lecture). Endpoints REST search/mutate. Vues "prêtes à afficher" (dashboard streak, séance courante)
exposées via contrôleurs dédiés quand un simple CRUD ne suffit pas.