Compare commits

...
2 Commits
Author SHA1 Message Date
javierandClaude Opus 4.8 7278c1363d feat(sync): prefetch del catálogo de plantillas para trabajar offline
Las plantillas solo llegaban a local en el bundle al abrir un proyecto
online. Ahora, al cargar la lista de proyectos con conexión, se prefetchan
TODAS las plantillas accesibles (GET /templates, antes sin usar) y se
guardan en local, para poder inspeccionar offline aunque el proyecto no se
haya abierto todavía. Tolerante a fallo: no bloquea la lista.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 14:08:40 +02:00
javierandClaude Opus 4.8 3ed07cae3c fix(sync): proyectar el valor del servidor en conflicto a columnas locales
El backend devuelve el modelo completo (toArray) en un conflicto: incluye
geometry como array y columnas inexistentes en local (uuid, created_at,
deleted_at, client_updated_at...). Volcarlo tal cual en SQLite lanzaba
"no such column"/error de bind y abortaba el ciclo de sync entero ante
cualquier edición concurrente. Ahora se proyecta por los mismos pick* del PULL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 14:08:28 +02:00
2 changed files with 45 additions and 2 deletions
+35 -1
View File
@@ -390,6 +390,20 @@ export async function getTemplates(): Promise<Template[]> {
}));
}
/**
* Persiste el catálogo global de plantillas (GET /templates) para tenerlas
* SIEMPRE en local y poder inspeccionar offline, aunque el proyecto concreto
* aún no se haya abierto. Upsert por id: no borra las que ya hubiera.
*/
export async function saveTemplates(templates: Template[]): Promise<void> {
const db = await getDb();
await db.withTransactionAsync(async () => {
for (const t of templates) {
await upsertById(db, 'templates', pickTemplate(t));
}
});
}
export async function getFeatureTypes(): Promise<FeatureType[]> {
const db = await getDb();
return db.getAllAsync<FeatureType>('SELECT * FROM feature_types ORDER BY name');
@@ -409,6 +423,22 @@ export async function getMediaFor(
}
/**
* Proyecta el valor crudo del servidor (un `Model::toArray()`) a SOLO las
* columnas que existen en la tabla local, reutilizando los mismos `pick*` del
* PULL. Imprescindible: el `toArray()` trae columnas inexistentes en local
* (uuid, created_at, deleted_at, client_updated_at, properties…) y, en features,
* `geometry` como array —que SQLite no puede bindear—. Sin esto, un conflicto
* lanzaría "no such column"/error de bind y abortaría el ciclo de sync entero.
*/
const CONFLICT_PROJECTOR: Record<string, (v: never) => Record<string, unknown>> = {
features: (v) => pickFeature(v as Feature),
issues: (v) => pickIssue(v as Issue),
issue_tasks: (v) => pickIssueTask(v as IssueTask),
inspections: (v) => pickInspection(v as Inspection),
issue_comments: (v) => pickIssueComment(v as IssueComment),
};
/** Aplica el valor del servidor (recibido en un conflicto) a la fila local. */
export async function applyServerValue(
table: string,
@@ -416,7 +446,11 @@ export async function applyServerValue(
serverValue: Record<string, unknown>,
): Promise<void> {
const db = await getDb();
await upsertById(db, table, { id, ...serverValue });
const project = CONFLICT_PROJECTOR[table];
// Si no hay proyector conocido, cae a un id-only (no-op seguro) en vez de
// volcar columnas crudas que podrían no existir en la tabla local.
const row = project ? project(serverValue as never) : { id };
await upsertById(db, table, { ...row, id });
}
// ---------- creaciones offline (optimista + reconciliación) ----------
+10 -1
View File
@@ -12,7 +12,7 @@ import {
import * as api from '../api/endpoints';
import { Project } from '../api/types';
import { useSession } from '../auth/session';
import { getProjects, saveProjectList, setActiveProjectId } from '../db/repositories';
import { getProjects, saveProjectList, saveTemplates, setActiveProjectId } from '../db/repositories';
import { isOnline } from '../net/connectivity';
import { runSync } from '../sync/engine';
import { RootStackParamList } from '../navigation/types';
@@ -38,6 +38,15 @@ export function ProjectsScreen({ navigation }: Props) {
if (await isOnline()) {
const list = normalize(await api.listProjects());
await saveProjectList(list);
// Prefetch del catálogo global de plantillas: así quedan SIEMPRE en
// local y se puede inspeccionar offline aunque el proyecto no se haya
// abierto todavía. No bloquea la lista si /templates falla.
try {
const { templates } = await api.getTemplates();
await saveTemplates(templates);
} catch (e) {
console.warn('No se pudieron prefetchar las plantillas:', e);
}
}
setProjects(await getProjects());
} catch (e) {