perf(geoportal): materialize simplified UAT geometries (fixes 90% CPU on PostgreSQL)

ST_SimplifyPreserveTopology in views runs on every Martin tile request,
causing constant CPU load. Fix: pre-compute simplified geometries into
dedicated columns (geom_z0, geom_z5, geom_z8) on the GisUat table.

POST /api/geoportal/optimize-views:
1. Adds geom_z0/z5/z8 columns to GisUat
2. Backfills with pre-computed simplifications (one-time cost)
3. Creates GiST spatial indexes on each
4. Replaces views to use pre-computed columns (zero CPU reads)
5. Updates trigger to auto-compute on INSERT/UPDATE

Setup banner: now checks optimization status and shows "Optimizeaza"
button if needed. One-click, then docker restart martin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-24 10:51:17 +02:00
parent c4122cea01
commit 53c241c20f
2 changed files with 163 additions and 32 deletions
@@ -1,49 +1,65 @@
"use client";
import { useEffect, useState } from "react";
import { Database, Loader2, CheckCircle2, AlertTriangle } from "lucide-react";
import { Database, Loader2, CheckCircle2, AlertTriangle, Zap } from "lucide-react";
import { Button } from "@/shared/components/ui/button";
type Status = "checking" | "needed" | "running" | "done" | "error" | "ready";
type Status = "checking" | "views-needed" | "optimize-needed" | "running" | "done" | "error" | "ready";
export function SetupBanner() {
const [status, setStatus] = useState<Status>("checking");
const [missing, setMissing] = useState<string[]>([]);
const [message, setMessage] = useState("");
const [error, setError] = useState("");
// Check on mount
useEffect(() => {
fetch("/api/geoportal/setup-views")
.then((r) => r.json())
.then((d: { ready: boolean; missing: string[] }) => {
if (d.ready) setStatus("ready");
else { setStatus("needed"); setMissing(d.missing); }
})
.catch(() => setStatus("ready")); // don't block if check fails
(async () => {
try {
// Check views
const vr = await fetch("/api/geoportal/setup-views").then((r) => r.json());
if (!vr.ready) {
setStatus("views-needed");
setMessage(`${vr.missing?.length ?? 0} view-uri PostGIS lipsesc`);
return;
}
// Check optimization
const or2 = await fetch("/api/geoportal/optimize-views").then((r) => r.json());
if (!or2.optimized) {
setStatus("optimize-needed");
setMessage("View-urile UAT calculeaza geometria on-the-fly (CPU intensiv). Optimizeaza pentru performanta.");
return;
}
setStatus("ready");
} catch {
setStatus("ready");
}
})();
}, []);
const runSetup = async () => {
const runAction = async (endpoint: string) => {
setStatus("running");
try {
const r = await fetch("/api/geoportal/setup-views", { method: "POST" });
const r = await fetch(endpoint, { method: "POST" });
const d = await r.json();
if (d.status === "ok") setStatus("done");
else { setStatus("error"); setError(d.error ?? "Eroare necunoscuta"); }
if (d.status === "ok") {
setStatus("done");
setMessage("Restart Martin pe server: docker restart martin");
} else {
setStatus("error");
setError(d.error ?? "Eroare");
}
} catch (e) {
setStatus("error");
setError(e instanceof Error ? e.message : "Eroare retea");
}
};
// Don't show if ready or still checking
if (status === "ready" || status === "checking") return null;
// Auto-hide after success
if (status === "done") {
return (
<div className="bg-green-500/10 border border-green-500/30 rounded-lg px-3 py-2 flex items-center gap-2 text-xs text-green-700 dark:text-green-400">
<CheckCircle2 className="h-4 w-4" />
View-uri create cu succes. Restarteaza Martin pe server: <code className="bg-muted px-1 rounded">docker restart martin</code>
<CheckCircle2 className="h-4 w-4 shrink-0" />
<span>{message}</span>
</div>
);
}
@@ -51,28 +67,27 @@ export function SetupBanner() {
if (status === "error") {
return (
<div className="bg-red-500/10 border border-red-500/30 rounded-lg px-3 py-2 flex items-center gap-2 text-xs text-red-700 dark:text-red-400">
<AlertTriangle className="h-4 w-4" />
Eroare: {error}
<Button variant="outline" size="sm" className="h-6 text-xs ml-auto" onClick={runSetup}>Reincearca</Button>
<AlertTriangle className="h-4 w-4 shrink-0" />
{error}
</div>
);
}
const isViews = status === "views-needed";
return (
<div className="bg-amber-500/10 border border-amber-500/30 rounded-lg px-3 py-2 flex items-center gap-2 text-xs">
<Database className="h-4 w-4 text-amber-600 dark:text-amber-400 shrink-0" />
<span className="text-amber-700 dark:text-amber-300">
{missing.length} view-uri PostGIS lipsesc ({missing.join(", ")}). Necesare pentru tile-uri UAT optimizate.
</span>
{isViews
? <Database className="h-4 w-4 text-amber-600 dark:text-amber-400 shrink-0" />
: <Zap className="h-4 w-4 text-amber-600 dark:text-amber-400 shrink-0" />}
<span className="text-amber-700 dark:text-amber-300 flex-1">{message}</span>
<Button
variant="outline"
size="sm"
className="h-6 text-xs ml-auto shrink-0"
onClick={runSetup}
variant="outline" size="sm" className="h-6 text-xs shrink-0"
disabled={status === "running"}
onClick={() => runAction(isViews ? "/api/geoportal/setup-views" : "/api/geoportal/optimize-views")}
>
{status === "running" ? <Loader2 className="h-3 w-3 animate-spin mr-1" /> : null}
{status === "running" ? "Se creaza..." : "Creaza view-uri"}
{status === "running" && <Loader2 className="h-3 w-3 animate-spin mr-1" />}
{isViews ? "Creaza view-uri" : "Optimizeaza"}
</Button>
</div>
);