1 Commits

Author SHA1 Message Date
javier ba363e7e18 docs: mobile offline-first sync protocol (Sanctum API tokens)
Approved plan/protocol for connecting a mobile app to the webapp: offline-first
with device outbox, PULL (bundle/delta/versioned templates/tombstones), PUSH
(/api/v1/sync idempotent by client uuid), media via multipart, conflict policy,
schema additions, security, and phased webapp deliverables. Auth decided:
Laravel Sanctum API tokens. No implementation yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:36:35 +02:00
+112
View File
@@ -0,0 +1,112 @@
# Protocolo de sincronización móvil offline-first
> Estado: **plan aprobado** (2026-06-17). Auth decidida: **Laravel Sanctum (API tokens)**.
> Alcance de este documento: lo necesario **en la webapp** para que una app móvil
> descargue plantillas/datos, trabaje sin conexión y sincronice al recuperar red.
> No cubre la implementación de la app móvil (la consume este contrato).
## 1. Modelo general
Offline-first con **cola en el dispositivo (outbox)** + sync bidireccional:
- **PULL (descarga):** la app baja un "paquete" del proyecto (estructura + plantillas + registros) para trabajar sin red.
- **Trabajo offline:** cada cambio se guarda local con un **UUID generado en el móvil** y se encola.
- **PUSH (subida):** al volver la conexión, la app envía la cola; el servidor hace *upsert idempotente* por UUID y responde resultado por ítem.
- Sincronización **delta** por `updated_at` (solo lo cambiado desde el último sync).
## 2. Autenticación — Laravel Sanctum (decidido)
- Instalar `laravel/sanctum`. Tokens personales por dispositivo (no SPA-cookie; modo **API token**).
- Endpoints:
- `POST /api/v1/login``{ email, password, device_name }``{ token, user }`.
- `POST /api/v1/logout` — revoca el token actual.
- `GET /api/v1/me` — usuario + permisos efectivos.
- El móvil envía `Authorization: Bearer <token>`.
- Token con **abilities** (p. ej. `mobile-sync`) y **registro de dispositivo** (tabla `devices`) para revocar/caducar.
- Caducidad de token configurable + endpoint de refresco o re-login.
## 3. Cambios de esquema
Añadir a las tablas sincronizables (`features`, `inspections`, `issues`, `progress_updates`, `media`):
- `uuid` CHAR(36) único — **lo genera el móvil**; permite crear offline y *upsert* idempotente.
- `updated_at` (ya existe) — delta + last-write-wins.
- `client_updated_at` TIMESTAMP nullable — marca de tiempo del dispositivo (resolución de conflictos).
- Soft-deletes (ya existen) — se exponen como **tombstones** (ids/uuids borrados) en el PULL.
Tablas nuevas:
- `devices` (id, user_id, name, token_id, last_seen_at, …).
- `sync_logs` (auditoría: device, operación, entidad, uuid, resultado, timestamp).
## 4. API (`routes/api.php`, prefijo `/api/v1`, stateless + Sanctum)
### Descarga / PULL
- `GET /api/v1/projects` → proyectos accesibles (reusa `Project::accessibleBy`).
- `GET /api/v1/projects/{id}/bundle?since=<ISO8601>`**paquete offline** (delta si viene `since`).
- `GET /api/v1/templates?since=<ISO8601>` → plantillas de inspección con `version`/`hash` (descarga incremental).
- `GET /api/v1/media/{id}` o URLs firmadas dentro del bundle → adjuntos existentes.
Ejemplo de respuesta `bundle`:
```json
{
"server_time": "2026-06-17T20:00:00Z",
"project": { "id": 1, "uuid": "…", "name": "…", "updated_at": "…" },
"phases": [ { "id": 4, "name": "…", "updated_at": "…" } ],
"layers": [ { "id": 4, "phase_id": 4, "name": "…", "updated_at": "…" } ],
"features": [ { "id": 5, "uuid": "…", "layer_id": 4, "geometry": {…}, "status": "in_progress", "progress": 40, "updated_at": "…" } ],
"templates":[ { "id": 1, "version": 3, "fields": [ … ] } ],
"inspections": [ … ],
"issues": [ … ],
"deleted": { "features": ["uuid…"], "inspections": ["uuid…"] }
}
```
### Subida / PUSH
- `POST /api/v1/sync` — lote de operaciones (idempotente por `uuid`):
```json
{ "operations": [
{ "entity": "progress_update", "op": "create", "uuid": "…", "client_updated_at": "…", "data": { "phase_id": 4, "progress": 60, "comment": "…", "location": {…} } },
{ "entity": "inspection", "op": "create", "uuid": "…", "client_updated_at": "…", "data": { "feature_id": 5, "template_id": 1, "data": {…}, "result": "pass" } },
{ "entity": "feature", "op": "update", "uuid": "…", "client_updated_at": "…", "data": { "status": "completed", "progress": 100 } },
{ "entity": "issue", "op": "create", "uuid": "…", "client_updated_at": "…", "data": { "feature_id": 5, "title": "…", "priority": "high" } }
] }
```
Respuesta por operación:
```json
{ "results": [
{ "uuid": "…", "status": "applied", "server_id": 123 },
{ "uuid": "…", "status": "duplicate", "server_id": 124 },
{ "uuid": "…", "status": "conflict", "server": { "status": "verified", "updated_at": "…" } },
{ "uuid": "…", "status": "error", "error": "validation: …" }
] }
```
- `POST /api/v1/media`**subida de fotos por multipart** (no base64), referenciando al padre por `uuid` (`parent_entity`, `parent_uuid`, `file`). Soporta reintento; troceado si el archivo es grande.
## 5. Idempotencia y conflictos
- **Idempotencia:** el `uuid` evita duplicados si se reenvía la cola (re-sync seguro).
- **Append-only (sin conflicto):** `progress_updates`, `inspections` → siempre insertan.
- **Editables (con política):** `feature.status/progress`, `issue`**last-write-wins** comparando `client_updated_at` vs `updated_at` del servidor. Si el servidor es más nuevo → `conflict` y se devuelve el valor del servidor para que el móvil decida/avise.
## 6. Seguridad
- **Nunca** `Model::create($payloadCliente)` crudo. Usar FormRequests/DTO; fijar `project_id`/`user_id` **en el servidor** desde el contexto autorizado; validar que `feature/phase` pertenece a un proyecto del usuario (anti-IDOR).
- Autorizar cada operación con permisos Spatie (`update progress`, `create inspections`, …) + pertenencia al proyecto (`accessibleBy`).
- Rate limiting, caducidad de token, `sync_logs` para auditoría.
## 7. Versionado
- Prefijo `/api/v1`; cabecera `X-App-Version`; el servidor responde versión mínima soportada (forzar update del móvil).
- Versión/hash por plantilla (descarga incremental).
## 8. Qué reutilizar / retirar
- `OfflineSyncController` + `PendingSync`: el **vocabulario de acciones** (progress_update, inspection, feature_create, media_upload, task_complete) es buena base para las operaciones de `/sync`. Pero hay que: pasar a API+token, añadir uuid/validación/autorización, y **mover la cola al dispositivo** (la `PendingSync` del servidor deja de ser necesaria para el móvil; se puede retirar o reaprovechar como `sync_logs`).
## 9. Entregables en la webapp (por fases)
- **Fase A — Auth & esqueleto API:** Sanctum, `routes/api.php`, `login`/`logout`/`me`, tabla `devices`, abilities.
- **Fase B — PULL:** `projects`, `bundle` + delta, `templates` versionadas, tombstones.
- **Fase C — PUSH:** `/sync` idempotente con validación/autorización/conflictos (recoge y endurece la lógica actual).
- **Fase D — Media:** subida multipart + descarga.
- **Fase E — Endurecimiento + Docs:** rate-limit, `sync_logs`, OpenAPI/Swagger como contrato para el equipo móvil.