Files
pilens/web/src/api.ts
T
Andy Dixon 01b568aaf7 Web UI, packaging, systemd units, installer scripts, namespace integration tests
React/TS analyser (virtualised packet list, protocol tree, verified hex mapping),
setup wizard, network/devices/health/library/settings pages. Installer with
backup and rollback, uninstall/repair/diagnose. Docker+netns integration suite
covering DHCP, DNS, NAT, isolation, IPv6 blocking, capture and export.
2026-10-09 17:56:02 +01:00

87 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Thin API client. All state-changing requests carry the per-session CSRF token.
let csrf = "";
export const setCSRF = (t: string) => { csrf = t; };
export class ApiError extends Error {
constructor(public status: number, message: string, public body?: any) { super(message); }
}
export async function api<T = any>(path: string, opts: { method?: string; body?: any; raw?: BodyInit; signal?: AbortSignal } = {}): Promise<T> {
const headers: Record<string, string> = { "X-Requested-With": "pilens" };
const method = opts.method ?? (opts.body !== undefined || opts.raw ? "POST" : "GET");
if (method !== "GET") headers["X-CSRF-Token"] = csrf;
let body: BodyInit | undefined = opts.raw;
if (opts.body !== undefined) { headers["Content-Type"] = "application/json"; body = JSON.stringify(opts.body); }
const r = await fetch(path, { method, headers, body, credentials: "same-origin", signal: opts.signal });
const text = await r.text();
let data: any = undefined;
try { data = text ? JSON.parse(text) : undefined; } catch { data = text; }
if (!r.ok) {
if (r.status === 401 && path !== "/api/auth/login") window.dispatchEvent(new Event("pilens:unauthorised"));
throw new ApiError(r.status, (data && data.error) || r.statusText, data);
}
return data as T;
}
// ---- types mirroring the Go API ----
export interface Row { n: number; t: number; s: string; d: string; p: string; l: number; i: string; sp?: number; dp?: number; st?: number; tf?: string; ttl?: number; vl?: number; c?: string }
export interface Page { total: number; first: number; offset: number; rows: Row[]; nums?: number[]; filtered: boolean }
export interface TreeNode { label: string; field?: string; show?: string; pos: number; size: number; verified: boolean; children?: TreeNode[] }
export interface Detail { row: Row; bytes: string; tree: TreeNode[] | null; segment: number; note?: string }
export interface Session {
id: string; name: string; description: string; kind: "live" | "import"; state: string; interfaces: string[]; capture_filter: string;
started_at: number; ended_at: number; packets: number; bytes: number; drops: string; error: string; preserved: boolean;
tags: string; notes: string; ring_segments: number; meta: any; paused: boolean; ingest_lag: number;
}
export interface Segment { seq: number; file: string; format: string; base: number; count: number; bytes: number; sha256: string; complete: boolean; deleted: boolean }
export interface Iface { name: string; mac: string; addrs: string[] | null; up: boolean; carrier: boolean; wireless: boolean; driver: string; default_gateway?: string; virtual: boolean; speed_mbps?: number }
export interface Network {
management: { interfaces: string[]; allowed_subnets: string[]; port: number };
uplink: string;
ap: { enabled: boolean; interface: string; ssid: string; passphrase?: string; security: string; band: string; channel: number; hidden: boolean; country: string; client_isolation: boolean };
lan: { address: string; dhcp_start: string; dhcp_end: string; lease_minutes: number; upstream_dns: string[] | null; reservations: { mac: string; ip: string; hostname?: string }[] | null; dns_overrides: { name: string; ip: string }[] | null };
forwarding: { mode: string; allow_cidrs: string[] | null };
}
export interface ApplyResult { applied: boolean; rolled_back: boolean; steps: string[]; error?: string }
export interface Limits { max_bytes: number; reserve_bytes: number; segment_bytes: number; import_bytes: number }
// ---- live events (SSE) ----
type Handler = (type: string, data: any) => void;
const handlers = new Set<Handler>();
let es: EventSource | null = null;
export function connectEvents() {
if (es) return;
es = new EventSource("/api/events");
for (const t of ["hello", "packets", "capture", "resync", "ping"]) {
es.addEventListener(t, (e) => {
let d: any = {};
try { d = JSON.parse((e as MessageEvent).data); } catch { /* ignore */ }
handlers.forEach((h) => h(t, d));
});
}
es.onerror = () => handlers.forEach((h) => h("disconnected", {})); // EventSource reconnects by itself
}
export function disconnectEvents() { es?.close(); es = null; }
export function onEvent(h: Handler) { handlers.add(h); return () => { handlers.delete(h); }; }
// ---- formatting ----
export const fmtBytes = (n: number) => {
if (!isFinite(n)) return "–";
const u = ["B", "KiB", "MiB", "GiB", "TiB"]; let i = 0;
while (Math.abs(n) >= 1024 && i < u.length - 1) { n /= 1024; i++; }
return `${n.toFixed(i ? 1 : 0)} ${u[i]}`;
};
export const fmtTime = (ms: number) => ms ? new Date(ms).toLocaleString() : "–";
export const fmtDur = (s: number) => {
if (!isFinite(s) || s < 0) return "–";
const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), x = Math.floor(s % 60);
return h ? `${h}h ${m}m` : m ? `${m}m ${x}s` : `${x}s`;
};