# 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.

---

## Addendum (2026-06-18): Incidencias enriquecidas — tareas, comentarios y fotos

El detalle de una incidencia incluye ahora un **checklist de tareas** y un **hilo de comentarios**, ambos con fotos. Todo es sincronizable offline:

- **Nuevas entidades de PULL** en el `bundle` (y en `deleted`): `issue_tasks`, `issue_comments`.
- **Nuevas operaciones de PUSH** en `/sync` (idempotentes por `uuid`):
  - `issue_task.create` — `data`: `issue_id`, `title`, `assigned_to?`, `due_date?`, `is_done?`. Requiere `edit issues`.
  - `issue_task.update` — `data`: `id`, y cualquiera de `title`/`assigned_to`/`due_date`/`is_done`. Last-write-wins por `client_updated_at`. Requiere `edit issues`.
  - `issue_comment.create` — `data`: `issue_id`, `body`. Requiere `view issues`.
- **Fotos**: `POST /media` admite `parent_entity` = `issue_task` y `issue_comment` (además de `issue`). Requiere `upload media`.
- El **% de avance** de la incidencia se deriva de las tareas completadas (no se almacena ni se sincroniza).
