Sync Agenda v2 — Plan d’implémentation (serveur puis mobile)
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans. Steps use checkbox (
- [ ]) syntax. TDD. Ne pas committer sauf demande explicite de l’utilisateur.
Goal : Exposer RDV + réservations/indispos en API/sync Projectiaon, puis pont Card2vcf Room ↔ Agenda Android multi-calendriers (Mes RDV + ressources sélectionnées), sans écran agenda in-app.
Architecture : Étendre sync v1 (tombstones, status/pull, SyncOp, LWW). Serveur d’abord. Mobile : Paramètres crée/lie des calendriers système ; event ressource = réservation Active ; 409 → refresh + dialog annulation.
Tech Stack : Rust/Axum (Projectiaon) ; Kotlin/Room/CalendarContract/OkHttp (Card2vcf).
Spec : docs/superpowers/specs/2026-07-22-sync-agenda-rdv-ressources-v2-design.md
Hors scope : UI demande/réclamation mobile ; SyncAdapter ; calendriers cloud GMS ; écran agenda Card2vcf.
Fichiers (carte)
Serveur (Projectiaon)
| Fichier | Rôle |
|---|---|
src/modeles.rs | mis_a_jour_le sur RendezVous, Reservation, Indisponibilite |
src/handlers/rdv.rs + chemins UI | Poser mis_a_jour_le à chaque mutation HTML |
src/services/ressources.rs | Idem résa/indispo ; mapper conflit → signal API 409 |
src/handlers/api.rs | Routes /api/rdv, /api/ressources*, OpenAPI |
src/services/sync.rs | Delta rdv/reservations/indisponibilites + tombstones + query ressources |
src/services/sync.rs TypeTombstone | Variantes Rdv, Reservation, Indisponibilite |
tests/api_agenda_http.rs | Tests HTTP Bearer CRUD + sync + 409 |
docs/guide-administrateur.md | Doc endpoints agenda mobile |
Mobile (EBII_mobileVCF)
| Fichier | Rôle |
|---|---|
AndroidManifest.xml | READ_CALENDAR, WRITE_CALENDAR |
sync/CalendarBindingsStore.kt | Prefs calendriers liés |
sync/CalendarBridge.kt | CRUD events / calendriers locaux |
data/RdvEntity.kt + Dao | Room RDV |
data/ReservationEntity.kt + Dao | Room résas |
data/IndisponibiliteEntity.kt + Dao | Room indispos lecture |
CrmDatabase.kt | version ↑ (3), entities |
sync/SyncModels.kt + AilianceApi* | DTOs + méthodes API |
sync/SyncEngine.kt | Agenda↔Room↔API, conflit 409 |
ui/settings/* | Section calendriers |
ui/sync/SyncBanner.kt + ViewModels | Bandeau « local en avance » + dialog conflit |
| Tests unitaires | Bridge mapping, SyncEngine conflit, bindings |
Partie A — Serveur
Task S1 — mis_a_jour_le sur RDV / Reservation / Indisponibilite
Files :
-
Modify:
Projectiaon/src/modeles.rs(RendezVous,Reservation,Indisponibilite) -
Modify: handlers/services qui écrivent ces entités (
handlers/rdv.rs,services/rdv.rs,services/ressources.rs,handlers/ressources.rs,handlers/planning.rssi besoin) -
Test: étendre tests unitaires existants ressources/rdv ou nouveau test modèle
-
Step 1 : Ajouter aux 3 structs :
#[serde(default)] pub mis_a_jour_le: Option<DateTime<Utc>>,
-
Step 2 : Helper
fn toucher_horodatage(m: &mut Option<DateTime<Utc>>)ou poserSome(Utc::now())à chaqueecrire_*create/update (et avant delete pour tombstone timestamp = now). -
Step 3 : Migration serde : fichiers JSON existants sans champ →
NoneOK (#[serde(default)]). -
Verify :
cargo test -p ailiance-brain --lib(filtres rdv/ressources si présents) vert.
Task S2 — Tombstones + helpers list-since
Files :
- Modify:
src/services/sync.rs(TypeTombstone,CompteursSync,PullSync,status,pull) - Modify: tous les
supprimer_rendezvous/supprimer_reservation/supprimer_indisponibilitechemins API+UI critiques →enregistrer_tombstoneavant hard delete
Schéma type JSON inchangé :
{ "type": "rdv|reservation|indisponibilite", "id": "…", "projet_id": null, "supprime_le": "…" }
Pour résa/indispo, stocker aussi la cible dans un champ optionnel si utile au client :
{ "type": "reservation", "id": "…", "cible_type": "salle", "cible_id": "abc", "supprime_le": "…" }
(cible_type/cible_id : #[serde(default)] pour ne pas casser tombstones v1.)
-
Étendre
Tombstone+TypeTombstone. - Brancher deletes RDV/résa/indispo (au minimum chemins qui seront exposés API ; idéalement UI aussi).
-
Verify : test unitaire
tombstones_depuisfiltre les nouveaux types.
Task S3 — API CRUD /api/rdv
Files :
- Modify:
src/handlers/api.rs - Test:
tests/api_agenda_http.rs(créer)
Contrat :
GET /api/rdv → [RendezVous] # option: ?mine=1 défaut pour mobile = filtre utilisateur==auteur clé POST /api/rdv → 201 RendezVous GET /api/rdv/:id PUT /api/rdv/:id DELETE /api/rdv/:id → 204 + tombstone
Body create/update (snake_case) :
{ "titre": "…", "description": "", "lieu": "", "debut": "ISO-8601", "fin": "ISO-8601", "contact_ids": [], "projet_id": null }
Règles :
-
utilisateur/cree_par= user Bearer (ignorer spoof). -
peut_modifierpour PUT/DELETE. -
Valider via
RendezVous::valider. -
Poser
mis_a_jour_le. -
RED : test HTTP create → get → auteur = user clé ; mauvaise clé 401 ; autre user ne peut pas DELETE.
-
GREEN : handlers + routes dans le routeur Bearer (pas
/auth/cle). -
OpenAPI : documenter les 5 routes.
-
Verify :
cargo test -p ailiance-brain --test api_agenda_http -- --nocapture
Task S4 — API catalogue ressources + CRUD réservations + GET indispos
Files : api.rs, éventuellement petit module handlers/api_ressources.rs si api.rs trop gros
Test : api_agenda_http.rs
Catalogue
GET /api/ressources
Réponse :
{ "salles": [ { "id", "nom", "actif", … } ], "materiels": [ … ], "vehicules": [ … ] }
Uniquement ressources actif == true pour le mobile (ou toutes + flag actif — préférer toutes avec actif pour que Paramètres puisse griser).
Réservations
GET /api/ressources/:genre/:id/reservations POST /api/ressources/:genre/:id/reservations PUT /api/ressources/:genre/:id/reservations/:rid DELETE /api/ressources/:genre/:id/reservations/:rid
genre ∈ salle|materiel|vehicule (pas utilisateur en v2 mobile).
Body create :
{ "debut": "…", "fin": "…", "motif": "…", "projet_id": null, "rdv_id": null }
reserve_par= Bearer user ;statut= Active forcé.- Appeler
ressources::creer_reservation/modifier_reservation. - Sur
ErreurReservation::Conflit→ HTTP 409 + JSON{ "message": "…", "reclamable": bool }(ne pas créer EnAttente depuis l’API mobile).
Indispos (lecture)
GET /api/ressources/:genre/:id/indisponibilites
- Tests : create résa OK ; second chevauchant → 409 ; delete → 204.
- OpenAPI.
-
Verify : mêmes tests
api_agenda_http.
Task S5 — Étendre /api/sync/status et /api/sync/pull
Files : services/sync.rs, handlers/api.rs
Query additionnelle (optionnelle) :
GET /api/sync/pull?since=…&ressources=salle:abc,vehicule:xyz GET /api/sync/status?since=…&ressources=…
Parser ressources → Vec<CibleRessource> ; vide = ne pas renvoyer résas/indispos (client n’a lié aucun calendrier ressource) ; RDV toujours filtrés utilisateur == Bearer.
Étendre CompteursSync / JSON changes :
"changes": { "contacts": 0, "entreprises": 0, "projets": 0, "taches": 0, "interactions": 0, "rdv": 1, "reservations": 2, "indisponibilites": 1, "tombstones": 0 }
PullSync :
{ "server_time": "…", "contacts": [], "entreprises": [], "projets": [], "taches": [], "interactions": [], "rdv": [ /* RendezVous */ ], "reservations": [ /* Reservation */ ], "indisponibilites": [ /* Indisponibilite */ ], "tombstones": [], "workflows": [] }
Filtre since : mis_a_jour_le.unwrap_or(cree_le) > since (RDV/résa/indispo).
Compat clients v1 : champs JSON nouveaux avec défaut [] / 0 — anciens parseurs Kotlin devront être mis à jour (Task M*). Serde côté serveur : toujours émettre les clés.
-
Tests : create RDV → status.rdv >= 1 pour owner ; autre user → 0 ; résa sur salle filtrée par query
ressources. -
Verify :
cargo test -p ailiance-brain --test api_agenda_http --test api_sync_http
Task S6 — Doc admin serveur
Files : Projectiaon/docs/guide-administrateur.md, OpenAPI déjà S3–S5
-
Documenter
/api/rdv,/api/ressources, syncrdv/reservations/indisponibilites, queryressources=, code 409. - Lien vers la spec Card2vcf v2.
-
Verify : relecture manuelle ;
cargo test --all-targetsvert sur Projectiaon.
Partie B — Mobile
Task M1 — Permissions + CalendarBindingsStore
Files :
- Modify:
android/app/src/main/AndroidManifest.xml - Create:
…/sync/CalendarBindingsStore.kt - Test:
…/sync/CalendarBindingsStoreTest.kt
@Serializable data class CalendarBinding( val kind: String, // "rdv" | "salle" | "materiel" | "vehicule" val serverResourceId: String? = null, val displayName: String, val androidCalendarId: Long? = null, )
-
Permissions
READ_CALENDAR/WRITE_CALENDAR. - Store JSON dans prefs (EncryptedSharedPreferences ou prefs dédiées sync — réutiliser pattern credentials).
-
list(),upsert(binding),remove(kind, serverResourceId?). - Verify : unit test round-trip.
Task M2 — CalendarBridge (TDD sur mapping pur + smoke)
Files :
- Create:
…/sync/CalendarBridge.kt - Create:
…/sync/CalendarEventSnapshot.kt(data class titre, debutMs, finMs, eventId, serverIdExtra?) - Test: mapping helpers sans ContentResolver si possible ; Robolectric limité pour insert calendar
API :
class CalendarBridge(private val resolver: ContentResolver) { fun ensureLocalCalendar(displayName: String): Long fun listEvents(calendarId: Long): List<CalendarEventSnapshot> fun upsertEvent(calendarId: Long, snap: CalendarEventSnapshot): Long fun deleteEvent(eventId: Long) }
-
Calendriers locaux (
ACCOUNT_TYPElocal /Calendars.ACCOUNT_NAME = "card2vcf") pour hors-GMS. -
Stocker
serverIddansEvents.DESCRIPTIONpréfixe stablecard2vcf:serverId=…\nou extended properties si fiable ; documenter le choix dans le code. -
Titre indispo : préfixe
[Indispo]. -
Verify : tests mapping parse/format serverId ;
assembleDebugOK.
Task M3 — Room v3 : RDV / Reservation / Indisponibilite
Files : entities + DAOs ; CrmDatabase version 3 + fallbackToDestructiveMigration (doc README)
Champs alignés spec §4.2 (dirtyLocal, conflictPending, calendarEventId, …).
- DAOs : upsert, getByServerId, listDirty, deleteByServerId, listByCible.
-
Test smoke Robolectric
AgendaSchemaDaoTest. -
Verify :
./gradlew :app:testDebugUnitTest --tests '*AgendaSchema*'+ tests data existants.
Task M4 — DTOs + AilianceApi méthodes agenda
Files : SyncModels.kt, AilianceApi.kt, AilianceApiClient.kt
Test : MockWebServer
-
DTOs
RendezVousDto,ReservationDto,IndisponibiliteDto,RessourcesCatalogueDto. -
Étendre
SyncStatusResponse.changes+SyncPullResponseavecrdv,reservations,indisponibilites(défaut empty). -
syncStatus(since, ressourcesQuery: String?),syncPull(since, ressourcesQuery: String?). -
CRUD rdv + résa ;
GET /api/ressources. -
Parser 409 →
ApiResult.Err(409, message). -
Verify : tests client verts ; régessions
AilianceApiClientTest.
Task M5 — SyncEngine : RDV LWW + résas + Agenda↔Room
Files : SyncEngine.kt (+ collaborator AgendaSyncCoordinator si fichier trop gros)
Étendre syncNow(bindings: List<CalendarBinding>, bridge: CalendarBridge) :
agendaToRoom(bindings)→ dirty → SyncOprdv/reservationpushOps()— sur résa create/update siErr.code == 409: setconflictPending, ne pas drop SyncOp tout de suite ; retournerSyncResult(conflicts = […])pullavec queryressources=dérivée des bindings- apply LWW RDV ; résas Active ; indispos append/replace by serverId
roomToAgenda(bindings)- watermark
localAheadCount() : dirty Room + SyncOp pending rdv/reservation (+ events orphelins).
-
Tests Fake API + in-memory Room : LWW RDV ; 409 set conflictPending ; cancel path helper
abandonLocalReservation(localId). -
Verify :
SyncEngineTest+ nouveaux tests.
Task M6 — UI Paramètres calendriers
Files : SettingsScreen.kt, SettingsViewModel.kt, strings FR/EN
-
Demande runtime permissions calendrier.
-
Toggle Mes RDV →
ensureLocalCalendar("Card2vcf — Mes RDV")+ binding. -
Fetch catalogue ressources si credentials → toggles par ressource active.
-
Confirmation avant remove + delete calendar optionnel.
-
Verify :
assembleDebug; test ViewModel permissions/bindings mock si possible.
Task M7 — Bandeaux + dialog conflit
Files : SyncBanner.kt, SyncChromeViewModel.kt, strings
-
Bandeau A : changements serveur (
pendingRemoteChanges) — existant. -
Bandeau B :
localCalendarAhead > 0→ « Calendrier local en avance sur le serveur ». -
Après
syncNow, siconflictsnon vide → dialog « Annuler ma réservation ? » (une par une ou première).- Oui →
abandonLocalReservation+ delete event Agenda. - Non → dismiss ; SyncOp peut rester / flag
conflictPending(manager serveur).
- Oui →
-
Verify : test ViewModel conflit ; compile.
Task M8 — Doc README Card2vcf
Files : EBII_mobileVCF/README.md
-
Section Agenda / calendriers : permissions, Paramètres, pas d’écran agenda, conflit 409, hors-GMS calendriers locaux.
-
Lien spec + ce plan.
-
Nuancer destructive migration Room v3.
-
Verify : relecture ;
./gradlew :app:testDebugUnitTest assembleDebug.
Ordre / commits suggérés (si demandé)
feat(api): mis_a_jour_le rdv reservation indispofeat(sync): tombstones agenda typesfeat(api): CRUD /api/rdvfeat(api): ressources catalog + reservations 409feat(api): sync pull rdv reservations indisposdocs(admin): agenda mobile APIfeat(android): calendar bindings + permissionsfeat(android): CalendarBridge local calendarsfeat(android): Room v3 agenda entitiesfeat(android): API DTOs agendafeat(android): SyncEngine agenda bridgefeat(ui): settings calendars + conflict dialogdocs: README agenda sync
Critère de done v2
- Curl Bearer : CRUD RDV + résa ; 409 chevauchement ; sync pull filtre
ressources=+ mes RDV. - App : lier Mes RDV + une salle → events Agenda ↔ serveur après Sync.
- Conflit : deux events visibles + dialog annulation.
- Bandeau local en avance.
cargo test --all-targets(Projectiaon) et./gradlew :app:testDebugUnitTest assembleDebugverts.- Pas d’écran agenda dans la nav.
Coverage spec (self-check)
| Spec § | Tasks |
|---|---|
| RDV LWW + mes RDV | S1, S3, S5, M3–M5 |
| Multi-calendriers Paramètres | M1, M6 |
| Event ressource → résa Active | S4, M5 |
| Indispos lecture | S4, S5, M3, M5 |
| 409 → refresh + annuler | S4, M5, M7 |
| Bandeau local en avance | M5, M7 |
| Pas UI agenda / pas demandes mobile | M6–M8 (hors scope respecté) |
| Doc | S6, M8 |
GitRust