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.
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
// 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`;
|
||||
};
|
||||
Reference in New Issue
Block a user