Files
construprogress/docs/MOBILE_SYNC_PROTOCOL.md
T
javier 3f240e5277 feat(issues): incidencias enriquecidas (tareas/comentarios/fotos/verificación) + tabla Rappasoft + logo
Web:
- IssueTask + IssueComment (modelos, migraciones, soft-deletes, campos de sync).
  Issue gana tasks()/comments() y accessor de % de avance derivado de tareas.
- IssueDetail (página): checklist con asignado/fecha límite/progreso, hilo de
  comentarios con foto por comentario, galería de fotos de la incidencia y flujo
  de verificación open→in_review→resolved/closed (+reabrir) con notas.
- Creación/edición en páginas propias (IssueForm), sin modal; al guardar redirige
  al detalle. Rutas projects.issues.create/edit/show.
- Listado con tabla Rappasoft (IssueTable): filtros por estado/prioridad, búsqueda,
  barra de progreso y acciones por fila gateadas por permisos; IssueManager queda
  como contenedor (cabecera + stats) que embebe la tabla.
- Seguridad: pertenencia al proyecto + permisos por acción (view/create/edit/delete
  issues, upload/delete media) en todos los componentes.

API móvil (offline):
- /sync: issue_task.create/update y issue_comment.create (idempotente, LWW).
- /media: parent_entity issue_task / issue_comment.
- bundle + tombstones incluyen issue_tasks / issue_comments.
- openapi.yaml + MOBILE_SYNC_PROTOCOL.md actualizados.

Tests: MobileApiTest 23 passing (+5); IssuesTablePageTest (3) smoke de la tabla.

Branding: logo RTE International — MAI Group (public/images/logo-rte.png) en login
y navegación; application-logo pasa de SVG por defecto a <img>.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 12:12:39 +02:00

7.6 KiB

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:

{
  "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):
{ "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:

{ "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/mediasubida 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, issuelast-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.createdata: issue_id, title, assigned_to?, due_date?, is_done?. Requiere edit issues.
    • issue_task.updatedata: id, y cualquiera de title/assigned_to/due_date/is_done. Last-write-wins por client_updated_at. Requiere edit issues.
    • issue_comment.createdata: 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).