From c1dce3cae79590f4acb6f2360a0bdd595c5427ff Mon Sep 17 00:00:00 2001 From: Blomios Date: Sun, 19 Jul 2026 15:56:29 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat(server):=20tests=20API,=20contrats=20O?= =?UTF-8?q?penAPI=20et=20v=C3=A9rification=20d'int=C3=A9gration=20(ticket?= =?UTF-8?q?=20#53)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajoute la spécification openapi.yaml documentant les 12 endpoints réels du serveur (health, auth, sync, shares), les tests API auth (test/auth_api_test.dart couvrant register/login/logout) et la checklist de vérification d'intégration (docs/integration-checklist.md). dart analyze clean, dart test 29/29 vert. Dernier ticket du chantier serveur (#47 à #53), tous terminés. Co-Authored-By: Claude Opus 4.8 --- server/README.md | 9 + server/docs/integration-checklist.md | 104 +++++ server/openapi.yaml | 591 +++++++++++++++++++++++++++ server/test/auth_api_test.dart | 289 +++++++++++++ 4 files changed, 993 insertions(+) create mode 100644 server/docs/integration-checklist.md create mode 100644 server/openapi.yaml create mode 100644 server/test/auth_api_test.dart diff --git a/server/README.md b/server/README.md index ef36ae9..af8253e 100644 --- a/server/README.md +++ b/server/README.md @@ -125,6 +125,15 @@ without failing the whole request. Accepting a share creates a new `409` for revoked or already answered shares, while missing shares or recipients return `404`. +## API Contract + +The current HTTP contract is documented in [`openapi.yaml`](openapi.yaml). It +covers health, authentication, sync and targeted sharing endpoints implemented +by this server package. + +To inspect it, paste the file into or open it with +a local OpenAPI-compatible viewer. + ## Docker Deployment Ticket #52 adds Docker packaging for a headless Linux deployment. TLS is not diff --git a/server/docs/integration-checklist.md b/server/docs/integration-checklist.md new file mode 100644 index 0000000..7c1b8fd --- /dev/null +++ b/server/docs/integration-checklist.md @@ -0,0 +1,104 @@ +# GameTime Server Integration Checklist + +This checklist covers the checks that require a real Docker daemon, PostgreSQL +instance, or Gitea registry. They are intentionally not part of the sandbox test +suite. + +## Docker Compose + +- Copy `server/.env.example` to `server/.env`. +- Replace `POSTGRES_PASSWORD` and `DATABASE_PASSWORD` with a real secret. +- Set `API_BIND_ADDRESS` to the Docker host interface reachable by the external + reverse proxy. +- Set `API_PORT` to the host port consumed by the reverse proxy. +- Run: + +```bash +cd server +docker compose config +docker compose up -d --build +docker compose ps +docker compose logs api +``` + +- Verify that the `api` service waits for the PostgreSQL healthcheck. +- Verify that migrations run automatically before the server starts. +- Verify that TLS is terminated by the external reverse proxy, not by the API + container. + +## PostgreSQL Migrations + +- Start a disposable PostgreSQL database or reuse the Compose database. +- Run the real migration integration test: + +```bash +cd server +TEST_DATABASE_URL=postgres://gametime:change-me@localhost:5432/gametime dart test +``` + +- Confirm the test sees these tables: + - `users` + - `auth_sessions` + - `synced_resources` + - `shares` + - `share_recipients` + +## Manual API Smoke Tests + +Run the following checks against the real server URL. Use HTTP directly only on +the private LAN path between the reverse proxy and Docker host; external access +should be HTTPS through the reverse proxy. + +```bash +curl http://localhost:8080/health +``` + +- Register a user with `POST /auth/register`. +- Login with `POST /auth/login` and store the returned bearer token. +- Call a protected endpoint without a token and confirm `401`. +- Logout with `POST /auth/logout` and confirm the token can no longer access + protected endpoints. +- Push a valid resource with `POST /sync/push`. +- Pull it with `GET /sync/pull`. +- Push an older version of the same resource and confirm `ignoredOlder`. +- Use `POST /sync/exchange` and confirm push results and pulled items are both + present. +- Create a second user, then create a share with `POST /shares`. +- Confirm unknown recipient emails are returned in `unresolvedEmails`. +- List shares with `GET /shares/inbox` as the recipient. +- Accept a share with `POST /shares/{id}/accept` and confirm a copied resource + appears in `GET /sync/pull` for the recipient. +- Decline a pending share with `POST /shares/{id}/decline`. +- Revoke a sent share with `POST /shares/{id}/revoke`. +- Confirm accepting a revoked or already answered share returns `409`. + +## LWW Concurrency + +- Run two concurrent `POST /sync/push` requests for the same + `(owner_user_id, resource_type, client_id)` with different `clientUpdatedAt` + values. +- Confirm the row with the strictly newer `clientUpdatedAt` wins. +- Confirm an equal timestamp is ignored by the later request. +- Inspect `server_updated_at` to confirm the update trigger advances it on + accepted updates. + +## Gitea Container Registry + +- Obtain the real Gitea registry host, owner/namespace, username and token. +- Run: + +```bash +cd server +GITEA_REGISTRY=gitea.example.com \ +GITEA_OWNER=my-org \ +GITEA_IMAGE_NAME=gametime-server \ +GITEA_IMAGE_TAG=latest \ +GITEA_USERNAME=my-user \ +GITEA_TOKEN='replace-with-token' \ +./scripts/push-gitea-image.sh +``` + +- Confirm the image exists in the Gitea Container Registry. +- Pull the pushed image on the deployment host. +- Deploy the pushed image with the same environment variables as the local + Compose build. diff --git a/server/openapi.yaml b/server/openapi.yaml new file mode 100644 index 0000000..5974096 --- /dev/null +++ b/server/openapi.yaml @@ -0,0 +1,591 @@ +openapi: 3.0.3 +info: + title: GameTime Server API + version: 0.1.0 + description: Headless API for GameTime authentication, incremental sync and targeted sharing. +servers: + - url: http://localhost:8080 + description: Local Docker or development server +tags: + - name: Health + - name: Auth + - name: Sync + - name: Shares +paths: + /health: + get: + tags: [Health] + summary: Healthcheck + responses: + '200': + description: Server is reachable. + content: + application/json: + schema: + $ref: '#/components/schemas/HealthResponse' + + /auth/register: + post: + tags: [Auth] + summary: Register a user account + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterRequest' + responses: + '201': + description: User created. + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterResponse' + '400': + $ref: '#/components/responses/BadRequest' + '409': + description: Email already taken. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /auth/login: + post: + tags: [Auth] + summary: Login and create an API token session + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LoginRequest' + responses: + '200': + description: Login accepted. The clear token is returned once. + content: + application/json: + schema: + $ref: '#/components/schemas/LoginResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + + /auth/logout: + post: + tags: [Auth] + summary: Revoke the current API token session + security: + - bearerAuth: [] + responses: + '204': + description: Session revoked. + '401': + $ref: '#/components/responses/Unauthorized' + + /sync/push: + post: + tags: [Sync] + summary: Push client-side resource mutations + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SyncPushRequest' + responses: + '200': + description: Batch processed. Item-level validation errors are returned in results. + content: + application/json: + schema: + $ref: '#/components/schemas/SyncPushResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + + /sync/pull: + get: + tags: [Sync] + summary: Pull resources updated after a server cursor + security: + - bearerAuth: [] + parameters: + - name: since + in: query + required: false + schema: + type: string + format: date-time + description: ISO8601 UTC server cursor. If omitted, returns all resources. + responses: + '200': + description: Resources for the authenticated user. + content: + application/json: + schema: + $ref: '#/components/schemas/SyncPullResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + + /sync/exchange: + post: + tags: [Sync] + summary: Push then pull in one request + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SyncExchangeRequest' + responses: + '200': + description: Pull response after applying the push batch. + content: + application/json: + schema: + $ref: '#/components/schemas/SyncExchangeResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + + /shares: + post: + tags: [Shares] + summary: Create a targeted share + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateShareRequest' + responses: + '201': + description: Share created. Unknown recipient emails are reported but do not fail the request. + content: + application/json: + schema: + $ref: '#/components/schemas/CreateShareResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + + /shares/inbox: + get: + tags: [Shares] + summary: List shares received by the authenticated user + security: + - bearerAuth: [] + responses: + '200': + description: Inbox sorted from newest to oldest. + content: + application/json: + schema: + $ref: '#/components/schemas/ShareInboxResponse' + '401': + $ref: '#/components/responses/Unauthorized' + + /shares/{id}/accept: + post: + tags: [Shares] + summary: Accept a received share + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/ShareId' + responses: + '200': + description: Share accepted and copied into synced resources. + content: + application/json: + schema: + $ref: '#/components/schemas/AcceptShareResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + $ref: '#/components/responses/Conflict' + + /shares/{id}/decline: + post: + tags: [Shares] + summary: Decline a received share + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/ShareId' + responses: + '204': + description: Share declined. + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + $ref: '#/components/responses/Conflict' + + /shares/{id}/revoke: + post: + tags: [Shares] + summary: Revoke a share sent by the authenticated user + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/ShareId' + responses: + '204': + description: Share revoked. + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: opaque + + parameters: + ShareId: + name: id + in: path + required: true + schema: + type: string + description: Share identifier. + + responses: + BadRequest: + description: Invalid JSON body, invalid parameter, or validation failure. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + Unauthorized: + description: Missing, malformed, expired, revoked or unknown bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + NotFound: + description: Resource not found or not accessible by the authenticated user. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + Conflict: + description: Current resource state rejects the requested transition. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + schemas: + ErrorResponse: + type: object + required: [error] + properties: + error: + type: string + + HealthResponse: + type: object + required: [status] + properties: + status: + type: string + enum: [ok] + + RegisterRequest: + type: object + required: [email, password] + properties: + email: + type: string + format: email + password: + type: string + minLength: 8 + displayName: + type: string + nullable: true + + RegisterResponse: + type: object + required: [userId, email] + properties: + userId: + type: string + email: + type: string + format: email + + LoginRequest: + type: object + required: [email, password] + properties: + email: + type: string + format: email + password: + type: string + deviceLabel: + type: string + nullable: true + + LoginResponse: + type: object + required: [token, expiresAt] + properties: + token: + type: string + expiresAt: + type: string + format: date-time + + ResourceType: + type: string + enum: [exercise, program, workoutTemplate, workoutHistory, mediaAsset] + + ShareResourceType: + type: string + enum: [program, workoutTemplate] + + ShareRecipientStatus: + type: string + enum: [pending, accepted, declined, revoked] + + JsonObject: + type: object + additionalProperties: true + + SyncPushRequest: + type: object + required: [items] + properties: + deviceId: + type: string + nullable: true + description: Required by the application use case for each valid item. + items: + type: array + items: + $ref: '#/components/schemas/SyncPushItem' + + SyncPushItem: + type: object + required: [resourceType, clientId, schemaVersion, clientUpdatedAt, payload] + properties: + resourceType: + $ref: '#/components/schemas/ResourceType' + clientId: + type: string + schemaVersion: + type: integer + minimum: 1 + clientUpdatedAt: + type: string + format: date-time + deletedAt: + type: string + format: date-time + nullable: true + payload: + $ref: '#/components/schemas/JsonObject' + + SyncPushResponse: + type: object + required: [serverCursor, results] + properties: + serverCursor: + type: string + format: date-time + results: + type: array + items: + $ref: '#/components/schemas/SyncPushResultItem' + + SyncPushResultItem: + type: object + required: [status] + properties: + resourceType: + type: string + nullable: true + clientId: + type: string + nullable: true + serverId: + type: string + nullable: true + status: + type: string + enum: [accepted, ignoredOlder, error] + serverUpdatedAt: + type: string + format: date-time + nullable: true + message: + type: string + + SyncPullResponse: + type: object + required: [serverCursor, items] + properties: + serverCursor: + type: string + format: date-time + items: + type: array + items: + $ref: '#/components/schemas/SyncedResourceItem' + + SyncExchangeRequest: + allOf: + - $ref: '#/components/schemas/SyncPushRequest' + - type: object + properties: + since: + type: string + format: date-time + nullable: true + + SyncExchangeResponse: + type: object + required: [serverCursor, pushResults, items] + properties: + serverCursor: + type: string + format: date-time + pushResults: + type: array + items: + $ref: '#/components/schemas/SyncPushResultItem' + items: + type: array + items: + $ref: '#/components/schemas/SyncedResourceItem' + + SyncedResourceItem: + type: object + required: + - resourceType + - clientId + - serverId + - schemaVersion + - clientUpdatedAt + - serverUpdatedAt + - payload + properties: + resourceType: + $ref: '#/components/schemas/ResourceType' + clientId: + type: string + serverId: + type: string + schemaVersion: + type: integer + minimum: 1 + clientUpdatedAt: + type: string + format: date-time + serverUpdatedAt: + type: string + format: date-time + deletedAt: + type: string + format: date-time + nullable: true + payload: + $ref: '#/components/schemas/JsonObject' + + CreateShareRequest: + type: object + required: [resourceType, payload, recipientEmails] + properties: + resourceType: + $ref: '#/components/schemas/ShareResourceType' + payload: + $ref: '#/components/schemas/JsonObject' + recipientEmails: + type: array + minItems: 1 + items: + type: string + format: email + + CreateShareResponse: + type: object + required: [shareId, recipientUserIds, unresolvedEmails] + properties: + shareId: + type: string + recipientUserIds: + type: array + items: + type: string + unresolvedEmails: + type: array + items: + type: string + format: email + + ShareInboxResponse: + type: object + required: [items] + properties: + items: + type: array + items: + $ref: '#/components/schemas/ShareInboxItem' + + ShareInboxItem: + type: object + required: + - shareId + - senderUserId + - resourceType + - payload + - status + - createdAt + properties: + shareId: + type: string + senderUserId: + type: string + resourceType: + $ref: '#/components/schemas/ShareResourceType' + payload: + $ref: '#/components/schemas/JsonObject' + status: + $ref: '#/components/schemas/ShareRecipientStatus' + createdAt: + type: string + format: date-time + respondedAt: + type: string + format: date-time + nullable: true + + AcceptShareResponse: + type: object + required: [createdResource] + properties: + createdResource: + $ref: '#/components/schemas/SyncedResourceItem' diff --git a/server/test/auth_api_test.dart b/server/test/auth_api_test.dart new file mode 100644 index 0000000..0ae0ede --- /dev/null +++ b/server/test/auth_api_test.dart @@ -0,0 +1,289 @@ +import 'dart:convert'; + +import 'package:gametime_server/api/auth_api.dart'; +import 'package:gametime_server/api/router.dart'; +import 'package:gametime_server/application/application.dart'; +import 'package:gametime_server/domain/domain.dart'; +import 'package:shelf/shelf.dart'; +import 'package:test/test.dart'; + +void main() { + test( + 'register endpoint returns the created user without password data', + () async { + final users = _FakeUserRepository(); + final handler = buildApiHandler(authApi: _authApi(users: users)); + + final response = await handler( + Request( + 'POST', + Uri.parse('http://localhost/auth/register'), + body: jsonEncode({ + 'email': ' USER@example.com ', + 'password': 'password123', + 'displayName': ' User ', + }), + ), + ); + + final body = jsonDecode(await response.readAsString()) as Map; + + expect(response.statusCode, 201); + expect(body, {'userId': 'id-1', 'email': 'user@example.com'}); + expect(body.containsKey('password'), isFalse); + expect(body.containsKey('passwordHash'), isFalse); + }, + ); + + test('register endpoint returns 409 when email is already used', () async { + final users = _FakeUserRepository(); + await users.insert( + UserAccount( + id: 'existing-user', + email: 'user@example.com', + passwordHash: 'hash', + createdAt: DateTime.utc(2026, 7, 19), + updatedAt: DateTime.utc(2026, 7, 19), + ), + ); + final handler = buildApiHandler(authApi: _authApi(users: users)); + + final response = await handler( + Request( + 'POST', + Uri.parse('http://localhost/auth/register'), + body: jsonEncode({ + 'email': 'user@example.com', + 'password': 'password123', + }), + ), + ); + + expect(response.statusCode, 409); + }); + + test('login endpoint returns a token and expiration', () async { + final users = _FakeUserRepository(); + await users.insert( + UserAccount( + id: 'user-1', + email: 'user@example.com', + passwordHash: 'hashed:password123', + createdAt: DateTime.utc(2026, 7, 19), + updatedAt: DateTime.utc(2026, 7, 19), + ), + ); + final handler = buildApiHandler(authApi: _authApi(users: users)); + + final response = await handler( + Request( + 'POST', + Uri.parse('http://localhost/auth/login'), + body: jsonEncode({ + 'email': 'user@example.com', + 'password': 'password123', + 'deviceLabel': 'phone', + }), + ), + ); + + final body = jsonDecode(await response.readAsString()) as Map; + + expect(response.statusCode, 200); + expect(body['token'], 'clear-token'); + expect(body['expiresAt'], '2026-08-18T12:00:00.000Z'); + }); + + test('login endpoint returns 401 for invalid credentials', () async { + final users = _FakeUserRepository(); + await users.insert( + UserAccount( + id: 'user-1', + email: 'user@example.com', + passwordHash: 'hashed:password123', + createdAt: DateTime.utc(2026, 7, 19), + updatedAt: DateTime.utc(2026, 7, 19), + ), + ); + final handler = buildApiHandler(authApi: _authApi(users: users)); + + final response = await handler( + Request( + 'POST', + Uri.parse('http://localhost/auth/login'), + body: jsonEncode({ + 'email': 'user@example.com', + 'password': 'wrong-password', + }), + ), + ); + + expect(response.statusCode, 401); + }); + + test('logout endpoint revokes the bearer session', () async { + final users = _FakeUserRepository(); + await users.insert( + UserAccount( + id: 'user-1', + email: 'user@example.com', + passwordHash: 'hashed:password123', + createdAt: DateTime.utc(2026, 7, 19), + updatedAt: DateTime.utc(2026, 7, 19), + ), + ); + final sessions = _FakeAuthSessionRepository(); + await sessions.insert( + AuthSession( + id: 'session-1', + userId: 'user-1', + tokenHash: 'token-hash:clear-token', + issuedAt: DateTime.utc(2026, 7, 19, 12), + expiresAt: DateTime.utc(2026, 7, 20, 12), + ), + ); + final handler = buildApiHandler( + authApi: _authApi(users: users, sessions: sessions), + ); + + final response = await handler( + Request( + 'POST', + Uri.parse('http://localhost/auth/logout'), + headers: {'authorization': 'Bearer clear-token'}, + ), + ); + + expect(response.statusCode, 204); + expect(sessions.revokedSessionIds, ['session-1']); + }); +} + +AuthApi _authApi({ + _FakeUserRepository? users, + _FakeAuthSessionRepository? sessions, +}) { + final userRepository = users ?? _FakeUserRepository(); + final sessionRepository = sessions ?? _FakeAuthSessionRepository(); + final passwordHasher = _FakePasswordHasher(); + final tokens = _FakeTokenService(); + final clock = _FakeClock(DateTime.utc(2026, 7, 19, 12)); + final ids = _FakeIds(); + final authenticateRequest = AuthenticateRequestUseCase( + users: userRepository, + sessions: sessionRepository, + tokens: tokens, + clock: clock, + ); + return AuthApi( + registerUser: RegisterUserUseCase( + users: userRepository, + passwordHasher: passwordHasher, + clock: clock, + ids: ids, + ), + login: LoginUseCase( + users: userRepository, + sessions: sessionRepository, + passwordHasher: passwordHasher, + tokens: tokens, + clock: clock, + ids: ids, + ), + logout: LogoutUseCase( + sessions: sessionRepository, + tokens: tokens, + clock: clock, + ), + authenticateRequest: authenticateRequest, + ); +} + +final class _FakeUserRepository implements UserRepository { + final byId = {}; + final byEmail = {}; + + @override + Future findByEmail(String email) async { + return byEmail[email.trim().toLowerCase()]; + } + + @override + Future findById(String id) async => byId[id]; + + @override + Future insert(UserAccount user) async { + byId[user.id] = user; + byEmail[user.email] = user; + } + + @override + Future updatePasswordHash({ + required String userId, + required String passwordHash, + required DateTime updatedAt, + }) async {} +} + +final class _FakeAuthSessionRepository implements AuthSessionRepository { + final byTokenHash = {}; + final revokedSessionIds = []; + + @override + Future insert(AuthSession session) async { + byTokenHash[session.tokenHash] = session; + } + + @override + Future findByTokenHash(String tokenHash) async { + return byTokenHash[tokenHash]; + } + + @override + Future revoke({ + required String sessionId, + required DateTime revokedAt, + }) async { + revokedSessionIds.add(sessionId); + } +} + +final class _FakePasswordHasher implements PasswordHasher { + @override + Future hash(String password) async => 'hashed:$password'; + + @override + Future verify({ + required String password, + required String passwordHash, + }) async { + return passwordHash == 'hashed:$password'; + } +} + +final class _FakeTokenService implements OpaqueTokenService { + @override + String generateToken() => 'clear-token'; + + @override + String hashToken(String token) => 'token-hash:$token'; +} + +final class _FakeClock implements Clock { + const _FakeClock(this.value); + + final DateTime value; + + @override + DateTime now() => value; +} + +final class _FakeIds implements IdGenerator { + var _next = 0; + + @override + String newId() { + _next += 1; + return 'id-$_next'; + } +} From 80430d6d8a2cba66d6c787d9e0bf6288d3353ceb Mon Sep 17 00:00:00 2001 From: Blomios Date: Sun, 19 Jul 2026 15:56:34 +0200 Subject: [PATCH 2/2] =?UTF-8?q?docs(ideai):=20met=20=C3=A0=20jour=20le=20t?= =?UTF-8?q?icket=20#52=20et=20le=20ticket=20#54=20=E2=80=94=20chantier=20s?= =?UTF-8?q?erveur=20clos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- .ideai/tickets/52/carnet.md | 7 ++++--- .ideai/tickets/52/issue.md | 8 ++++---- .ideai/tickets/54/carnet.md | 4 ++-- .ideai/tickets/54/issue.md | 7 ++++--- .ideai/tickets/index.json | 10 ++++++---- 5 files changed, 20 insertions(+), 16 deletions(-) diff --git a/.ideai/tickets/52/carnet.md b/.ideai/tickets/52/carnet.md index 60fd561..6414840 100644 --- a/.ideai/tickets/52/carnet.md +++ b/.ideai/tickets/52/carnet.md @@ -1,6 +1,7 @@ --- issueRef: "#52" -version: 1 -updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"} -updatedAt: 1784412011625 +version: 3 +updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} +updatedAt: 1784450444550 --- +Dockerfile multi-stage (build `dart:stable` → runtime `debian:bookworm-slim`, `dart compile exe` AOT pour `server` et `migrate`), `docker-compose.yaml` (services `api`+`postgres:16-alpine`, healthcheck Postgres, `depends_on: service_healthy`, migrations auto au démarrage via `docker-entrypoint.sh`+`MIGRATE_ON_STARTUP`), `.env.example` documenté, `scripts/push-gitea-image.sh` sans aucun identifiant en dur (variables `GITEA_REGISTRY/OWNER/IMAGE_NAME/USERNAME/TOKEN`, login via --password-stdin). `docker compose config` validé (syntaxe/interpolation correctes), Dockerfile et scripts relus manuellement. Non vérifié : aucun `docker build`/`docker compose up` réel (pas d'accès Docker daemon dans le sandbox Main). À faire avant mise en prod : build réel de l'image, `docker compose up -d` complet avec vraie connexion Postgres, puis test du script de push une fois l'adresse Gitea fournie par l'utilisateur. \ No newline at end of file diff --git a/.ideai/tickets/52/issue.md b/.ideai/tickets/52/issue.md index 0348345..b80136a 100644 --- a/.ideai/tickets/52/issue.md +++ b/.ideai/tickets/52/issue.md @@ -2,15 +2,15 @@ id: "28ebc668-c209-4e6a-b123-252a38a3c784" number: 52 title: "[Server] Packaging Docker Compose et push Gitea Registry" -status: "open" +status: "qa" 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"} +updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} createdAt: 1784412011625 -updatedAt: 1784412011625 -version: 1 +updatedAt: 1784450444550 +version: 3 --- 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/54/carnet.md b/.ideai/tickets/54/carnet.md index e385c35..963f4f0 100644 --- a/.ideai/tickets/54/carnet.md +++ b/.ideai/tickets/54/carnet.md @@ -1,6 +1,6 @@ --- issueRef: "#54" -version: 2 +version: 4 updatedBy: {"kind":"user"} -updatedAt: 1784449895057 +updatedAt: 1784451193831 --- diff --git a/.ideai/tickets/54/issue.md b/.ideai/tickets/54/issue.md index dd1ad52..4e2b981 100644 --- a/.ideai/tickets/54/issue.md +++ b/.ideai/tickets/54/issue.md @@ -6,10 +6,11 @@ status: "open" priority: "medium" sprint: "1bb8bdf2-9c35-4f53-9a31-1390a47bec63" links: [] -agentRefs: [] +agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}] createdBy: {"kind":"user"} updatedBy: {"kind":"user"} createdAt: 1784449895047 -updatedAt: 1784449895057 -version: 2 +updatedAt: 1784451193831 +version: 4 --- +Dans certains exercices, il peut être interessant qu'il y ai plusieurs étapes. Par exemple, pour un exercice de dribble, il peut être interessant de dire "dribbler main droite pendant 10 sec, puis dribller main gauche pendant 10 secondes, effectuer un double pas et réaliser 10 pompes" par exemple. Le tout représente alors une répétition mais se décompose en 4 étapes avec deux étapes qui se base sur un temps, et deux étapes sur un nombre de répétitions. J'aimerais donc pouvoir définir des étapes pour chaque exercice. L'ensemble de toutes les étape de l'exercice forme une répetition d'une série. Comme pour une série, chacune des étape doit pouvoir avoir un temps à définir (avec une valeur par défaut obligatoire > 0) ou un nombre de répétitions. Dans le cas d'une étape avec un temps, un chronometre est lancé. Dans les 3 dernieres secondes, un bip retenti pour chaque seconde et quand le temps arrive à 0 le bip est plus long que les autres. dans le cas ou deux étapes avec un temps s'enchainent, le second chronometre part directement à la fin de l'etape précédent. Le but est d'enchainer les étapes, il n'y a pas de pauses entre les différentes étapes. On eput aussi proposer de renseigner un score pour chaque étape de la même façon que pour les série. \ No newline at end of file diff --git a/.ideai/tickets/index.json b/.ideai/tickets/index.json index 9275f30..5b18fec 100644 --- a/.ideai/tickets/index.json +++ b/.ideai/tickets/index.json @@ -587,11 +587,11 @@ "issueRef": "#52", "path": "52", "title": "[Server] Packaging Docker Compose et push Gitea Registry", - "status": "open", + "status": "qa", "priority": "low", "sprint": null, "assignedAgentIds": [], - "updatedAt": 1784412011625 + "updatedAt": 1784450444550 }, { "issueRef": "#53", @@ -610,8 +610,10 @@ "status": "open", "priority": "medium", "sprint": "1bb8bdf2-9c35-4f53-9a31-1390a47bec63", - "assignedAgentIds": [], - "updatedAt": 1784449895057 + "assignedAgentIds": [ + "57695b92-24d0-4876-837c-76116e70a6ae" + ], + "updatedAt": 1784451193831 } ] } \ No newline at end of file