Files
Kazier/AGENTS.md
T
m 197c7db8a9 feat(mobile): boucle sync client↔serveur — outbox push + snapshot permissions
- api/types.ts : SyncOperation, SyncResult (sémantique `applied` = INDEX documentée),
  ResourcePermission (miroir docs/api-v1.md §6, consommé tel quel par saveResourcePermission)
- api/client.ts : syncOps (POST /sync/ops), getSyncPermissions (GET /sync/permissions?after=),
  hasAuthToken() ; INVALID_RESPONSE documenté dans docs/api-v1.md §1/§4
- services/db/repositories/pendingOps.ts : listQueuedOperations(limit) (batch FIFO due)
  + scheduleRetries(ids, retryAt) qui ne modifie JAMAIS attempts (erreur transitoire,
  contrairement à markPendingOperation('failed') qui incrémente + backoff)
- features/syncOutbox.ts (nouveau) :
  * pushPendingOps : batch ≤ SYNC_BATCH_SIZE (50) → ops [0, applied) passent 'completed' ;
    op à l'index applied refusée → 'failed' (backoff, dead-letter MAX_PENDING_ATTEMPTS) ;
    ops suivantes intactes (re-soumission) ; réseau/5xx → scheduleRetries (retried) sans
    dead-letter prématurée ; applied==0 && failed → rien commité, aucun compteur touché
  * refreshPermissions : delta monotone en mémoire lastPermissionCachedAt → `after`,
    upsert saveResourcePermission ; no-op sans token
- features/syncDevice.ts : le tick useSyncDevice() enchaîne SAF walk → pushPendingOps()
  → refreshPermissions(), tous deux gardés par hasAuthToken()
- tests/syncOutbox.test.ts : 12 cas (FIFO/limit, succès complet/partiel, applied=0,
  transitoire réseau + 5xx sans incrément, dead-letter après MAX_PENDING_ATTEMPTS,
  delta snapshot croisé) — npm run test:sync
- tests/e2e.live.test.ts : smoke real backend (register → push → relecture /files/folders →
  snapshot), skip si serveur down, SORTI de npm test via npm run test:e2e
- gates : npx tsc --noEmit + npm run test → 56/56 verts (24 db + 20 api + 12 sync)
2026-09-10 20:23:55 +02:00

6.8 KiB

VaultDrop

Project Status

Project initialized — mobile/ (React Native / Expo) and backend/ (Go) have scaffolding in place. The SQLite layer (schema v4, migrations, repositories) is implemented and covered by tests.

Architecture

  • Backend: Go, Gin HTTP framework, PostgreSQL, Tesseract OCR (system call)
  • Frontend: React Native (Expo SDK 57), expo-router, expo-sqlite, expo-file-system (SAF)

Key Commands

# Backend
cd backend && go run cmd/server/main.go

# PostgreSQL (via docker-compose)
docker compose up postgres -d

# Frontend
cd mobile && npx expo start

# Typecheck frontend
cd mobile && npx tsc --noEmit

# SQLite layer tests (migrations + repositories)
cd mobile && npm run test:db

Backend Structure

  • Entry point: backend/cmd/server/main.go (wiring gin + config + routes)
  • config/ — env (godotenv, optionnel) + defaults: PORT, DATABASE_URL, UPLOAD_DIR, MAX_FILE_SIZE_MB, OCR_LANG, secret paseto
  • models/ — domain entities (users, devices, documents/resources, clients)
  • service/ — business logic (permissions, upload, create folder, move, sync outbox + snapshot)
  • handlers/ — HTTP handlers (health, devices register + paseto, files CRUD/upload/search, folders, sync/ops + sync/permissions, ocr/jobs — réels)
  • repository/ — Postgres persistence réelle (repository.Resources : insert/list/get/soft-delete scoping owner_id, search, move, rename, root-name unique index, repository.Devices.Upsert, repository.Operations : trace outbox idempotente (device_id, operation_id), ListOwned pour le snapshot, repository.OcrJobs : jobs queued→processing→done/failed) ; IDs sont TEXT 32-hex, NewID() = crypto/rand 16 octets hex (jamais UUID conversion, cf. docs/api-v1.md)
  • db/ — package migrations (golang-migrate/v4, embarquées via embed dans db/migrations/*.sql) : db.MigrateDatabase(url) au boot du serveur ; test harness db/migrations_test.go (up → assertions schéma → down, TEST_DATABASE_URL, skip si PG indisponible) ; dbtest/ — helper cross-package pour les tests repo/handlers (crée la DB test si absente, reset schema, migrate ; skip si PG down)
  • ocr/ — OCR engine behind an interface (Tesseract system call, OCR_LANG défaut fra+eng)
  • Response helpers: pkg/api/response.go
  • File uploads stored in backend/uploads/
  • Standard JSON response envelope: { "data": ..., "meta": { "page": ..., "total": ... } }
  • Error format: { "error": { "code": "...", "message": "..." } }
  • Route list is a tracked contract (cmd/server/router_test.go mirrors mobile/api/client.ts)

Frontend Structure

  • Entry point: mobile/App.tsx (expo-router layout + Auth context)
  • Data layer — mobile/services/:
    • safDirectory.ts + safDirectory.types.ts: physical access via expo-file-system (pick/list/create, Documents/SAF uris)
    • db/ — SQLite persistence, see mobile/AGENTS.md for the full contract (schema, migrations, repositories, tests)
    • localStorage.ts — thin re-export of services/db (legacy alias)
  • features/syncDevice.ts — device sync orchestration (two-pass SAF walk, single transaction per root, exists = 0 reconciliation)
  • features/syncOutbox.ts — pulls pending_operations to POST /sync/ops (resume-at-applied index, transient retries never bump attempts) and GET /sync/permissions delta snapshot; both no-ops without an auth token
  • context/AuthContext.tsx — session context: exposes deviceUserId (bootstrapped from getDeviceUserId()) and starts the background syncDevice loop
  • app/ — expo-router screens: index.tsx (dossiers racines + ajout SAF), folder/[id].tsx (sous-dossiers + fichiers)
  • api/ — REST client (client.ts fetch wrapper + types.ts = contrat d'API : enveloppe { data, meta }, erreurs { error: { code, message } })
  • hooks/ — TanStack Query hooks: useFiles, useSearch, useUpload (+ OCR jobs)
  • No business logic on the client — heavy processing stays server-side
  • API base URL via EXPO_PUBLIC_API_BASE_URL (défaut http://localhost:8080/api/v1)

Data Conventions

  • Canonical identity for folders/files/shares/share_links is resource_id: opaque lower(hex(randomblob(16))), generated locally, never reused. The physical uri is nullable (NULL = cloud-only) and is the reconciliation key for the SAF walk.
  • owner_id is NOT NULL on every folder/file row, seeded from the device's device_user_id.
  • Folder/file sync_status is a placement state: local | cloud | local-cloud (transitions via transitionSyncStatus). It is not a push progress marker.
  • Shares/share_links carry no sync_status; their pushStatus (pending/synced/failed) is derived from the pending_operations outbox.
  • Decisions are made offline from a cached resource_permissions snapshot pushed by the server; the server remains the source of truth. canAccess enforces ranking (viewer < commenter < editor < owner), inherit, expires_at, and a 24h stale-cache read-only downgrade.
  • password_hash and download counters are server-side only; the client only stores the has_password boolean and a counter mirror.

API Contract (V1)

  • The mobile client is the contract: endpoint shapes in mobile/api/types.ts + mobile/api/client.ts are authoritative and must match exactly; the server does not renegotiate them. Consolidated spec: docs/api-v1.md.
  • Identity: device-first. The device registers (POST /devices) and authenticates with a paseto bearer token; no user accounts in V1 (users table exists but devices.user_id stays NULL).
  • Identifiers: resource_id / device_user_id / share-link token are opaque lowercase 32-hex TEXT (^[0-9a-f]{32}$, CHECK-enforced), stored as-is server-side (no UUID conversion). The mobile always generates lower(hex(randomblob(16))).
  • Outbox idempotence + ordering: client pushes batches of pending_operations; each operation carries operation_id (= client pending_operations.id), server enforces uniqueness per device. Batches are applied sequentially; the server stops at the first non-idempotent failure and returns the index reached so the client resumes there (outbox retry/backoff can reorder).
  • Permissions snapshot: the server pushes resource_permissions snapshots (effective_access ranking viewer < commenter < editor < owner, inherit, expires_at, TTL 24h → read-only downgrade) that the offline canAccess consumes.

Non-Goals (V1)

  • Plugin system
  • On-device OCR
  • Full multi-tenant federation / public discovery
  • Multi-writer sync conflicts (single-owner device identity; device-local device_user_id)

References

  • docs/api-v1.mdcontrat API V1 (autoritatif, consolidé depuis mobile/api/types.ts)
  • V2.md — modèle cible Postgres/ReBAC (identifiants en TEXT 32-hex, cf. docs/api-v1.md)