3f240e5277
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>
7.6 KiB
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 (tabladevices) 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):
uuidCHAR(36) único — lo genera el móvil; permite crear offline y upsert idempotente.updated_at(ya existe) — delta + last-write-wins.client_updated_atTIMESTAMP 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 (reusaProject::accessibleBy).GET /api/v1/projects/{id}/bundle?since=<ISO8601>→ paquete offline (delta si vienesince).GET /api/v1/templates?since=<ISO8601>→ plantillas de inspección conversion/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 poruuid):
{ "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/media— subida de fotos por multipart (no base64), referenciando al padre poruuid(parent_entity,parent_uuid,file). Soporta reintento; troceado si el archivo es grande.
5. Idempotencia y conflictos
- Idempotencia: el
uuidevita 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 comparandoclient_updated_atvsupdated_atdel servidor. Si el servidor es más nuevo →conflicty se devuelve el valor del servidor para que el móvil decida/avise.
6. Seguridad
- Nunca
Model::create($payloadCliente)crudo. Usar FormRequests/DTO; fijarproject_id/user_iden el servidor desde el contexto autorizado; validar quefeature/phasepertenece 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_logspara auditoría.
7. Versionado
- Prefijo
/api/v1; cabeceraX-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 (laPendingSyncdel servidor deja de ser necesaria para el móvil; se puede retirar o reaprovechar comosync_logs).
9. Entregables en la webapp (por fases)
- Fase A — Auth & esqueleto API: Sanctum,
routes/api.php,login/logout/me, tabladevices, abilities. - Fase B — PULL:
projects,bundle+ delta,templatesversionadas, tombstones. - Fase C — PUSH:
/syncidempotente 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 endeleted):issue_tasks,issue_comments. - Nuevas operaciones de PUSH en
/sync(idempotentes poruuid):issue_task.create—data:issue_id,title,assigned_to?,due_date?,is_done?. Requiereedit issues.issue_task.update—data:id, y cualquiera detitle/assigned_to/due_date/is_done. Last-write-wins porclient_updated_at. Requiereedit issues.issue_comment.create—data:issue_id,body. Requiereview issues.
- Fotos:
POST /mediaadmiteparent_entity=issue_taskyissue_comment(además deissue). Requiereupload media. - El % de avance de la incidencia se deriva de las tareas completadas (no se almacena ni se sincroniza).