docs(ideai): mémoire architecture serveur, tickets #44/#45 en QA, cadrage #47-#53
Ajoute la note mémoire d'architecture serveur (sync/sharing), passe les tickets #44/#45 en QA, ajoute le cadrage du chantier serveur (tickets #47 à #53). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
200
.ideai/memory/gametime-server-architecture-sync-sharing.md
Normal file
200
.ideai/memory/gametime-server-architecture-sync-sharing.md
Normal file
@ -0,0 +1,200 @@
|
||||
---
|
||||
name: gametime-server-architecture-sync-sharing
|
||||
description: memory note gametime-server-architecture-sync-sharing
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
# GameTime — Architecture serveur headless, sync et partage
|
||||
|
||||
Décision serveur pour le ticket #46.
|
||||
|
||||
## Stack retenue
|
||||
|
||||
- Langage/runtime : Dart serveur.
|
||||
- Framework HTTP : `shelf` + `shelf_router`.
|
||||
- Base serveur : PostgreSQL.
|
||||
- Containerisation : Docker + `docker-compose.yaml` dans `server/`.
|
||||
|
||||
Raison : cohérence forte avec le client Flutter/Dart et les contrats métier existants, faible surface framework, testabilité correcte, packaging Docker simple avec image officielle Dart, PostgreSQL robuste pour comptes, tokens, ownership, sync incrémentale et partage ciblé.
|
||||
|
||||
Alternatives écartées pour la v1 :
|
||||
- FastAPI/Python : excellent écosystème, mais introduit un second langage et des DTO à dupliquer.
|
||||
- Node/NestJS : robuste mais plus lourd et moins cohérent avec l'existant.
|
||||
- Serverpod : intéressant en Dart, mais plus structurant/opinionated que nécessaire pour un serveur API headless simple.
|
||||
|
||||
## Architecture hexagonale serveur
|
||||
|
||||
Sous-répertoire dédié : `server/`.
|
||||
|
||||
Couches attendues :
|
||||
- `domain/` : entités serveur et invariants purs.
|
||||
- `application/` : use cases, ports repositories/services, DTO API indépendants de Shelf/PostgreSQL.
|
||||
- `infrastructure/postgres/` : adapters repositories PostgreSQL, migrations.
|
||||
- `infrastructure/security/` : hash password, token signing/verification, clock/id providers.
|
||||
- `api/` : routes Shelf, middleware auth, mapping request/response.
|
||||
- `bin/server.dart` : composition root.
|
||||
|
||||
Le domaine serveur ne dépend pas de Shelf, Docker ou PostgreSQL.
|
||||
|
||||
## Entités serveur
|
||||
|
||||
- `UserAccount` : id serveur, email/login unique, passwordHash, displayName?, createdAt, updatedAt, disabledAt?.
|
||||
- `AuthSession` / refresh token : id, userId, tokenHash, issuedAt, expiresAt, revokedAt?, userAgent?, deviceLabel?.
|
||||
- `SyncedResource` : ressource possédée par un utilisateur pour Exercise, Program, WorkoutTemplate, WorkoutHistory, MediaAsset metadata.
|
||||
- `Share` : partage ciblé vers un ou plusieurs comptes, jamais public ouvert.
|
||||
- `ShareRecipient` : user destinataire + statut `pending|accepted|declined|revoked`.
|
||||
|
||||
## Modèle sync serveur
|
||||
|
||||
Une table générique `synced_resources` est acceptable pour la v1 afin d'éviter de dupliquer tout le schéma Drift côté serveur. Champs recommandés :
|
||||
|
||||
- `server_id` UUID primary key.
|
||||
- `owner_user_id` FK users.
|
||||
- `resource_type` enum text : `exercise|program|workoutTemplate|workoutHistory|mediaAsset`.
|
||||
- `client_id` text : id local stable du client.
|
||||
- `payload_json` jsonb : snapshot de la ressource côté client.
|
||||
- `schema_version` int.
|
||||
- `client_updated_at` timestamptz.
|
||||
- `server_updated_at` timestamptz.
|
||||
- `deleted_at` timestamptz nullable.
|
||||
- `origin_device_id` text nullable.
|
||||
|
||||
Contraintes/index :
|
||||
- unique `(owner_user_id, resource_type, client_id)`.
|
||||
- index `(owner_user_id, resource_type, server_updated_at)`.
|
||||
- index `(owner_user_id, server_updated_at)` pour pull global.
|
||||
|
||||
Les médias binaires ne sont pas couverts en profondeur par #46 : stocker d'abord les métadonnées et prévoir le port `MediaObjectStore` pour ajout futur.
|
||||
|
||||
## Protocole sync LWW v1
|
||||
|
||||
Stratégie : last-write-wins simple basé sur `clientUpdatedAt`. En cas d'égalité, tie-breaker stable côté serveur (`serverUpdatedAt`, puis `serverId` si nécessaire). Pas de résolution interactive en v1.
|
||||
|
||||
Endpoints recommandés :
|
||||
|
||||
### `POST /sync/push`
|
||||
|
||||
Requête :
|
||||
|
||||
```json
|
||||
{
|
||||
"deviceId": "...",
|
||||
"items": [
|
||||
{
|
||||
"resourceType": "exercise",
|
||||
"clientId": "...",
|
||||
"schemaVersion": 3,
|
||||
"clientUpdatedAt": "2026-07-18T10:00:00Z",
|
||||
"deletedAt": null,
|
||||
"payload": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Réponse :
|
||||
|
||||
```json
|
||||
{
|
||||
"serverCursor": "...",
|
||||
"results": [
|
||||
{
|
||||
"resourceType": "exercise",
|
||||
"clientId": "...",
|
||||
"serverId": "...",
|
||||
"status": "accepted|ignoredOlder|conflictLwwApplied|error",
|
||||
"serverUpdatedAt": "2026-07-18T10:00:01Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /sync/pull?since=<cursor>`
|
||||
|
||||
Retourne toutes les ressources de l'utilisateur modifiées après le curseur serveur, soft deletes inclus.
|
||||
|
||||
Réponse :
|
||||
|
||||
```json
|
||||
{
|
||||
"serverCursor": "...",
|
||||
"items": [
|
||||
{
|
||||
"resourceType": "workoutTemplate",
|
||||
"clientId": "...",
|
||||
"serverId": "...",
|
||||
"schemaVersion": 3,
|
||||
"clientUpdatedAt": "...",
|
||||
"serverUpdatedAt": "...",
|
||||
"deletedAt": null,
|
||||
"payload": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /sync/exchange` optionnel
|
||||
|
||||
Combine push puis pull pour simplifier le futur client Flutter.
|
||||
|
||||
## Partage ciblé
|
||||
|
||||
Le partage n'est pas un lien public. Un utilisateur authentifié envoie un snapshot de `program` ou `workoutTemplate` à des destinataires identifiés.
|
||||
|
||||
Endpoints :
|
||||
- `POST /shares` : créer un partage vers un ou plusieurs comptes.
|
||||
- `GET /shares/inbox` : lister les partages reçus.
|
||||
- `POST /shares/{id}/accept` : importer/copier la ressource dans l'espace du destinataire.
|
||||
- `POST /shares/{id}/decline`.
|
||||
- `POST /shares/{id}/revoke` pour l'émetteur.
|
||||
|
||||
À l'acceptation, créer une nouvelle ressource syncable détenue par le destinataire avec nouveaux IDs côté serveur et payload importable côté client. Ne jamais modifier la ressource source de l'émetteur.
|
||||
|
||||
## Structure `server/`
|
||||
|
||||
Structure cible :
|
||||
|
||||
```text
|
||||
server/
|
||||
pubspec.yaml
|
||||
README.md
|
||||
Dockerfile
|
||||
docker-compose.yaml
|
||||
.env.example
|
||||
bin/
|
||||
server.dart
|
||||
lib/
|
||||
domain/
|
||||
application/
|
||||
infrastructure/
|
||||
postgres/
|
||||
security/
|
||||
config/
|
||||
api/
|
||||
migrations/
|
||||
scripts/
|
||||
push-gitea-image.sh
|
||||
test/
|
||||
```
|
||||
|
||||
`docker-compose.yaml` doit laisser libres via variables :
|
||||
- bind host/IP de la machine Docker.
|
||||
- port API exposé.
|
||||
- URL publique HTTPS derrière reverse proxy.
|
||||
- origine/IP reverse proxy autorisée si contrôle réseau ajouté.
|
||||
- paramètres PostgreSQL (`POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, volume).
|
||||
- secrets auth/JWT.
|
||||
|
||||
Le script `scripts/push-gitea-image.sh` ne doit contenir aucune URL/identifiant en dur. Il accepte registry/image/tag/user/token via variables d'environnement ou arguments.
|
||||
|
||||
## Tickets créés
|
||||
|
||||
- #47 `[Server] Scaffolding serveur Dart headless hexagonal`.
|
||||
- #48 `[Server] Auth comptes utilisateurs et tokens API`, dépend de #47.
|
||||
- #49 `[Server] Schéma PostgreSQL sync-ready GameTime`, dépend de #47.
|
||||
- #50 `[Server] API de synchronisation incrémentale LWW`, dépend de #48 et #49.
|
||||
- #51 `[Server] Partage ciblé de programmes et séances entre comptes`, dépend de #48 et #49.
|
||||
- #52 `[Server] Packaging Docker Compose et push Gitea Registry`, dépend de #47.
|
||||
- #53 `[Server] Tests API, contrats OpenAPI et vérification d'intégration`, dépend de #50, #51 et #52.
|
||||
|
||||
Ordre recommandé : #47, puis #48 et #49 en parallèle, puis #50 et #51, puis #52, puis #53.
|
||||
Reference in New Issue
Block a user