chore(wip): consolidation intermédiaire multi-tickets (sprints Statistiques, UI, Bug resolution, Serveur-client)

Regroupe l'état de travail en cours réalisé dans un même worktree sur
plusieurs tickets/sprints (#85, #136, #145, #155-160, #162-164),
mélangeant des tickets QA et inProgress. Ne constitue pas une feature
terminée : commit de sauvegarde avant triage/split par ticket en
branches feature/* dédiées. Exclut les dossiers d'environnement de
build locaux et le heap dump parasite (.gitignore mis à jour).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 16:48:54 +02:00
parent 58272e354a
commit 917777e18b
279 changed files with 13546 additions and 674 deletions

View File

@ -18,18 +18,20 @@ final class PostgresShareRepository implements ShareRepository {
await connection.execute(
Sql.named('''
INSERT INTO shares (
id, sender_user_id, resource_type, payload_json, created_at,
revoked_at
id, sender_user_id, share_kind, pack_name, resource_type,
payload_json, created_at, revoked_at
)
VALUES (
@id::uuid, @sender_user_id::uuid, @resource_type,
@payload_json::jsonb, @created_at, @revoked_at
@id::uuid, @sender_user_id::uuid, @share_kind, @pack_name,
@resource_type, @payload_json::jsonb, @created_at, @revoked_at
)
'''),
parameters: {
'id': share.id,
'sender_user_id': share.senderUserId,
'resource_type': share.resourceType.wireName,
'share_kind': share.kind.wireName,
'pack_name': share.packName,
'resource_type': share.resourceType?.wireName,
'payload_json': jsonEncode(share.payloadJson),
'created_at': share.createdAt,
'revoked_at': share.revokedAt,
@ -66,6 +68,8 @@ final class PostgresShareRepository implements ShareRepository {
SELECT
s.id AS share_id,
s.sender_user_id,
s.share_kind,
s.pack_name,
s.resource_type,
s.payload_json,
s.created_at,
@ -91,8 +95,8 @@ final class PostgresShareRepository implements ShareRepository {
Future<Share?> findShareById(String shareId) async {
final result = await connection.execute(
Sql.named('''
SELECT id, sender_user_id, resource_type, payload_json, created_at,
revoked_at
SELECT id, sender_user_id, share_kind, pack_name, resource_type,
payload_json, created_at, revoked_at
FROM shares
WHERE id = @id::uuid
LIMIT 1
@ -146,6 +150,33 @@ final class PostgresShareRepository implements ShareRepository {
);
}
@override
Future<List<SyncedResource>> acceptShare({
required String recipientId,
required DateTime respondedAt,
required List<SyncedResource> resources,
}) {
return connection.runTx((session) async {
final written = <SyncedResource>[];
for (final resource in resources) {
written.add(await _upsertResource(session, resource));
}
await session.execute(
Sql.named('''
UPDATE share_recipients
SET status = @status, responded_at = @responded_at
WHERE id = @id::uuid
'''),
parameters: {
'id': recipientId,
'status': ShareRecipientStatus.accepted.wireName,
'responded_at': respondedAt.toUtc(),
},
);
return written;
});
}
@override
Future<void> revokeShare({
required String shareId,
@ -173,9 +204,11 @@ Share _shareFromValues(Map<String, Object?> values) {
return Share(
id: _stringValue(values['share_id'] ?? values['id']),
senderUserId: _stringValue(values['sender_user_id']),
resourceType: SyncedResourceType.parse(
_stringValue(values['resource_type']),
kind: ShareKind.parse(
_stringValue(values['share_kind'] ?? ShareKind.single.wireName),
),
packName: values['pack_name'] as String?,
resourceType: _nullableResourceType(values['resource_type']),
payloadJson: _payloadValue(values['payload_json']),
createdAt: _dateTimeValue(values['created_at']),
revokedAt: _nullableDateTimeValue(values['revoked_at']),
@ -192,6 +225,90 @@ ShareRecipient _recipientFromValues(Map<String, Object?> values) {
);
}
Future<SyncedResource> _upsertResource(
Session session,
SyncedResource resource,
) async {
final result = await session.execute(
Sql.named('''
WITH upserted AS (
INSERT INTO synced_resources (
server_id, owner_user_id, resource_type, client_id, payload_json,
schema_version, client_updated_at, deleted_at, origin_device_id
)
VALUES (
@server_id::uuid, @owner_user_id::uuid, @resource_type, @client_id,
@payload_json::jsonb, @schema_version, @client_updated_at,
@deleted_at, @origin_device_id
)
ON CONFLICT (owner_user_id, resource_type, client_id)
DO UPDATE SET
payload_json = EXCLUDED.payload_json,
schema_version = EXCLUDED.schema_version,
client_updated_at = EXCLUDED.client_updated_at,
deleted_at = EXCLUDED.deleted_at,
origin_device_id = EXCLUDED.origin_device_id
WHERE synced_resources.client_updated_at < EXCLUDED.client_updated_at
RETURNING
server_id, owner_user_id, resource_type, client_id, payload_json,
schema_version, client_updated_at, server_updated_at, deleted_at,
origin_device_id
)
SELECT * FROM upserted
UNION ALL
SELECT
existing.server_id, existing.owner_user_id, existing.resource_type,
existing.client_id, existing.payload_json, existing.schema_version,
existing.client_updated_at, existing.server_updated_at,
existing.deleted_at, existing.origin_device_id
FROM synced_resources existing
WHERE existing.owner_user_id = @owner_user_id::uuid
AND existing.resource_type = @resource_type
AND existing.client_id = @client_id
AND NOT EXISTS (SELECT 1 FROM upserted)
LIMIT 1
'''),
parameters: {
'server_id': resource.serverId,
'owner_user_id': resource.ownerUserId,
'resource_type': resource.resourceType.wireName,
'client_id': resource.clientId,
'payload_json': jsonEncode(resource.payloadJson),
'schema_version': resource.schemaVersion,
'client_updated_at': resource.clientUpdatedAt,
'deleted_at': resource.deletedAt,
'origin_device_id': resource.originDeviceId,
},
);
return _resourceFromValues(
result.single.toColumnMap() as Map<String, Object?>,
);
}
SyncedResource _resourceFromValues(Map<String, Object?> values) {
return SyncedResource(
serverId: _stringValue(values['server_id']),
ownerUserId: _stringValue(values['owner_user_id']),
resourceType: SyncedResourceType.parse(
_stringValue(values['resource_type']),
),
clientId: _stringValue(values['client_id']),
payloadJson: _payloadValue(values['payload_json']),
schemaVersion: values['schema_version'] as int,
clientUpdatedAt: _dateTimeValue(values['client_updated_at']),
serverUpdatedAt: _dateTimeValue(values['server_updated_at']),
deletedAt: _nullableDateTimeValue(values['deleted_at']),
originDeviceId: values['origin_device_id'] as String?,
);
}
SyncedResourceType? _nullableResourceType(Object? value) {
if (value == null) {
return null;
}
return SyncedResourceType.parse(_stringValue(value));
}
Map<String, Object?> _payloadValue(Object? value) {
if (value is Map<String, Object?>) {
return value;