Rewrite frontend site identity, version stamp, and Excel export.

Changelog remains the source of APP_VERSION via overlay sync-version.mjs.
This commit is contained in:
2026-09-20 20:00:15 +02:00
parent d21a303e8b
commit a9ce71f17a
5 changed files with 229 additions and 0 deletions
@@ -0,0 +1,52 @@
#!/usr/bin/env node
/** Stamp APP_VERSION from the latest changelog heading into version.ts and package.json. */
import { readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const changelog = join(root, "content", "changelog.md");
const versionFile = join(root, "src", "lib", "version.ts");
const pkgFile = join(root, "package.json");
const text = readFileSync(changelog, "utf-8");
const hit = text.match(
/^##\s+(\d+\.\d+\.\d+)(?:\s+[—-]\s+(\d{4}-\d{2}-\d{2}))?/m,
);
if (!hit) {
console.error(`[sync-version] no ## X.Y.Z heading in ${changelog}`);
process.exit(1);
}
const version = hit[1];
const date = hit[2] ?? "";
const generated = `/** Stamped from content/changelog.md by scripts/sync-version.mjs. */
export const APP_VERSION = "${version}";
export const APP_VERSION_DATE = "${date}";
`;
let tsDirty = true;
try {
tsDirty = readFileSync(versionFile, "utf-8") !== generated;
} catch {
tsDirty = true;
}
if (tsDirty) writeFileSync(versionFile, generated);
const pkg = JSON.parse(readFileSync(pkgFile, "utf-8"));
const pkgDirty = pkg.version !== version;
if (pkgDirty) {
pkg.version = version;
writeFileSync(pkgFile, JSON.stringify(pkg, null, 2) + "\n");
}
if (tsDirty || pkgDirty) {
const bits = [
tsDirty ? "version.ts" : "",
pkgDirty ? "package.json" : "",
].filter(Boolean);
console.log(`[sync-version] Synced to ${version} (${bits.join(" + ")}).`);
} else {
console.log(`[sync-version] Already at ${version}.`);
}
@@ -0,0 +1,60 @@
/** Client-side findings workbook (.xlsx) for a finished review. */
import * as XLSX from "xlsx";
import type { DesignGraph, Finding, ValidationReport } from "./types";
import { sortFindings } from "./utils";
const COLUMNS = [
"Designator",
"MPN",
"ID",
"Severity",
"Title",
"Description",
"Recommendation",
"Source",
"Net",
"Pins",
"Rule",
] as const;
const WIDTHS = [12, 20, 12, 10, 44, 60, 50, 24, 16, 16, 14];
function sourceCell(f: Finding): string {
if (f.source && f.source !== "review") return "Automated check";
const who = f.source_designator || f.designator;
return f.source_page ? `${who} datasheet p.${f.source_page}` : `${who} datasheet`;
}
function fileStem(name: string): string {
const slug = name.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "");
return slug || "report";
}
export function exportReportToExcel(
report: ValidationReport,
graph: DesignGraph,
projectName?: string,
): void {
const body = sortFindings(report.findings).map((f) => [
f.designator,
f.mpn || graph.components[f.designator]?.mpn || "",
f.finding_id ?? "",
f.status,
f.finding,
f.why ?? "",
f.recommendation ?? "",
sourceCell(f),
f.net ?? "",
(f.pins ?? []).join(", "),
f.rule_id ?? "",
]);
const sheet = XLSX.utils.aoa_to_sheet([COLUMNS, ...body]);
sheet["!cols"] = WIDTHS.map((wch) => ({ wch }));
const book = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(book, sheet, "Findings");
XLSX.writeFile(
book,
`${fileStem(projectName || report.project || "report")}-findings.xlsx`,
);
}
+67
View File
@@ -0,0 +1,67 @@
/** Public identity of this Periscope instance (not Faradworks). */
import type { Metadata } from "next";
export const SITE_URL = (
process.env.NEXT_PUBLIC_SITE_URL?.trim().replace(/\/$/, "") ||
"https://periscope.michelebigi.it"
);
export const SITE_NAME = "Periscope";
export const SITE_DESCRIPTION =
"Periscope reviews your schematic against every datasheet and catches the errors that would otherwise surface at bring-up. Works with KiCad, Altium, OrCAD, Cadence, Siemens, EasyEDA, and EAGLE.";
export const SITE_TAGLINE = "Agentic schematic validation";
export const OPERATOR_NAME = "Michele Bigi";
export const CONTACT_EMAIL = "mikbigi@gmail.com";
/** Unset: this operator has no X/Twitter handle. */
export const TWITTER_HANDLE: string | undefined = undefined;
export function pageMetadata({
title,
description,
path,
}: {
title: string;
description: string;
path: string;
}): Metadata {
const url = path.startsWith("/") ? path : `/${path}`;
const fullTitle = `${title} · ${SITE_NAME}`;
const twitter: NonNullable<Metadata["twitter"]> = {
card: "summary_large_image",
title: fullTitle,
description,
images: ["/twitter-image"],
};
if (TWITTER_HANDLE) {
twitter.site = TWITTER_HANDLE;
twitter.creator = TWITTER_HANDLE;
}
return {
title,
description,
alternates: { canonical: url },
openGraph: {
type: "website",
siteName: SITE_NAME,
locale: "en_US",
url,
title: fullTitle,
description,
images: [
{
url: "/opengraph-image",
width: 1200,
height: 630,
alt: `${SITE_NAME}${SITE_TAGLINE}`,
},
],
},
twitter,
};
}
@@ -0,0 +1,3 @@
/** Stamped from content/changelog.md by scripts/sync-version.mjs. */
export const APP_VERSION = "2.52.0";
export const APP_VERSION_DATE = "2026-09-20";
@@ -0,0 +1,47 @@
"""Native frontend site/version/export modules live under periscope/src."""
from __future__ import annotations
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SRC = ROOT / "periscope" / "src" / "frontend"
def test_site_version_export_are_src():
for rel in (
"src/lib/site.ts",
"src/lib/version.ts",
"src/lib/report-export.ts",
"scripts/sync-version.mjs",
):
path = SRC / rel
assert path.is_file(), rel
assert "Native Periscope overlay" not in path.read_text(encoding="utf-8")[:400]
def test_site_exports():
text = (SRC / "src/lib/site.ts").read_text(encoding="utf-8")
for name in (
"SITE_URL",
"SITE_NAME",
"SITE_DESCRIPTION",
"SITE_TAGLINE",
"OPERATOR_NAME",
"CONTACT_EMAIL",
"TWITTER_HANDLE",
"pageMetadata",
):
assert name in text
def test_report_export_entry():
text = (SRC / "src/lib/report-export.ts").read_text(encoding="utf-8")
assert "export function exportReportToExcel" in text
assert "Findings" in text
def test_version_constants():
text = (SRC / "src/lib/version.ts").read_text(encoding="utf-8")
assert "export const APP_VERSION" in text
assert "export const APP_VERSION_DATE" in text