diff --git a/periscope/src/backend/periscopex/models.py b/periscope/src/backend/periscopex/models.py index 66f8897..fb7fdf9 100644 --- a/periscope/src/backend/periscopex/models.py +++ b/periscope/src/backend/periscopex/models.py @@ -385,7 +385,7 @@ class ValidationReport(BaseModel): coverage: dict[str, list[str]] = {} # designator -> areas checked and found OK review_errors: dict[str, str] = {} # designator -> error message for ICs whose review raised not_reviewed: list[dict] = [] # [{"designator","reason"}] — ICs skipped (e.g. no datasheet PDF) - # M0 protocol cert: empty instances / "nessun protocollo riconosciuto". No Z. + # M0 protocol cert: empty instances / "No protocol recognized.". No Z. protocol_certification: dict[str, Any] | None = None diff --git a/periscope/src/backend/periscopex/protocol_catalog.py b/periscope/src/backend/periscopex/protocol_catalog.py index 5ee5e95..6072456 100644 --- a/periscope/src/backend/periscopex/protocol_catalog.py +++ b/periscope/src/backend/periscopex/protocol_catalog.py @@ -25,7 +25,7 @@ CATALOG_DIR = Path(__file__).resolve().parent / "protocol_data" CATALOG_PATH = CATALOG_DIR / "catalog.json" SCHEMA_PATH = CATALOG_DIR / f"schema-{SCHEMA_VERSION}.json" -EMPTY_PROTOCOL_MESSAGE = "nessun protocollo riconosciuto" +EMPTY_PROTOCOL_MESSAGE = "No protocol recognized." BusType = Literal[ "MEMORY", diff --git a/periscope/src/backend/services/pcb_pipeline.py b/periscope/src/backend/services/pcb_pipeline.py index 98fca49..b78dc6b 100644 --- a/periscope/src/backend/services/pcb_pipeline.py +++ b/periscope/src/backend/services/pcb_pipeline.py @@ -10,6 +10,7 @@ from __future__ import annotations import json import logging +import time from datetime import datetime, timezone from pathlib import Path @@ -220,6 +221,14 @@ async def run_pcb_pipeline( pcb_state=None, ) + t_total = time.perf_counter() + stage_seconds: dict[str, float] = {} + + def _lap(name: str, started: float) -> float: + seconds = round(time.perf_counter() - started, 3) + stage_seconds[name] = seconds + return seconds + try: async with PipelineWorkspace(storage, user_id, project_id) as ws: if _cancelled(storage, user_id, project_id): @@ -247,7 +256,10 @@ async def run_pcb_pipeline( return _step(project_id, "classify", "running", "domains and groups") + t_extract = time.perf_counter() cmap = _load_constraints_map(ws.local_path("extracted"), storage) + extract_s = _lap("extract", t_extract) + _step(project_id, "extract", "complete", f"loaded stored extractions; {extract_s}s") plan = build_placement_plan(graph, cmap) fg_path = ws.local_path("functional_groups.json") fg_path.write_text(plan.model_dump_json(indent=2) + "\n") @@ -306,7 +318,9 @@ async def run_pcb_pipeline( return _step(project_id, "checks", "running") + t_checks = time.perf_counter() findings = run_pcb_checks(graph, cmap, layout, plan, zrep) + checks_s = _lap("pcb_checks", t_checks) scan_ver = app_settings.get_default_model_version() si_skip = si_extract_needed_skips(graph, cmap, scan_ver) sipath = ws.local_path("si_extract_needed.json") @@ -315,7 +329,8 @@ async def run_pcb_pipeline( _step( project_id, "checks", "complete", f"{len(findings)} deterministic findings" - + (f"; {len(si_skip)} SI re-extract" if si_skip else ""), + + (f"; {len(si_skip)} SI re-extract" if si_skip else "") + + f"; {checks_s}s", ) if _cancelled(storage, user_id, project_id): @@ -323,6 +338,7 @@ async def run_pcb_pipeline( return _step(project_id, "af_trace", "running", "AF trigger + closed-form/cascade (not OpenEMS)") + t_af_trace = time.perf_counter() try: extra_af = check_af_traces(graph, cmap, layout, zrep, findings) findings.extend(extra_af) @@ -330,9 +346,10 @@ async def run_pcb_pipeline( except Exception: logger.exception("AF trace analysis failed — keeping PCB checks") n_trace = 0 + af_trace_s = _lap("af_trace", t_af_trace) _step( project_id, "af_trace", "complete", - f"{n_trace} AF trace findings (SI/HF checks kept)", + f"{n_trace} AF trace findings (SI/HF checks kept); {af_trace_s}s", ) if _cancelled(storage, user_id, project_id): @@ -340,10 +357,13 @@ async def run_pcb_pipeline( return _step(project_id, "af_ai", "running", "AI HF flags then deterministic investigate") + t_af_ai = time.perf_counter() n_af = _run_af_ai_section(ws, graph, cmap, layout, zrep, findings) + af_ai_s = _lap("af_ai", t_af_ai) + stage_seconds["af"] = round(stage_seconds.get("af_trace", 0) + af_ai_s, 3) _step( project_id, "af_ai", "complete", - f"{n_af} extra AF+AI findings (SI/HF checks kept)", + f"{n_af} extra AF+AI findings (SI/HF checks kept); {af_ai_s}s; af={stage_seconds['af']}s", ) if _cancelled(storage, user_id, project_id): @@ -387,11 +407,14 @@ async def run_pcb_pipeline( return _step(project_id, "write_report", "running") + t_protocol = time.perf_counter() proto_section, proto_findings = protocol_exam( graph, layout=layout, constraints_map=cmap, impedance_nets=zrep, channel_data=_load_channel_data(ws), ) + protocol_s = _lap("protocol", t_protocol) + _step(project_id, "protocol", "complete", f"{protocol_s}s") findings.extend(proto_findings) assign_pcb_finding_ids(findings) annotate_findings_cad(findings, cad_index_from_graph(graph)) @@ -422,10 +445,22 @@ async def run_pcb_pipeline( ws._upload_file("periscope-findings.json") _step(project_id, "write_report", "complete", f"{len(findings)} findings") + stage_seconds["total"] = round(time.perf_counter() - t_total, 3) + _step( + project_id, "timing", "complete", + "extract={extract}s pcb_checks={pcb_checks}s protocol={protocol}s af={af}s total={total}s".format( + extract=stage_seconds.get("extract"), + pcb_checks=stage_seconds.get("pcb_checks"), + protocol=stage_seconds.get("protocol"), + af=stage_seconds.get("af"), + total=stage_seconds.get("total"), + ), + ) _publish(project_id, "pcb_complete", { "findings": len(findings), "domains": len(plan.domains), "groups": len(plan.groups), + "stage_seconds": stage_seconds, }) proj_svc.update_project( storage, user_id, project_id, @@ -435,15 +470,17 @@ async def run_pcb_pipeline( "domains": len(plan.domains), "groups": len(plan.groups), "skipped": len(skipped), + "stage_seconds": stage_seconds, }, pcb_cancel_requested=False, ) except Exception as e: logger.exception("pcb pipeline failed for %s", project_id) + stage_seconds["total"] = round(time.perf_counter() - t_total, 3) proj_svc.update_project( storage, user_id, project_id, pcb_status="error", - pcb_state={"error": str(e)}, + pcb_state={"error": str(e), "stage_seconds": stage_seconds}, ) _publish(project_id, "pcb_error", {"error": str(e)}) diff --git a/periscope/src/frontend/content/changelog.md b/periscope/src/frontend/content/changelog.md index 4aefd20..65aa38e 100644 --- a/periscope/src/frontend/content/changelog.md +++ b/periscope/src/frontend/content/changelog.md @@ -14,6 +14,7 @@ PE-AF-002 on an Ethernet MDI pair compares one calculated pair Z to the packed c - [Changed] `bom_pcb_check.py`, `esd_return_check.py`, `protocol_l0.py`, `af_trace_check.py`. - [Changed] pytest `tests/pcb/test_bom_esd_ep_cc_eth.py`. +- [Changed] Findings tree labels are English: domains, Certifications, Protocols, actions, and empty states. Error / Warning / Info stay. ## 2.83.0 — 2026-09-22 — Apple-like shell; USB-C vSafe still UNKNOWN diff --git a/periscope/src/frontend/package.json b/periscope/src/frontend/package.json index 39764ac..290489c 100644 --- a/periscope/src/frontend/package.json +++ b/periscope/src/frontend/package.json @@ -1,6 +1,6 @@ { "name": "periscope-web", - "version": "2.83.0", + "version": "2.84.0", "private": true, "scripts": { "sync-version": "node scripts/sync-version.mjs", diff --git a/periscope/src/frontend/src/app/(app)/project/[id]/report/page.tsx b/periscope/src/frontend/src/app/(app)/project/[id]/report/page.tsx index 7b63c5e..80f596c 100644 --- a/periscope/src/frontend/src/app/(app)/project/[id]/report/page.tsx +++ b/periscope/src/frontend/src/app/(app)/project/[id]/report/page.tsx @@ -343,9 +343,9 @@ function ReportContent({ projectId }: { projectId: string }) { {report.summary.total === 0 && !(report.protocol_certification?.recognized_instances?.length) ? (
Nessun finding
+No findings
- Carica i datasheet degli IC sulla pagina progetto e rilancia l’esame. + Upload the IC datasheets on the project page and run the exam again.
- {props.protocolSection?.message || "Nessun protocollo riconosciuto."} + {protocolEmptyMessage(props.protocolSection?.message)}
) : null}Nessun finding visibile
+No visible findings
- I filtri nascondono l’elenco. Togli un filtro, oppure apri i findings già rivisti sotto. + Filters are hiding the list. Clear a filter, or open the reviewed findings below.
FAIL in testa — non sotto Warning.
+FAIL is listed first — not under Warning.
) : null}Catena: {instance.chainExample}
+Chain: {instance.chainExample}
) : null} {checks .filter((c) => c.result === "FAIL" || c.skip_visible) @@ -113,12 +114,12 @@ export function ProtocolSection({ section?: ProtocolCertificationSection | null; }) { const instances = protocolInstancesFromSection(section); - const message = section?.message || "Nessun protocollo riconosciuto."; + const message = protocolEmptyMessage(section?.message); const maxLevel = section?.max_level_reached || "—"; return (- Livello massimo {maxLevel} (L0–L3). HDMI 1.4 TMDS resta UNKNOWN — niente Zdiff inventato. + Max level {maxLevel} (L0–L3). HDMI 1.4 TMDS stays UNKNOWN — no invented Zdiff.
{instances.length === 0 ? ({message}
diff --git a/periscope/src/frontend/src/components/report/report-summary.tsx b/periscope/src/frontend/src/components/report/report-summary.tsx index e1683e6..2c340aa 100644 --- a/periscope/src/frontend/src/components/report/report-summary.tsx +++ b/periscope/src/frontend/src/components/report/report-summary.tsx @@ -20,18 +20,18 @@ export function ReportSummary({ const infos = summary.INFO ?? 0; const total = summary.total || errors + warnings + infos; const outcome = - errors > 0 ? "Error" : warnings > 0 ? "Attenzione" : total > 0 ? "Completato" : "Senza findings"; + errors > 0 ? "Error" : warnings > 0 ? "Warning" : total > 0 ? "Complete" : "No findings"; const bits = [ `${errors} Error`, `${warnings} Warning`, `${infos} Info`, - `${reviewedCount} rivisti`, + `${reviewedCount} reviewed`, ]; if (typeof totalCostUsd === "number" && totalCostUsd > 0) { bits.push(`$${totalCostUsd.toFixed(2)}`); } if (typeof creditsSpent === "number" && creditsSpent > 0) { - bits.push(`${creditsSpent.toFixed(2)} crediti`); + bits.push(`${creditsSpent.toFixed(2)} credits`); } return ( diff --git a/periscope/src/frontend/src/lib/findings-forest.test.ts b/periscope/src/frontend/src/lib/findings-forest.test.ts index 93c3c76..e870cf0 100644 --- a/periscope/src/frontend/src/lib/findings-forest.test.ts +++ b/periscope/src/frontend/src/lib/findings-forest.test.ts @@ -1,6 +1,13 @@ import assert from "node:assert/strict"; import { test } from "node:test"; -import { groupByWorstStatus, groupDomains, isProtocolFinding, worstStatus } from "./findings-forest"; +import { + groupByWorstStatus, + groupDomains, + isProtocolFinding, + protocolEmptyMessage, + ruleDomain, + worstStatus, +} from "./findings-forest"; import { isPcbExamFinding } from "./layout-finding"; import type { Finding } from "./types"; @@ -72,6 +79,16 @@ test("Error folder lists domains and rules after status, protocol stays out of t assert.equal(groupDomains(groups[0].findings.filter((x) => !isProtocolFinding(x))).length, 1); }); +test("protocol folder label is Protocols, including a stored Italian empty message", () => { + const domain = ruleDomain( + f({ designator: "J2", status: "ERROR", rule_id: "PE-PRT-L0-001", source: "protocol_l0" }), + ); + assert.equal(domain.label, "Protocols"); + assert.equal(protocolEmptyMessage("nessun protocollo riconosciuto"), "No protocol recognized."); + assert.equal(protocolEmptyMessage(""), "No protocol recognized."); + assert.equal(protocolEmptyMessage("USB2 recognized"), "USB2 recognized"); +}); + test("PE-PLC stays in the PCB exam bucket with PE-BOM", () => { assert.equal( isPcbExamFinding({ source: "placement_check", rule_id: "PE-PLC-002" }), diff --git a/periscope/src/frontend/src/lib/findings-forest.ts b/periscope/src/frontend/src/lib/findings-forest.ts index 36d7037..4826a68 100644 --- a/periscope/src/frontend/src/lib/findings-forest.ts +++ b/periscope/src/frontend/src/lib/findings-forest.ts @@ -1,4 +1,4 @@ -/** Group report findings for the tree: status folders, then domains/rules, then Certificazioni. */ +/** Group report findings for the tree: status folders, then domains/rules, then Certifications. */ import type { Finding, FindingStatus, ProtocolCertificationSection } from "./types"; import { @@ -45,7 +45,7 @@ const DOMAIN_LABEL: Record