Rewrite frontend types and HTTP client in src.

Export names match the leftover pages. Billing HTTP wrappers stay so
gateway seams are not restyled here.
This commit is contained in:
2026-09-20 19:41:15 +02:00
parent bac75a2fba
commit 2613678442
4 changed files with 1792 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+533
View File
@@ -0,0 +1,533 @@
/** Frontend mirrors of Periscope backend contracts (`periscopex.models` + HTTP). */
export type FindingStatus = "ERROR" | "WARNING" | "INFO";
export type FindingClass = "RULE" | "RISK" | "REVIEW" | "INFO";
export type DatasheetProvenance = "MANDATORY" | "RECOMMENDED" | "TYPICAL" | "EXAMPLE";
export type EvidenceStatus = "SUFFICIENT" | "INSUFFICIENT";
export type FindingReviewState = "open" | "false_positive" | "accepted" | "wontfix";
export interface Finding {
finding_id: string | null;
designator: string;
mpn: string;
aspect: string | null;
finding: string;
why: string;
source_page: number | null;
source_quote?: string;
source_designator?: string | null;
status: FindingStatus;
recommendation?: string;
reference: string;
source?: string | null;
net?: string | null;
pins?: string[];
rule_id?: string | null;
cad_sheet?: string | null;
cad_uuid?: string | null;
variant?: string | null;
facts?: string;
requirement?: string;
inference?: string;
provenance?: DatasheetProvenance | null;
finding_class?: FindingClass | null;
confidence?: number | null;
evidence_status?: EvidenceStatus | null;
calculation?: string;
assumptions?: string[];
action?: string;
decision_id?: string | null;
suppressed?: boolean;
}
export interface FindingComment {
comment_id: string;
finding_id: string;
user_id: string;
user_name: string;
text: string;
mentions: string[];
created_at: string;
}
export interface FindingReview {
state: FindingReviewState;
reason: string;
user_id: string;
user_name: string;
updated_at: string;
}
export interface ReportRelease {
sha256: string;
user_id: string;
timestamp: string;
}
export interface ValidationReport {
project: string;
timestamp: string;
findings: Finding[];
summary: Record<string, number>;
coverage: Record<string, string[]>;
review_errors?: Record<string, string>;
not_reviewed?: { designator: string; reason: string }[];
comments?: Record<string, FindingComment[]>;
review_states?: Record<string, FindingReview>;
release?: ReportRelease;
}
export type NetType = "power" | "ground" | "signal" | "unknown";
export type ComponentType =
| "resistor"
| "capacitor"
| "inductor"
| "ic"
| "connector"
| "crystal"
| "discrete"
| "transformer"
| "fuse"
| "switch"
| "test_point"
| "fiducial"
| "mechanical"
| "unknown";
export interface ComponentSpecs {
specs_type: string;
value_formatted?: string;
values?: Record<string, string | number | null>;
[key: string]: unknown;
}
export interface Component {
reference: string;
value: string;
footprint: string;
component_type: ComponentType;
component_subtype: string | null;
mpn: string | null;
pins: Record<string, string>;
specs: ComponentSpecs | null;
}
export interface PinConnection {
component_ref: string;
pin_number: string;
pin_name: string | null;
}
export interface Net {
name: string;
net_type: NetType;
voltage: number | null;
pins: PinConnection[];
}
export interface DesignGraph {
components: Record<string, Component>;
nets: Record<string, Net>;
bom_fields?: Record<string, { mpn?: string | null; value?: string; dnp?: boolean; variant?: string | null }>;
schematic_fields?: Record<string, { mpn?: string | null; value?: string; cad_uuid?: string; cad_sheet?: string }>;
cad_index?: Record<string, { uuid?: string; sheet?: string }>;
}
export interface BomSummaryRow {
mpn: string | null;
designators: string[];
value: string;
category: string | null;
specs: Record<string, string | number> | null;
description: string | null;
hasDatasheet: boolean;
}
export type ProjectStatus =
| "draft"
| "queued"
| "running"
| "complete"
| "error"
| "cancelled"
| "paused_insufficient_credits"
| "paused_by_user";
export interface PauseCheckpoint {
paused_at?: string | null;
paused_stage?: string | null;
last_completed_label?: string | null;
completed_review_refs?: string[];
pending_review_refs?: string[];
}
export interface SkippedComponent {
identifier: string;
stage: string;
error: string;
}
export interface LcscPayload {
mpn?: string | null;
manufacturer?: string | null;
package?: string | null;
description?: string | null;
category?: string | null;
subcategory?: string | null;
}
export interface ComponentMpnBuckets {
ic: string[];
passive: string[];
simple: string[];
}
export interface Project {
id: string;
name: string;
created: string;
status: ProjectStatus;
summary?: Record<string, number>;
hasNetlist: boolean;
hasBom: boolean;
hasPcb?: boolean;
datasheetCount: number;
skippedComponents?: SkippedComponent[];
completedReviewRefs?: string[];
userId?: string;
collaborators?: string[];
creditsSpent?: number;
totalCostUsd?: number | null;
pauseCheckpoint?: PauseCheckpoint | null;
pauseReason?: string | null;
bomColumns?: { reference: string; mpn: string } | null;
pipelineError?: string | null;
lcscToMpn?: Record<string, string> | null;
lcscPayloads?: Record<string, LcscPayload> | null;
componentMpns?: ComponentMpnBuckets | null;
periscopeVersion?: string | null;
netlistFormat?: "pads" | "edif" | "kicad_xml" | "kicad_sexp" | "kicad_sch" | null;
netlistSubdesigns?: string[] | null;
placementStatus?: "draft" | "queued" | "running" | "complete" | "error" | "cancelled";
placementState?: Record<string, unknown> | null;
pcbStatus?: "draft" | "queued" | "running" | "complete" | "error" | "cancelled";
pcbState?: Record<string, unknown> | null;
}
export type RoleHint =
| "decoupling"
| "bulk"
| "load_cap"
| "filter"
| "pullup"
| "series"
| "divider"
| "bridge"
| "crystal"
| "other";
export interface PlacementSatellite {
ref: string;
component_type?: string;
component_subtype?: string | null;
nets?: string[];
hop?: number;
role_hint?: RoleHint;
}
export interface PlacementIcGroup {
ref: string;
mpn?: string | null;
component_subtype?: string | null;
rank?: number;
nets?: string[];
satellites: PlacementSatellite[];
layout_rules?: unknown[];
assemble_order?: string[];
}
export interface PlacementDomain {
domain_id: string;
power_nets: string[];
ic_refs: string[];
assemble_order: string[];
}
export interface PlacementPlan {
objective?: string;
domains: PlacementDomain[];
groups: PlacementIcGroup[];
}
export interface PlacementProposal {
ref: string;
anchor_ref: string;
rule_kind: string;
max_distance_mm: number;
proposed_x: number;
proposed_y: number;
layer?: string;
basis?: string;
}
export interface PlacementPack {
objective?: string;
status: "packed" | "skipped";
skip_reason?: string | null;
placements: PlacementProposal[];
}
export interface EdifSubDesign {
id: string | null;
instance_count: number;
designators: string[];
}
export interface Collaborator {
user_id: string;
name: string | null;
email: string | null;
image_url: string | null;
role: "owner" | "collaborator";
}
export interface ApiLogEntry {
timestamp: string;
stage: string;
identifier: string;
model: string;
input_tokens: number;
output_tokens: number;
duration_ms: number;
stop_reason: string;
skill_id?: string | null;
turns?: number | null;
error?: string | null;
cost_usd?: number | null;
}
export interface DeratingRow {
designator: string;
mpn: string | null;
value_formatted: string | null;
rated_voltage_v: number | null;
operating_voltage_v: number | null;
operating_voltage_source: string | null;
net_plus: string | null;
net_minus: string | null;
dielectric_category: "ceramic" | "tantalum" | "electrolytic" | null;
dielectric?: string | null;
c_nominal_f?: number | null;
dc_bias_factor?: number | null;
c_eff_f?: number | null;
c_eff_formatted?: string | null;
dc_bias_model?: "stima" | null;
stress?: "PASS" | "MARGIN" | "RISK" | "UNKNOWN";
}
export interface DeratingSettings {
ceramic: number;
tantalum: number;
electrolytic: number;
}
export type ImpedanceKind = "microstrip" | "stripline" | "cpw" | "diff";
export interface ImpedanceTraceResult {
kind: ImpedanceKind;
z0?: number | null;
zodd?: number | null;
zeven?: number | null;
zdiff?: number | null;
w_mm?: number | null;
s_mm?: number | null;
}
export interface ImpedanceTarget {
kind: string;
w_mm: number | null;
s_mm: number | null;
z0: number | null;
zdiff: number | null;
formula: string;
}
export interface ImpedanceStackupResult {
targets: Record<string, ImpedanceTarget>;
kicad_dru: string;
}
export interface ImpedanceNetRow {
net_name: string;
length_mm?: number;
branch_count?: number;
is_differential?: boolean;
partner_net_name?: string | null;
topologies?: string[];
z0_min_ohms?: number | null;
z0_max_ohms?: number | null;
z0_avg_ohms?: number | null;
flags?: string[];
sample_count?: number;
error?: string;
}
export interface ImpedanceNetsReport {
pitch_mm: number;
nets: ImpedanceNetRow[];
skipped: string | null;
}
export interface AntennaVerifyRow {
ic_ref: string;
pin: string;
net: string;
topology: string;
parts: string[];
target_z_ohm: number;
status: "ok" | "warning" | "info";
detail: string;
feed_z0?: number | null;
feed_length_mm?: number | null;
marker_ref?: string | null;
}
export interface AntennaGeometry {
template: "ifa" | "meander" | "stub";
fit: "ok" | "scaled" | "overflow" | "need_f0";
segments: { points: number[][]; width_mm: number }[];
total_length_mm?: number | null;
length_ideal_mm?: number | null;
scale?: number;
svg?: string | null;
kicad_mod?: string | null;
footprint_name?: string | null;
note?: string;
detail?: string;
}
export interface AntennaDesignRecipe {
status: "ready" | "need_pcb" | "need_stackup" | "need_marker";
feed_point?: Record<string, unknown> | null;
feed_line?: {
kind: string;
target_z_ohm: number;
w_mm?: number | null;
h_mm?: number | null;
er?: number | null;
t_mm?: number | null;
} | null;
radiator?: {
length_mm_suggest?: number | null;
f0_mhz?: number | null;
note?: string;
} | null;
geometry?: AntennaGeometry | null;
zone?: {
net: string;
layer: string;
bbox_mm?: number[] | null;
area_mm2?: number | null;
} | null;
keepout_checklist: string[];
detail: string;
}
export interface AntennaReport {
verify: AntennaVerifyRow[];
design: AntennaDesignRecipe | null;
marker_help: string;
}
export interface NetlistPreviewDesignator {
ref: string;
pins: { number: string; net_name: string }[];
}
export interface PipelineSubstep {
key: string;
label: string;
status: "pending" | "running" | "complete";
cached?: boolean;
}
export interface PipelineStep {
title: string;
description: string;
substeps: PipelineSubstep[];
status: "pending" | "running" | "complete";
totalNew?: number;
}
export interface CreditSnapshot {
user_id: string;
balance: number;
plan: string;
last_entry_ts?: string | null;
next_expiry?: string | null;
}
export interface CreditGrant {
grant_id: string;
user_id: string;
amount_usd: number;
remaining: number;
granted_at: string;
expires_at: string | null;
source:
| "top_up"
| "trial_grant"
| "admin_adjust"
| "refund_system_error"
| "pre_migration";
stripe_event_id?: string | null;
expired?: boolean;
note?: string | null;
}
export interface AutoTopupConfig {
enabled: boolean;
threshold_credits: number;
amount_usd: number;
has_payment_method: boolean;
last_attempt_ts?: string | null;
last_attempt_status?: "ok" | "failed" | "pending" | "";
last_failure_reason?: string | null;
}
export interface CreditLedgerEntry {
user_id: string;
timestamp: string;
delta: number;
balance_after: number;
reason: string;
run_id?: string | null;
unit_id?: string | null;
stripe_event_id?: string | null;
note?: string | null;
}
export interface CostItem {
identifier: string;
kind: string;
api_cost_usd: number;
source: "cache_hit" | "api_call" | "api_call_estimated";
note?: string | null;
}
export interface CostEstimate {
api_cost_low: number;
api_cost_high: number;
api_cost_mid: number;
credits_low: number;
credits_high: number;
credits_mid: number;
breakdown: CostItem[];
ic_count: number;
simple_count: number;
passive_count: number;
cached_ic_count: number;
cached_simple_count: number;
cached_passive_count: number;
review_ic_count: number;
}
+44
View File
@@ -0,0 +1,44 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
import type { Finding, FindingClass, FindingStatus } from "./types";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function groupBy<T>(items: T[], key: (item: T) => string): Record<string, T[]> {
const out: Record<string, T[]> = {};
for (const item of items) {
const k = key(item);
(out[k] ??= []).push(item);
}
return out;
}
const BY_STATUS: Record<FindingStatus, number> = { ERROR: 0, WARNING: 1, INFO: 2 };
const BY_CLASS: Record<FindingClass, number> = { RULE: 0, RISK: 1, REVIEW: 2, INFO: 3 };
export function sortFindings(findings: Finding[]): Finding[] {
return [...findings].sort((a, b) => {
const status = BY_STATUS[a.status] - BY_STATUS[b.status];
if (status) return status;
const ac = BY_CLASS[a.finding_class ?? "INFO"] ?? 9;
const bc = BY_CLASS[b.finding_class ?? "INFO"] ?? 9;
if (ac !== bc) return ac - bc;
return (
(a.designator || "").localeCompare(b.designator || "") ||
(a.rule_id || "").localeCompare(b.rule_id || "") ||
(a.finding || "").localeCompare(b.finding || "")
);
});
}
export function getFindingKey(finding: Finding, index: number): string {
return finding.finding_id ?? `${finding.designator}-idx-${index}`;
}
export function subtypeLabel(subtype: string | null): string {
if (!subtype) return "";
const last = subtype.split(".").pop() ?? subtype;
return last.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
}
@@ -0,0 +1,39 @@
"""Native frontend types and API client live under periscope/src."""
from __future__ import annotations
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SRC = ROOT / "periscope" / "src" / "frontend" / "src" / "lib"
DEP = ROOT / "periscope" / "dependency" / "frontend" / "src" / "lib"
def _exports(path: Path) -> set[str]:
text = path.read_text(encoding="utf-8")
names = set(re.findall(r"^export (?:async )?function (\w+)", text, re.M))
names |= set(re.findall(r"^export (?:interface|type|class|const) (\w+)", text, re.M))
return names
def test_frontend_lib_is_src():
for name in ("api.ts", "types.ts", "utils.ts"):
path = SRC / name
assert path.is_file(), name
head = path.read_text(encoding="utf-8")[:400]
assert "Native Periscope overlay" not in head
def test_api_keeps_page_exports():
src = _exports(SRC / "api.ts")
dep = _exports(DEP / "api.ts")
missing = dep - src
assert not missing, sorted(missing)
def test_types_keep_page_exports():
src = _exports(SRC / "types.ts")
dep = _exports(DEP / "types.ts")
missing = dep - src
assert not missing, sorted(missing)