// ZFS: pools, datasets, snapshots, and a schedule for taking and expiring them. // // Split out of the Disks tab on 2026-09-24. Disks is the hardware; this is the filesystem // on top of it, which has enough of its own to say. // // On the schedule: `grid` has run zfSnap from root cron since before Ham Lab existed, and it // works. So this reads and writes the same snapshot names — `2026-09-24_02.30.01--6w`, with // the retention in the name — rather than inventing a second convention for the two tools to // disagree over. An existing cron job is detected and shown, so the page tells the truth // about the machine whether Ham Lab or something else is doing the work. import { FormEvent, Fragment, useCallback, useEffect, useState } from 'react' import { api, ARCView, CacheDev, Landing, PoolCaching, PoolWrites, ZFSExternal, ZFSInfo, ZFSOverview, ZFSPolicy, ZFSRun, ZFSSnapshot, ZFSWrites } from './api' import { fmtBytes, fmtRate } from './fmt' import { useRefreshMs } from './prefs' import { fetchHistories } from './sensors' import Sparkline from './Sparkline' import { askText, askYes, Btn, Loading, Meter, Spinner, toast, useEscape } from './ui' const ago = (t?: string | number) => { if (!t) return 'never' const s = Math.max(0, (Date.now() - new Date(t).getTime()) / 1000) return s < 60 ? 'just now' : s < 3600 ? `${Math.floor(s / 60)} min ago` : s < 86400 ? `${Math.floor(s / 3600)} h ago` : `${Math.floor(s / 86400)} d ago` } const soon = (t?: string) => { if (!t) return '—' const s = (new Date(t).getTime() - Date.now()) / 1000 return s < 60 ? 'any moment' : s < 3600 ? `in ${Math.round(s / 60)} min` : s < 86400 ? `in ${Math.round(s / 3600)} h` : `in ${Math.round(s / 86400)} d` } const WEEKDAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] const describe = (p: ZFSPolicy) => { const when = p.every === 'custom' ? `on ${p.cron}` : p.every === 'hourly' ? `every hour at :${(p.at.split(':')[1] ?? '00').padStart(2, '0')}` : p.every === 'daily' ? `every day at ${p.at}` : p.every === 'weekly' ? `every ${WEEKDAYS[p.weekday] ?? 'Sunday'} at ${p.at}` : `on day ${p.day} of the month at ${p.at}` return `${when} · keep ${p.keep_for}${p.recursive ? ' · recursive' : ''}` } // ---- pools and datasets (moved from the Disks tab) ---- function PoolCard({ p, onChanged }: { p: ZFSInfo['pools'][number]; onChanged: () => void }) { const scrubbing = /scrub in progress/i.test(p.scan ?? '') const paused = /scrub paused/i.test(p.scan ?? '') const [cfg, setCfg] = useState(false) const act = (a: 'start' | 'pause' | 'stop') => async () => { try { await api.zfsScrub(p.name, a); toast(`scrub ${a}: ok`); onChanged() } catch (e) { toast(e instanceof Error ? e.message : 'failed', 'err') } } return (
{p.name} {p.health} {fmtBytes(p.alloc)} / {fmtBytes(p.size)} · {p.cap}%
= 90 ? 'bad' : p.cap >= 80 ? 'warn' : ''} />
Free{fmtBytes(p.free)} · fragmentation {p.frag}% · dedup {p.dedup}
{p.scan &&
Scan{p.scan}
} {p.errors &&
Errors{p.errors}
}
{!scrubbing && {paused ? 'Resume scrub' : 'Scrub'}} {scrubbing && Pause scrub} {(scrubbing || paused) && Stop scrub}
{cfg &&
{p.config}
}
) } // A snapshot's name carries when it was taken and how long it lives — 2026-09-24_02.30.01--6w // — so the explorer can say when each one goes without asking the server anything. function snapMeta(full: string): { taken?: Date; expires?: Date } { const snap = full.split('@')[1] ?? '' const [body, ttl] = snap.split('--') const m = /(\d{4})-(\d{2})-(\d{2})_(\d{2})\.(\d{2})\.(\d{2})$/.exec(body) if (!m) return {} const taken = new Date(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6]) const a = /^(\d+)([mhdw])$/.exec(ttl ?? '') if (!a) return { taken } const ms = { m: 60e3, h: 3600e3, d: 86400e3, w: 7 * 86400e3 }[a[2]]! return { taken, expires: new Date(taken.getTime() + +a[1] * ms) } } // One dataset in the snapshot explorer: how many it has and what they cost, opening into // the snapshots themselves, each of which can be taken now or deleted. function DatasetRow({ d, onChanged }: { d: ZFSInfo['datasets'][number]; onChanged: () => void }) { const [snaps, setSnaps] = useState(null) const [open, setOpen] = useState(false) const reload = useCallback(async () => { try { setSnaps(await api.zfsSnapshots(d.name)) } catch (e) { toast(e instanceof Error ? e.message : 'failed', 'err'); setSnaps([]) } }, [d.name]) async function toggle() { setOpen(!open) if (!open && !snaps) await reload() } async function snapshot() { // zfSnap's name is the machine's local time, and snapMeta reads it back as local time — so the // default is built from local fields, not toISOString(), which is UTC and read seven hours wrong. const n = new Date(), two = (x: number) => String(x).padStart(2, '0') const def = `${n.getFullYear()}-${two(n.getMonth() + 1)}-${two(n.getDate())}_${two(n.getHours())}.${two(n.getMinutes())}.${two(n.getSeconds())}` const name = await askText(`Snapshot name for ${d.name}@`, def) if (!name) return try { await api.zfsSnapshot(d.name, name.trim()); toast(`${d.name}@${name.trim()} created`); await reload(); onChanged() } catch (e) { toast(e instanceof Error ? e.message : 'failed', 'err') } } async function destroy(s: ZFSSnapshot) { const short = s.name.split('@')[1] if (!await askYes(`Delete ${s.name}? It is removed from this dataset and from every dataset beneath it that has a snapshot of the same name. This cannot be undone.`, 'Delete')) return try { await api.deleteZfsSnapshot(s.name, true); toast(`${short} deleted`); await reload(); onChanged() } catch (e) { toast(e instanceof Error ? e.message : 'failed', 'err') } } const depth = d.name.split('/').length - 1 return ( <>
{open ? '▾' : '▸'}{d.name.split('/').pop()}{d.type === 'volume' && zvol} {d.snapshots} {d.used_by_snapshots ? fmtBytes(d.used_by_snapshots) : '—'} {fmtBytes(d.used)} {fmtBytes(d.refer)} {d.mount}
{open && (
Snapshot now{d.snapshots} snapshot{d.snapshots === 1 ? '' : 's'} · {fmtBytes(d.avail)} free on this dataset
{snaps === null ?
listing…
: snaps.length === 0 ?
No snapshots.
: (
{snaps.slice().reverse().map((s) => { const { taken, expires } = snapMeta(s.name) const gone = expires && expires.getTime() < Date.now() return (
{s.name.split('@')[1]} {taken ? taken.toLocaleString() : new Date(s.created).toLocaleString()} {expires ? (gone ? 'expired' : `expires ${expires.toLocaleDateString()}`) : 'kept'} {fmtBytes(s.used)} destroy(s)}>Delete
) })}
)}
)} ) } // ---- schedules ---- function ExternalNotice({ x, mine, onAdopt }: { x: ExternalJob; mine: number; onAdopt: () => void }) { return (
0 ? ' attn' : '')} style={{ marginBottom: 8 }}> {x.tool} is already taking snapshots on this machine, from cron as {x.user}:
{x.schedule}   {x.command}
in {x.source}{x.last ? ` · last ran ${ago(x.last)}` : ''} {mine > 0 ?
A Ham Lab schedule is running as well. Both use the same names and both only remove snapshots that have passed the expiry written into them, so they will not fight — but they will take twice as many. Keep one.
:
Adopt as a schedule Copies it into a Ham Lab schedule you can edit here. Turn the old cron job off on the Jobs tab once you are happy with it.
}
) } type ExternalJob = ZFSExternal function ScheduleCard({ p, cron, next, last, onChanged, onEdit }: { p: ZFSPolicy; cron: string; next?: string; last: ZFSRun; onChanged: () => void; onEdit: () => void }) { const [busy, setBusy] = useState('') const run = async () => { if (!await askYes(`Take a snapshot of ${p.dataset} now? Snapshots past their retention are removed at the same time, as they would be on the schedule.`, 'Run now')) return setBusy('run') try { const r = await api.runZfsSchedule(p.id); toast(`${r.taken}${r.pruned.length ? ` · ${r.pruned.length} expired removed` : ''}`); onChanged() } catch (e) { toast(e instanceof Error ? e.message : 'failed', 'err') } finally { setBusy('') } } const remove = async () => { if (!await askYes(`Remove the schedule "${p.name}"? The snapshots it already took are left exactly as they are.`, 'Remove')) return try { await api.deleteZfsSchedule(p.id); toast('schedule removed'); onChanged() } catch (e) { toast(e instanceof Error ? e.message : 'failed', 'err') } } const toggle = async () => { try { await api.saveZfsSchedule({ ...p, enabled: !p.enabled }); onChanged() } catch (e) { toast(e instanceof Error ? e.message : 'failed', 'err') } } return (
{p.name || p.dataset} {!p.enabled && paused} {p.enabled ? soon(next) : 'not scheduled'}
Dataset{p.dataset}
When{describe(p)}
Cron{cron}
Last run{ago(last.at)}{last.pruned?.length ? ` · ${last.pruned.length} expired removed` : ''}
{last.taken &&
Last snapshot{last.taken.split('@')[1]}
} {p.keep_min > 0 &&
Flooralways keep the newest {p.keep_min}
}
{last.error &&
{last.error}
}
Run now {p.enabled ? 'Pause' : 'Resume'} Remove
) } const BLANK: Partial = { name: '', dataset: '', recursive: true, every: 'daily', at: '02:30', weekday: 0, day: 1, keep_for: '6w', keep_min: 1, enabled: true } function ScheduleDialog({ p, datasets, onClose, onSaved }: { p: Partial; datasets: string[]; onClose: () => void; onSaved: () => void }) { const [f, setF] = useState>({ ...BLANK, ...p }) const [err, setErr] = useState('') useEscape(onClose) const set = (k: keyof ZFSPolicy, v: unknown) => setF((x) => ({ ...x, [k]: v })) const save = async (e: FormEvent) => { e.preventDefault(); setErr('') try { await api.saveZfsSchedule({ ...f, name: (f.name || '').trim() || f.dataset }); onSaved() } catch (e) { setErr(e instanceof Error ? e.message : 'failed') } } return (
e.stopPropagation()} onSubmit={save}>

{f.id ? 'Edit schedule' : 'New snapshot schedule'}

{f.every === 'custom' ? ( ) : ( )} {f.every === 'weekly' && ( )} {f.every === 'monthly' && ( )} {err &&
{err}
}
) } // adopt reads an existing cron job into the fields of a new schedule, so replacing it is a // matter of checking the numbers rather than retyping them. zfSnap's own flags are read // where they can be: -a is the retention, -r is recursive. function adopt(x: ExternalJob, fallbackDataset: string): Partial { const cmd = x.command const keep = /-a\s+(\d+[mhdwMy])/.exec(cmd)?.[1] ?? /--\s*(\d+[mhdw])\b/.exec(cmd)?.[1] ?? '6w' const f = x.schedule.trim().split(/\s+/) const min = Number(f[0]), hour = Number(f[1]) const simple = f.length === 5 && Number.isInteger(min) && Number.isInteger(hour) && f[2] === '*' && f[3] === '*' && f[4] === '*' const ds = /\b(tank[\w/-]*)\b/.exec(cmd)?.[1] ?? fallbackDataset return { ...BLANK, name: `${x.tool} (adopted)`, dataset: ds, recursive: /\s-[a-zA-Z]*r\b/.test(cmd), // -r, or -r folded into another flag group every: simple ? 'daily' : 'custom', at: simple ? `${String(hour).padStart(2, '0')}:${String(min).padStart(2, '0')}` : '02:30', cron: simple ? '' : x.schedule.trim(), keep_for: keep, keep_min: 1, enabled: true, } } // ---- the cache: the ARC in RAM, and what each pool has in front of its disks ---- // // Two kinds of number. The live ones (size, the hit rate over the last sampling interval) // come from the sampler, which also keeps their history for the sparklines; the since-boot // counters, the tunables and the pool layout come with the same request from the storage // package. A tier a pool does not have is named and said to be absent, with what that means, // because "nothing shown" and "none fitted" are different answers. const hitPct = (h: number, m: number) => (h + m > 0 ? (h / (h + m)) * 100 : NaN) const fmtPct = (v: number) => (Number.isNaN(v) ? '—' : `${v.toFixed(1)}%`) const fmtN = (n: number) => n.toLocaleString() const cssVar = (n: string) => getComputedStyle(document.documentElement).getPropertyValue(n).trim() || '#3987e5' function Tier({ name, what, devs, none, note }: { name: string; what: string; devs: CacheDev[]; none: string; note?: string }) { return (
{name} {devs.length === 0 ? {none} : devs.map((x) => ( {x.name}{x.size ? ` · ${fmtBytes(x.alloc ?? 0)} of ${fmtBytes(x.size)} used` : ''}{x.health && x.health !== 'ONLINE' ? ` · ${x.health}` : ''} ))} {devs.length > 0 && note && {note}}
) } function PoolTiers({ p, v }: { p: PoolCaching; v: ARCView }) { const L = v.detail.lifetime, z = v.detail.zil, a = v.live return (
{p.pool} caching tiers primarycache={p.primarycache || '?'} · secondarycache={p.secondarycache || '?'}
{p.spare.length > 0 && }
sync={p.sync || '?'} · logbias={p.logbias || '?'} · compression={p.compression || '?'} · recordsize={p.recordsize || '?'}
) } // ---- the write path: the dirty buffer in RAM, what each pool writes, and a landing tier ---- // // A write reaches a pool in up to three steps. If a landing tier fronts the pool (a mergerfs // pool whose first branch is fast storage and whose last is the pool), new files land there // first and a mover drains them. Either way the data then waits in RAM as dirty buffers, and // each txg sync writes it to the disks. Each step's live number is here, with its history. const dur = (s: number) => (!Number.isFinite(s) || s <= 0 ? '—' : s < 90 ? `${Math.round(s)} s` : s < 5400 ? `${Math.round(s / 60)} min` : `${(s / 3600).toFixed(1)} h`) type WHist = Record const wIds = (w: ZFSWrites) => ['zfs.write.dirty', ...w.pools.flatMap((p) => { const b = `zfs.${p.pool}.` return [b + 'write_bps', b + 'logical_bps', ...(p.landing ? [b + 'landing.pending_bytes', b + 'landing.in_bps', b + 'landing.drain_bps'] : [])] })] function MoverLine({ l }: { l: Landing }) { const m = l.mover if (!m) return
No mover is reporting for this landing tier (nothing in /run/landing-mover/). Files that land here stay here until something moves them.
const frac = m.current_size > 0 ? m.partial / m.current_size : 0 return ( <>
Mover {m.state} {' '}{m.state === 'moving' && m.batch_files > 1 ? `batch of ${m.batch_files} files, ${fmtBytes(m.batch_bytes)}` : m.state === 'holding' ? 'a mount it needs is missing — waiting' : m.state === 'stopped' ? `no word since ${ago(m.updated_ms)}` : `queue ${m.queue}`} {m.state === 'moving' && m.queue > 0 ? ` · ${m.queue} more queued` : ''}
{m.state === 'moving' && m.current &&
Now{m.current}{m.current_size ? ` · ${fmtBytes(m.partial)} of ${fmtBytes(m.current_size)}` : ''}
}
Since start{fmtN(m.moved_files)} files · {fmtBytes(m.moved_bytes)} moved · running {ago(m.started_ms).replace(' ago', '')}
{m.last_move &&
Last moved{m.last_move} · {ago(m.last_move_ms)}
}
Rulesa file moves once it has been closed and left alone for {m.settle} s · limit {m.bwlimit && m.bwlimit !== '0' ? `${m.bwlimit}B/s` : 'none'} · oldest first
{m.last_error &&
Last error{m.last_error} · {ago(m.last_error_ms)}
}
{m.state === 'moving' && m.current_size > 0 && } ) } function LandingBlockInner({ l, pending, inn, drain }: { l: Landing; pending: number[]; inn: number[]; drain: number[] }) { const eta = l.drain_bps > 0 ? l.pending_bytes / l.drain_bps : NaN const fill = l.size > 0 ? l.used / l.size : 0 return (
Landing {l.branch}{l.device ? ` · ${l.device}` : ''}{l.fstype ? ` · ${l.fstype}` : ''} · in front of {l.pool_branch} as {l.mount}
{fmtBytes(l.used)} of {fmtBytes(l.size)} used {l.pending_files ? `${fmtN(l.pending_files)}${l.pending_partial ? '+' : ''} waiting · ${fmtBytes(l.pending_bytes)}` : 'nothing waiting'}
Waiting{l.pending_files ? `${fmtN(l.pending_files)}${l.pending_partial ? '+' : ''} files, ${fmtBytes(l.pending_bytes)} · oldest landed ${ago(l.oldest_ms)}` : 'nothing — everything that landed is on the pool'} · counted {ago(l.counted_ms)}
Arriving{fmtRate(l.in_bps)}{l.device ? · writes to {l.device} : ''}
To the pool{fmtRate(l.drain_bps)}{l.pending_bytes > 0 ? ` · ${Number.isFinite(eta) ? `clear in about ${dur(eta)}` : 'not draining right now'}` : ''}
Waiting on the landing tier · {fmtBytes(l.pending_bytes)}
Arriving {fmtRate(l.in_bps)} · to the pool {fmtRate(l.drain_bps)}
) } function WritePath({ w, p, h }: { w: ZFSWrites; p: PoolWrites; h: WHist }) { const b = `zfs.${p.pool}.` const throttling = w.delayed_per_sec > 0 || w.over_max_per_sec > 0 const delayAt = (w.limit * w.delay_min_pct) / 100 const ratio = p.write_bps > 0 && p.logical_bps > 0 ? p.logical_bps / p.write_bps : 0 return (
0 ? 'bad' : throttling ? 'warn' : 'ok')} /> {p.pool} write path {fmtRate(p.write_bps)} to disk
{p.landing ? :
Landingnone — writes go straight into the pool's RAM buffer
}
RAM write buffer · {fmtBytes(w.dirty)} of {fmtBytes(w.limit)}
Writes · {fmtRate(p.write_bps)} to disk · {fmtRate(p.logical_bps)} from programs
In RAM{fmtBytes(w.dirty)} waiting · holds up to {fmtBytes(w.limit)} {w.limit < w.dirty_max ? `(half the ARC target; zfs_dirty_data_max is ${fmtBytes(w.dirty_max)})` : '(zfs_dirty_data_max)'}
Throttle {w.over_max_per_sec > 0 ? {fmtN(w.over_max_per_sec)}/s writes stopped at the limit : w.delayed_per_sec > 0 ? slowing {fmtN(w.delayed_per_sec)} writes/s — the disks are the limit now : 'not slowing anything'} · starts at {w.delay_min_pct}% ({fmtBytes(delayAt)})
To disk{fmtRate(p.write_bps)} written by syncs{ratio > 0 ? ` · ${fmtRate(p.logical_bps)} from programs (${ratio >= 1 ? `${ratio.toFixed(2)}x compressed` : 'parity and metadata included'})` : ''} · a sync at least every {w.txg_timeout_sec} s, sooner as the buffer fills
) } function LandingBlockWithHist({ l, h, b }: { l: Landing; h: WHist; b: string }) { return } function ARCSection() { const [v, setV] = useState(null) const [hist, setHist] = useState<{ size: number[]; hit: number[] }>({ size: [], hit: [] }) const [whist, setWhist] = useState({}) const [err, setErr] = useState('') const rate = useRefreshMs() useEffect(() => { let stop = false const poll = async () => { let r: ARCView | null = null try { r = await api.zfsARC(); if (!stop) { setV(r); setErr('') } } catch (e) { if (!stop) setErr(e instanceof Error ? e.message : 'failed') } try { const extra = r?.writes ? wIds(r.writes) : [] const hs = await fetchHistories(['zfs.arc.size', 'zfs.arc.hit_pct', ...extra].join(',')) if (!stop) { setHist({ size: hs.find((h) => h.id === 'zfs.arc.size')?.v ?? [], hit: hs.find((h) => h.id === 'zfs.arc.hit_pct')?.v ?? [] }) setWhist(Object.fromEntries(hs.filter((h) => extra.includes(h.id)).map((h) => [h.id, h.v]))) } } catch { /* the sparklines stay empty; the numbers still show */ } } poll() const t = setInterval(poll, rate) return () => { stop = true; clearInterval(t) } }, [rate]) if (err && !v) return
{err}
if (!v) return
reading the cache…
const d = v.detail, a = v.live if (!d.available || !a) return
The ZFS module reports no ARC statistics on this machine.
const L = d.lifetime const ratio = a.compressed > 0 ? a.uncompressed / a.compressed : 0 const maxSet = d.tunables.zfs_arc_max && d.tunables.zfs_arc_max !== '0' const atFloor = a.target <= a.min * 1.02 const rows: [string, number, number][] = [ ['Demand data', L.DemandDataHits, L.DemandDataMisses], ['Demand metadata', L.DemandMetaHits, L.DemandMetaMisses], ['Prefetch data', L.PrefetchDataHits, L.PrefetchDataMisses], ['Prefetch metadata', L.PrefetchMetaHits, L.PrefetchMetaMisses], ] return ( <>
ARC in RAM {fmtBytes(a.size)} / {fmtBytes(a.max)} · {Math.round((a.size / a.max) * 100)}%
{a.no_grow && (
Held by memory pressure. ZFS is not letting the cache grow (arc_no_grow): the system is short of free memory {atFloor ? <>, and the target sits at its floor of {fmtBytes(a.min)} against a ceiling of {fmtBytes(a.max)} : ''}. {' '}Whatever no longer fits is read from the disks.
)}
Size · ceiling {fmtBytes(a.max)}
Hit rate · {a.hit_pct_known ? `${a.hit_pct.toFixed(1)}% now` : 'no reads yet'} · {fmtN(Math.round(a.reads_per_sec))} reads/s
`${x.toFixed(1)}%`} height={48} />
Target{fmtBytes(a.target)} · floor {fmtBytes(a.min)} · ceiling {fmtBytes(a.max)} {maxSet ? '(zfs_arc_max)' : '(default: half of RAM)'}
Hit rate{a.hit_pct_known ? `${a.hit_pct.toFixed(1)}% now` : 'idle'} · {a.lifetime_hit_pct.toFixed(1)}% since boot · {fmtN(a.hits)} hits, {fmtN(a.misses)} misses
HoldsMRU {fmtBytes(a.mru)} · MFU {fmtBytes(a.mfu)} · dirty {fmtBytes(a.anon)}
Of whichdata {fmtBytes(a.data)} · metadata {fmtBytes(a.metadata)} · headers {fmtBytes(a.headers)}
Compressed{ratio > 0 ? `${fmtBytes(a.uncompressed)} held in ${fmtBytes(a.compressed)} · ${ratio.toFixed(2)}x` : 'not compressed'}
GhostsMRU {fmtBytes(a.mru_ghost)} · MFU {fmtBytes(a.mfu_ghost)}
Prefetch{d.tunables.zfs_prefetch_disable === '1' ? 'disabled (zfs_prefetch_disable)' : `${fmtPct(hitPct(d.zfetch.Hits, d.zfetch.Misses))} of ${fmtN(d.zfetch.Hits + d.zfetch.Misses)} predictions since boot`}
dbuf cache{fmtBytes(d.dbuf.Size)} of {fmtBytes(d.dbuf.Target)} · {fmtPct(hitPct(d.dbuf.Hits, d.dbuf.Misses))} hits
Pressure{fmtN(a.throttles)} throttles · {fmtN(a.prunes)} prunes · {fmtN(a.evict_skip)} evict skips
Since boothit ratehitsmisses {rows.map(([k, h, m]) => {k}{fmtPct(hitPct(h, m))}{fmtN(h)}{fmtN(m)})} All reads{fmtPct(hitPct(L.Hits, L.Misses))}{fmtN(L.Hits)}{fmtN(L.Misses)}
{d.pools.length > 0 &&
{d.pools.map((p) => )}
} {v.writes && v.writes.pools.length > 0 && ( <>

Writes

what is waiting to reach each pool, and how fast it is getting there
{v.writes.pools.map((p) => )}
)} ) } // ---- page ---- export default function ZFS() { const [ov, setOv] = useState(null) const [err, setErr] = useState('') const [editing, setEditing] = useState | null>(null) const rate = useRefreshMs() const load = useCallback(async () => { try { setOv(await api.zfsOverview()); setErr('') } catch (e) { setErr(e instanceof Error ? e.message : 'failed') } }, []) useEffect(() => { load() }, [load]) useEffect(() => { const t = setInterval(load, Math.max(5000, rate * 10)); return () => clearInterval(t) }, [load, rate]) if (err && !ov) return
{err}
if (!ov) return
const zfs: ZFSInfo = ov.zfs const datasets = zfs.datasets.filter((d) => d.type !== 'volume').map((d) => d.name) const mine = ov.policies.filter((x) => x.policy.enabled).length if (!zfs.available) return
{zfs.message || 'No ZFS on this host.'}
return (

Pools

{zfs.pools.length === 0 &&
ZFS is present but no pools are imported.
}
{zfs.pools.map((p) => )}

Cache

the ARC in RAM, and what each pool has in front of its disks

Snapshot schedules

setEditing({ ...BLANK, dataset: zfs.pools[0]?.name ?? '' })}>+ New schedule
{ov.external && setEditing(adopt(ov.external!, zfs.pools[0]?.name ?? ''))} />} {ov.policies.length === 0 && !ov.external &&
Nothing is taking snapshots on a schedule. Snapshots are written the way zfSnap writes them, with the retention in the name, so anything already here is understood.
}
{ov.policies.map(({ policy, cron, next, last }) => ( setEditing(policy)} /> ))}

Snapshots

{zfs.datasets.reduce((a, d) => a + d.snapshots, 0)} across {zfs.datasets.length} datasets · open one to browse, take or delete
DatasetSnapshotsThey use Dataset usesReferencedMount
{zfs.datasets.map((d) => )}
{editing && p.name)} onClose={() => setEditing(null)} onSaved={() => { setEditing(null); load() }} />}
) }