Reorganiza backend

This commit is contained in:
Oxy8
2026-03-02 17:33:45 -03:00
parent bba0ae887d
commit d4bfa5f064
8 changed files with 419 additions and 124 deletions

View File

@@ -1,10 +1,14 @@
import { useEffect, useRef, useState } from "react";
import { Renderer } from "./renderer";
function sleep(ms: number): Promise<void> {
return new Promise((r) => setTimeout(r, ms));
}
export default function App() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const rendererRef = useRef<Renderer | null>(null);
const [status, setStatus] = useState("Loading node positions…");
const [status, setStatus] = useState("Waiting for backend…");
const [nodeCount, setNodeCount] = useState(0);
const [stats, setStats] = useState({
fps: 0,
@@ -16,7 +20,7 @@ export default function App() {
const [error, setError] = useState("");
const [hoveredNode, setHoveredNode] = useState<{ x: number; y: number; screenX: number; screenY: number } | null>(null);
const [selectedNodes, setSelectedNodes] = useState<Set<number>>(new Set());
const [backendStats, setBackendStats] = useState<{ nodes: number; edges: number; parsed_triples: number } | null>(null);
const [backendStats, setBackendStats] = useState<{ nodes: number; edges: number; backend?: string } | null>(null);
// Store mouse position in a ref so it can be accessed in render loop without re-renders
const mousePos = useRef({ x: 0, y: 0 });
@@ -36,68 +40,79 @@ export default function App() {
let cancelled = false;
// Optional: fetch backend stats (proxied via Vite) so you can confirm backend is up.
fetch("/api/stats")
.then((r) => (r.ok ? r.json() : null))
.then((j) => {
if (!j || cancelled) return;
if (typeof j.nodes === "number" && typeof j.edges === "number" && typeof j.parsed_triples === "number") {
setBackendStats({ nodes: j.nodes, edges: j.edges, parsed_triples: j.parsed_triples });
}
})
.catch(() => {
// Backend is optional; ignore failures.
});
// Fetch CSVs, parse, and init renderer
(async () => {
try {
setStatus("Fetching data files…");
const [nodesResponse, edgesResponse] = await Promise.all([
fetch("/node_positions.csv"),
fetch("/edges.csv"),
]);
if (!nodesResponse.ok) throw new Error(`Failed to fetch nodes: ${nodesResponse.status}`);
if (!edgesResponse.ok) throw new Error(`Failed to fetch edges: ${edgesResponse.status}`);
// Wait for backend (docker-compose also gates startup via healthcheck, but this
// handles running the frontend standalone).
const deadline = performance.now() + 180_000;
let attempt = 0;
while (performance.now() < deadline) {
attempt++;
setStatus(`Waiting for backend… (attempt ${attempt})`);
try {
const res = await fetch("/api/health");
if (res.ok) break;
} catch {
// ignore and retry
}
await sleep(1000);
if (cancelled) return;
}
const [nodesText, edgesText] = await Promise.all([
nodesResponse.text(),
edgesResponse.text(),
]);
setStatus("Fetching graph…");
const graphRes = await fetch("/api/graph");
if (!graphRes.ok) throw new Error(`Failed to fetch graph: ${graphRes.status}`);
const graph = await graphRes.json();
if (cancelled) return;
setStatus("Parsing positions…");
const nodeLines = nodesText.split("\n").slice(1).filter(l => l.trim().length > 0);
const count = nodeLines.length;
const nodes = Array.isArray(graph.nodes) ? graph.nodes : [];
const edges = Array.isArray(graph.edges) ? graph.edges : [];
const meta = graph.meta || null;
const count = nodes.length;
// Build positions from backend-provided node coordinates.
setStatus("Preparing buffers…");
const xs = new Float32Array(count);
const ys = new Float32Array(count);
for (let i = 0; i < count; i++) {
const nx = nodes[i]?.x;
const ny = nodes[i]?.y;
xs[i] = typeof nx === "number" ? nx : 0;
ys[i] = typeof ny === "number" ? ny : 0;
}
const vertexIds = new Uint32Array(count);
for (let i = 0; i < count; i++) {
const parts = nodeLines[i].split(",");
vertexIds[i] = parseInt(parts[0], 10);
xs[i] = parseFloat(parts[1]);
ys[i] = parseFloat(parts[2]);
const id = nodes[i]?.id;
vertexIds[i] = typeof id === "number" ? id >>> 0 : i;
}
setStatus("Parsing edges…");
const edgeLines = edgesText.split("\n").slice(1).filter(l => l.trim().length > 0);
const edgeData = new Uint32Array(edgeLines.length * 2);
for (let i = 0; i < edgeLines.length; i++) {
const parts = edgeLines[i].split(",");
edgeData[i * 2] = parseInt(parts[0], 10);
edgeData[i * 2 + 1] = parseInt(parts[1], 10);
// Build edges as vertex-id pairs.
const edgeData = new Uint32Array(edges.length * 2);
for (let i = 0; i < edges.length; i++) {
const s = edges[i]?.source;
const t = edges[i]?.target;
edgeData[i * 2] = typeof s === "number" ? s >>> 0 : 0;
edgeData[i * 2 + 1] = typeof t === "number" ? t >>> 0 : 0;
}
if (cancelled) return;
// Use /api/graph meta; don't do a second expensive backend call.
if (meta && typeof meta.nodes === "number" && typeof meta.edges === "number") {
setBackendStats({
nodes: meta.nodes,
edges: meta.edges,
backend: typeof meta.backend === "string" ? meta.backend : undefined,
});
} else {
setBackendStats({ nodes: nodes.length, edges: edges.length });
}
setStatus("Building spatial index…");
await new Promise(r => setTimeout(r, 0));
await new Promise((r) => setTimeout(r, 0));
const buildMs = renderer.init(xs, ys, vertexIds, edgeData);
setNodeCount(renderer.getNodeCount());
setStatus("");
console.log(`Init complete: ${count.toLocaleString()} nodes, ${edgeLines.length.toLocaleString()} edges in ${buildMs.toFixed(0)}ms`);
console.log(`Init complete: ${count.toLocaleString()} nodes, ${edges.length.toLocaleString()} edges in ${buildMs.toFixed(0)}ms`);
} catch (e) {
if (!cancelled) {
setError(e instanceof Error ? e.message : String(e));
@@ -295,7 +310,7 @@ export default function App() {
<div style={{ color: "#f80" }}>Selected: {selectedNodes.size}</div>
{backendStats && (
<div style={{ color: "#8f8" }}>
Backend: {backendStats.nodes.toLocaleString()} nodes, {backendStats.edges.toLocaleString()} edges
Backend{backendStats.backend ? ` (${backendStats.backend})` : ""}: {backendStats.nodes.toLocaleString()} nodes, {backendStats.edges.toLocaleString()} edges
</div>
)}
</div>