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:
@@ -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`,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user