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