merge(main): correctifs critiques serveur non lançable et score par défaut à 0 (tickets #71, #74)

Fusionne feature/bugfix-71-74-critical — pub get OK, build_runner OK
(migration schemaVersion 12→13), analyze propre, 133/133 tests verts,
build APK debug validé. Fix #74 reproduit et confirmé manuellement en
isolant le scénario Docker exact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 07:50:40 +02:00
7 changed files with 142 additions and 12 deletions

View File

@ -1,6 +1,6 @@
---
issueRef: "#69"
version: 1
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
updatedAt: 1784488364646
version: 2
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedAt: 1784525029743
---

View File

@ -2,15 +2,15 @@
id: "05482a20-83a6-44da-b0c6-d065a5a1ffba"
number: 69
title: "[DevFrontend] Partage sortant et boîte de réception"
status: "open"
status: "qa"
priority: "medium"
sprint: null
links: [{"target":"#63","kind":"relatesTo"},{"target":"#66","kind":"dependsOn"},{"target":"#67","kind":"dependsOn"}]
agentRefs: [{"agentId":"9933c93a-b8a1-4164-a3bb-7063fdad747d","role":"assigned"}]
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: 1784488364646
updatedAt: 1784488364646
version: 1
updatedAt: 1784525029743
version: 2
---
Ajouter l'action `Partager` dans les écrans programme/séance enregistrés, formulaire email destinataire, états déconnecté et messages discrets. Ajouter `Profil > Partages reçus`, liste inbox, accepter/refuser, import local des programmes/séances reçus. Les erreurs réseau restent inline/neutres; accepter un partage rend la copie disponible offline.

View File

@ -775,13 +775,13 @@
"issueRef": "#69",
"path": "69",
"title": "[DevFrontend] Partage sortant et boîte de réception",
"status": "open",
"status": "qa",
"priority": "medium",
"sprint": null,
"assignedAgentIds": [
"9933c93a-b8a1-4164-a3bb-7063fdad747d"
],
"updatedAt": 1784488364646
"updatedAt": 1784525029743
},
{
"issueRef": "#70",

View File

@ -46,7 +46,7 @@ final class AppDatabase extends _$AppDatabase {
}
@override
int get schemaVersion => 12;
int get schemaVersion => 13;
@override
MigrationStrategy get migration {
@ -97,6 +97,9 @@ final class AppDatabase extends _$AppDatabase {
if (from < 12) {
await _migrateToSchema12(migrator);
}
if (from < 13) {
await _migrateToSchema13();
}
await _createIndexes();
},
beforeOpen: (details) async {
@ -359,4 +362,111 @@ extension on AppDatabase {
await migrator.createTable(shareInboxItems);
await migrator.createTable(pendingShareActions);
}
Future<void> _migrateToSchema13() async {
await customStatement('PRAGMA foreign_keys = OFF');
await customStatement('''
CREATE TABLE exercises_new (
id TEXT NOT NULL PRIMARY KEY,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
deleted_at INTEGER,
schema_version INTEGER NOT NULL DEFAULT 1,
sync_state TEXT NOT NULL CHECK (sync_state IN ('localOnly', 'dirty', 'synced', 'deleted')),
local_revision INTEGER NOT NULL CHECK (local_revision >= 0),
origin_device_id TEXT NOT NULL,
future_owner_profile_id TEXT,
last_synced_at INTEGER,
remote_revision TEXT,
name TEXT NOT NULL,
description TEXT,
icon_media_id TEXT REFERENCES media_assets(id),
video_media_id TEXT REFERENCES media_assets(id),
has_time_measure INTEGER NOT NULL CHECK (has_time_measure IN (0, 1)),
has_reps_measure INTEGER NOT NULL CHECK (has_reps_measure IN (0, 1)),
has_score_measure INTEGER NOT NULL CHECK (has_score_measure IN (0, 1)),
score_input_mode TEXT NOT NULL DEFAULT 'manual',
score_label TEXT,
score_unit TEXT,
default_target_time_seconds INTEGER,
default_target_reps INTEGER,
default_target_score REAL,
default_target_score_time_ms INTEGER,
archived_at INTEGER,
CHECK (length(trim(origin_device_id)) > 0),
CHECK (length(trim(name)) > 0),
CHECK (has_time_measure OR has_reps_measure OR has_score_measure),
CHECK (score_input_mode IN ('manual', 'stopwatch')),
CHECK (has_score_measure OR score_input_mode = 'manual'),
CHECK (score_input_mode != 'manual' OR NOT has_score_measure OR (score_label IS NOT NULL AND length(trim(score_label)) > 0 AND score_unit IS NOT NULL AND length(trim(score_unit)) > 0)),
CHECK (default_target_time_seconds IS NULL OR default_target_time_seconds > 0),
CHECK (default_target_reps IS NULL OR default_target_reps > 0),
CHECK (default_target_score IS NULL OR default_target_score >= 0),
CHECK (default_target_score_time_ms IS NULL OR default_target_score_time_ms > 0),
CHECK (future_owner_profile_id IS NULL OR length(trim(future_owner_profile_id)) > 0),
CHECK (remote_revision IS NULL OR length(trim(remote_revision)) > 0)
)
''');
await customStatement('''
INSERT INTO exercises_new (
id,
created_at,
updated_at,
deleted_at,
schema_version,
sync_state,
local_revision,
origin_device_id,
future_owner_profile_id,
last_synced_at,
remote_revision,
name,
description,
icon_media_id,
video_media_id,
has_time_measure,
has_reps_measure,
has_score_measure,
score_input_mode,
score_label,
score_unit,
default_target_time_seconds,
default_target_reps,
default_target_score,
default_target_score_time_ms,
archived_at
)
SELECT
id,
created_at,
updated_at,
deleted_at,
schema_version,
sync_state,
local_revision,
origin_device_id,
future_owner_profile_id,
last_synced_at,
remote_revision,
name,
description,
icon_media_id,
video_media_id,
has_time_measure,
has_reps_measure,
has_score_measure,
score_input_mode,
score_label,
score_unit,
default_target_time_seconds,
default_target_reps,
default_target_score,
default_target_score_time_ms,
archived_at
FROM exercises
''');
await customStatement('DROP TABLE exercises');
await customStatement('ALTER TABLE exercises_new RENAME TO exercises');
await customStatement('PRAGMA foreign_keys = ON');
}
}

View File

@ -204,7 +204,7 @@ class Exercises extends SyncableTable {
'CHECK (default_target_time_seconds IS NULL OR '
'default_target_time_seconds > 0)',
'CHECK (default_target_reps IS NULL OR default_target_reps > 0)',
'CHECK (default_target_score IS NULL OR default_target_score > 0)',
'CHECK (default_target_score IS NULL OR default_target_score >= 0)',
'CHECK (default_target_score_time_ms IS NULL OR '
'default_target_score_time_ms > 0)',
];

View File

@ -6,7 +6,8 @@ COPY pubspec.* ./
RUN dart pub get
COPY . .
RUN dart compile exe bin/server.dart -o build/server \
RUN mkdir -p build \
&& dart compile exe bin/server.dart -o build/server \
&& dart compile exe bin/migrate.dart -o build/migrate
FROM debian:bookworm-slim AS runtime

View File

@ -79,6 +79,25 @@ void main() {
expect(changes.every((change) => change.entityType == 'Exercise'), isTrue);
});
test('exercise repository accepts zero manual default score', () async {
final exercise = Exercise(
metadata: _metadata('exercise-score-zero', DateTime.utc(2026, 7, 17, 12)),
name: 'Score nul',
hasTimeMeasure: false,
hasRepsMeasure: false,
hasScoreMeasure: true,
scoreLabel: 'Score',
scoreUnit: 'pts',
defaultTargetScore: 0,
);
await exerciseRepository.save(exercise);
final restored = await exerciseRepository.findById(exercise.metadata.id);
expect(restored, isNotNull);
expect(restored!.defaultTargetScore, 0);
});
test('exercise repository round-trips configured steps', () async {
final now = DateTime.utc(2026, 7, 17, 12);
final exercise = Exercise(