Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8.9 KiB
ConstruProgress — Brief para la App Móvil
Documento único de traspaso para construir la app móvil que consume la API de
ConstruProgress. La fuente de verdad del contrato es openapi.yaml;
el modelo offline está en MOBILE_SYNC_PROTOCOL.md. Este
brief los resume y añade ejemplos de payloads reales y el modelo de datos.
Para trabajar en el repo móvil con Claude Code viendo este backend:
claude --add-dir C:\xampp\htdocs\construprogress
1. Objetivo
App de seguimiento de obra que funciona sin conexión en campo: descarga los datos de un proyecto (estructura + plantillas), permite trabajar offline (actualizar progreso, registrar inspecciones, gestionar incidencias con tareas/comentarios/fotos) y sincroniza cuando hay red.
2. Autenticación (Laravel Sanctum)
- Token Bearer por dispositivo, con ability
mobile-sync. POST /logincon{ email, password, device_name, app_version? }→{ token, user }.- En el resto de llamadas: cabecera
Authorization: Bearer <token>. POST /logoutrevoca el token del dispositivo actual.- Guarda el token en almacenamiento seguro (Expo SecureStore / flutter_secure_storage).
Base URL: https://<host>/api/v1 (confirmar host de despliegue; en local XAMPP
suele ser http://localhost/construprogress/public/api/v1).
curl -X POST https://<host>/api/v1/login \
-H "Content-Type: application/json" \
-d '{"email":"user@mai.group","password":"secret","device_name":"Pixel-8"}'
# → { "token": "12|abc...", "user": { "id":1, "name":"...", "roles":[...], "permissions":[...] } }
3. Endpoints (8)
| Método | Ruta | Uso | Rate limit |
|---|---|---|---|
| POST | /login |
Token de dispositivo | 10/min |
| GET | /me |
Usuario + permisos | — |
| POST | /logout |
Revocar token | — |
| GET | /projects |
Proyectos accesibles | — |
| GET | /projects/{id}/bundle?since= |
PULL: snapshot o delta + tombstones | — |
| GET | /templates?since= |
Plantillas de inspección (version+hash) | — |
| POST | /sync |
PUSH: lote de mutaciones offline | 60/min |
| POST | /media |
Subir fichero (multipart) | 120/min |
4. PULL — descarga de datos
4.1 Primera sincronización (snapshot completo)
GET /projects/{id}/bundle devuelve:
{
"server_time": "2026-06-18T12:00:00+00:00", // úsalo como próximo `since`
"project": { ... },
"phases": [ ... ],
"layers": [ ... ],
"features": [ ... ],
"inspections": [ ... ],
"issues": [ ... ],
"issue_tasks": [ ... ],
"issue_comments": [ ... ],
"templates": [ ... ],
"media": [ ... ],
"deleted": {} // vacío en snapshot completo
}
4.2 Sincronizaciones siguientes (delta)
GET /projects/{id}/bundle?since=<ISO8601 URL-encoded> → solo lo cambiado tras since
y un objeto deleted con los ids borrados (tombstones) por entidad:
"deleted": {
"phases": [], "layers": [], "features": [], "inspections": [],
"issues": [], "issue_tasks": [], "issue_comments": []
}
⚠️ URL-encodea el
since(el+del offset horario). Guardaserver_timede cada respuesta y úsalo como el siguientesince.
4.3 Plantillas
GET /templates?since= devuelve las plantillas de inspección de los proyectos
accesibles, cada una con version (timestamp) y hash (para detectar cambios).
5. PUSH — POST /sync
Envía un lote. Cada operación lleva una uuid generada en el cliente (clave de
idempotencia) y client_updated_at:
{
"operations": [
{
"entity": "feature",
"op": "update",
"uuid": "0f8e2b6c-....", // único y estable por operación
"client_updated_at": "2026-06-18T11:30:00+00:00",
"data": { "id": 5, "status": "completed", "progress": 100 }
}
]
}
Respuesta — un resultado por operación:
{ "results": [
{ "uuid": "0f8e...", "status": "applied", "server_id": 5 }
] }
status ∈ applied | duplicate | conflict | error.
duplicate: ya se había aplicado esauuid(reintento seguro).conflict: el servidor es más nuevo → trae"server": {...}con el valor actual (last-write-wins porclient_updated_at). Resuélvelo en el cliente y reintenta.error: trae"error": "..."(validación o permisos).
5.1 Operaciones soportadas (entity.op → data → permiso requerido)
| entity.op | data |
Permiso |
|---|---|---|
progress_update.create |
{ phase_id, progress(0-100), comment?, location? } |
update progress |
feature.update |
{ id, status?, progress?(0-100), responsible? } |
update progress |
inspection.create |
{ feature_id, template_id?, data?, status?, result?, notes? } |
create inspections |
issue.create |
{ project_id, feature_id?, title, description?, priority?, status?, type? } |
create issues |
issue.update |
{ id, title?, description?, priority?, status?, type?, assigned_to?, resolution_notes? } |
edit issues |
issue_task.create |
{ issue_id, title, assigned_to?, due_date?, is_done? } |
edit issues |
issue_task.update |
{ id, title?, assigned_to?, due_date?, is_done? } |
edit issues |
issue_comment.create |
{ issue_id, body } |
view issues |
Valores enum:
issue.priority:low | medium | high | criticalissue.status:open | in_review | resolved | closedissue.type:defect | safety | quality | documentation | other
El servidor SIEMPRE fija
user_id/reported_by/project_idy valida permiso + pertenencia al proyecto. El cliente nunca los manda.
6. Media — POST /media (multipart/form-data)
Campos: uuid (idempotencia), parent_entity, parent_id, file, category?
(image|document|other), description?.
parent_entity ∈ feature | issue | issue_task | issue_comment | project | phase | layer.
curl -X POST https://<host>/api/v1/media \
-H "Authorization: Bearer <token>" \
-F "uuid=4b1f...-uuid" \
-F "parent_entity=issue" -F "parent_id=12" \
-F "category=image" -F "file=@/path/defecto.jpg"
# → { "status":"applied", "media": { "id":99, "url":"/storage/...", ... } }
Requiere permiso upload media + pertenencia al proyecto. Idempotente por uuid.
7. Modelo de datos (campos que devuelve el bundle)
project : id, reference, name, address, lat, lng, status, updated_at
phase : id, name, order, color, progress_percent, updated_at
layer : id, phase_id, name, color, updated_at
feature : id, layer_id, name, geometry(GeoJSON), status, progress,
responsible, template_id, updated_at
inspection : id, feature_id, layer_id, template_id, user_id, data(obj),
status, result, notes, created_at, updated_at
issue : id, feature_id, title, description, status, priority, type,
reported_by, assigned_to, resolved_at, updated_at
issue_task : id, issue_id, title, is_done, done_at, done_by, assigned_to,
due_date, order, updated_at
issue_comment : id, issue_id, user_id, body, created_at, updated_at
template : id, project_id, phase_id, name, description, fields(array),
version, hash, updated_at
media : id, uuid, parent_entity, parent_id, url, name, file_type,
category, updated_at
8. Arquitectura cliente recomendada
Stack: React Native + Expo (alternativa: Flutter). Offline-first:
- BD local: SQLite (expo-sqlite / WatermelonDB) — o Drift/Isar en Flutter. Refleja las entidades del bundle.
- Sincronización PULL: guarda
server_time; en cada arranque/con red llama abundle?since=<último server_time>, aplica upserts y borra losdeleted. - Outbox (cola de salida): cada cambio offline genera una operación con
uuidpropio y se encola. Con red, envías el lote a/syncy procesas los resultados:applied/duplicate→ marcar enviado;conflict→ re-mergear;error→ revisar. - Media: sube los ficheros pendientes a
/media(también conuuid) y referencia laurldevuelta. - Token: en almacenamiento seguro; si 401 → re-login.
Flujo típico de sesión
login → guardar token
GET /projects → elegir proyecto
GET /projects/{id}/bundle (sin since) → poblar BD local
... trabajo offline (encolar operaciones + fotos) ...
con red: POST /sync (lote) → POST /media (ficheros) → GET bundle?since=server_time
9. Checklist de arranque del repo móvil
- Elegir stack (RN+Expo / Flutter) y crear el proyecto.
claude --add-dir C:\xampp\htdocs\construprogresspara tener el contrato a mano.- Capa de API (login/me/logout, projects, bundle, templates, sync, media).
- BD local + repositorios por entidad.
- Motor de sync (PULL delta + outbox PUSH + media) con manejo de conflictos.
- UI: lista de proyectos, mapa/fases, inspecciones, incidencias (checklist, comentarios, fotos), indicador de estado de sincronización.