feat(r4.3): file d'écriture, verrou optimiste D2, Synchro & conflits

Serveur : updatedAt exposé au détail OT ; baseUpdatedAt optionnel sur
transition/coche/bilan → 409 « Conflit de version » contextualisé (qui,
quand) ; toute écriture secondaire (coche, bilan, commentaire, conso,
MO) fait avancer la version — sans version fournie, le web est
inchangé. ADR-003 : sécurité & protocole de routage mobile (qui vit où
sur l'appareil, purge complète à la déconnexion — correctif réel : la
file et le cache persisté survivaient au logout).

Mobile : file persistée AsyncStorage rejouée dans l'ordre — succès →
propagation de la version fraîche aux saisies restantes du même OT
(nos écritures ne se conflictent pas entre elles, un écart étranger
reste détecté) ; coupure → tout attend ; refus → CONFLIT, la file
s'arrête, l'humain tranche (voir l'OT / rejouer sur version à jour /
abandonner). Transitions, coches, bilan et photos (D5, compressées
~1600 px) passent par la file avec patch optimiste du cache ; écran
Synchro (badge tabbar ambre/rouge) ; préchargement parc + référentiels
(le bilan hors-ligne a ses vocabulaires).

Recette « mode avion » 13/13 en Expo web piloté : gestes hors-ligne →
3 en file → modification concurrente de Salma → conflit tranché →
serveur Terminé avec bilan. 17 tests jest-expo, 74 tests API.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pr-daaif
2026-07-17 09:50:02 +01:00
parent a22ea60f83
commit c8b3c1769a
26 changed files with 1218 additions and 84 deletions

View File

@@ -87,6 +87,9 @@ export const WorkOrderDetailSchema = WorkOrderSummarySchema.extend({
allowedTransitions: z.array(z.enum(WORK_ORDER_STATUSES)),
/** Ce qui bloque la clôture (vide = clôturable) — messages métier. */
closureBlockers: z.array(z.string()),
/** Version pour le verrou optimiste (D2 mobile) : la file d'écriture la
* capture à la lecture et la renvoie avec chaque saisie. */
updatedAt: z.iso.datetime(),
});
export type WorkOrderDetail = z.infer<typeof WorkOrderDetailSchema>;
@@ -104,6 +107,8 @@ export type WorkOrderCreate = z.infer<typeof WorkOrderCreateSchema>;
export const TransitionRequestSchema = z.object({
to: z.enum(WORK_ORDER_STATUSES),
comment: z.string().max(500).optional(),
/** Verrou optimiste (D2) : updatedAt lu — 409 si l'OT a changé depuis. */
baseUpdatedAt: z.iso.datetime().optional(),
});
export type TransitionRequest = z.infer<typeof TransitionRequestSchema>;
@@ -126,11 +131,13 @@ export const ReportUpsertSchema = z.object({
externalCauseId: z.uuid().nullable().optional(),
actionTakenId: z.uuid().nullable().optional(),
componentConcernedId: z.uuid().nullable().optional(),
baseUpdatedAt: z.iso.datetime().optional(), // verrou optimiste (D2)
});
export type ReportUpsert = z.infer<typeof ReportUpsertSchema>;
export const ChecklistPatchSchema = z.object({
state: z.enum(CHECKLIST_STATES),
baseUpdatedAt: z.iso.datetime().optional(), // verrou optimiste (D2)
});
export type ChecklistPatch = z.infer<typeof ChecklistPatchSchema>;