Files
streakfit-api/DOMAIN.md
T
neckfireandClaude Opus 4.8 af32669931
Build & Deploy / build (push) Successful in 18s
StreakFit — API Laravel 13 OSDD + Octane/FrankenPHP
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 03:00:17 +02:00

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: layer sous functional/ (métier) ou technical/ (infra), autonome (src/, database/migrations|factories|seeders, routes/, provider extends LayerServiceProvider). Placer chaque fichier via php 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 par functional/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'), units enum('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 sur WorkoutCompleted → enregistre un streak_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/) : interface ActivityProvider { key():string; label():string; connectUrl(User):string; handleCallback(User,array):Integration; fetchActivities(Integration, ?Carbon $since):iterable; } — et un singleton ProviderRegistry { 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.