diff --git a/internal/cron/cron.go b/internal/cron/cron.go index e5ae2cb..5bfa1f9 100644 --- a/internal/cron/cron.go +++ b/internal/cron/cron.go @@ -1,3 +1,6 @@ +// Package cron reads and writes what the UI calls the **Jobs** tab (named "Cron" until +// 2026-09-24; the package and its /api/cron endpoints kept the old name). +// // Package cron reads every scheduled job on the host into one list: /etc/crontab, // /etc/cron.d/*, per-user crontabs, the run-parts directories they drive, and systemd // timers. It computes next runs from the cron expressions, pulls last runs from the diff --git a/internal/space/walk.go b/internal/space/walk.go index 57dc146..09245a0 100644 --- a/internal/space/walk.go +++ b/internal/space/walk.go @@ -1,4 +1,6 @@ -// Package space measures what is using a disk: a parallel walk of the filesystems a disk +// Package space measures what is using a disk — it is what the UI calls the **Usage** tab +// (renamed from "Space" on 2026-09-24; the package and its /api/space endpoints kept the +// old name rather than churn a working API): a parallel walk of the filesystems a disk // backs, kept as a tree of directories and the files worth seeing, the way WizTree does it. // // The walk never crosses a filesystem boundary by itself — /proc, /sys, another disk's diff --git a/web/src/Cron.tsx b/web/src/Jobs.tsx similarity index 98% rename from web/src/Cron.tsx rename to web/src/Jobs.tsx index 2058155..45819a8 100644 --- a/web/src/Cron.tsx +++ b/web/src/Jobs.tsx @@ -175,11 +175,11 @@ function TimerDetail({ t, onChanged, onBack }: { t: CronTimer; onChanged: () => } // ---- page ---- -export default function Cron() { +export default function Jobs() { const [ov, setOv] = useState(null) const [err, setErr] = useState('') const [q, setQ] = useState('') - const [hashSel, setHashSel] = useHashSel('cron') // "job:" or "timer:", in the URL + const [hashSel, setHashSel] = useHashSel('jobs') // "job:" or "timer:", in the URL const sel = /^(job|timer):./.test(hashSel) ? { kind: hashSel.split(':')[0] as 'job' | 'timer', id: hashSel.slice(hashSel.indexOf(':') + 1) } : null const setSel = (v: { kind: 'job' | 'timer'; id: string } | null) => setHashSel(v ? `${v.kind}:${v.id}` : '') const [editing, setEditing] = useState(null) @@ -206,7 +206,7 @@ export default function Cron() { else pane =
Select a job or timer.
return ( -
+
setQ(e.target.value)} />↻
@@ -218,7 +218,7 @@ export default function Cron() { {ov && !ov.logging &&
cron isn’t logging to the journal on this host, so “last run” is unavailable for cron jobs.
}
{!ov && !err && } - {ov &&

Cron jobs · {jobs.length}

} + {ov &&

Scheduled jobs · {jobs.length}

} {jobs.map((j) => (
{ setEditing(null); setSel({ kind: 'job', id: j.id }) }}> diff --git a/web/src/Shell.tsx b/web/src/Shell.tsx index bdaaccc..4c6aa29 100644 --- a/web/src/Shell.tsx +++ b/web/src/Shell.tsx @@ -8,14 +8,14 @@ import Gallery from './Gallery' import Docker from './Docker' import Alerts from './Alerts' import Services from './Services' -import Cron from './Cron' +import Jobs from './Jobs' import Updates from './Updates' import Disks from './Disks' import Files from './Files' import Top from './Top' import Machines from './Machines' import Settings from './Settings' -import Space from './Space' +import Usage from './Usage' import Users from './Users' import MachineSwitcher from './MachineSwitcher' import { MachineStatus, setMachine } from './api' @@ -54,10 +54,10 @@ const GROUPS: Group[] = [ { id: 'terminal', label: 'Terminal', icon: '>_' }, { id: 'services', label: 'Services', icon: '◈' }, { id: 'docker', label: 'Docker', icon: DockerIcon }, - { id: 'cron', label: 'Cron', icon: ClockIcon }, + { id: 'jobs', label: 'Jobs', icon: ClockIcon }, { id: 'updates', label: 'Updates', icon: '⇪' }, { id: 'disks', label: 'Disks', icon: '▤' }, - { id: 'space', label: 'Space', icon: '▧' }, + { id: 'usage', label: 'Usage', icon: '▧' }, { id: 'files', label: 'Files', icon: FolderIcon }, { id: 'alerts', label: 'Alerts', icon: BellIcon }, { id: 'machines', label: 'Machines', icon: '⌂' }, @@ -66,16 +66,21 @@ const GROUPS: Group[] = [ { id: 'settings', label: 'Settings', icon: '⚙' }, ] -type View = 'terminal' | 'widgets' | 'docker' | 'alerts' | 'services' | 'cron' | 'updates' | 'disks' | 'space' | 'files' | 'procs' | 'machines' | 'users' | 'settings' -const VIEWS: View[] = ['terminal', 'widgets', 'docker', 'alerts', 'services', 'cron', 'updates', 'disks', 'space', 'files', 'procs', 'machines', 'users', 'settings'] +type View = 'terminal' | 'widgets' | 'docker' | 'alerts' | 'services' | 'jobs' | 'updates' | 'disks' | 'usage' | 'files' | 'procs' | 'machines' | 'users' | 'settings' +const VIEWS: View[] = ['terminal', 'widgets', 'docker', 'alerts', 'services', 'jobs', 'updates', 'disks', 'usage', 'files', 'procs', 'machines', 'users', 'settings'] const HUB_ONLY: View[] = ['machines', 'users', 'settings', 'alerts'] // URL hash is the source of truth for where you are: #//. Back and // forward work, reload restores the tab, and links can point at a page on a machine. +// Pages that were renamed, so a link or a bookmark made before it still lands where it meant. +const RENAMED_VIEWS: Record = { space: 'usage', cron: 'jobs', top: 'procs', dashboard: 'procs' } + function parseHash(): { machine: string | null; view: View } | null { const m = location.hash.match(/^#\/([a-z0-9-]+)\/([a-z]+)(?:\/.*)?$/) // anything after the page is that page's own selection - if (!m || !VIEWS.includes(m[2] as View)) return null - return { machine: m[1] === 'hub' ? null : m[1], view: m[2] as View } + if (!m) return null + const view = VIEWS.includes(m[2] as View) ? (m[2] as View) : RENAMED_VIEWS[m[2]] + if (!view) return null + return { machine: m[1] === 'hub' ? null : m[1], view } } function fmtUptime(s: number): string { @@ -262,14 +267,14 @@ export default function Shell({ me: hubMe, onLoggedOut }: { me: Me; onLoggedOut:
{view === 'settings' && } {view === 'users' && } - {view === 'space' && } + {view === 'usage' && } {view === 'machines' && { switchMachine(n); setView('procs') }} />} {view === 'widgets' && } {view === 'docker' && } {view === 'alerts' && m.name)} titleOf={(n) => titleOf(n === hubMe.host ? null : n, machines, hubMe.host)} />} {view === 'procs' && } {view === 'services' && } - {view === 'cron' && } + {view === 'jobs' && } {view === 'updates' && } {view === 'disks' && } {view === 'files' && } @@ -298,7 +303,7 @@ export default function Shell({ me: hubMe, onLoggedOut }: { me: Me; onLoggedOut: {machine ? `${title} · ` : ''}{me.version.startsWith('v') ? me.version : 'v' + me.version} up {fmtUptime(me.uptime)} - {view === 'widgets' ? 'Widgets' : view === 'docker' ? 'Docker' : view === 'alerts' ? 'Alerts' : view === 'services' ? 'Services' : view === 'cron' ? 'Cron' : view === 'updates' ? 'Updates' : view === 'disks' ? 'Disks' : view === 'space' ? 'Space' : view === 'files' ? 'Files' : view === 'procs' ? 'Processes' : view === 'machines' ? 'Machines' : view === 'users' ? 'Users' : view === 'settings' ? 'Settings' : activeTerm ? activeTerm.name : ''} + {view === 'widgets' ? 'Widgets' : view === 'docker' ? 'Docker' : view === 'alerts' ? 'Alerts' : view === 'services' ? 'Services' : view === 'jobs' ? 'Jobs' : view === 'updates' ? 'Updates' : view === 'disks' ? 'Disks' : view === 'usage' ? 'Usage' : view === 'files' ? 'Files' : view === 'procs' ? 'Processes' : view === 'machines' ? 'Machines' : view === 'users' ? 'Users' : view === 'settings' ? 'Settings' : activeTerm ? activeTerm.name : ''}
) diff --git a/web/src/Space.tsx b/web/src/Usage.tsx similarity index 99% rename from web/src/Space.tsx rename to web/src/Usage.tsx index 1eaea1f..564d8a9 100644 --- a/web/src/Space.tsx +++ b/web/src/Usage.tsx @@ -1,4 +1,5 @@ -// Space: what is using a disk, WizTree-style. Pick a disk, scan it (a parallel walk of every +// Usage: what is using a disk, WizTree-style. (The tab was called Space until 2026-09-24; +// the Go package and the /api/space endpoints behind it still carry the old name.) Pick a disk, scan it (a parallel walk of every // filesystem the disk backs, as root when the host allows), then read the result two ways: // a tree of folders largest-first, and a treemap of whatever folder is selected, where every // file is a tile sized by what it takes on disk and coloured by what kind of thing it is. @@ -45,10 +46,10 @@ const ago = (iso?: string) => { const fmtDur = (a: string, b?: string) => { const ms = (b ? new Date(b).getTime() : Date.now()) - new Date(a).getTime(); return ms < 1000 ? `${ms} ms` : ms < 60000 ? `${(ms / 1000).toFixed(1)} s` : `${Math.floor(ms / 60000)} min ${Math.round((ms % 60000) / 1000)} s` } const parentOf = (p: string) => (p === '/' ? '/' : p.slice(0, p.lastIndexOf('/')) || '/') -export default function Space() { +export default function Usage() { const [targets, setTargets] = useState(null) const [err, setErr] = useState('') - const [sel, setSel] = useHashSel('space') + const [sel, setSel] = useHashSel('usage') const [tab, setTab] = useState<'tree' | 'files'>('tree') const rate = useRefreshMs() diff --git a/web/src/railorder.ts b/web/src/railorder.ts index 297e4fb..7ccfd41 100644 --- a/web/src/railorder.ts +++ b/web/src/railorder.ts @@ -5,8 +5,25 @@ import { useEffect, useState } from 'react' const KEY = 'hamlab.rail' +// Pages that were renamed, so an order saved before it still puts the page where it was put: +// Top became Processes, Space became Usage, Cron became Jobs. A page that was removed +// outright (Dashboard) is not here — orderBy drops ids that match no page. +const RENAMED: Record = { top: 'procs', space: 'usage', cron: 'jobs' } + export function getRailOrder(): string[] { - try { const v = JSON.parse(localStorage.getItem(KEY) ?? '[]'); return Array.isArray(v) ? v.filter((x) => typeof x === 'string') : [] } catch { return [] } + let raw: unknown + try { raw = JSON.parse(localStorage.getItem(KEY) ?? '[]') } catch { return [] } + if (!Array.isArray(raw)) return [] + const seen = new Set() + const out: string[] = [] + for (const x of raw) { + if (typeof x !== 'string') continue + const id = RENAMED[x] ?? x + if (seen.has(id)) continue // a merged page keeps the earlier of the two slots + seen.add(id) + out.push(id) + } + return out } export function setRailOrder(ids: string[], save = true) {