sync_card2vcf_projectiaon_v1.md 14413 octets

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)

SujetDécision
Rôle mobileCRM terrain offline = mêmes infos serveur, partage équipe
SyncIncrémental + file d’ops (pas full overwrite seul)
AuthClés par user ; mdp jamais stocké ; retape pour déchiffrer la clé une fois
ConflitsLWW horodatage ; CR append-only ; move kanban = LWW (colonne_id, ordre)
DéclenchementManuel + bandeau status à l’ouverture (pas de sync auto silencieuse)
Kanbanv1 (API taches déjà là)
Filtre tâchesDé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.3m=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)

ActionMéthode
ContactPOST/PUT/DELETE /api/contacts[/:id]
EntreprisePOST/PUT/DELETE /api/entreprises[/:id]
ProjetPOST/PUT/DELETE /api/projets[/:id]
TâchePOST/PUT/DELETE /api/projets/:id/taches[/:tid]
DéplacerPOST /api/projets/:id/taches/:tid/deplacer body {colonne_id, ordre?}
CRPOST /api/contacts/:id/interactions

Champs clés tâche : titre, description, colonne_id, ordre, assigne_a, mis_a_jour_le.


Fichiers (carte)

FichierRôle
android/app/src/main/AndroidManifest.xmlINTERNET
android/app/build.gradle.ktsOkHttp, serialization, security-crypto, argon2, libsodium/BC
…/sync/ApiCleCrypto.ktDéchiffrement clé API (miroir Rust)
…/sync/SyncCredentialsStore.ktURL + Bearer + nom user (EncryptedSharedPreferences)
…/sync/AilianceApiClient.ktHTTP auth/status/pull/CRUD
…/sync/SyncModels.ktDTO JSON (pull, tombstone, tache…)
…/sync/SyncOpEntity.kt + DaoFile d’ops offline
…/sync/SyncEngine.ktpush → pull → LWW / append / watermark
…/sync/LwwMerger.ktRègles fusion horodatage
…/data/*Entity.ktserverId, CRM fields, Projet/Tache/Interaction/Workflow
…/data/CrmDatabase.ktversion ↑, entities, fallbackToDestructiveMigration v1 OK
…/ui/settings/SettingsScreen.ktURL + login clé
…/ui/projets/*Liste, fiche, CR
…/ui/kanban/*Board + filtre Mes/Toutes
…/ui/nav/Card2vcfNavHost.ktRoutes + bottom nav Carnet | Projets | …
…/ui/sync/SyncBanner.ktBandeau « N changements »
README.mdSync 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 (et usesCleartextTraffic non — 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-libsodium ou 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 updatedAt local ; mapper mis_a_jour_le ISO ↔ 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 last server_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 serverId inconnu.
  • 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 :

  1. Push chaque SyncOp (ordre FIFO) via CRUD ; sur succès retirer op + mapper serverId si create.
  2. pull(since=watermark) ; appliquer LWW / append / tombstones ; stocker workflows.
  3. Watermark = server_time du pull.

À l’ouverture (léger) :

  • Si credentials : status(since) → exposer pendingRemoteChanges = 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 workflowServerId du projet.
  • Cartes : titre, pastille assigné, ordre.
  • Créer / éditer / supprimer / déplacer → Room + SyncOp (move avec colonne+ordre).
  • Sélecteur assigne_a : membres projet (+ soi = userName du 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é)

  1. feat(sync): credentials store + INTERNET + deps
  2. feat(sync): ApiCleCrypto argon2id xchacha
  3. feat(sync): AilianceApiClient + DTOs
  4. feat(sync): Room schema v2 entities
  5. feat(sync): LwwMerger + SyncEngine
  6. feat(ui): settings auth key
  7. feat(ui): sync banner + projets + CR
  8. feat(ui): kanban assigne_a filter
  9. docs: 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 > 0 sans pull auto
  • ./gradlew :app:testDebugUnitTest vert ; assembleDebug OK
  • 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.)