New sensor cpu.power: watts from the RAPL energy counters (Intel, and AMD Zen through the same driver), summed over every package and averaged into history. Parts of a package, psys and the MMIO duplicate are left out. The counters are root-only, so a daemon that isn't root reads them through `hamlab cpu-energy`, a root helper started once with sudo or doas and asked over a pipe each sample, the same shape as the GPU client walk's helper. It reads only the counters it finds itself. No counters or no root means no reading, and the CPU box shows nothing extra. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
466 lines
45 KiB
TypeScript
466 lines
45 KiB
TypeScript
export class Unauthenticated extends Error { constructor(msg = 'unauthenticated') { super(msg) } }
|
||
|
||
// Which machine the console is looking at. '' is this host; otherwise every /api/… call
|
||
// is rewritten to /api/nodes/<name>/… and the hub proxies it. Hub-only calls (login,
|
||
// the machine list itself) pass local=true and are never rewritten.
|
||
let base = ''
|
||
export function setMachine(name: string | null) { base = name ? `/api/nodes/${name}` : '' }
|
||
export const apiPath = (p: string) => (base && p.startsWith('/api/') ? base + p.slice(4) : p)
|
||
|
||
// A machine that just failed to answer is left alone for a few seconds instead of
|
||
// being hit again on every tick of every page.
|
||
const downUntil: Record<string, number> = {}
|
||
function machineOf(p: string): string | null { const m = p.match(/^\/api\/nodes\/([a-z0-9-]+)\//); return m ? m[1] : null }
|
||
|
||
let inflight = 0
|
||
function track(delta: number) {
|
||
inflight += delta
|
||
window.dispatchEvent(new CustomEvent('hamlab:inflight', { detail: inflight }))
|
||
}
|
||
|
||
async function req<T>(method: string, path: string, body?: unknown, local = false): Promise<T> {
|
||
const target = local ? path : apiPath(path)
|
||
const mach = machineOf(target)
|
||
if (mach && downUntil[mach] > Date.now()) throw new Error(`${mach} unreachable (retrying shortly)`)
|
||
track(1)
|
||
let r: Response
|
||
try {
|
||
r = await fetch(target, {
|
||
method,
|
||
credentials: 'same-origin',
|
||
headers: body !== undefined ? { 'Content-Type': 'application/json' } : undefined,
|
||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||
})
|
||
} catch (e) {
|
||
track(-1)
|
||
throw new Error('network error')
|
||
}
|
||
track(-1)
|
||
if (r.status === 502 && mach) downUntil[mach] = Date.now() + 5000
|
||
if (r.status === 401) {
|
||
// The hub itself said no: the session is gone. (A 401 relayed from a managed machine is that
|
||
// machine rejecting the hub, which is not ours to fix by logging in again.)
|
||
if (!mach && target !== '/api/login') window.dispatchEvent(new Event('hamlab:unauthenticated'))
|
||
let msg = 'unauthenticated'
|
||
try { msg = (await r.json()).error ?? msg } catch { /* ignore */ }
|
||
throw new Unauthenticated(msg)
|
||
}
|
||
if (!r.ok) {
|
||
let msg = r.statusText
|
||
try { msg = (await r.json()).error ?? msg } catch { /* ignore */ }
|
||
throw new Error(msg)
|
||
}
|
||
if (r.status === 204) return undefined as T
|
||
return r.json()
|
||
}
|
||
|
||
export interface UpdateStatus { current: string; latest: string; arch: string; behind: boolean; checking: boolean; installing: boolean; enabled: boolean; auto: boolean; url: string; notes?: string; published?: string; checked?: string; error?: string }
|
||
export interface Me { addr?: string; host: string; user: string; account?: string; guest?: boolean; guest_until?: string; update_available?: string; version: string; load: string; uptime: number; reboot_needed?: boolean; fingerprint?: string; arch?: string }
|
||
export interface PushResult { name: string; ok: boolean; version?: string; error?: string }
|
||
// What a machine is called on screen: the hostname it reports, which follows a rename within
|
||
// a poll. The name it was added under stays its identity — the URL, its accent colour, and
|
||
// what the hub's own API calls it — and is what we fall back to until it has been reached.
|
||
export const titleOf = (name: string | null, machines: MachineStatus[], hubHost: string): string =>
|
||
name ? machines.find((m) => m.name === name)?.host || name : hubHost
|
||
|
||
export interface MachineStatus { name: string; url: string; fingerprint?: string; added: string; online: boolean; host?: string; version?: string; load?: string; uptime?: number; reboot_needed?: boolean; last_seen?: string; error?: string }
|
||
export interface Term { id: string; name: string; running: boolean; exit_code: number; created: string; clients: number }
|
||
|
||
export interface Temp { chip: string; label: string; c: number; max?: number; crit?: number }
|
||
export interface Fan { chip: string; label: string; rpm: number }
|
||
export interface NetIf { iface: string; up: boolean; rx_bps: number; tx_bps: number; rx_total: number; tx_total: number }
|
||
export interface Disk { name: string; model?: string; size: number; read_bps: number; write_bps: number; util_pct: number; temp?: number }
|
||
export interface FS { mount: string; type: string; size: number; used: number; remote?: boolean; disk?: string }
|
||
export interface UPS { driver: string; model: string; state: string; source: string; on_battery: boolean; in_v: number; out_v: number; battery_pct: number; runtime_min: number; load_w: number; load_pct: number; last_event?: string }
|
||
export interface Proc { pid: number; ppid: number; name: string; cmd?: string; user: string; cpu: number; gpu?: number; cpu_time: number; rss: number; threads: number; state: string; nice: number; started: number }
|
||
export interface MemInfo { total: number; used: number; avail: number; free?: number; cached?: number; buffers?: number }
|
||
// A graphics device: usage_src says what measures it ("idle" residency, the driver's own "busy", or
|
||
// the busiest "clients" engine) and is absent when nothing does; temp_src is "gpu" for the device's own
|
||
// sensor or "package" for the CPU die an integrated GPU shares.
|
||
export interface GPUClient { pid: number; name: string; usage: number; engines?: Record<string, number>; mem?: number }
|
||
export interface GPU {
|
||
name: string; driver: string; bus?: string; integrated?: boolean
|
||
usage: number; usage_src?: 'idle' | 'busy' | 'clients'; engines?: Record<string, number>
|
||
temp?: number; temp_src?: 'gpu' | 'package'; power_w?: number; power_cap_w?: number; fan_rpm?: number
|
||
mhz?: number; req_mhz?: number; max_mhz?: number; mem_used?: number; mem_total?: number
|
||
clients: GPUClient[]; clients_ok: boolean; stalled?: boolean
|
||
}
|
||
export interface ProcTable { t: number; cpu: number; cores: number[]; cpu_hist: number[]; cpu_model?: string; cpu_mhz?: number; cpu_max_mhz?: number; cpu_power_w?: number; cpu_temp?: number; core_ids?: number[]; load: [number, number, number]; mem: MemInfo; swap: MemInfo; net: NetIf[]; gpus?: GPU[]; uptime: number; procs: Proc[]; total: number; running: number }
|
||
export interface Sample {
|
||
t: number; interval_ms: number; cpu: number; cores: number[]; load: [number, number, number]
|
||
mem: { total: number; used: number; avail: number }; swap: { total: number; used: number; avail: number }
|
||
temps: Temp[]; fans: Fan[]; net: NetIf[]; disks: Disk[]; fs: FS[]; ups: UPS | null; uptime: number
|
||
}
|
||
export interface Series {
|
||
t: number[]; cpu: number[]; mem: number[]
|
||
net: Record<string, { rx: number[]; tx: number[] }>
|
||
disk: Record<string, { r: number[]; w: number[]; u: number[] }>
|
||
temp: Record<string, number[]>
|
||
}
|
||
|
||
// Dashboard layout. `order` lists module ids top-to-bottom; `hidden` are modules switched
|
||
// off; `hiddenItems` are individual rows ("net:wlp1s0", "fs:/boot/efi", "disk:sda", "temp:chip/label").
|
||
export interface HistoryWindow { seconds: number; points: number; stride_sec: number; min_seconds: number; max_seconds: number }
|
||
|
||
export interface ServiceUnit { name: string; description: string; load: string; active: string; sub: string; enabled: string; failed: boolean }
|
||
export interface ServiceDetail extends ServiceUnit { props: Record<string, string> }
|
||
export interface LogEntry { t: number; p: number; ident: string; pid?: string; msg: string }
|
||
|
||
export interface CronJob { id: string; source: string; line: number; user: string; schedule: string; command: string; enabled: boolean; next?: number; last?: number; out_file?: string; note?: string; editable: boolean }
|
||
export interface CronTimer { unit: string; activates: string; description: string; active: string; sub: string; enabled: string; next?: number; last?: number }
|
||
export interface CronOverview { jobs: CronJob[]; timers: CronTimer[]; logging: boolean; anacron: boolean; sudo: boolean }
|
||
export interface CronRun { id: string; job_id: string; command: string; user: string; started: string; finished?: string; running: boolean; exit_code: number; output: string }
|
||
|
||
export interface TaskInfo { id: string; kind: string; title: string; command: string; started: string; finished?: string; running: boolean; exit_code: number; error?: string; output?: string }
|
||
export interface UpdatePackage { name: string; current: string; candidate: string; suite?: string; security: boolean; description?: string }
|
||
export interface UpdatesStatus { manager: string; can_upgrade: boolean; selective: boolean; packages: UpdatePackage[]; security_count: number; last_refresh?: number; last_upgrade?: number; reboot_needed: boolean; reboot_pkgs: string[]; auto_upgrades: boolean; running_task?: string; sudo: boolean }
|
||
|
||
export interface StoragePart { name: string; type: string; fstype?: string; mount?: string; label?: string; size: number; used?: number; fs_size?: number }
|
||
export interface StorageDevice { name: string; kind: string; model?: string; vendor?: string; serial?: string; tran?: string; size: number; rotational: boolean; removable: boolean; temp?: number; parts: StoragePart[]; namespaces?: string[] }
|
||
export interface SmartAttr { id: number; name: string; value: number; worst: number; thresh: number; raw: string; failed?: string; warn: boolean }
|
||
export interface SmartInfo { device: string; available: boolean; supported: boolean; standby: boolean; passed: boolean | null; temp: number; power_on_hours: number; power_cycles: number; model?: string; serial?: string; firmware?: string; attrs: SmartAttr[]; nvme?: { critical_warning: number; percentage_used: number; available_spare: number; spare_threshold: number; media_errors: number; error_log_entries: number; data_read_bytes: number; data_written_bytes: number; unsafe_shutdowns: number }; mmc?: { life_a: number; life_b: number; pre_eol: number }; self_test?: string; self_test_remaining?: number; self_test_supported: boolean; self_test_note?: string; warnings: string[]; error?: string; fetched_at: number }
|
||
export interface DockerContainer { id: string; name: string; image: string; state: string; status: string; health: string; ports: string; stack: string; service: string; created: string; mem: number }
|
||
export interface UpdateJob { id: string; name: string; stack: string; machine?: string; services?: string[]; cron: string; apply: boolean; enabled: boolean }
|
||
export interface UpdateJobRow { job: UpdateJob; next?: string; last?: { at?: string; taken?: string; error?: string } }
|
||
export interface MachineUpdates { images: ImageCheck[]; checked: number; stacks: Record<string, string[]> }
|
||
|
||
export interface DockerHealthProbe { Start: string; End: string; ExitCode: number; Output: string }
|
||
export interface DockerHealth { configured: boolean; state: string; status?: string; failing_streak?: number; log?: DockerHealthProbe[] }
|
||
export interface DockerEvent { t: number; name: string; action: string; image?: string; exit_code?: string; signal?: string; health?: string }
|
||
|
||
// ---- hardware (the Hardware tab: what the machine is made of) ----
|
||
export interface HWSystem { model?: string; serial?: string; family?: string; kernel?: string; arch?: string; virt?: string }
|
||
export interface HWSlot { designation: string; type?: string; usage?: string; length?: string; width?: string; bus?: string; device?: string }
|
||
export interface HWBoard {
|
||
maker?: string; product?: string; version?: string; serial?: string; asset_tag?: string; type?: string; location?: string
|
||
chassis?: string; chassis_maker?: string; bios?: string; bios_date?: string; bios_revision?: string; rom_size?: string
|
||
uefi?: boolean; slots?: HWSlot[]
|
||
}
|
||
export interface HWCache { level: string; size: string }
|
||
export interface HWCPU {
|
||
model?: string; vendor?: string; arch?: string; sockets?: number; socket_names?: string[]
|
||
cores?: number; threads?: number; threads_per_core?: number; min_mhz?: number; max_mhz?: number
|
||
family?: string; model_id?: string; stepping?: string; microcode?: string; virtualization?: string
|
||
caches?: HWCache[]; flags?: string[]
|
||
}
|
||
export interface HWModule {
|
||
locator: string; bank?: string; empty?: boolean; size?: number; type?: string; form_factor?: string
|
||
speed_mts?: number; running_mts?: number; maker?: string; part?: string; serial?: string; rank?: number; voltage?: string
|
||
}
|
||
export interface HWMemory { total: number; installed?: number; max_capacity?: number; slots?: number; filled?: number; ecc?: string; modules?: HWModule[] }
|
||
export interface HWDrive { name: string; kind: string; model?: string; vendor?: string; serial?: string; firmware?: string; tran?: string; size: number; rotational?: boolean; removable?: boolean }
|
||
export interface HWGPU { model?: string; vendor?: string; slot?: string; driver?: string; node?: string; memory?: number; class?: string }
|
||
export interface Hardware { at: string; system: HWSystem; board: HWBoard; cpu: HWCPU; memory: HWMemory; drives: HWDrive[] | null; gpus: HWGPU[] | null; notes?: string[] }
|
||
|
||
export interface DockerLogLine { t: string; m: string; err?: boolean }
|
||
export interface DockerInfo { available: boolean; message?: string; tool?: string; containers: DockerContainer[]; images: Record<string, unknown>[]; volumes: Record<string, unknown>[]; networks: Record<string, unknown>[] }
|
||
export interface Stack { name: string; containers: string[]; running: number; file: string; host_file: string; files: string[]; host_files: string[] | null; images: string[] | null; via: string; editable: boolean; note?: string; has_env: boolean }
|
||
export interface StackHome { dir: string; inner: string; via: string; stacks: number; default: boolean }
|
||
export interface ImageCheck { image: string; state: 'current' | 'update' | 'unknown'; detail?: string }
|
||
export interface StackDiff { name: string; base: '' | 'deployed' | 'saved'; old: string; new: string }
|
||
export interface AlertRule { id: string; name: string; sensor: string; op: string; value: number | boolean | string; for_sec: number; severity: 'info' | 'warn' | 'crit'; enabled: boolean; machines: string }
|
||
export interface AlertChannel { type: 'ntfy' | 'webhook'; url: string }
|
||
export interface AlertEvent { t: number; kind: 'firing' | 'resolved'; rule: string; rule_id: string; machine: string; sensor: string; value: unknown; severity: string }
|
||
export interface AlertsInfo { rules: AlertRule[]; channels: AlertChannel[]; events: AlertEvent[]; firing: (AlertEvent & { since: number })[]; checked: number; every_sec: number }
|
||
export interface ZFSSnapshot { name: string; used: number; created: number }
|
||
export interface ZFSInfo { available: boolean; message?: string; pools: { name: string; size: number; alloc: number; free: number; frag: number; cap: number; health: string; dedup: string; scan?: string; errors?: string; config?: string }[]; datasets: { name: string; type: string; used: number; avail: number; refer: number; used_by_snapshots: number; mount?: string; compressratio: string; snapshots: number }[] }
|
||
|
||
export interface UIPrefs { theme?: string; refreshMs?: number; accents?: Record<string, string>; rail?: string[] }
|
||
export interface User { name: string; created: string; last_seen?: string; sessions: number; self: boolean }
|
||
export interface Session { id: string; created: string; last_seen: string; expires: string; ip?: string; agent?: string; current: boolean }
|
||
export interface TokenInfo { name: string; created: string; scope?: string; sensors?: string[]; issued_by?: string }
|
||
// A guest login: a full account with a clock on it, gone at `expires` or when revoked.
|
||
export interface Guest { name: string; created: string; expires: string; issued_by?: string; sessions: number; used: boolean }
|
||
export interface FreshGuest { name: string; password: string; expires: string; minutes: number }
|
||
|
||
export interface SpaceProgress { files: number; dirs: number; reused: number; bytes: number; denied: number; current: string }
|
||
export interface SpaceScan { disk: string; roots: string[]; started: string; finished?: string; running: boolean; loading?: boolean; error?: string; progress: SpaceProgress; as_root: boolean; method?: string; by_root?: Record<string, string>; partial?: boolean; files: number; dirs: number; size: number; denied: number; read: number; reused: number; partial_dirs: number; bases?: Record<string, string> }
|
||
export interface SpaceTarget { id: string; kind: 'disk' | 'pool'; name: string; model?: string; layout?: string; health?: string; size: number; used?: number; disks?: string[]; roots: string[]; scan?: SpaceScan }
|
||
export interface SpaceNode { name: string; dir: boolean; size: number; apparent: number; mtime: number; files: number; dirs: number; small_n?: number; small_size?: number; mount?: boolean; denied?: boolean; partial?: boolean }
|
||
export interface SpaceTree extends SpaceNode { path: string; children?: SpaceTree[]; more_n?: number; more_size?: number; total: number }
|
||
export interface SpaceFile { path: string; size: number; apparent: number; mtime: number }
|
||
|
||
export interface KVM { id: string; name: string; url: string; machine: string; added: string }
|
||
|
||
// ---- the Network tab ----
|
||
export interface IfAddr { ip: string; prefix: number; v6?: boolean }
|
||
export interface Iface {
|
||
name: string; kind: string; mac?: string; mtu: number; up: boolean; carrier: boolean
|
||
speed_mbps?: number; duplex?: string; driver?: string; signal_dbm?: number
|
||
addrs: IfAddr[]; rx_total: number; tx_total: number; default?: boolean
|
||
}
|
||
export interface NetReport { at: string; host?: string; ifaces: Iface[]; gateway?: string; gateway_via?: string; dns?: string[]; search?: string[] }
|
||
export interface Outside { at: string; ip?: string; colo?: string; country?: string; error?: string }
|
||
export interface SpeedResult {
|
||
at: string; down_mbps: number; up_mbps: number; latency_ms: number; jitter_ms: number
|
||
down_bytes: number; up_bytes: number; took_sec: number; colo?: string; ip?: string; source?: string; note?: string; error?: string
|
||
}
|
||
// A test in progress, or the last one to have finished. `done` is 0–1 across the whole test
|
||
// and `mbps` is the live figure for the phase under way.
|
||
export interface SpeedRun { started: string; running: boolean; phase: string; done: number; mbps: number; bytes: number; result?: SpeedResult }
|
||
export interface NetworkInfo { report: NetReport; outside: Outside; run?: SpeedRun; history: SpeedResult[]; method: string }
|
||
|
||
// ---- the VPN tab ----
|
||
// A peer is a device that dials in (on a server tunnel) or the way out (on a client one).
|
||
// `online` is a handshake in the last few minutes, not a connection: WireGuard has none.
|
||
export interface VPNPeer {
|
||
name?: string; public_key: string; endpoint?: string; allowed_ips: string[]
|
||
handshake?: string; online: boolean; rx: number; tx: number; keepalive?: number; psk?: boolean
|
||
// The configuration section holding it, and the id anything that edits it uses. Absent when
|
||
// the peer is not in a configuration Ham Lab can edit.
|
||
section?: string
|
||
}
|
||
export interface VPNTunnel {
|
||
name: string; public_key: string; kind: 'server' | 'client' | ''; listen_port?: number
|
||
fwmark?: string; addrs: string[]; mtu?: number; peers: VPNPeer[]; exit?: string
|
||
// Whether devices on this tunnel can be added and removed from here.
|
||
managed?: boolean
|
||
}
|
||
export interface NewVPNPeer { name: string; address?: string; endpoint: string; dns?: string; split?: boolean }
|
||
// A device just made, and the one chance to read its profile: the private key in it is
|
||
// generated for that call and kept nowhere.
|
||
export interface CreatedVPNPeer { section: string; name: string; public_key: string; address: string; config: string }
|
||
export interface VPNReport { at: string; tunnels: VPNTunnel[]; tool: boolean; root: boolean; note?: string }
|
||
|
||
export interface ZFSPolicy { id: string; name: string; dataset: string; recursive: boolean; every: 'hourly' | 'daily' | 'weekly' | 'monthly' | 'custom'; at: string; weekday: number; day: number; cron?: string; keep_for: string; keep_min: number; prefix?: string; enabled: boolean }
|
||
export interface ZFSRun { at?: string; taken?: string; pruned?: string[]; error?: string }
|
||
export interface ZFSExternal { tool: string; schedule: string; command: string; user: string; source: string; id: string; next?: number; last?: number }
|
||
export interface ZFSOverview { zfs: ZFSInfo; policies: { policy: ZFSPolicy; cron: string; next?: string; last: ZFSRun }[]; external?: ZFSExternal | null }
|
||
// The ARC as the sampler sees it (sizes in bytes; hit_pct over the last sampling interval).
|
||
export interface ARCLive {
|
||
size: number; target: number; min: number; max: number
|
||
mru: number; mfu: number; anon: number; data: number; metadata: number; meta_used: number; headers: number
|
||
compressed: number; uncompressed: number; mru_ghost: number; mfu_ghost: number; l2_size: number; l2_asize: number
|
||
reads_per_sec: number; misses_per_sec: number; hit_pct: number; hit_pct_known: boolean; l2_hit_pct: number
|
||
hits: number; misses: number; lifetime_hit_pct: number; no_grow: boolean; throttles: number; prunes: number; evict_skip: number
|
||
}
|
||
export interface CacheDev { name: string; size?: number; alloc?: number; health?: string }
|
||
export interface PoolCaching {
|
||
pool: string; cache: CacheDev[]; log: CacheDev[]; special: CacheDev[]; dedup: CacheDev[]; spare: CacheDev[]
|
||
primarycache: string; secondarycache: string; sync: string; logbias: string; dedup_prop: string; compression: string; recordsize: string; dedup_ratio: string
|
||
}
|
||
export interface ARCDetail {
|
||
available: boolean; mem_total: number; tunables: Record<string, string>
|
||
lifetime: { Hits: number; Misses: number; DemandDataHits: number; DemandDataMisses: number; DemandMetaHits: number; DemandMetaMisses: number; PrefetchDataHits: number; PrefetchDataMisses: number; PrefetchMetaHits: number; PrefetchMetaMisses: number; L2Hits: number; L2Misses: number; L2ReadBytes: number; L2WriteBytes: number; L2Feeds: number; L2RebuildSize: number; EvictL2Eligible: number; Deleted: number; EvictSkip: number; MutexMiss: number; Prune: number; Throttle: number }
|
||
zfetch: { Hits: number; Misses: number; IOIssued: number }
|
||
zil: { commits: number; NormalBytes: number; NormalWrite: number; SlogBytes: number; SlogWrite: number }
|
||
dbuf: { Size: number; Target: number; Hits: number; Misses: number }
|
||
pools: PoolCaching[]
|
||
}
|
||
export interface ARCView { detail: ARCDetail; live: ARCLive | null }
|
||
|
||
export interface FileEntry { name: string; type: 'dir' | 'file' | 'link' | 'other'; size: number; mode: string; perm: string; mtime: number; user: string; group: string; target?: string; broken?: boolean }
|
||
export interface FileInfo extends FileEntry { path: string; text: boolean }
|
||
|
||
const q = (path: string, sudo: boolean, extra = '') => `path=${encodeURIComponent(path)}${sudo ? '&sudo=1' : ''}${extra}`
|
||
|
||
// ---- Virtualization: Incus, per machine ----
|
||
export interface VirtLayout { driver: string; pool_source: string; pool_size_gib?: number; bridge: string; subnet: string; docker_rules: boolean; arc_max_gib?: number; attempted: string; hamlab_version?: string }
|
||
export interface VirtStatus { state: 'ready' | 'repair' | 'installable' | 'unavailable'; reason?: string; kvm: boolean; arch: string; version?: string; pm?: string; drivers: string[]; docker: boolean; zfs_host: boolean; arc_max_gib?: number; mem_total: number; mem_avail: number; disk_free: number; dir: string; subnet: string; pool_size_gib?: number; layout?: VirtLayout; user: string; root: boolean; unit?: string; btrfs_tools: boolean }
|
||
export interface VirtSnapshot { name: string; created: string; expires?: string; size?: number }
|
||
export interface VirtInstance { name: string; type: 'container' | 'virtual-machine'; status: string; arch: string; description?: string; image: string; os?: string; release?: string; cpu?: string; memory?: string; created: string; last_used: string; ipv4: string[]; snapshots: VirtSnapshot[]; profiles: string[]; ephemeral: boolean; cpu_pct: number; mem_usage: number; mem_peak: number; disk_usage: number; disk_total: number; pid: number; processes: number; config: Record<string, string>; devices: Record<string, Record<string, string>>; schedule?: string; expiry?: string }
|
||
export interface VirtImage { fingerprint: string; aliases: string[]; arch: string; type: string; size: number; created: string; last_used: string; description: string; os?: string; release?: string; variant?: string; cached: boolean; source?: string; public: boolean }
|
||
export interface VirtPool { name: string; driver: string; source: string; used: number; total: number; used_by: number }
|
||
export interface VirtNetwork { name: string; type: string; managed: boolean; address?: string; nat: boolean; used_by: number }
|
||
export interface LibraryFile { name: string; size: number; modified: string; attached_to?: string[] }
|
||
export interface ExportConfig { enabled: boolean; hour: number; minute: number; keep: number; instance_only: boolean; instances?: string[]; updated?: string }
|
||
export interface ExportRun { at: string; exported: string[]; pruned: string[]; errors?: string[]; seconds: number }
|
||
export interface ExportFile { instance: string; name: string; size: number; modified: string }
|
||
export interface ExportsView { config: ExportConfig; cron: string; next?: string; last: ExportRun | null; files: ExportFile[]; dir: string; cron_file: string }
|
||
export interface FromISO { name: string; iso: string; cpu: string; memory: string; disk: string; network: string; secureboot: boolean; start: boolean }
|
||
export interface VirtOverview { status: VirtStatus; error?: string; instances?: VirtInstance[]; images?: VirtImage[]; pools?: VirtPool[]; networks?: VirtNetwork[]; library: { images: LibraryFile[]; isos: LibraryFile[] } }
|
||
export interface VirtInstallOpts { driver: string; pool_size_gib: number; subnet: string; docker_rules: boolean; arc_max_gib: number }
|
||
export interface VirtCreate { name: string; image: string; vm: boolean; cpu: string; memory: string; disk: string; network: string; cloud_init: string; profiles: string[]; description: string; start: boolean }
|
||
|
||
export const api = {
|
||
procs: () => req<ProcTable>('GET', '/api/procs'),
|
||
space: () => req<{ targets: SpaceTarget[] }>('GET', '/api/space'),
|
||
spaceStatus: (disk: string) => req<SpaceScan>('GET', `/api/space/${encodeURIComponent(disk)}`),
|
||
spaceScan: (disk: string) => req<SpaceScan>('POST', `/api/space/${encodeURIComponent(disk)}/scan`),
|
||
spaceCancel: (disk: string) => req<void>('POST', `/api/space/${encodeURIComponent(disk)}/cancel`),
|
||
spaceForget: (disk: string) => req<void>('DELETE', `/api/space/${encodeURIComponent(disk)}`),
|
||
spaceTree: (disk: string, path: string, depth = 1, limit = 200, min = 0) => req<SpaceTree>('GET', `/api/space/${encodeURIComponent(disk)}/tree?path=${encodeURIComponent(path)}&depth=${depth}&limit=${limit}&min=${min}`),
|
||
spaceLargest: (disk: string, n = 500) => req<SpaceFile[]>('GET', `/api/space/${encodeURIComponent(disk)}/largest?n=${n}`),
|
||
procSignal: (pid: number, signal: 'TERM' | 'KILL') => req<void>('POST', `/api/procs/${pid}/signal`, { signal }),
|
||
files: (path: string, sudo: boolean) => req<{ path: string; parent?: string; entries: FileEntry[] }>('GET', `/api/files?${q(path, sudo)}`),
|
||
fileStat: (path: string, sudo: boolean) => req<FileInfo>('GET', `/api/files/stat?${q(path, sudo)}`),
|
||
fileRead: async (path: string, sudo: boolean): Promise<string> => {
|
||
const r = await fetch(apiPath(`/api/files/read?${q(path, sudo, '&max=2097152')}`), { credentials: 'same-origin' })
|
||
if (r.status === 401) throw new Unauthenticated()
|
||
if (!r.ok) { let m = r.statusText; try { m = (await r.json()).error ?? m } catch { /* */ } throw new Error(m) }
|
||
return r.text()
|
||
},
|
||
fileReadURL: (path: string, sudo: boolean) => apiPath(`/api/files/read?${q(path, sudo)}`),
|
||
fileDownloadURL: (path: string, sudo: boolean) => apiPath(`/api/files/download?${q(path, sudo)}`),
|
||
fileWrite: async (path: string, text: string, sudo: boolean) => {
|
||
const r = await fetch(apiPath(`/api/files/write?${q(path, sudo)}`), { method: 'PUT', credentials: 'same-origin', body: text, headers: { 'Content-Type': 'application/octet-stream' } })
|
||
if (r.status === 401) throw new Unauthenticated()
|
||
if (!r.ok) { let m = r.statusText; try { m = (await r.json()).error ?? m } catch { /* */ } throw new Error(m) }
|
||
},
|
||
fileUpload: (dir: string, file: File, sudo: boolean, onProgress: (pct: number) => void, overwrite = false) => new Promise<void>((resolve, reject) => {
|
||
const x = new XMLHttpRequest()
|
||
x.open('PUT', apiPath(`/api/files/upload?${q(dir, sudo)}&name=${encodeURIComponent(file.name)}${overwrite ? '&overwrite=1' : ''}`))
|
||
x.upload.onprogress = (e) => { if (e.lengthComputable) onProgress(Math.round((e.loaded / e.total) * 100)) }
|
||
x.onload = () => { if (x.status >= 200 && x.status < 300) resolve(); else { let m = x.statusText; try { m = JSON.parse(x.responseText).error ?? m } catch { /* */ } reject(new Error(m)) } }
|
||
x.onerror = () => reject(new Error('network error'))
|
||
x.send(file)
|
||
}),
|
||
fileMkdir: (path: string, sudo: boolean) => req<void>('POST', `/api/files/mkdir${sudo ? '?sudo=1' : ''}`, { path }),
|
||
fileTouch: (path: string, sudo: boolean) => req<void>('POST', `/api/files/touch${sudo ? '?sudo=1' : ''}`, { path }),
|
||
fileRename: (path: string, to: string, sudo: boolean) => req<void>('POST', `/api/files/rename${sudo ? '?sudo=1' : ''}`, { path, to }),
|
||
fileDelete: (path: string, recursive: boolean, sudo: boolean) => req<void>('POST', `/api/files/delete${sudo ? '?sudo=1' : ''}`, { path, recursive }),
|
||
fileChmod: (path: string, mode: string, sudo: boolean) => req<void>('POST', `/api/files/chmod${sudo ? '?sudo=1' : ''}`, { path, mode }),
|
||
storage: () => req<{ devices: StorageDevice[]; zfs: ZFSInfo; smartctl: boolean }>('GET', '/api/storage'),
|
||
smart: (dev: string, force = false) => req<SmartInfo>('GET', `/api/storage/smart/${dev}${force ? '?force=1' : ''}`),
|
||
smartTest: (dev: string, kind: 'short' | 'long') => req<void>('POST', `/api/storage/smart/${dev}/test/${kind}`),
|
||
// Always the hub's: it holds every machine's KVM, so the tab works when the machine does not.
|
||
kvms: () => req<KVM[]>('GET', '/api/kvms', undefined, true),
|
||
addKVM: (name: string, url: string, machine: string) => req<KVM>('POST', '/api/kvms', { name, url, machine }, true),
|
||
removeKVM: (id: string) => req<void>('DELETE', `/api/kvms/${encodeURIComponent(id)}`, undefined, true),
|
||
zfsOverview: () => req<ZFSOverview>('GET', '/api/zfs'),
|
||
zfsARC: () => req<ARCView>('GET', '/api/zfs/arc'),
|
||
saveZfsSchedule: (p: Partial<ZFSPolicy>) => req<ZFSPolicy>('PUT', '/api/zfs/schedules', p),
|
||
deleteZfsSchedule: (id: string) => req<void>('DELETE', `/api/zfs/schedules/${encodeURIComponent(id)}`),
|
||
runZfsSchedule: (id: string) => req<{ taken: string; pruned: string[] }>('POST', `/api/zfs/schedules/${encodeURIComponent(id)}/run`),
|
||
deleteZfsSnapshot: (name: string, recursive: boolean) => req<void>('POST', '/api/zfs/snapshots/delete', { name, confirm: name, recursive }),
|
||
zfsSnapshots: (dataset: string) => req<ZFSSnapshot[]>('GET', `/api/storage/zfs/snapshots?dataset=${encodeURIComponent(dataset)}`),
|
||
zfsScrub: (pool: string, action: 'start' | 'pause' | 'stop') => req<void>('POST', `/api/storage/zfs/scrub/${encodeURIComponent(pool)}/${action}`),
|
||
zfsSnapshot: (dataset: string, name: string) => req<void>('POST', '/api/storage/zfs/snapshot', { dataset, name }),
|
||
installPackages: (packages: string[]) => req<TaskInfo>('POST', '/api/updates/install', { packages }),
|
||
tasks: () => req<TaskInfo[]>('GET', '/api/tasks'),
|
||
task: (id: string) => req<TaskInfo>('GET', `/api/tasks/${id}`),
|
||
cancelTask: (id: string) => req<void>('POST', `/api/tasks/${id}/cancel`),
|
||
updates: () => req<UpdatesStatus>('GET', '/api/updates'),
|
||
refreshUpdates: () => req<TaskInfo>('POST', '/api/updates/refresh'),
|
||
upgrade: (packages: string[]) => req<TaskInfo>('POST', '/api/updates/upgrade', { packages }),
|
||
changelog: (pkg: string) => req<{ text: string }>('GET', `/api/updates/changelog?pkg=${encodeURIComponent(pkg)}`),
|
||
power: (action: 'reboot' | 'poweroff', confirm: string) => req<void>('POST', `/api/power/${action}`, { confirm }),
|
||
cron: () => req<CronOverview>('GET', '/api/cron'),
|
||
cronSource: (name: string) => req<{ text: string; editable: boolean }>('GET', `/api/cron/source?name=${encodeURIComponent(name)}`),
|
||
cronWriteSource: (name: string, text: string) => req<void>('PUT', `/api/cron/source?name=${encodeURIComponent(name)}`, { text }),
|
||
cronJobLogs: (id: string, n = 200) => req<LogEntry[]>('GET', `/api/cron/jobs/${id}/logs?n=${n}`),
|
||
cronJobOutput: (id: string, n = 200) => req<{ file: string; text: string }>('GET', `/api/cron/jobs/${id}/output?n=${n}`),
|
||
cronJobToggle: (id: string, enable: boolean) => req<void>('POST', `/api/cron/jobs/${id}/${enable ? 'enable' : 'disable'}`),
|
||
cronJobRun: (id: string) => req<CronRun>('POST', `/api/cron/jobs/${id}/run`),
|
||
cronRun: (id: string) => req<CronRun>('GET', `/api/cron/runs/${id}`),
|
||
cronTimerAction: (unit: string, action: string) => req<void>('POST', `/api/cron/timers/${encodeURIComponent(unit)}/${action}`),
|
||
services: async () => (await req<{ units: ServiceUnit[]; sudo: boolean }>('GET', '/api/services')).units,
|
||
service: (unit: string) => req<ServiceDetail>('GET', `/api/services/${encodeURIComponent(unit)}`),
|
||
serviceFile: (unit: string) => req<{ content: string }>('GET', `/api/services/${encodeURIComponent(unit)}/file`),
|
||
serviceLogs: (unit: string, n = 200) => req<LogEntry[]>('GET', `/api/services/${encodeURIComponent(unit)}/logs?n=${n}`),
|
||
serviceAction: (unit: string, action: string) => req<void>('POST', `/api/services/${encodeURIComponent(unit)}/${action}`),
|
||
historyWindow: () => req<HistoryWindow>('GET', '/api/prefs/history', undefined, true),
|
||
setHistoryWindow: (seconds: number) => req<HistoryWindow>('PUT', '/api/prefs/history', { seconds }, true),
|
||
login: (username: string, password: string) => req<{ user: string }>('POST', '/api/login', { username, password }, true),
|
||
users: () => req<User[]>('GET', '/api/users', undefined, true),
|
||
addUser: (name: string, password: string) => req<{ name: string }>('POST', '/api/users', { name, password }, true),
|
||
removeUser: (name: string, current: string) => req<void>('DELETE', `/api/users/${encodeURIComponent(name)}`, { current }, true),
|
||
resetPassword: (name: string, current: string, password: string) => req<void>('POST', `/api/users/${encodeURIComponent(name)}/password`, { current, password }, true),
|
||
guests: () => req<Guest[]>('GET', '/api/guests', undefined, true),
|
||
createGuest: (minutes: number) => req<FreshGuest>('POST', '/api/guests', { minutes }, true),
|
||
revokeGuest: (name: string) => req<void>('DELETE', `/api/guests/${encodeURIComponent(name)}`, undefined, true),
|
||
sessions: () => req<Session[]>('GET', '/api/sessions', undefined, true),
|
||
revokeSession: (id: string) => req<void>('DELETE', `/api/sessions/${encodeURIComponent(id)}`, undefined, true),
|
||
revokeOtherSessions: () => req<{ revoked: number }>('POST', '/api/sessions/revoke-others', undefined, true),
|
||
revokeIdleSessions: (hours = 24) => req<{ revoked: number }>('POST', `/api/sessions/revoke-idle?hours=${hours}`, undefined, true),
|
||
logout: () => req<void>('POST', '/api/logout', undefined, true),
|
||
meHere: () => req<Me>('GET', '/api/me', undefined, true),
|
||
me: () => req<Me>('GET', '/api/me'),
|
||
machines: () => req<{ nodes: MachineStatus[]; host: string; fingerprint?: string }>('GET', '/api/nodes', undefined, true),
|
||
addMachine: (name: string, url: string, username: string, password: string) => req<MachineStatus>('POST', '/api/nodes', { name, url, username, password }, true),
|
||
updateMachine: (name: string, force = false) => req<PushResult>('POST', `/api/machines/${encodeURIComponent(name)}/update${force ? '?force=1' : ''}`, undefined, true),
|
||
updateAllMachines: (force = false) => req<PushResult[]>('POST', `/api/machines-update-all${force ? '?force=1' : ''}`, undefined, true),
|
||
changePassword: (current: string, password: string) => req<void>('POST', '/api/password', { current, password }, true),
|
||
update: () => req<UpdateStatus>('GET', '/api/update'),
|
||
checkUpdate: () => req<UpdateStatus>('POST', '/api/update/check'),
|
||
installUpdate: (version: string, force = false) => req<{ version: string; previous: string }>('POST', `/api/update/install${force ? '?force=1' : ''}`, { version }),
|
||
tokens: () => req<TokenInfo[]>('GET', '/api/tokens', undefined, true),
|
||
createReadToken: (name: string, sensors: string[] = []) => req<{ token: string; name: string }>('POST', '/api/tokens/read', { name, sensors }, true),
|
||
hardware: (refresh = false) => req<Hardware>('GET', `/api/hardware${refresh ? '?refresh=1' : ''}`),
|
||
network: () => req<NetworkInfo>('GET', '/api/network'),
|
||
vpn: () => req<VPNReport>('GET', '/api/vpn'),
|
||
addVPNPeer: (iface: string, p: NewVPNPeer) => req<CreatedVPNPeer>('POST', `/api/vpn/${encodeURIComponent(iface)}/peers`, p),
|
||
editVPNPeer: (iface: string, section: string, p: { name?: string; address?: string }) =>
|
||
req<void>('PATCH', `/api/vpn/${encodeURIComponent(iface)}/peers/${encodeURIComponent(section)}`, p),
|
||
removeVPNPeer: (iface: string, section: string, confirm: string) =>
|
||
req<void>('DELETE', `/api/vpn/${encodeURIComponent(iface)}/peers/${encodeURIComponent(section)}`, { confirm }),
|
||
speedtest: () => req<SpeedRun>('GET', '/api/network/speedtest'),
|
||
startSpeedtest: () => req<SpeedRun>('POST', '/api/network/speedtest'),
|
||
cancelSpeedtest: () => req<void>('POST', '/api/network/speedtest/cancel'),
|
||
docker: () => req<DockerInfo>('GET', '/api/docker'),
|
||
dockerAction: (id: string, action: 'start' | 'stop' | 'restart' | 'remove') => req<void>('POST', `/api/docker/containers/${encodeURIComponent(id)}/${action}`),
|
||
dockerLogs: (id: string, since = '') => req<{ lines: DockerLogLine[] }>('GET', `/api/docker/containers/${encodeURIComponent(id)}/logs${since ? `?since=${encodeURIComponent(since)}` : '?n=300'}`),
|
||
dockerInspect: (id: string) => req<Record<string, unknown>>('GET', `/api/docker/containers/${encodeURIComponent(id)}/inspect`),
|
||
machineUpdates: (refresh = false) => req<MachineUpdates>('GET', `/api/docker/updates${refresh ? '?refresh=1' : ''}`),
|
||
updateJobs: () => req<{ jobs: UpdateJobRow[]; file: string }>('GET', '/api/docker/schedules'),
|
||
saveUpdateJob: (j: Partial<UpdateJob>) => req<UpdateJob>('PUT', '/api/docker/schedules', j),
|
||
deleteUpdateJob: (id: string) => req<void>('DELETE', `/api/docker/schedules/${encodeURIComponent(id)}`),
|
||
runUpdateJob: (id: string) => req<{ output: string }>('POST', `/api/docker/schedules/${encodeURIComponent(id)}/run`),
|
||
dockerHealth: (id: string) => req<DockerHealth>('GET', `/api/docker/containers/${encodeURIComponent(id)}/health`),
|
||
dockerEvents: (id: string, since = '7d') => req<{ events: DockerEvent[]; since: string; kept: number; capacity: number }>('GET', `/api/docker/containers/${encodeURIComponent(id)}/events?since=${since}`),
|
||
dockerExec: (id: string) => req<{ id: string; name: string }>('POST', `/api/docker/containers/${encodeURIComponent(id)}/exec`, {}),
|
||
dockerLimits: (id: string, p: { memory?: string; cpus?: string; restart?: string }) => req<void>('POST', `/api/docker/containers/${encodeURIComponent(id)}/limits`, p),
|
||
stacks: () => req<{ stacks: Stack[] }>('GET', '/api/docker/stacks'),
|
||
stackHomes: () => req<{ homes: StackHome[] }>('GET', '/api/docker/stacks/homes'),
|
||
createStack: (p: { name: string; text: string; env: string; home: string; env_from?: string }) => req<{ stack: Stack }>('POST', '/api/docker/stacks', p),
|
||
deleteStack: (name: string) => req<{ trash: string; moved: number }>('POST', `/api/docker/stacks/${encodeURIComponent(name)}/delete`, { confirm: name }),
|
||
pullImage: (image: string) => req<TaskInfo>('POST', '/api/docker/images/pull', { image }),
|
||
removeImage: (image: string) => req<void>('POST', '/api/docker/images/remove', { image }),
|
||
pruneImages: (all = false) => req<{ result: string }>('POST', '/api/docker/images/prune', { all }),
|
||
removeVolume: (name: string) => req<void>('POST', '/api/docker/volumes/remove', { name, confirm: name }),
|
||
pruneVolumes: () => req<{ result: string }>('POST', '/api/docker/volumes/prune'),
|
||
removeNetwork: (name: string) => req<void>('POST', '/api/docker/networks/remove', { name }),
|
||
pruneNetworks: () => req<{ result: string }>('POST', '/api/docker/networks/prune'),
|
||
stackDiff: (name: string) => req<{ files: StackDiff[] }>('GET', `/api/docker/stacks/${encodeURIComponent(name)}/diff`),
|
||
stackUpdates: (name: string, refresh = false) => req<{ images: ImageCheck[]; checked: number }>('GET', `/api/docker/stacks/${encodeURIComponent(name)}/updates${refresh ? '?refresh=1' : ''}`),
|
||
stackFile: (name: string, file: number | 'env' = 0) => req<{ stack: Stack; text: string; sum: string }>('GET', `/api/docker/stacks/${encodeURIComponent(name)}?file=${file}`),
|
||
saveStack: (name: string, file: number | 'env', text: string, sum: string) => req<void>('PUT', `/api/docker/stacks/${encodeURIComponent(name)}?file=${file}`, { text, sum }),
|
||
stackAction: (name: string, action: 'deploy' | 'update' | 'recreate' | 'restart' | 'stop' | 'down') => req<TaskInfo>('POST', `/api/docker/stacks/${encodeURIComponent(name)}/${action}`),
|
||
alerts: () => req<AlertsInfo>('GET', '/api/alerts', undefined, true),
|
||
saveAlerts: (p: { rules?: AlertRule[]; channels?: AlertChannel[] }) => req<void>('PUT', '/api/alerts', p, true),
|
||
testAlerts: () => req<void>('POST', '/api/alerts/test', undefined, true),
|
||
revokeToken: (name: string) => req<void>('DELETE', `/api/tokens/${encodeURIComponent(name)}`, undefined, true),
|
||
uiPrefs: () => req<UIPrefs>('GET', '/api/prefs/ui', undefined, true),
|
||
saveUIPrefs: (p: UIPrefs) => req<void>('PUT', '/api/prefs/ui', p, true),
|
||
removeMachine: (name: string) => req<void>('DELETE', `/api/nodes/${encodeURIComponent(name)}`, undefined, true),
|
||
terminals: () => req<Term[]>('GET', '/api/terminals'),
|
||
createTerminal: (name?: string) => req<{ id: string; name: string }>('POST', '/api/terminals', { name }),
|
||
renameTerminal: (id: string, name: string) => req<void>('PATCH', `/api/terminals/${id}`, { name }),
|
||
deleteTerminal: (id: string) => req<void>('DELETE', `/api/terminals/${id}`),
|
||
virt: () => req<VirtOverview>('GET', '/api/virt'),
|
||
virtInstall: (o: VirtInstallOpts) => req<TaskInfo>('POST', '/api/virt/install', o),
|
||
virtRemote: (type: string, refresh = false) => req<{ images: VirtImage[] }>('GET', `/api/virt/images/remote?type=${type}${refresh ? '&refresh=1' : ''}`),
|
||
virtImageDownload: (alias: string) => req<TaskInfo>('POST', '/api/virt/images/download', { alias }),
|
||
virtImageImport: (path: string, alias: string, type: string) => req<TaskInfo>('POST', '/api/virt/images/import', { path, alias, type }),
|
||
virtImagePublish: (instance: string, alias: string) => req<TaskInfo>('POST', '/api/virt/images/publish', { instance, alias }),
|
||
virtImageDelete: (ref: string) => req<void>('POST', '/api/virt/images/delete', { ref }),
|
||
virtCreate: (c: VirtCreate) => req<TaskInfo>('POST', '/api/virt/instances', c),
|
||
virtAction: (name: string, action: 'start' | 'stop' | 'restart' | 'kill') => req<void>('POST', `/api/virt/instances/${encodeURIComponent(name)}/${action}`),
|
||
virtDelete: (name: string, force: boolean) => req<void>('POST', `/api/virt/instances/${encodeURIComponent(name)}/delete`, { confirm: name, force }),
|
||
virtTerminal: (name: string, kind: 'console' | 'exec') => req<{ id: string; name: string }>('POST', `/api/virt/instances/${encodeURIComponent(name)}/${kind}`),
|
||
virtRename: (name: string, to: string) => req<void>('POST', `/api/virt/instances/${encodeURIComponent(name)}/rename`, { name: to }),
|
||
virtConfig: (name: string, cfg: Record<string, string>) => req<void>('PUT', `/api/virt/instances/${encodeURIComponent(name)}/config`, cfg),
|
||
virtSnapshot: (name: string, snap: string) => req<{ name: string }>('POST', `/api/virt/instances/${encodeURIComponent(name)}/snapshots`, { name: snap }),
|
||
virtSnapshotAction: (name: string, snap: string, action: 'restore' | 'delete') => req<void>('POST', `/api/virt/instances/${encodeURIComponent(name)}/snapshots/${encodeURIComponent(snap)}/${action}`),
|
||
virtISOFetch: (url: string, sha256: string, name: string) => req<TaskInfo>('POST', '/api/virt/isos/fetch', { url, sha256, name }),
|
||
virtISOImport: (path: string) => req<TaskInfo>('POST', '/api/virt/isos/import', { path }),
|
||
virtISODelete: (name: string) => req<void>('POST', '/api/virt/isos/delete', { name }),
|
||
virtISORename: (name: string, to: string) => req<void>('POST', '/api/virt/isos/rename', { name, to }),
|
||
virtISOAttach: (inst: string, iso: string, bootFirst: boolean) => req<{ device: string }>('POST', `/api/virt/instances/${encodeURIComponent(inst)}/iso`, { iso, boot_first: bootFirst }),
|
||
virtISODetach: (inst: string, device: string) => req<void>('POST', `/api/virt/instances/${encodeURIComponent(inst)}/iso/detach`, { device }),
|
||
virtFromISO: (f: FromISO) => req<TaskInfo>('POST', '/api/virt/instances/from-iso', f),
|
||
virtMountAdd: (inst: string, m: { device: string; source: string; path: string; readonly: boolean; shift: boolean }) => req<{ device: string }>('POST', `/api/virt/instances/${encodeURIComponent(inst)}/mounts`, m),
|
||
virtMountRemove: (inst: string, device: string) => req<void>('POST', `/api/virt/instances/${encodeURIComponent(inst)}/mounts/remove`, { device }),
|
||
virtExports: () => req<ExportsView>('GET', '/api/virt/exports'),
|
||
virtExportsPut: (c: ExportConfig) => req<void>('PUT', '/api/virt/exports', c),
|
||
virtExportsRun: (instance = '') => req<TaskInfo>('POST', '/api/virt/exports/run', { instance }),
|
||
virtExportDelete: (instance: string, name: string) => req<void>('POST', '/api/virt/exports/delete', { instance, name }),
|
||
virtSSHKeys: () => req<{ keys: string[]; user: string }>('GET', '/api/virt/sshkeys'),
|
||
}
|
||
|
||
export function wsURLPath(path: string): string {
|
||
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||
return `${proto}//${location.host}${apiPath(path)}`
|
||
}
|
||
export const wsURL = (id: string) => wsURLPath(`/api/terminals/${id}/attach`)
|