v0.4.4: Settings tab, API reference, ISO category, branding, disk space

Settings:
- New top-level Settings tab. Carries a placeholder for the planned
  LDAP / OIDC / user-management work, the new branding controls, and
  the API reference at the bottom.
- Custom logo upload (PNG/SVG/JPEG/WebP/GIF up to 2 MB) replaces the
  bundled brand mark via /assets/logo.svg; bytes live at
  <work_dir>/branding/ and survive restart. The original "OpenPXE
  v<x.y.z>" pins to the sidebar footer for support.
- API reference rendered from a new GET /api/docs into a per-method
  coloured pill list grouped by area.

ISO category (Storage):
- New IsoCategory { Os, Tools } on IsoMeta with PUT
  /api/isos/:id/category. Storage table's Type cell becomes a
  dropdown; selecting Tools moves the ISO into the Tools submenu next
  to memtest / shell / NIC info and removes it from the OS Installers
  family submenu. Family detection still drives BIOS/UEFI / kernel
  args; only the menu placement changes.

Storage telemetry:
- New IsoStore::disk_usage (libc::statvfs, lives in iso-store so the
  http-api crate stays #![forbid(unsafe_code)]) and GET
  /api/storage/disk. The Storage tab now shows free/used/total for
  the volume hosting the ISO directory with an 80%/95% colour ramp.

UI polish:
- Brand block in the sidebar now matches the topbar height exactly,
  so the divider runs straight across the top of the app rather than
  stepping; version label moved out of the brand and pinned to the
  sidebar footer ("OpenPXE v0.4.4").
- Light-mode terminal: --terminal-bg + per-level text colours track
  the active theme rather than being hard-coded dark.
- About: lead paragraph spans the full content width; new Docs row
  links to https://openpxe.com/.

106 tests passing (was 89 in v0.4.1, +17 across branding unit tests
and new integration coverage for category / disk / docs / branding).
cargo clippy --workspace --all-targets clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
Miles Ward
2026-05-25 17:46:52 -04:00
co-authored by Claude Opus 4.7
parent a171331a7a
commit 7b972dc049
14 changed files with 1385 additions and 45 deletions
+113 -18
View File
@@ -54,7 +54,11 @@
--ok: #1f9b54;
--border: #d8dde6;
--border-soft: #e7eaf0;
--terminal-bg: #0d1219; /* terminal stays dark even in light mode */
/* Light-mode terminal: the pane background and chrome track the rest
of the light theme. Per-level text colours below recolour-on-light
so log lines stay readable on a pale background — previously the
terminal was locked to dark and looked like a stuck panel. */
--terminal-bg: #ffffff;
--shadow-card: 0 1px 0 rgba(0,0,0,0.02), 0 6px 18px rgba(20,28,52,0.06);
}
@@ -87,14 +91,23 @@ code, kbd { font-family: var(--mono); font-size: 12.5px;
border-right: 1px solid var(--border);
display: flex; flex-direction: column;
}
/* The brand block sits flush with the topbar so the sidebar+topbar reads
as one continuous bar across the top of the app, rather than a chunky
2-line logo block plus a separate (smaller-typeface) page title. The
height/border-bottom match the topbar exactly so the divider runs
straight across without a step. */
.sidebar .brand {
display: flex; align-items: center; gap: 12px;
padding: 14px 18px;
padding: 0 18px;
height: var(--topbar-h);
border-bottom: 1px solid var(--border);
}
.sidebar .brand img { width: 40px; height: auto; }
.sidebar .brand strong { font-size: 16px; letter-spacing: 0.4px; }
.sidebar .brand .sub { color: var(--fg-dim); font-size: 11px; }
.sidebar .brand img { width: 26px; height: 26px; flex: none; }
.sidebar .brand strong {
font-size: 15px; font-weight: 600;
letter-spacing: 0.2px;
color: var(--fg);
}
.sidebar nav { padding: 10px 0; flex: 1; overflow-y: auto; }
.sidebar nav a {
display: flex; align-items: center; gap: 10px;
@@ -140,6 +153,16 @@ code, kbd { font-family: var(--mono); font-size: 12.5px;
.sidebar .footer .status-value.err { color: var(--err); }
.sidebar .footer .status-value.warn { color: var(--warn); }
.sidebar .footer .footer-sub { color: var(--fg-dimmer); margin-top: 2px; }
/* Persistent backend identity. Sits below the advertised URL so even
when an operator has uploaded their own logo, "what is this" stays
answerable from the bottom-left of every page. */
.sidebar .footer .footer-version {
margin-top: 8px; padding-top: 8px;
border-top: 1px dashed var(--border-soft);
color: var(--fg-dim);
font-variant-numeric: tabular-nums;
letter-spacing: 0.2px;
}
/* ── Top bar ───────────────────────────────────────────────────────── */
@@ -436,26 +459,29 @@ tr.unbootable td:first-child { border-left: 3px solid var(--warn); }
min-height: 480px;
box-shadow: var(--shadow-card);
}
/* Terminal pane colours follow the active theme. Hard-coded hexes
(#050505, #181818, #cfd6e2 etc.) were leaving the light-mode pane
looking dark; we keep palette-aware vars instead so the toggle works. */
.terminal .pane {
flex: 1; overflow: auto;
padding: 10px 14px;
font-family: var(--mono); font-size: 12.5px; line-height: 1.5;
color: #cfd6e2;
color: var(--fg);
white-space: pre-wrap; word-break: break-word;
}
.terminal .pane .lvl-error { color: var(--err); }
.terminal .pane .lvl-warn { color: var(--warn); }
.terminal .pane .lvl-info { color: #cfd6e2; }
.terminal .pane .lvl-debug { color: #8b94a8; }
.terminal .pane .lvl-trace { color: #5a6379; }
.terminal .pane .ts { color: #5a6379; }
.terminal .pane .tg { color: #7cd3ff; }
.terminal .pane .lvl-info { color: var(--fg); }
.terminal .pane .lvl-debug { color: var(--fg-dim); }
.terminal .pane .lvl-trace { color: var(--fg-dimmer); }
.terminal .pane .ts { color: var(--fg-dimmer); }
.terminal .pane .tg { color: var(--accent); }
.terminal .pane .echo { color: var(--accent); }
.terminal .input-row {
display: flex; align-items: center; gap: 8px;
padding: 8px 14px;
background: #050505;
border-top: 1px solid #181818;
background: var(--bg-panel-2);
border-top: 1px solid var(--border);
}
.terminal .input-row .prompt { color: var(--accent); font-family: var(--mono); }
.terminal .input-row input {
@@ -466,22 +492,91 @@ tr.unbootable td:first-child { border-left: 3px solid var(--warn); }
.terminal .toolbar {
display: flex; gap: 8px; align-items: center;
padding: 8px 14px;
background: #050505;
border-bottom: 1px solid #181818;
background: var(--bg-panel-2);
border-bottom: 1px solid var(--border);
font-size: 12px; color: var(--fg-dim);
}
.terminal .toolbar .right { margin-left: auto; display: flex; gap: 6px; }
.terminal .toolbar button {
padding: 3px 9px; font-size: 11px;
background: transparent; color: var(--fg-dim); border: 1px solid #181818;
background: transparent; color: var(--fg-dim); border: 1px solid var(--border);
font-weight: 500;
}
.terminal .toolbar button:hover { color: var(--fg); background: #181818; }
.terminal .toolbar button:hover { color: var(--fg); background: var(--bg-elev); }
/* ── About card ─────────────────────────────────────────────────── */
.about-hero { padding: 20px 24px; }
.about-hero h2 { font-size: 22px; margin: 0 0 8px; color: var(--fg); }
.about-hero .lead { color: var(--fg-dim); font-size: 14px; max-width: 60ch; }
/* Span the full main column rather than capping at 60ch — the page is
read at typical desktop widths and the cap was leaving the right two
thirds of the panel awkwardly empty. */
.about-hero .lead { color: var(--fg-dim); font-size: 14px; max-width: none; }
.about-hero .who { margin-top: 18px; font-size: 13px; }
.about-hero .who span { color: var(--fg-dim); }
.about-hero .who strong { color: var(--accent); }
.about-hero a { color: var(--accent); }
/* ── API reference (Settings → bottom) ─────────────────────────── */
.api-ref { display: grid; gap: 18px; padding: 16px; }
.api-ref .group h3 {
margin: 0 0 8px; font-size: 13px; color: var(--fg-dim);
text-transform: uppercase; letter-spacing: 0.8px;
}
.api-ref .ep {
display: grid; grid-template-columns: 64px minmax(200px, 1fr) 2fr;
gap: 12px; align-items: baseline;
padding: 6px 0; border-top: 1px solid var(--border-soft);
font-size: 13px;
}
.api-ref .ep:first-child { border-top: 0; }
.api-ref .ep .method {
font-family: var(--mono); font-weight: 600; font-size: 11px;
padding: 2px 6px; border-radius: 4px;
text-align: center; letter-spacing: 0.6px;
}
.api-ref .ep .method.get { background: color-mix(in srgb, var(--ok) 22%, transparent); color: var(--ok); }
.api-ref .ep .method.post { background: color-mix(in srgb, var(--accent) 22%, transparent); color: var(--accent); }
.api-ref .ep .method.put { background: color-mix(in srgb, var(--warn) 22%, transparent); color: var(--warn); }
.api-ref .ep .method.delete { background: color-mix(in srgb, var(--err) 22%, transparent); color: var(--err); }
.api-ref .ep .path { font-family: var(--mono); color: var(--fg); word-break: break-all; }
.api-ref .ep .desc { color: var(--fg-dim); }
@media (max-width: 900px) {
.api-ref .ep { grid-template-columns: 1fr; gap: 4px; }
.api-ref .ep .method { justify-self: start; }
}
/* ── Disk space card ───────────────────────────────────────────── */
.diskbar {
height: 10px; border-radius: 5px;
background: var(--bg-elev);
overflow: hidden; margin-top: 8px;
}
.diskbar .fill {
height: 100%;
background: linear-gradient(90deg, var(--accent-dim), var(--accent));
transition: width 0.4s ease;
}
.diskbar.warn .fill { background: var(--warn); }
.diskbar.full .fill { background: var(--err); }
.disk-meta { display: flex; gap: 14px; font-size: 12px; color: var(--fg-dim); margin-top: 8px; flex-wrap: wrap; }
.disk-meta strong { color: var(--fg); font-weight: 600; font-variant-numeric: tabular-nums; }
/* ── Logo upload (Settings) ────────────────────────────────────── */
.logo-preview {
display: flex; align-items: center; gap: 14px;
padding: 12px;
background: var(--bg-panel-2);
border: 1px solid var(--border);
border-radius: var(--radius);
}
.logo-preview .swatch {
width: 56px; height: 56px;
display: flex; align-items: center; justify-content: center;
background: var(--bg); border: 1px solid var(--border);
border-radius: var(--radius);
flex: none;
}
.logo-preview .swatch img { max-width: 48px; max-height: 48px; }
.logo-preview .info { flex: 1; min-width: 0; }
.logo-preview .info .name { color: var(--fg); font-weight: 600; }
.logo-preview .info .meta { color: var(--fg-dim); font-size: 12px; margin-top: 2px; }