Sync Card2vcf ↔ Projectiaon — Plan client v1
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking. TDD (RED → GREEN). Ne pas committer sauf demande explicite de l’utilisateur.
Goal : Card2vcf synchronise offline bidirectionnellement CRM + projets/tâches kanban avec Projectiaon (ailiance-brain), via clés API par utilisateur et sync incrémental.
Architecture : Room local + file SyncOp ; SyncEngine pousse les ops puis tire /api/sync/pull ; LWW sur horodatages pour contacts/entreprises/projets/tâches ; append-only pour interactions (CR). Auth : login POST /api/auth/cle → déchiffrement local Argon2id + XChaCha20-Poly1305 → Bearer en stockage chiffré.
Tech Stack : Kotlin, Jetpack Compose, Room 2.6, OkHttp + kotlinx.serialization, EncryptedSharedPreferences, Argon2 (params crate Rust argon2 0.5) + XChaCha20-Poly1305, Coroutines. Design : design.md (N&B éditorial, coins 0).
Prérequis serveur (FAIT) : plan Projectiaon/docs/plans/sync_api_mobile_v1.md — code présent : AILIANCE_API_CLES, POST /api/auth/cle, tombstones, GET /api/sync/status|pull, assigne_a / mis_a_jour_le.
Hors scope v1 : RDV, ressources/réservations, Agenda Android, médias serveur, multi-assignés.
Décisions produit (validées)
| Sujet | Décision |
|---|---|
| Rôle mobile | CRM terrain offline = mêmes infos serveur, partage équipe |
| Sync | Incrémental + file d’ops (pas full overwrite seul) |
| Auth | Clés par user ; mdp jamais stocké ; retape pour déchiffrer la clé une fois |
| Conflits | LWW horodatage ; CR append-only ; move kanban = LWW (colonne_id, ordre) |
| Déclenchement | Manuel + bandeau status à l’ouverture (pas de sync auto silencieuse) |
| Kanban | v1 (API taches déjà là) |
| Filtre tâches | Défaut « Mes tâches » (assigne_a == user) | « Toutes » |
Contrats serveur à respecter
Auth clé
POST /api/auth/cle Content-Type: application/json {"nom":"alice","mot_de_passe":"…"}
{ "nom": "alice", "cle_chiffree": "<base64>", "sel": "<base64>", "kdf": "argon2id", "cipher": "xchacha20poly1305" }
Blob cle_chiffree (binaire) : [0..24) nonce XChaCha20 + ciphertext (clé API UTF-8 + tag Poly1305).
Sel : 16 octets.
KDF : Argon2id defaults crate argon2 0.5.3 — m=19456 (KiB), t=2, p=1, output 32 octets.
401 credentials ; 404 user sans clé API.
Sync
GET /api/sync/status?since=1970-01-01T00:00:00Z Authorization: Bearer <cle>
{ "server_time": "…", "changes": { "contacts": 2, "entreprises": 0, "projets": 1, "taches": 3, "interactions": 1, "tombstones": 1 }, "total": 8 }
GET /api/sync/pull?since=…
{ "server_time": "…", "contacts": [], "entreprises": [], "projets": [], "taches": [{ "projet_id": "…", /* flatten Tache */ }], "interactions": [], "tombstones": [], "workflows": [] }
Tombstone : { "type": "contact|entreprise|projet|tache|interaction", "id", "projet_id?", "supprime_le" }.
Projets pull : taches[] vidé (tâches dans taches[] top-level). ACL : si membres non vide, user Bearer doit ∈ membres.
CRUD utiles (Bearer)
| Action | Méthode |
|---|---|
| Contact | POST/PUT/DELETE /api/contacts[/:id] |
| Entreprise | POST/PUT/DELETE /api/entreprises[/:id] |
| Projet | POST/PUT/DELETE /api/projets[/:id] |
| Tâche | POST/PUT/DELETE /api/projets/:id/taches[/:tid] |
| Déplacer | POST /api/projets/:id/taches/:tid/deplacer body {colonne_id, ordre?} |
| CR | POST /api/contacts/:id/interactions |
Champs clés tâche : titre, description, colonne_id, ordre, assigne_a, mis_a_jour_le.
Fichiers (carte)
| Fichier | Rôle |
|---|---|
android/app/src/main/AndroidManifest.xml | INTERNET |
android/app/build.gradle.kts | OkHttp, serialization, security-crypto, argon2, libsodium/BC |
…/sync/ApiCleCrypto.kt | Déchiffrement clé API (miroir Rust) |
…/sync/SyncCredentialsStore.kt | URL + Bearer + nom user (EncryptedSharedPreferences) |
…/sync/AilianceApiClient.kt | HTTP auth/status/pull/CRUD |
…/sync/SyncModels.kt | DTO JSON (pull, tombstone, tache…) |
…/sync/SyncOpEntity.kt + Dao | File d’ops offline |
…/sync/SyncEngine.kt | push → pull → LWW / append / watermark |
…/sync/LwwMerger.kt | Règles fusion horodatage |
…/data/*Entity.kt | serverId, CRM fields, Projet/Tache/Interaction/Workflow |
…/data/CrmDatabase.kt | version ↑, entities, fallbackToDestructiveMigration v1 OK |
…/ui/settings/SettingsScreen.kt | URL + login clé |
…/ui/projets/* | Liste, fiche, CR |
…/ui/kanban/* | Board + filtre Mes/Toutes |
…/ui/nav/Card2vcfNavHost.kt | Routes + bottom nav Carnet | Projets | … |
…/ui/sync/SyncBanner.kt | Bandeau « N changements » |
README.md | Sync optionnelle documentée |
Tests unitaires sous android/app/src/test/… | Crypto, LWW, SyncEngine, filtre assigné |
Task 0 — Prérequis serveur (skip si déjà vert)
Verify :
cd /home/monsieurb/Documents/devel/Projectiaon cargo test -p ailiance-brain --test api_sync_http -- --nocapture
-
Multi-clés +
/api/auth/cle+ sync status/pull (code Projectiaon présent).
Si rouge : corriger le serveur avant le mobile.
Task 1 — Dépendances + INTERNET + store credentials
Files :
-
Modify:
android/app/src/main/AndroidManifest.xml -
Modify:
android/app/build.gradle.kts -
Create:
…/sync/SyncCredentialsStore.kt -
Test:
…/test/…/sync/SyncCredentialsStoreTest.kt(Robolectric) -
Step 1 : Ajouter permission
INTERNET(etusesCleartextTrafficnon — HTTPS only ; debug exception optionnelle documentée). -
Step 2 : Dépendances :
okhttp,kotlinx-serialization-json,androidx.security:security-crypto, lib Argon2 JVM compatible params, lib XChaCha20-Poly1305 (ex.com.github.ionspin.kotlin:multiplatform-crypto-libsodiumou BouncyCastle + wrapper — choisir une stack qui tourne en unit test JVM). -
Step 3 :
SyncCredentialsStore:baseUrl,apiKey,userName; clear ; jamais le mot de passe. -
Step 4 : Test Robolectric round-trip store.
-
Verify :
./gradlew :app:testDebugUnitTest --tests '*SyncCredentialsStore*'
Task 2 — Crypto ApiCleCrypto (TDD)
Files :
-
Create:
…/sync/ApiCleCrypto.kt -
Test:
…/test/…/sync/ApiCleCryptoTest.kt -
Step 1 (RED) : Test vector : sel 16 bytes, mdp connu, blob produit par un fixture base64 généré côté Rust (
chiffrer_cle_api) — coller sel+blob+mdp+clair attendu dans le test. -
Step 2 (GREEN) : Implémenter
decryptApiKey(password, saltB64, cipherB64)avec m=19456, t=2, p=1, Argon2id, XChaCha20-Poly1305, nonce 24 prefix. -
Step 3 : Test mauvais mdp → erreur ; blob trop court → erreur.
-
Verify : unit tests verts. Ne jamais logger la clé claire.
Aide génération fixture (une fois) :
cd Projectiaon && cargo test -p ailiance-brain --lib api_cle_crypto -- --nocapture # ou petit bin/println temporaire — retirer ensuite
Task 3 — Client HTTP + DTOs sync
Files :
-
Create:
…/sync/SyncModels.kt,…/sync/AilianceApiClient.kt -
Test:
…/test/…/sync/AilianceApiClientTest.kt(MockWebServer) -
authCle(nom, mdp)→ parse réponse. -
syncStatus(since),syncPull(since)Bearer. -
Stubs CRUD contacts / taches create|update|delete|move / interactions create (signatures utilisées par SyncEngine).
-
401/404 mappés en sealed class
ApiResult. -
Verify : MockWebServer tests verts.
Task 4 — Schéma Room sync
Files : modifier entities + CrmDatabase version 2 ; DAOs ; Converters si besoin.
Extensions contact :
serverId: String?statut,etape,tags: List<String>,entrepriseServerId: String?- garder
updatedAtlocal ; mappermis_a_jour_leISO ↔ epoch ms
Nouvelles tables :
-
entreprises(localId,serverId, champs miroir,updatedAt) -
projets(+membresJson,workflowServerId,updatedAt) -
taches(projetLocalId/projetServerId,serverId,colonneId,ordre,assigneA,updatedAt, …) -
interactions(contactServerId,serverId,type,sujet,description,createdAt— append-only) -
workflows+workflow_colonnes(ou JSON colonnes) -
sync_ops:id,entityType,op(create|update|delete|move),payloadJson,localId?,serverId?,createdAt,attempts -
prefs watermark : table
sync_meta(key,value)cléwatermark= ISO lastserver_time -
Migration :
fallbackToDestructiveMigration()acceptable v1 (doc README : reset local à l’upgrade schema). -
Tests DAO smoke (insert/query) Robolectric comme
CrmContactDaoTest. -
Verify :
./gradlew :app:testDebugUnitTest --tests '*Dao*'
Task 5 — LwwMerger + règles (TDD)
Files : …/sync/LwwMerger.kt + tests
-
Contact/Entreprise/Projet/Tache : si
remote.updatedAt > local.updatedAt→ remote gagne ; égalité → remote gagne (déterministe). -
Interaction : jamais écraser ; insert si
serverIdinconnu. -
Tombstone : supprimer local par
serverId(+ cascade tâches si projet). -
Move : LWW sur
(colonneId, ordre, updatedAt). - Verify : tests pure Kotlin verts.
Task 6 — SyncEngine
Files : …/sync/SyncEngine.kt + tests avec fake API + Room in-memory
Séquence bouton Sync :
- Push chaque
SyncOp(ordre FIFO) via CRUD ; sur succès retirer op + mapperserverIdsi create. pull(since=watermark); appliquer LWW / append / tombstones ; stocker workflows.- Watermark =
server_timedu pull.
À l’ouverture (léger) :
-
Si credentials :
status(since)→ exposerpendingRemoteChanges = total(UI bandeau). Ne pas pull auto. -
Tests : 2 contacts LWW ; 2 CR append ; move conflit LWW ; tombstone delete.
-
Verify : unit tests SyncEngine.
Task 7 — UI Paramètres + auth clé
Files : SettingsScreen, ViewModel, routes nav, strings FR/EN
- Champs : URL base HTTPS, nom, mot de passe (temporaire).
-
Bouton « Obtenir la clé » →
authCle→ dialog retape mdp →ApiCleCrypto.decrypt→ store. - Afficher user connecté + « Déconnecter » (clear store).
-
Style
design.md: coins 0, ink/canvas, Manrope/Lora/Playfair existants. - Entrée depuis overflow / engrenage carnet.
- Verify : compile + smoke manuel ou UI test minimal si déjà du setup.
Task 8 — Bandeau sync + câblage ouverture
Files : SyncBanner.kt, CarnetScreen / scaffold app, MainActivity/Card2vcfApp
-
Si
pendingRemoteChanges > 0: bandeau hairline « N changement(s) sur le serveur » + bouton Sync. - Bouton Sync toujours accessible (Paramètres ou bandeau) même si total=0 (push local).
- Pendant sync : état loading ; erreur réseau affichée (ink, pas snackbar coloré flashy).
- Verify : compile ; test ViewModel status mock.
Task 9 — Onglet Projets + CR
Files : nav bottom Carnet | Projets ; ProjetsListScreen, ProjetDetailScreen ; création CR = Interaction type note liée contact
- Liste projets Room (après sync).
- Fiche : nom, description, membres, contacts liés (ids serveur résolus localement si possible).
- Section CR : liste interactions du contact sélectionné / du projet (v1 : CR sur contact, append SyncOp create).
- Mutations locales → enqueue SyncOp + update Room optimiste.
- Verify : unit tests repository ; compile UI.
Task 10 — Kanban + assigne_a + filtre
Files : KanbanBoard.kt, filter Mes/Toutes, CRUD tâche, swipe/déplacer
-
Colonnes = workflow local du
workflowServerIddu projet. - Cartes : titre, pastille assigné, ordre.
-
Créer / éditer / supprimer / déplacer → Room + SyncOp (
moveavec colonne+ordre). -
Sélecteur
assigne_a: membres projet (+ soi =userNamedu store). - Filtre défaut Mes tâches ; toggle Toutes. Non assigné = visible seulement dans Toutes.
- Verify : tests filtre + merge move ; compile.
Task 11 — Mapping IDs + push contacts carnet existant
Files : adapters repository carnet ↔ server contact
- À la création locale d’un contact après login : enqueue create avec payload API.
-
Pull contact → upsert par
serverId; si match email/tél fort optionnel hors v1 (LWW sur serverId suffit). - Édition / suppression locale → update/delete SyncOp.
-
Verify : tests mapping ; pas de régression
ContactRepositoryTest.
Task 12 — Doc README
Files : README.md
- Section Sync (optionnelle) : prérequis serveur, flux clé, hors-ligne, pas GMS.
- Nuancer « aucune permission Internet » → Internet uniquement pour sync (OCR reste offline).
- Lien vers ce plan + plan serveur Projectiaon.
Ordre d’exécution / commits suggérés (si demandé)
feat(sync): credentials store + INTERNET + depsfeat(sync): ApiCleCrypto argon2id xchachafeat(sync): AilianceApiClient + DTOsfeat(sync): Room schema v2 entitiesfeat(sync): LwwMerger + SyncEnginefeat(ui): settings auth keyfeat(ui): sync banner + projets + CRfeat(ui): kanban assigne_a filterdocs: README sync optionnelle
Critère de done client v1
- Login Paramètres → clé déchiffrée stockée ; Bearer sur status/pull
- Sync manuelle : push ops + pull delta ; watermark avancé
- 2 devices / 2 users : LWW contact ; 2 CR présents ; move tâche LWW ; filtre Mes tâches
- Bandeau à l’ouverture si
total > 0sans pull auto ./gradlew :app:testDebugUnitTestvert ;assembleDebugOK- Pas de clé/mdp en clair dans logs
v2 (plan séparé)
Spec design : docs/superpowers/specs/2026-07-22-sync-agenda-rdv-ressources-v2-design.md
(RDV + calendriers ressources Agenda Android, pas d’écran agenda in-app.)
GitRust