Ajoute les notes mémoire d'architecture, de philosophie de la couche online et d'UX pour le client online, clôture le ticket #64, reflète les tickets #46/#54/#57/#62, ajoute le cadrage des tickets #65 à #70. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8.8 KiB
name, description, metadata
| name | description | metadata | ||
|---|---|---|---|---|
| gametime-architecture-online-client | memory note gametime-architecture-online-client |
|
GameTime — Architecture client online, auth, sync et partage
Décision d'architecture pour le ticket #63, basée sur les mémoires gametime-ux-online-client, gametime-online-layer-philosophy et gametime-server-architecture-sync-sharing.
Conclusion serveur : pas d'adaptation requise pour les étapes d'exercice
Le serveur ne valide pas la forme interne des payloads synchronisés. synced_resources.payload_json est un JSONB opaque stocké et retourné tel quel.
Preuves dans le code :
server/migrations/0001_initial_schema.sql:payload_json jsonb NOT NULLsursynced_resources, avec contraintes seulement surresource_type,client_id,schema_version, pas sur la forme depayload_json.server/lib/infrastructure/postgres/synced_resource_repository.dart: l'upsert écrit@payload_json::jsonb, avecjsonEncode(resource.payloadJson), puis retournepayload_jsonvia_payloadValue(...). Aucune validation métier de structure exercice/programme/template n'est faite dans ce repository.server/openapi.yaml:SyncPushItem.payloadetSyncedResourceItem.payloadréférencent seulementJsonObject.CreateShareRequest.payloadetShareInboxItem.payloadidem.
Conclusion : l'ajout des étapes d'exercice côté client ne nécessite pas de modification serveur pour la sync v1. Le client peut envoyer la nouvelle forme JSON dans payload tant qu'elle reste un objet JSON valide et porte schemaVersion correctement.
Principe produit non négociable
La couche online est optionnelle et additive :
- pas d'écran de login au démarrage ;
- aucune action locale ne dépend du serveur ;
- aucun échec réseau ne déclenche de popup globale ;
- les données nécessaires à l'UI restent en local ;
- logout ne supprime jamais exercices/programmes/séances/historique locaux.
Architecture lib/
Conserver l'architecture existante :
lib/domain/entities.dart: entités puresUserAccountSession,SyncStatusSnapshot,ShareInboxItem,PendingShareActionsi elles sont stables métier.lib/application/ports.dart: portsAuthTokenStore,OnlineAccountRepository,RemoteSyncApi,RemoteShareApi,OnlineSessionRepository,SyncMetadataRepository,ShareInboxRepository.lib/application/use_cases.dart:AuthUseCases,SyncUseCases,ShareUseCases.lib/infrastructure/local/: cache profil, curseur sync, inbox partages, queue d'actions pending, mapping Drift.lib/infrastructure/remote/: adapter HTTP versserver/openapi.yaml, DTO wire, sérialisation JSON.lib/presentation/: Profil, auth, statut sync, partage sortant, inbox.
Le domaine/application ne dépend pas de http, flutter_secure_storage ni Drift.
Dépendances client retenues
- Tokens :
flutter_secure_storage.- Justification : stockage sécurisé cross-platform via mécanismes natifs ; adapté à access/refresh tokens.
- Ne pas stocker le profil affichable uniquement dedans.
- HTTP :
package:httpavecClientinjecté.- Justification : API simple, composable, facile à fake en tests, dépendance plus légère que Dio. Ajouter un wrapper pour baseUrl, auth bearer, JSON, timeouts et mapping d'erreurs.
Authentification client
Entité locale recommandée : UserAccountSession.
Champs :
idlocal.serverUserId.email.displayName?.avatarLocalUri?ouavatarRemoteUri?si disponible plus tard.isLoggedIn.createdAt,updatedAt,lastAuthenticatedAt?.
Stockage :
- tokens opaques :
flutter_secure_storagevia portAuthTokenStore. - profil/cache visible : Drift, pour affichage instantané hors ligne.
Use cases :
register(email, password)-> APIPOST /auth/register, stocke tokens si réponse connectante, cache profil, déclenche sync en arrière-plan.login(email, password)->POST /auth/login, stocke tokens, cache profil, déclenche sync en arrière-plan.logout()-> tentePOST /auth/logout, mais supprime tokens localement même si réseau KO ; conserve données locales et profil dernier connu si utile à l'historique d'affichage, avec état déconnecté.currentSession()-> lit cache local + présence token.
Erreurs :
- erreurs credentials : retournées au formulaire auth en erreur inline.
- réseau/timeout : erreur typée non intrusive ; jamais popup globale.
Synchronisation client
Ressources synchronisées v1 :
exerciseprogramworkoutTemplateworkoutHistorymediaAssetmetadata uniquement
Mapping push :
{
"resourceType": "exercise",
"clientId": "<local entity id>",
"schemaVersion": <local schema/payload version>,
"clientUpdatedAt": "<entity.metadata.updatedAt UTC>",
"deletedAt": "<entity.metadata.deletedAt|null>",
"payload": { "...": "snapshot complet local" }
}
Le payload doit être le snapshot local complet et autonome nécessaire pour reconstruire l'entité. Pour les exercices, il inclut les étapes ajoutées par #54. Le serveur ne l'interprète pas.
Source des changements à pousser : utiliser change_log, syncState, updatedAt, deletedAt, localRevision. Le gateway doit lire les entités concernées, assembler les payloads, appeler POST /sync/push ou POST /sync/exchange, puis marquer les items acceptés comme synced et enregistrer les mappings serveur si nécessaires.
Pull :
- stocker
serverCursorlocalement. - appeler
GET /sync/pull?since=<cursor>ouPOST /sync/exchange. - appliquer chaque item selon LWW côté client : si
server.clientUpdatedAtest plus récent quelocal.updatedAt, appliquer ; sinon ignorer localement. - soft delete distant : appliquer
deletedAtlocal sans hard delete. - mettre à jour le curseur seulement après transaction locale réussie.
Déclencheurs :
- après login/register : sync en arrière-plan, sans loader bloquant.
- au démarrage si connecté : tentative silencieuse.
- après mutation locale : planifier une sync arrière-plan courte/debounced.
- périodique quand app active : intervalle raisonnable, pas besoin de temps réel.
- manuel depuis Profil :
Synchroniser maintenant.
Échec sync :
- pas de popup ;
- statut neutre dans Profil ;
- retry différé ;
- l'app reste utilisable localement.
Drift / migration client
Le schéma local actuel est schemaVersion = 9. Si les tables online sont ajoutées, passer à schemaVersion = 10.
Tables recommandées :
online_account_sessions
- cache profil et état connecté/déconnecté.
- pas de tokens en clair.
sync_metadata
- singleton ou key/value :
serverCursor,lastSuccessfulSyncAt,lastAttemptAt,lastFailureAt,status,pendingPushCount?.
remote_resource_mappings
resourceType,clientId,serverId,serverUpdatedAt, unique(resourceType, clientId).
share_inbox_items
shareId, sender info,resourceType,payloadJson,status,createdAt,updatedAt, cache offline.
pending_share_actions
- queue locale pour share/send/accept/decline/revoke si réseau indisponible.
- champs :
id,actionType,shareId?,resourceType?,payloadJson?,recipientEmailsJson?,createdAt,lastAttemptAt?,attemptCount,status.
Partage client
Ports/use cases :
sendShare(resourceType, localResourceId, recipientEmails): construit payload snapshot local de Program ou WorkoutTemplate, appellePOST /shares; si réseau KO, met en queue.refreshInbox(): appelleGET /shares/inbox, cache les items.acceptShare(shareId): appellePOST /shares/{id}/accept.declineShare(shareId).revokeShare(shareId).
Acceptation : OpenAPI indique que AcceptShareResponse.createdResource renvoie directement un SyncedResourceItem. Le client peut donc importer immédiatement la ressource créée dans le stockage local, puis le prochain pull sert de convergence. Si l'accusé serveur ne peut pas partir mais que le payload inbox est déjà local, l'app peut créer une copie locale indépendante et mettre l'action accept en queue selon le choix d'implémentation, avec message neutre.
Invariant : un partage accepté devient une copie locale normale, non liée dynamiquement à l'expéditeur. Les doublons de noms sont autorisés.
Tickets créés
- #64
[DevBackend] Client online : session compte, stockage sécurisé et adapter API. - #65
[DevBackend] Sync client incrémentale LWW vers API serveur, dépend de #64. - #66
[DevBackend] Partage client : use cases, inbox cache et import local, dépend de #64 et #65. - #67
[DevFrontend] Profil et écrans auth optionnels, dépend de #64. - #68
[DevFrontend] Statut de synchronisation discret et action manuelle, dépend de #65 et #67. - #69
[DevFrontend] Partage sortant et boîte de réception, dépend de #66 et #67. - #70
[QA] Validation client online offline-first, dépend de #68 et #69.
Ordre recommandé : #64 -> #65 -> #66, en parallèle #67 après #64, puis #68 et #69, puis #70.