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:
co-authored by
Claude Opus 4.7
parent
a171331a7a
commit
7b972dc049
+113
-18
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user