feat(parcel-sync): sync-first architecture — DB as ground truth

- Rewrite export-bundle to sync-first: check freshness -> sync layers -> enrich (magic) -> build GPKG/CSV from local DB
- Rewrite export-layer-gpkg to sync-first: sync if stale -> export from DB
- Create enrich-service.ts: extracted magic enrichment logic (CF, owners, addresses) with DB storage
- Add enrichment + enrichedAt columns to GisFeature schema
- Update PostGIS views to include enrichment data
- UI: update button labels for sync-first semantics, refresh sync status after exports
- Smart caching: skip sync if data is fresh (168h / 1 week default)
This commit is contained in:
AI Assistant
2026-03-07 11:12:54 +02:00
parent 0d0b1f8c9f
commit b0927ee075
7 changed files with 898 additions and 698 deletions
@@ -337,6 +337,7 @@ export function ParcelSyncModule() {
const [syncingLayer, setSyncingLayer] = useState<string | null>(null);
const [syncProgress, setSyncProgress] = useState("");
const [exportingLocal, setExportingLocal] = useState(false);
const refreshSyncRef = useRef<(() => void) | null>(null);
/* ── PostGIS setup ───────────────────────────────────────────── */
const [postgisRunning, setPostgisRunning] = useState(false);
@@ -597,6 +598,8 @@ export function ParcelSyncModule() {
pollingRef.current = null;
}
setExporting(false);
// Refresh sync status — data was synced to DB
refreshSyncRef.current?.();
},
[siruta, exporting, startPolling],
);
@@ -699,6 +702,9 @@ export function ParcelSyncModule() {
}
}, [siruta]);
// Keep ref in sync so callbacks defined earlier can trigger refresh
refreshSyncRef.current = () => void fetchSyncStatus();
// Auto-fetch sync status when siruta changes
useEffect(() => {
if (siruta && /^\d+$/.test(siruta)) {
@@ -876,6 +882,8 @@ export function ParcelSyncModule() {
pollingRef.current = null;
}
setDownloadingLayer(null);
// Refresh sync status — layer was synced to DB
refreshSyncRef.current?.();
},
[siruta, downloadingLayer, startPolling],
);
@@ -1775,7 +1783,7 @@ export function ParcelSyncModule() {
Sync
</span>
</Button>
{/* Direct GPKG from eTerra */}
{/* GPKG (sync-first: syncs to DB if needed, then exports from DB) */}
<Button
size="sm"
variant="outline"
@@ -1783,6 +1791,11 @@ export function ParcelSyncModule() {
onClick={() =>
void handleExportLayer(layer.id)
}
title={
localCount > 0
? "Descarcă GPKG (din cache dacă e proaspăt)"
: "Sincronizează + descarcă GPKG"
}
>
{isDownloading ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
@@ -1793,21 +1806,6 @@ export function ParcelSyncModule() {
GPKG
</span>
</Button>
{/* Export from local DB */}
{localCount > 0 && (
<Button
size="sm"
variant="ghost"
disabled={exportingLocal}
onClick={() =>
void handleExportLocal([layer.id])
}
title="Exportă din baza de date locală"
className="text-violet-600 dark:text-violet-400"
>
<HardDrive className="h-3.5 w-3.5" />
</Button>
)}
</div>
</div>
</div>
@@ -2033,7 +2031,7 @@ export function ParcelSyncModule() {
Descarcă Terenuri și Clădiri
</div>
<div className="text-xs opacity-70 font-normal">
GPKG terenuri.gpkg + cladiri.gpkg
Sync + GPKG (din cache dacă e proaspăt)
</div>
</div>
</Button>
@@ -2052,7 +2050,7 @@ export function ParcelSyncModule() {
<div className="text-left">
<div className="font-semibold">Magic</div>
<div className="text-xs opacity-70 font-normal">
GPKG îmbogățit + CSV cu proprietari, CF, adresă
Sync + îmbogățire (CF, proprietari, adresă) + GPKG + CSV
</div>
</div>
</Button>