diff --git a/.ideai/memory/MEMORY.md b/.ideai/memory/MEMORY.md index ff1decd..43334f1 100644 --- a/.ideai/memory/MEMORY.md +++ b/.ideai/memory/MEMORY.md @@ -13,3 +13,4 @@ - [gametime-ux-exercise-default-targets](gametime-ux-exercise-default-targets.md) — memory note gametime-ux-exercise-default-targets - [gametime-ux-series-counter](gametime-ux-series-counter.md) — memory note gametime-ux-series-counter - [gametime-ux-exercise-media-viewer](gametime-ux-exercise-media-viewer.md) — memory note gametime-ux-exercise-media-viewer +- [gametime-server-architecture-sync-sharing](gametime-server-architecture-sync-sharing.md) — memory note gametime-server-architecture-sync-sharing diff --git a/.ideai/memory/gametime-server-architecture-sync-sharing.md b/.ideai/memory/gametime-server-architecture-sync-sharing.md new file mode 100644 index 0000000..21814db --- /dev/null +++ b/.ideai/memory/gametime-server-architecture-sync-sharing.md @@ -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=` + +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. \ No newline at end of file diff --git a/.ideai/tickets/44/carnet.md b/.ideai/tickets/44/carnet.md index 9003dc2..5e4b072 100644 --- a/.ideai/tickets/44/carnet.md +++ b/.ideai/tickets/44/carnet.md @@ -1,6 +1,6 @@ --- issueRef: "#44" -version: 4 -updatedBy: {"kind":"user"} -updatedAt: 1784379295672 +version: 5 +updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} +updatedAt: 1784406980453 --- diff --git a/.ideai/tickets/44/issue.md b/.ideai/tickets/44/issue.md index 99bafc3..ce401aa 100644 --- a/.ideai/tickets/44/issue.md +++ b/.ideai/tickets/44/issue.md @@ -2,15 +2,15 @@ id: "c63e88a9-5868-4bd0-a71b-ebc4e7e6552a" number: 44 title: "[Bug] Je veux pouvoir mettre un score par défaut à 0." -status: "open" +status: "qa" priority: "medium" sprint: "abc4f969-b169-45f7-988c-daeeab762201" links: [] agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}] createdBy: {"kind":"user"} -updatedBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} createdAt: 1784379259225 -updatedAt: 1784379295672 -version: 4 +updatedAt: 1784406980453 +version: 5 --- Je veux pouvoir renseigner un score par défaut à 0 (je ne parle pas du nombre de répétition qui doit lui rester comme actuellement). \ No newline at end of file diff --git a/.ideai/tickets/45/carnet.md b/.ideai/tickets/45/carnet.md index d92b242..a5ebb2a 100644 --- a/.ideai/tickets/45/carnet.md +++ b/.ideai/tickets/45/carnet.md @@ -1,6 +1,6 @@ --- issueRef: "#45" -version: 4 -updatedBy: {"kind":"user"} -updatedAt: 1784379490098 +version: 5 +updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} +updatedAt: 1784406980638 --- diff --git a/.ideai/tickets/45/issue.md b/.ideai/tickets/45/issue.md index 2c05818..aa61b1b 100644 --- a/.ideai/tickets/45/issue.md +++ b/.ideai/tickets/45/issue.md @@ -2,15 +2,15 @@ id: "2f075be4-bcb8-4fdd-89fc-2f897e97bde4" number: 45 title: "[Bug] Affichage de répétition en séance à 0" -status: "open" +status: "qa" priority: "medium" sprint: "abc4f969-b169-45f7-988c-daeeab762201" links: [] agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}] createdBy: {"kind":"user"} -updatedBy: {"kind":"user"} +updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} createdAt: 1784379414910 -updatedAt: 1784379490098 -version: 4 +updatedAt: 1784406980638 +version: 5 --- Quand je suis en séance sur un exercice qui demande un nombre de répétition, l'affichage m'affiche 0 alors que le nombre de répétition est set à une valeur > 0 dans l'exercice. Il devrait être à la même valeur \ No newline at end of file diff --git a/.ideai/tickets/47/carnet.md b/.ideai/tickets/47/carnet.md new file mode 100644 index 0000000..be7ee11 --- /dev/null +++ b/.ideai/tickets/47/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#47" +version: 1 +updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} +updatedAt: 1784411985578 +--- diff --git a/.ideai/tickets/47/issue.md b/.ideai/tickets/47/issue.md new file mode 100644 index 0000000..03cb49c --- /dev/null +++ b/.ideai/tickets/47/issue.md @@ -0,0 +1,16 @@ +--- +id: "f3283335-8f96-40aa-88a4-bcc58a530ad2" +number: 47 +title: "[Server] Scaffolding serveur Dart headless hexagonal" +status: "open" +priority: "low" +sprint: null +links: [{"target":"#46","kind":"relatesTo"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} +updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} +createdAt: 1784411985578 +updatedAt: 1784411985578 +version: 1 +--- +Créer le sous-répertoire `server/` à la racine avec une application Dart headless basée sur `shelf`/`shelf_router`, structure hexagonale (`domain/`, `application/`, `infrastructure/`, `api/`), configuration env, healthcheck et tests de base. Inclure `Dockerfile`, `.env.example`, `docker-compose.yaml` paramétrable et un README d'exploitation minimal. Le serveur doit écouter derrière reverse proxy sans HTTPS interne obligatoire. \ No newline at end of file diff --git a/.ideai/tickets/48/carnet.md b/.ideai/tickets/48/carnet.md new file mode 100644 index 0000000..0847406 --- /dev/null +++ b/.ideai/tickets/48/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#48" +version: 1 +updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} +updatedAt: 1784411990980 +--- diff --git a/.ideai/tickets/48/issue.md b/.ideai/tickets/48/issue.md new file mode 100644 index 0000000..1524466 --- /dev/null +++ b/.ideai/tickets/48/issue.md @@ -0,0 +1,16 @@ +--- +id: "6df36ec4-94cd-438b-a2ee-10c022f6848a" +number: 48 +title: "[Server] Auth comptes utilisateurs et tokens API" +status: "open" +priority: "low" +sprint: null +links: [{"target":"#46","kind":"relatesTo"},{"target":"#47","kind":"dependsOn"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} +updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} +createdAt: 1784411990980 +updatedAt: 1784411990980 +version: 1 +--- +Implémenter le domaine serveur `UserAccount` et `AuthSession` : création de compte, connexion, hash de mot de passe Argon2id ou bcrypt, émission de tokens d'accès + refresh tokens, middleware d'auth API, révocation/logout. Stockage PostgreSQL. Endpoints attendus : `POST /auth/register`, `POST /auth/login`, `POST /auth/refresh`, `POST /auth/logout`, `GET /me`. Aucun écran client dans ce ticket. \ No newline at end of file diff --git a/.ideai/tickets/49/carnet.md b/.ideai/tickets/49/carnet.md new file mode 100644 index 0000000..b251bcc --- /dev/null +++ b/.ideai/tickets/49/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#49" +version: 1 +updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} +updatedAt: 1784411997184 +--- diff --git a/.ideai/tickets/49/issue.md b/.ideai/tickets/49/issue.md new file mode 100644 index 0000000..004d7c5 --- /dev/null +++ b/.ideai/tickets/49/issue.md @@ -0,0 +1,16 @@ +--- +id: "c73b39be-061c-44b4-b5c3-f482878fe58d" +number: 49 +title: "[Server] Schéma PostgreSQL sync-ready GameTime" +status: "open" +priority: "low" +sprint: null +links: [{"target":"#46","kind":"relatesTo"},{"target":"#47","kind":"dependsOn"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} +updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} +createdAt: 1784411997184 +updatedAt: 1784411997184 +version: 1 +--- +Créer le modèle serveur PostgreSQL pour les ressources synchronisables : Exercise, Program, WorkoutTemplate, WorkoutHistory, MediaAsset metadata, plus tables d'ownership par utilisateur. Chaque ressource doit stocker `ownerUserId`, `clientId`, `serverId`, `payloadJson`, `clientUpdatedAt`, `serverUpdatedAt`, `deletedAt`, `schemaVersion`, `originDeviceId`. Prévoir contraintes d'unicité `(owner_user_id, resource_type, client_id)`, index pull incrémental `(owner_user_id, resource_type, server_updated_at)`, migrations et tests repository. \ No newline at end of file diff --git a/.ideai/tickets/50/carnet.md b/.ideai/tickets/50/carnet.md new file mode 100644 index 0000000..3e46a85 --- /dev/null +++ b/.ideai/tickets/50/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#50" +version: 1 +updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} +updatedAt: 1784412001974 +--- diff --git a/.ideai/tickets/50/issue.md b/.ideai/tickets/50/issue.md new file mode 100644 index 0000000..ba2c8c9 --- /dev/null +++ b/.ideai/tickets/50/issue.md @@ -0,0 +1,16 @@ +--- +id: "983aa02b-7577-4fde-b77a-12355df0efdb" +number: 50 +title: "[Server] API de synchronisation incrémentale LWW" +status: "open" +priority: "low" +sprint: null +links: [{"target":"#46","kind":"relatesTo"},{"target":"#48","kind":"dependsOn"},{"target":"#49","kind":"dependsOn"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} +updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} +createdAt: 1784412001974 +updatedAt: 1784412001974 +version: 1 +--- +Implémenter les ports/use cases et endpoints de sync incrémentale authentifiée. Endpoints : `POST /sync/push`, `GET /sync/pull?since=...`, optionnel `POST /sync/exchange`. Stratégie v1 last-write-wins basée sur `clientUpdatedAt` puis tie-breaker `serverUpdatedAt`/`serverId`. Supporter upsert, soft delete, réponses par item (`accepted`, `ignoredOlder`, `conflictLwwApplied`, `error`) et cursor serveur monotone. Ne pas intégrer le client Flutter dans ce ticket. \ No newline at end of file diff --git a/.ideai/tickets/51/carnet.md b/.ideai/tickets/51/carnet.md new file mode 100644 index 0000000..30d042f --- /dev/null +++ b/.ideai/tickets/51/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#51" +version: 1 +updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} +updatedAt: 1784412007538 +--- diff --git a/.ideai/tickets/51/issue.md b/.ideai/tickets/51/issue.md new file mode 100644 index 0000000..39c6d95 --- /dev/null +++ b/.ideai/tickets/51/issue.md @@ -0,0 +1,16 @@ +--- +id: "850ce975-99c3-4ee4-8d39-759ecc55139c" +number: 51 +title: "[Server] Partage ciblé de programmes et séances entre comptes" +status: "open" +priority: "low" +sprint: null +links: [{"target":"#46","kind":"relatesTo"},{"target":"#48","kind":"dependsOn"},{"target":"#49","kind":"dependsOn"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} +updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} +createdAt: 1784412007538 +updatedAt: 1784412007538 +version: 1 +--- +Implémenter le domaine de partage ciblé : `Share`, destinataires par compte utilisateur, statut `pending|accepted|declined|revoked`, payload snapshot autonome de Program ou WorkoutTemplate. Endpoints : `POST /shares`, `GET /shares/inbox`, `POST /shares/{id}/accept`, `POST /shares/{id}/decline`, `POST /shares/{id}/revoke`. À l'acceptation, créer une copie importable dans l'espace du destinataire, sans lien public ouvert. \ No newline at end of file diff --git a/.ideai/tickets/52/carnet.md b/.ideai/tickets/52/carnet.md new file mode 100644 index 0000000..60fd561 --- /dev/null +++ b/.ideai/tickets/52/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#52" +version: 1 +updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} +updatedAt: 1784412011625 +--- diff --git a/.ideai/tickets/52/issue.md b/.ideai/tickets/52/issue.md new file mode 100644 index 0000000..0348345 --- /dev/null +++ b/.ideai/tickets/52/issue.md @@ -0,0 +1,16 @@ +--- +id: "28ebc668-c209-4e6a-b123-252a38a3c784" +number: 52 +title: "[Server] Packaging Docker Compose et push Gitea Registry" +status: "open" +priority: "low" +sprint: null +links: [{"target":"#46","kind":"relatesTo"},{"target":"#47","kind":"dependsOn"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} +updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} +createdAt: 1784412011625 +updatedAt: 1784412011625 +version: 1 +--- +Finaliser l'exploitation container : `server/docker-compose.yaml` paramétrable via `.env` pour host bind, port API interne/externe, origine reverse proxy, URL publique HTTPS, config PostgreSQL, secrets, volumes persistants. Ajouter `server/scripts/push-gitea-image.sh` acceptant registry, image, tag, username/token via variables d'environnement ou arguments, sans valeur en dur. Documenter le flux build/push/run. \ No newline at end of file diff --git a/.ideai/tickets/53/carnet.md b/.ideai/tickets/53/carnet.md new file mode 100644 index 0000000..f6c492d --- /dev/null +++ b/.ideai/tickets/53/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#53" +version: 1 +updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} +updatedAt: 1784412015509 +--- diff --git a/.ideai/tickets/53/issue.md b/.ideai/tickets/53/issue.md new file mode 100644 index 0000000..53bb79d --- /dev/null +++ b/.ideai/tickets/53/issue.md @@ -0,0 +1,16 @@ +--- +id: "d5fce252-f3c4-499f-ad0a-d3c81ea13305" +number: 53 +title: "[Server] Tests API, contrats OpenAPI et vérification d'intégration" +status: "open" +priority: "low" +sprint: null +links: [{"target":"#46","kind":"relatesTo"},{"target":"#50","kind":"dependsOn"},{"target":"#51","kind":"dependsOn"},{"target":"#52","kind":"dependsOn"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} +updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} +createdAt: 1784412015509 +updatedAt: 1784412015509 +version: 1 +--- +Ajouter la couverture de validation serveur : tests unitaires domain/application, tests repository PostgreSQL, tests API auth/sync/share, fixtures de payload GameTime, vérification Docker Compose locale. Produire un contrat OpenAPI ou équivalent documentant auth, sync et partage pour les futurs tickets d'intégration Flutter. \ No newline at end of file diff --git a/.ideai/tickets/counter.json b/.ideai/tickets/counter.json index f501ae3..fadf2d9 100644 --- a/.ideai/tickets/counter.json +++ b/.ideai/tickets/counter.json @@ -1,3 +1,3 @@ { - "nextNumber": 47 + "nextNumber": 54 } \ No newline at end of file diff --git a/.ideai/tickets/index.json b/.ideai/tickets/index.json index 49d83e6..09483fb 100644 --- a/.ideai/tickets/index.json +++ b/.ideai/tickets/index.json @@ -501,25 +501,25 @@ "issueRef": "#44", "path": "44", "title": "[Bug] Je veux pouvoir mettre un score par défaut à 0.", - "status": "open", + "status": "qa", "priority": "medium", "sprint": "abc4f969-b169-45f7-988c-daeeab762201", "assignedAgentIds": [ "57695b92-24d0-4876-837c-76116e70a6ae" ], - "updatedAt": 1784379295672 + "updatedAt": 1784406980453 }, { "issueRef": "#45", "path": "45", "title": "[Bug] Affichage de répétition en séance à 0", - "status": "open", + "status": "qa", "priority": "medium", "sprint": "abc4f969-b169-45f7-988c-daeeab762201", "assignedAgentIds": [ "57695b92-24d0-4876-837c-76116e70a6ae" ], - "updatedAt": 1784379490098 + "updatedAt": 1784406980638 }, { "issueRef": "#46", @@ -532,6 +532,76 @@ "57695b92-24d0-4876-837c-76116e70a6ae" ], "updatedAt": 1784380502263 + }, + { + "issueRef": "#47", + "path": "47", + "title": "[Server] Scaffolding serveur Dart headless hexagonal", + "status": "open", + "priority": "low", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784411985578 + }, + { + "issueRef": "#48", + "path": "48", + "title": "[Server] Auth comptes utilisateurs et tokens API", + "status": "open", + "priority": "low", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784411990980 + }, + { + "issueRef": "#49", + "path": "49", + "title": "[Server] Schéma PostgreSQL sync-ready GameTime", + "status": "open", + "priority": "low", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784411997184 + }, + { + "issueRef": "#50", + "path": "50", + "title": "[Server] API de synchronisation incrémentale LWW", + "status": "open", + "priority": "low", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784412001974 + }, + { + "issueRef": "#51", + "path": "51", + "title": "[Server] Partage ciblé de programmes et séances entre comptes", + "status": "open", + "priority": "low", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784412007538 + }, + { + "issueRef": "#52", + "path": "52", + "title": "[Server] Packaging Docker Compose et push Gitea Registry", + "status": "open", + "priority": "low", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784412011625 + }, + { + "issueRef": "#53", + "path": "53", + "title": "[Server] Tests API, contrats OpenAPI et vérification d'intégration", + "status": "open", + "priority": "low", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784412015509 } ] } \ No newline at end of file diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..7eaad89 --- /dev/null +++ b/server/README.md @@ -0,0 +1,51 @@ +# GameTime server + +Headless Dart server for future GameTime account, sync and sharing features. +This package is separate from the Flutter app at the repository root. + +## Local run + +Install dependencies from this directory: + +```bash +dart pub get +``` + +Run the server: + +```bash +dart run bin/server.dart +``` + +The HTTP server binds to `0.0.0.0` and reads `PORT` from the environment. +When `PORT` is not set, it listens on `8080`. + +```bash +PORT=9090 dart run bin/server.dart +``` + +Healthcheck: + +```bash +curl http://localhost:8080/health +``` + +Expected response: + +```json +{"status":"ok"} +``` + +## Scope + +Ticket #47 only scaffolds the Dart server, the hexagonal directory layout and +the `/health` endpoint. + +Upcoming tickets will fill the empty adapters and use cases: + +- #48: user authentication and API tokens. +- #49: PostgreSQL schema and repositories. +- #50: incremental sync API. +- #51: targeted sharing. +- #52: Docker and registry packaging. +- #53: API, contract and integration tests. diff --git a/server/analysis_options.yaml b/server/analysis_options.yaml new file mode 100644 index 0000000..572dd23 --- /dev/null +++ b/server/analysis_options.yaml @@ -0,0 +1 @@ +include: package:lints/recommended.yaml diff --git a/server/bin/server.dart b/server/bin/server.dart new file mode 100644 index 0000000..9375207 --- /dev/null +++ b/server/bin/server.dart @@ -0,0 +1,17 @@ +import 'dart:io'; + +import 'package:gametime_server/api/router.dart'; +import 'package:shelf/shelf_io.dart' as shelf_io; + +Future main(List arguments) async { + final port = int.tryParse(Platform.environment['PORT'] ?? '') ?? 8080; + final server = await shelf_io.serve( + buildApiHandler(), + InternetAddress.anyIPv4, + port, + ); + + print( + 'GameTime server listening on ${server.address.address}:${server.port}', + ); +} diff --git a/server/lib/api/router.dart b/server/lib/api/router.dart new file mode 100644 index 0000000..dce7fcd --- /dev/null +++ b/server/lib/api/router.dart @@ -0,0 +1,16 @@ +import 'dart:convert'; + +import 'package:shelf/shelf.dart'; +import 'package:shelf_router/shelf_router.dart'; + +Handler buildApiHandler() { + final router = Router() + ..get('/health', (Request request) { + return Response.ok( + jsonEncode({'status': 'ok'}), + headers: {'content-type': 'application/json'}, + ); + }); + + return const Pipeline().addMiddleware(logRequests()).addHandler(router.call); +} diff --git a/server/lib/application/README.md b/server/lib/application/README.md new file mode 100644 index 0000000..d26d3be --- /dev/null +++ b/server/lib/application/README.md @@ -0,0 +1,5 @@ +# Application + +Server use cases, ports and API DTOs independent from Shelf and PostgreSQL. + +Concrete adapters are wired from `bin/server.dart`. diff --git a/server/lib/domain/README.md b/server/lib/domain/README.md new file mode 100644 index 0000000..7260459 --- /dev/null +++ b/server/lib/domain/README.md @@ -0,0 +1,6 @@ +# Domain + +Pure server domain entities and invariants. + +This layer must not import Shelf, PostgreSQL adapters, Docker configuration or +other infrastructure concerns. diff --git a/server/lib/infrastructure/postgres/README.md b/server/lib/infrastructure/postgres/README.md new file mode 100644 index 0000000..5afddc0 --- /dev/null +++ b/server/lib/infrastructure/postgres/README.md @@ -0,0 +1,5 @@ +# PostgreSQL infrastructure + +Placeholder for PostgreSQL repository adapters. + +The schema and concrete persistence code are planned for ticket #49. diff --git a/server/lib/infrastructure/security/README.md b/server/lib/infrastructure/security/README.md new file mode 100644 index 0000000..8297849 --- /dev/null +++ b/server/lib/infrastructure/security/README.md @@ -0,0 +1,5 @@ +# Security infrastructure + +Placeholder for password hashing, token signing and token verification. + +Authentication is planned for ticket #48. diff --git a/server/migrations/README.md b/server/migrations/README.md new file mode 100644 index 0000000..0d9e9b4 --- /dev/null +++ b/server/migrations/README.md @@ -0,0 +1,4 @@ +# Migrations + +PostgreSQL migrations will be added with the server persistence schema in +ticket #49. diff --git a/server/pubspec.lock b/server/pubspec.lock new file mode 100644 index 0000000..e14a0b3 --- /dev/null +++ b/server/pubspec.lock @@ -0,0 +1,405 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "9a3386eea899815698dd55995277cf7cb8572ee52b399a6edfb7ae2b50e5fc19" + url: "https://pub.dev" + source: hosted + version: "105.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "62993bed6eadbe9596c5c20d5c167e7bc563c5fe266657a04ddeb93bdb84f4c9" + url: "https://pub.dev" + source: hosted + version: "14.1.0" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "956a3de0725ca232ad353565a8290d3357592bf4250f6f298a185e2d949c5d3d" + url: "https://pub.dev" + source: hosted + version: "1.15.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + http_methods: + dependency: transitive + description: + name: http_methods + sha256: "6bccce8f1ec7b5d701e7921dca35e202d425b57e317ba1a37f2638590e29e566" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + lints: + dependency: "direct dev" + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" + url: "https://pub.dev" + source: hosted + version: "0.12.20" + meta: + dependency: transitive + description: + name: meta + sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" + url: "https://pub.dev" + source: hosted + version: "1.19.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: ffcf4cf3d6c0b74ac43708d9f56625506e8a68aa935abe9d267a7330f320eb5d + url: "https://pub.dev" + source: hosted + version: "3.0.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + shelf: + dependency: "direct main" + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_router: + dependency: "direct main" + description: + name: shelf_router + sha256: f5e5d492440a7fb165fe1e2e1a623f31f734d3370900070b2b1e0d0428d59864 + url: "https://pub.dev" + source: hosted + version: "1.1.4" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test: + dependency: "direct dev" + description: + name: test + sha256: "0d5ba5602ec3baa28c8ce365e1efc5575969c765f45c554a3e167dc7945b9c30" + url: "https://pub.dev" + source: hosted + version: "1.31.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "475610b2aa23c19687cce2961e44b0cc57cafe220f67c2b80201231b2a07fbe7" + url: "https://pub.dev" + source: hosted + version: "0.7.13" + test_core: + dependency: transitive + description: + name: test_core + sha256: a39c204a4fc7a7ccb04a2b985e359fda3cc37e45e0b8ac61c3fb1a05aa832132 + url: "https://pub.dev" + source: hosted + version: "0.6.19" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.11.0 <4.0.0" diff --git a/server/pubspec.yaml b/server/pubspec.yaml new file mode 100644 index 0000000..99945d0 --- /dev/null +++ b/server/pubspec.yaml @@ -0,0 +1,14 @@ +name: gametime_server +description: Headless Dart server for GameTime sync and sharing. +publish_to: 'none' + +environment: + sdk: ^3.10.0 + +dependencies: + shelf: ^1.4.2 + shelf_router: ^1.1.4 + +dev_dependencies: + lints: ^6.0.0 + test: ^1.25.14 diff --git a/server/scripts/README.md b/server/scripts/README.md new file mode 100644 index 0000000..53046b2 --- /dev/null +++ b/server/scripts/README.md @@ -0,0 +1,5 @@ +# Scripts + +Operational scripts for the server package. + +Docker image publishing is planned for ticket #52. diff --git a/server/test/health_test.dart b/server/test/health_test.dart new file mode 100644 index 0000000..5093cce --- /dev/null +++ b/server/test/health_test.dart @@ -0,0 +1,18 @@ +import 'dart:convert'; + +import 'package:gametime_server/api/router.dart'; +import 'package:shelf/shelf.dart'; +import 'package:test/test.dart'; + +void main() { + test('GET /health returns ok status', () async { + final handler = buildApiHandler(); + final response = await handler( + Request('GET', Uri.parse('http://localhost/health')), + ); + + expect(response.statusCode, 200); + expect(response.headers['content-type'], contains('application/json')); + expect(jsonDecode(await response.readAsString()), {'status': 'ok'}); + }); +}