From 292d2e20772f381314914ec24a323f0e852b1ac3 Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Sun, 20 Sep 2026 20:04:04 +0200 Subject: [PATCH] Rewrite Next.js local BFF routes and mock catalog. Projects, report, and graph JSON handlers overlay from periscope/src. Clerk proxy stays the inherited open-core seam. --- .../src/app/(app)/api/_project-json.ts | 30 ++++++++++ .../src/app/(app)/api/graph/[id]/route.ts | 9 +++ .../src/app/(app)/api/projects/route.ts | 6 ++ .../src/app/(app)/api/report/[id]/route.ts | 9 +++ periscope/src/frontend/src/lib/mock-data.ts | 47 +++++++++++++++ tests/test_periscope_frontend_bff_rewrite.py | 58 +++++++++++++++++++ 6 files changed, 159 insertions(+) create mode 100644 periscope/src/frontend/src/app/(app)/api/_project-json.ts create mode 100644 periscope/src/frontend/src/app/(app)/api/graph/[id]/route.ts create mode 100644 periscope/src/frontend/src/app/(app)/api/projects/route.ts create mode 100644 periscope/src/frontend/src/app/(app)/api/report/[id]/route.ts create mode 100644 periscope/src/frontend/src/lib/mock-data.ts create mode 100644 tests/test_periscope_frontend_bff_rewrite.py diff --git a/periscope/src/frontend/src/app/(app)/api/_project-json.ts b/periscope/src/frontend/src/app/(app)/api/_project-json.ts new file mode 100644 index 0000000..322ec3c --- /dev/null +++ b/periscope/src/frontend/src/app/(app)/api/_project-json.ts @@ -0,0 +1,30 @@ +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { NextResponse } from "next/server"; + +const SIBLING_ROOT = path.resolve(process.cwd(), ".."); + +function safeSegment(id: string): string | null { + if (!id || id.includes("/") || id.includes("\\") || id === "." || id === "..") { + return null; + } + return id; +} + +export async function jsonFromProjectFile( + id: string, + filename: string, + missing: string, +): Promise { + const projectId = safeSegment(id); + if (!projectId) { + return NextResponse.json({ error: missing }, { status: 404 }); + } + const filePath = path.join(SIBLING_ROOT, projectId, filename); + try { + const raw = await readFile(filePath, "utf-8"); + return NextResponse.json(JSON.parse(raw) as unknown); + } catch { + return NextResponse.json({ error: missing }, { status: 404 }); + } +} diff --git a/periscope/src/frontend/src/app/(app)/api/graph/[id]/route.ts b/periscope/src/frontend/src/app/(app)/api/graph/[id]/route.ts new file mode 100644 index 0000000..b840d55 --- /dev/null +++ b/periscope/src/frontend/src/app/(app)/api/graph/[id]/route.ts @@ -0,0 +1,9 @@ +import { jsonFromProjectFile } from "../../_project-json"; + +export async function GET( + _req: Request, + { params }: { params: Promise<{ id: string }> }, +) { + const { id } = await params; + return jsonFromProjectFile(id, "design_graph.json", "Graph not found"); +} diff --git a/periscope/src/frontend/src/app/(app)/api/projects/route.ts b/periscope/src/frontend/src/app/(app)/api/projects/route.ts new file mode 100644 index 0000000..13f74f0 --- /dev/null +++ b/periscope/src/frontend/src/app/(app)/api/projects/route.ts @@ -0,0 +1,6 @@ +import { NextResponse } from "next/server"; +import { PROJECTS } from "@/lib/mock-data"; + +export async function GET() { + return NextResponse.json(PROJECTS); +} diff --git a/periscope/src/frontend/src/app/(app)/api/report/[id]/route.ts b/periscope/src/frontend/src/app/(app)/api/report/[id]/route.ts new file mode 100644 index 0000000..104298f --- /dev/null +++ b/periscope/src/frontend/src/app/(app)/api/report/[id]/route.ts @@ -0,0 +1,9 @@ +import { jsonFromProjectFile } from "../../_project-json"; + +export async function GET( + _req: Request, + { params }: { params: Promise<{ id: string }> }, +) { + const { id } = await params; + return jsonFromProjectFile(id, "report.json", "Report not found"); +} diff --git a/periscope/src/frontend/src/lib/mock-data.ts b/periscope/src/frontend/src/lib/mock-data.ts new file mode 100644 index 0000000..0f0526f --- /dev/null +++ b/periscope/src/frontend/src/lib/mock-data.ts @@ -0,0 +1,47 @@ +import type { PipelineStep, Project } from "./types"; + +/** Demo row for the local Next BFF when the FastAPI catalog is not in front. */ +export const PROJECTS: Project[] = [ + { + id: "simple_project", + name: "TI MSP Tutorial Board", + created: "2026-04-09T00:00:00Z", + status: "complete", + summary: { total: 8, ERROR: 2, WARNING: 4, INFO: 2 }, + hasNetlist: true, + hasBom: true, + datasheetCount: 3, + }, +]; + +function pending(title: string, description: string, substeps: PipelineStep["substeps"]): PipelineStep { + return { title, description, status: "pending", substeps }; +} + +function pendingItem(key: string, label: string) { + return { key, label, status: "pending" as const }; +} + +export function createPipelineSteps(): PipelineStep[] { + return [ + pending("IC Datasheet Extraction", "Parse component datasheets and extract constraints", [ + pendingItem("SPX3819M5-L-3-3/TR", "SPX3819M5-L-3-3/TR (U1)"), + pendingItem("CH340E", "CH340E (U2)"), + pendingItem("MSPM0G3507SPTR", "MSPM0G3507SPTR (U3)"), + ]), + pending("Passive Pattern Extraction", "Resolve passive component values from MPN patterns", [ + pendingItem("samsung", "Samsung capacitors"), + pendingItem("uniroyal", "Uniroyal resistors"), + ]), + pending("Build Design Graph", "Parse BOM and netlist into structured graph", [ + pendingItem("parse-bom", "Parse BOM (17 components)"), + pendingItem("parse-netlist", "Parse netlist (36 nets)"), + pendingItem("enrich", "Enrich with datasheet data"), + ]), + pending("Review Design", "Review each IC against its datasheet", [ + pendingItem("U1", "Review U1 — LDO"), + pendingItem("U2", "Review U2 — USB Bridge"), + pendingItem("U3", "Review U3 — MCU"), + ]), + ]; +} diff --git a/tests/test_periscope_frontend_bff_rewrite.py b/tests/test_periscope_frontend_bff_rewrite.py new file mode 100644 index 0000000..92f4d04 --- /dev/null +++ b/tests/test_periscope_frontend_bff_rewrite.py @@ -0,0 +1,58 @@ +"""Next.js BFF routes for local JSON live under periscope/src.""" + +from __future__ import annotations + +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SRC = ROOT / "periscope" / "src" / "frontend" / "src" + + +def _text(rel: str) -> str: + path = SRC / rel + assert path.is_file(), rel + return path.read_text(encoding="utf-8") + + +def test_bff_routes_are_src(): + for rel in ( + "lib/mock-data.ts", + "app/(app)/api/_project-json.ts", + "app/(app)/api/projects/route.ts", + "app/(app)/api/report/[id]/route.ts", + "app/(app)/api/graph/[id]/route.ts", + ): + head = _text(rel)[:400] + assert "Native Periscope overlay" not in head + + +def test_projects_route_serves_mock_catalog(): + route = _text("app/(app)/api/projects/route.ts") + assert "export async function GET" in route + assert "PROJECTS" in route + mock = _text("lib/mock-data.ts") + assert "export const PROJECTS" in mock + assert "simple_project" in mock + assert "export function createPipelineSteps" in mock + + +def test_report_and_graph_read_sibling_json(): + report = _text("app/(app)/api/report/[id]/route.ts") + graph = _text("app/(app)/api/graph/[id]/route.ts") + helper = _text("app/(app)/api/_project-json.ts") + assert "report.json" in report + assert "Report not found" in report + assert "design_graph.json" in graph + assert "Graph not found" in graph + assert "jsonFromProjectFile" in helper + assert "params: Promise<{ id: string }>" in report + assert "params: Promise<{ id: string }>" in graph + + +def test_proxy_stays_clerk_seam_in_dependency(): + src_proxy = ROOT / "periscope" / "src" / "frontend" / "src" / "proxy.ts" + dep_proxy = ROOT / "periscope" / "dependency" / "frontend" / "src" / "proxy.ts" + assert not src_proxy.exists() + text = dep_proxy.read_text(encoding="utf-8") + assert "Open-core seam" in text + assert "export function proxy" in text