Files
felhom.eu/documentation/audits/hub-system-page-2026-10-10/system-fixture.html
T
admin cf6fec8d87
gates / gates (push) Successful in 6m17s
hub (unreleased): the System page answers 'is anything wrong?' and 'is anything waiting for me?' first
Needs attention, Waiting for you (one card per kernel/Docker/Proxmox set the button may approve), a
7-column Boxes table whose rows open to every old value, and a closed Details (release ids, cancelled
approvals, ring-0 counts, floors, crash guard and root files, Approve now - which now asks first).
Same data, buttons, routes and CSRF field. No deploy. Screenshots in audits/hub-system-page-2026-10-10/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
2026-10-10 15:38:25 +02:00

1645 lines
70 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>System — Felhom Hub</title>
<style>/* Felhom Hub — Dark theme */
/* Felhom Hub — design system v2 (TASK-D4). Canonical tokens:
documentation/design/design-system.md */
@font-face {
font-family: 'Plus Jakarta Sans';
font-style: normal;
font-weight: 400 800;
font-display: swap;
src: url('file:///mnt/5_hdd/felhom.eu/git/felhom.eu/hub/internal/web/static/fonts/pjs-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Plus Jakarta Sans';
font-style: normal;
font-weight: 400 800;
font-display: swap;
src: url('file:///mnt/5_hdd/felhom.eu/git/felhom.eu/hub/internal/web/static/fonts/pjs-latin-ext.woff2') format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400 500;
font-display: swap;
src: url('file:///mnt/5_hdd/felhom.eu/git/felhom.eu/hub/internal/web/static/fonts/jbm-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400 500;
font-display: swap;
src: url('file:///mnt/5_hdd/felhom.eu/git/felhom.eu/hub/internal/web/static/fonts/jbm-latin-ext.woff2') format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
:root {
--bg-0: #0A1220; --bg-1: #0F1B2E; --bg-2: #16263F;
--line: #22344F; --line-soft: #1A2A42;
--text-1: #EDF2F9; --text-2: #94A6BF; --text-3: #5E7392;
--blue: #0083D8; --blue-bright: #2EA8F5; --blue-dim: rgba(0,131,216,.13);
--warn: #E0A93E; --warn-dim: rgba(224,169,62,.12);
--crit: #E5534B; --crit-dim: rgba(229,83,75,.12);
--radius: 2px;
--font-ui: 'Plus Jakarta Sans', -apple-system, sans-serif;
--font-data: 'JetBrains Mono', monospace;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-ui);
background: var(--bg-0);
color: var(--text-1);
line-height: 1.6;
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem 1.5rem;
}
/* Header */
header {
margin-bottom: 2rem;
}
header h1 {
font-size: 1.75rem;
font-weight: 700;
color: var(--text-1);
}
.subtitle {
color: var(--text-2);
font-size: 0.9rem;
margin-top: 0.25rem;
}
.back-link {
color: var(--blue-bright);
text-decoration: none;
font-size: 0.9rem;
display: inline-block;
margin-bottom: 0.5rem;
}
.back-link:hover {
text-decoration: underline;
}
/* Dashboard table */
.dashboard-table {
width: 100%;
border-collapse: collapse;
background: var(--bg-1);
border-radius: var(--radius);
overflow: hidden;
}
.dashboard-table th {
text-align: left;
padding: 0.75rem 1rem;
background: var(--bg-1);
color: var(--text-2);
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
border-bottom: 1px solid var(--line);
}
.dashboard-table td {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--line);
font-size: 0.9rem;
}
.dashboard-table tbody tr {
cursor: pointer;
transition: background 0.15s;
}
.dashboard-table tbody tr:hover {
background: var(--bg-2);
}
.dashboard-table tbody tr:last-child td {
border-bottom: none;
}
.customer-name {
font-weight: 600;
}
.status-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 0.35rem;
vertical-align: baseline;
background: var(--text-3);
}
.status-dot-nominal { background: var(--blue); }
.status-dot-warn { background: var(--warn); }
.status-dot-crit { background: var(--crit); }
.status-dot-neutral { background: transparent; border: 1px solid var(--text-3); }
/* Status badges */
.status-badge {
display: inline-block;
white-space: nowrap;
padding: 0.15rem 0.5rem;
border-radius: var(--radius);
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Status = STATE -> tag semantics (outline + dot; design-system addendum). Keyed by the
raw status value the templates emit; exception-color: ok is blue, pending/disabled quiet. */
.status-badge { border: 1px solid var(--line); background: transparent; color: var(--text-2); }
.status-badge::before {
content: '';
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
margin-right: 0.35rem;
background: var(--text-3);
vertical-align: 0.05em;
}
.status-badge-ok { border-color: rgba(0,131,216,.45); color: var(--blue-bright); }
.status-badge-ok::before { background: var(--blue); }
.status-badge-warn, .status-badge-blocked, .status-badge-stale { border-color: var(--warn); color: var(--warn); background: var(--warn-dim); }
.status-badge-warn::before, .status-badge-blocked::before, .status-badge-stale::before { background: var(--warn); }
.status-badge-down, .status-badge-fail { border-color: var(--crit); color: var(--crit); background: var(--crit-dim); }
.status-badge-down::before, .status-badge-fail::before { background: var(--crit); }
.status-badge-disabled, .status-badge-pending { color: var(--text-3); }
.status-badge-disabled::before, .status-badge-pending::before { background: transparent; border: 1px solid var(--text-3); }
/* Config badges */
.config-badge {
display: inline-block;
padding: 0.15em 0.5em;
border-radius: var(--radius);
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.03em;
}
.config-badge-managed { background: var(--blue-dim); color: var(--blue-bright); }
.config-badge-manual { background: var(--bg-2); color: var(--text-2); }
.config-badge-blocked { background: var(--crit-dim); color: var(--crit); }
/* Blocked banner */
.flash-blocked {
background: var(--crit-dim);
border: 1px solid rgba(229,83,75,.3);
color: var(--crit);
padding: 0.75rem 1rem;
border-radius: var(--radius);
margin-bottom: 1rem;
font-size: 0.9rem;
}
/* Blocked row in tables */
.row-blocked { opacity: 0.5; }
/* Cards */
.card {
background: var(--bg-1);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 1.25rem;
margin-bottom: 1rem;
}
.card h2 {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 1rem;
color: var(--text-1);
}
.card h3 {
font-size: 0.9rem;
font-weight: 600;
margin-top: 0.75rem;
margin-bottom: 0.5rem;
color: var(--text-2);
}
/* Info grid */
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 0.75rem;
}
.info-item .label {
display: block;
font-size: 0.75rem;
color: var(--text-3);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.info-item .value {
font-size: 0.9rem;
color: var(--text-1);
}
/* Metrics */
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem;
margin-top: 0.75rem;
}
.metric {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.metric-label {
font-size: 0.8rem;
color: var(--text-2);
}
.metric-value {
font-size: 1.25rem;
font-weight: 700;
font-family: var(--font-data);
}
.metric-detail {
font-size: 0.8rem;
color: var(--text-3);
}
.bar {
height: 6px;
background: var(--line);
border-radius: var(--radius);
overflow: hidden;
}
.bar-fill {
height: 100%;
background: var(--blue-bright);
border-radius: var(--radius);
transition: width 0.3s ease;
}
/* Fill bands (v0.64.0, R-5) — exception color: neutral when nominal, amber ≥ warn, red ≥ crit; no green. */
.bar-fill.bar-ok { background: var(--text-3); }
.bar-fill.bar-warning { background: var(--warn); }
.bar-fill.bar-critical { background: var(--crit); }
/* Offsite dashboard gauges (v0.65.0, R-5) — two side-by-side tiles (Restic, PBS DR). */
.offsite-gauges { display: flex; gap: 0.75rem; flex-wrap: wrap; margin-bottom: 1rem; }
.offsite-gauges .offsite-tile { margin-bottom: 0; }
/* Offsite pool dashboard tile (v0.64.0, R-5) — compact, band-colored, links to /offsite. */
.offsite-tile {
display: inline-flex; align-items: baseline; gap: 0.5rem;
padding: 0.4rem 0.8rem; margin-bottom: 1rem;
border: 1px solid var(--line); border-radius: var(--radius);
text-decoration: none; font-size: 0.85rem;
}
.offsite-tile-label { color: var(--text-3); text-transform: uppercase; font-size: 0.72rem; letter-spacing: 0.03em; }
.offsite-tile-val { color: var(--text-1); font-weight: 600; }
.offsite-tile-stale { color: var(--warn); font-size: 0.72rem; }
.offsite-tile-warning { border-color: var(--warn); }
.offsite-tile-warning .offsite-tile-val { color: var(--warn); }
.offsite-tile-critical { border-color: var(--crit); }
.offsite-tile-critical .offsite-tile-val { color: var(--crit); }
/* Container table */
.container-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
}
.container-table th {
text-align: left;
padding: 0.5rem 0.75rem;
color: var(--text-3);
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
border-bottom: 1px solid var(--line);
}
.container-table td {
padding: 0.4rem 0.75rem;
border-bottom: 1px solid rgba(51, 65, 85, 0.5);
}
.container-state {
font-size: 0.8rem;
font-weight: 600;
}
.container-state-running { color: var(--blue-bright); }
.container-state-stopped, .container-state-exited { color: var(--crit); }
.container-state-unhealthy { color: var(--warn); }
/* Health */
.health-status {
font-size: 1.1rem;
font-weight: 700;
text-transform: uppercase;
}
.health-status-ok { color: var(--blue-bright); }
.health-status-warn { color: var(--warn); }
.health-status-fail { color: var(--crit); }
.issue-list, .warning-list {
list-style: none;
padding-left: 0;
}
.issue-list li::before { content: "● "; color: var(--crit); }
.warning-list li::before { content: "● "; color: var(--warn); }
.issue, .warning {
padding: 0.25rem 0;
font-size: 0.9rem;
}
/* History */
.history-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
margin-top: 0.5rem;
}
.history-table th {
text-align: left;
padding: 0.4rem 0.5rem;
color: var(--text-3);
font-size: 0.75rem;
border-bottom: 1px solid var(--line);
}
.history-table td {
padding: 0.3rem 0.5rem;
border-bottom: 1px solid rgba(51, 65, 85, 0.3);
}
details summary {
cursor: pointer;
color: var(--blue-bright);
font-size: 0.9rem;
}
/* Empty state */
.empty-state {
text-align: center;
padding: 4rem 2rem;
color: var(--text-2);
}
.empty-state .hint {
margin-top: 0.5rem;
font-size: 0.85rem;
color: var(--text-3);
}
.empty-state code {
background: var(--bg-2);
padding: 0.15rem 0.4rem;
border-radius: var(--radius);
font-family: var(--font-data);
font-size: 0.85rem;
}
/* Footer */
footer {
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid var(--line);
text-align: center;
font-size: 0.8rem;
color: var(--text-3);
}
footer a {
color: var(--blue-bright);
text-decoration: none;
}
/* Code */
code {
font-family: var(--font-data);
font-size: 0.85em;
color: var(--blue-bright);
}
/* Navigation */
.nav-links {
display: flex;
gap: 1.5rem;
margin-top: 0.5rem;
}
.nav-link {
color: var(--text-2);
text-decoration: none;
font-size: 0.9rem;
padding-bottom: 2px;
border-bottom: 2px solid transparent;
transition: color 0.15s, border-color 0.15s;
}
.nav-link:hover {
color: var(--text-1);
}
.nav-link.active {
color: var(--blue-bright);
border-bottom-color: var(--blue-bright);
font-weight: 600;
}
/* Buttons */
.btn {
display: inline-block;
padding: 0.5rem 1rem;
background: var(--blue-bright);
color: var(--bg-0);
border: none;
border-radius: var(--radius);
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
text-decoration: none;
transition: opacity 0.15s;
}
.btn:hover { opacity: 0.85; }
.btn-outline {
background: transparent;
color: var(--text-2);
border: 1px solid var(--line);
}
.btn-outline:hover {
color: var(--text-1);
border-color: var(--text-2);
}
.btn-danger {
background: var(--crit);
color: #fff;
}
.btn-sm {
padding: 0.3rem 0.6rem;
font-size: 0.8rem;
}
/* Forms */
.config-form .form-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.form-group label {
font-size: 0.8rem;
font-weight: 600;
color: var(--text-2);
text-transform: uppercase;
letter-spacing: 0.03em;
}
.form-group input[type="text"],
.form-group input[type="email"] {
background: var(--bg-0);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 0.5rem 0.75rem;
color: var(--text-1);
font-size: 0.9rem;
font-family: inherit;
}
.form-group input:focus {
outline: none;
border-color: var(--blue-bright);
}
.form-group input[readonly] {
opacity: 0.6;
cursor: not-allowed;
}
.form-hint {
font-size: 0.75rem;
color: var(--text-3);
}
/* Install-command generator (GL-7) */
.gen-controls {
background: var(--bg-0);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 1rem;
margin-bottom: 1rem;
}
.gen-radios {
display: flex;
gap: 1.25rem;
flex-wrap: wrap;
}
.gen-radio, .gen-check {
display: flex;
align-items: center;
gap: 0.4rem;
font-size: 0.85rem;
color: var(--text-1);
text-transform: none;
letter-spacing: normal;
font-weight: 400;
cursor: pointer;
}
.gen-radio input, .gen-check input {
accent-color: var(--blue-bright);
cursor: pointer;
}
.gen-checks {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 0.85rem;
padding-top: 0.85rem;
border-top: 1px solid var(--line-soft);
}
.gen-check code, .gen-radio code { color: var(--blue-bright); font-size: 0.8rem; }
.gen-controls input[type="number"] {
background: var(--bg-1);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 0.5rem 0.75rem;
color: var(--text-1);
font-size: 0.9rem;
font-family: inherit;
}
.gen-controls input[type="number"]:focus,
.gen-controls input[type="text"]:focus { outline: none; border-color: var(--blue-bright); }
.gen-msg {
color: #E5534B;
font-size: 0.82rem;
margin: 0 0 0.75rem;
padding: 0.5rem 0.75rem;
background: rgba(229,83,75,.10);
border: 1px solid rgba(229,83,75,.35);
border-radius: var(--radius);
}
/* Credentials */
.credential-row {
margin-bottom: 0.5rem;
}
.credential-box {
display: flex;
align-items: center;
gap: 0.5rem;
background: var(--bg-0);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 0.5rem 0.75rem;
margin-top: 0.25rem;
overflow-x: auto;
}
.credential-box code {
flex: 1;
font-size: 0.8rem;
word-break: break-all;
color: var(--text-1);
}
.copy-btn {
background: transparent;
border: 1px solid var(--line);
border-radius: var(--radius);
color: var(--text-2);
cursor: pointer;
padding: 0.2rem 0.5rem;
font-size: 0.85rem;
flex-shrink: 0;
transition: color 0.15s, border-color 0.15s;
}
.copy-btn:hover {
color: var(--blue-bright);
border-color: var(--blue-bright);
}
/* Flash messages */
.flash {
padding: 0.75rem 1rem;
border-radius: var(--radius);
margin-bottom: 1rem;
font-size: 0.9rem;
}
.flash-success {
background: var(--blue-dim);
color: var(--blue-bright);
border: 1px solid rgba(74, 222, 128, 0.3);
}
.flash-error {
background: var(--crit-dim);
color: var(--crit);
border: 1px solid rgba(229,83,75,.3);
}
/* v0.67.0 — the deviation tier between success and error: a state that is not a failure but is
also not nominal, and that the operator must act on (post-RESET staleness R-37, enabled-but-
unprovisioned offsite R-36). Amber per the exception-color principle: it appears ONLY on
deviation, never on a healthy page. */
.flash-warn {
background: var(--warn-dim);
color: var(--warn);
border: 1px solid rgba(224,169,62,.3);
}
.flash-warn strong { color: var(--text-1); }
.flash-warn code {
font-family: var(--font-data);
font-size: .85em;
}
/* YAML Preview */
.yaml-preview {
background: var(--bg-0);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 1rem;
max-height: 500px;
overflow: auto;
}
.yaml-preview pre {
font-family: var(--font-data);
font-size: 0.8rem;
color: var(--text-2);
white-space: pre-wrap;
word-break: break-word;
margin: 0;
}
/* Muted text */
.text-muted {
color: var(--text-3);
}
/* Severity badges */
.severity-badge {
display: inline-block;
padding: 0.15em 0.5em;
border-radius: var(--radius);
font-size: 0.8em;
font-weight: 600;
line-height: 1.4;
}
.severity-error {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
}
.severity-warning {
background: rgba(245, 158, 11, 0.15);
color: #f59e0b;
}
.severity-info {
background: rgba(59, 130, 246, 0.15);
color: #3b82f6;
}
.severity-critical {
background: var(--crit-dim);
color: var(--crit);
}
.severity-ok {
background: var(--bg-2);
color: var(--text-2);
}
/* Event filter buttons */
.event-filter {
font-size: 0.8em;
padding: 0.2em 0.6em;
cursor: pointer;
}
.event-filter.active {
background: var(--blue-bright);
color: #fff;
border-color: var(--blue-bright);
}
/* Notification channel badges */
.status-badge-operator {
background: rgba(139, 92, 246, 0.15);
color: #8b5cf6;
}
.status-badge-customer {
background: rgba(59, 130, 246, 0.15);
color: #3b82f6;
}
/* Config diff table */
.diff-changed td { color: #f59e0b; }
.diff-hub_only td { color: #3b82f6; }
.diff-controller_only td { color: #fb923c; }
/* Badge styles */
.badge {
display: inline-block;
padding: 0.15rem 0.5rem;
border-radius: var(--radius);
font-size: 0.8rem;
font-weight: 600;
font-family: var(--font-data);
}
.badge-error {
background: var(--crit-dim);
color: var(--crit);
}
.badge-warn {
background: rgba(250, 204, 21, 0.2);
color: var(--warn);
}
.badge-neutral {
background: var(--bg-2);
color: var(--text-3);
}
/* v0.51.0: badge-ok was referenced by the DR-recipe rows but never defined (fell back to the
bare .badge). Blue = healthy, per the status-badge-ok convention. */
.badge-ok {
background: rgba(0, 131, 216, 0.15);
color: var(--blue-bright);
}
/* Summary cards row */
.summary-cards {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.summary-card {
flex: 1;
min-width: 160px;
background: var(--bg-1);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 1rem 1.25rem;
}
.summary-card .card-number {
font-size: 2rem;
font-weight: 700;
color: var(--text-1);
font-family: var(--font-data);
line-height: 1.1;
}
.summary-card .card-label {
font-size: 0.8rem;
color: var(--text-3);
margin-top: 0.25rem;
}
/* Chart container */
.chart-container {
position: relative;
width: 100%;
height: 280px;
margin: 1rem 0;
}
/* Period selector button group */
.period-selector {
display: flex;
gap: 0.25rem;
margin-bottom: 1rem;
}
.period-btn {
padding: 0.3rem 0.75rem;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--bg-1);
color: var(--text-2);
font-size: 0.85rem;
text-decoration: none;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.period-btn:hover, .period-btn.active {
background: var(--blue-bright);
color: var(--bg-0);
border-color: var(--blue-bright);
}
/* Accuracy dot */
.accuracy-dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-left: 0.25rem;
}
.accuracy-ok { background: var(--blue-bright); }
.accuracy-warn { background: var(--warn); }
.accuracy-danger { background: var(--crit); }
/* Memory color classes */
.mem-ok { color: var(--blue-bright); }
.mem-warn { color: var(--warn); }
.mem-danger { color: var(--crit); }
/* Data table (apps page) */
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
.data-table th {
text-align: left;
padding: 0.6rem 0.75rem;
font-weight: 600;
color: var(--text-2);
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.04em;
border-bottom: 1px solid var(--line);
white-space: nowrap;
}
.data-table td {
padding: 0.6rem 0.75rem;
border-bottom: 1px solid rgba(100,116,139,0.15);
color: var(--text-1);
font-family: var(--font-data);
font-size: 0.875rem;
}
/* Plain links only — :not(.btn) keeps <a class="btn"> buttons on the .btn palette
(blue-bright link text on a blue-bright button background is invisible). */
.data-table td a:not(.btn) {
color: var(--blue-bright);
text-decoration: none;
font-family: var(--font-sans, sans-serif);
}
.data-table td a:not(.btn):hover { text-decoration: underline; }
.data-table th a {
color: var(--text-2);
text-decoration: none;
}
.data-table th a:hover { color: var(--text-1); }
.data-table tr:hover td { background: rgba(96,165,250,0.04); }
.data-table input[type="checkbox"] {
width: 1rem;
height: 1rem;
cursor: pointer;
accent-color: var(--blue-bright);
}
/* Responsive */
@media (max-width: 768px) {
.container { padding: 1rem; }
.dashboard-table { font-size: 0.8rem; }
.dashboard-table th, .dashboard-table td { padding: 0.5rem; }
.info-grid { grid-template-columns: 1fr 1fr; }
.metrics-grid { grid-template-columns: 1fr; }
}
/* Row tint — exception rule: only warn/crit rows carry a wash; ok/pending stay quiet. */
.dashboard-table tbody tr.status-warn { background: var(--warn-dim); }
.dashboard-table tbody tr.status-down { background: var(--crit-dim); }
/* Two-tone brand heading */
header h1 span { color: var(--blue-bright); }
/* Inline icon (Lucide sprite) */
.ico { width: 16px; height: 16px; flex-shrink: 0; vertical-align: -0.18em; }
/* Hairline table rows (v2): soft dividers, header underline stays --line. */
.dashboard-table td, .data-table td { border-bottom: 1px solid var(--line-soft); }
:focus-visible { outline: 2px solid var(--blue-bright); outline-offset: 1px; }
/* ── Customer page tabs (v0.47.0) ──────────────────────────────────────────
Hash-based client-side tabs. Graceful degradation is load-bearing: panels
are hidden ONLY under body.js-tabs (added by JS on DOMContentLoaded) — with
JS off every panel stays visible and the page reads top-to-bottom. */
.summary-strip {
position: sticky;
top: 0;
z-index: 40;
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
padding: 0.5rem 0.75rem;
margin: 0.75rem 0 0;
background: var(--bg-0);
border: 1px solid var(--line);
border-radius: var(--radius);
font-size: 0.85rem;
}
.summary-strip .strip-name { font-weight: 600; color: var(--text-1); }
.summary-strip .strip-item { color: var(--text-2); white-space: nowrap; }
.summary-strip .strip-item code { font-size: 0.85em; }
.tab-nav {
display: flex;
gap: 0.25rem;
margin: 0.75rem 0 1rem;
border-bottom: 1px solid var(--line);
overflow-x: auto;
white-space: nowrap;
}
.tab-nav a {
color: var(--text-2);
text-decoration: none;
font-size: 0.9rem;
padding: 0.45rem 0.75rem;
border-bottom: 2px solid transparent;
flex-shrink: 0;
}
.tab-nav a:hover { color: var(--text-1); }
.tab-nav a.active {
color: var(--blue-bright);
border-bottom-color: var(--blue-bright);
font-weight: 600;
}
.tab-badge {
display: inline-block;
min-width: 1.2em;
margin-left: 0.35em;
padding: 0 0.35em;
background: var(--crit);
color: #fff;
border-radius: var(--radius);
font-size: 0.75em;
font-weight: 600;
text-align: center;
}
body.js-tabs .tab-panel:not(.tab-panel-active) { display: none; }
/* Inline two-step confirm (take-two F-16) — the in-place replacement for native confirm() */
.inline-confirm { display: inline-flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.inline-confirm-q { font-size: .8rem; color: var(--warn); }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation: none !important; transition: none !important; }
}
/* MAIL-HOLD release (internal/mailhold): the button and its one-line explanation sit on one row under the banner. */
.mail-hold-release {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
margin-bottom: 1rem;
}
.mail-hold-release .text-muted { font-size: 0.8rem; }
#mail-hold a { color: var(--text-1); }
</style>
<style>
.c-warn { color: var(--warn); font-weight: 600; }
.c-bad { color: var(--crit); font-weight: 700; }
.sys-sec { margin-bottom: 1.5rem; }
.sys-sec > h3 { margin: 0 0 0.15rem; font-size: 1.05rem; color: var(--text-1); }
.att a, .bx-row a, .bx-more a { color: var(--blue-bright); text-decoration: none; }
.sys-sec > .why { margin: 0 0 0.8rem; color: var(--text-2); font-size: 0.85em; }
.term { text-decoration: underline dotted; cursor: help; }
.sys form { display: inline; }
.rel-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap: 0.75rem; }
.att { list-style: none; margin: 0; padding: 0; }
.att li { padding: 0.45rem 0; border-top: 1px solid var(--line); }
.att li:first-child { border-top: 0; }
.att .rs { display: block; margin: 0.15rem 0 0 1.4rem; font-size: 0.88em; }
.att .rs span { font-weight: normal; }
.mark { display: inline-block; min-width: 3.4rem; font-size: 0.8em; text-transform: uppercase; letter-spacing: 0.03em; }
.mark::before { content: "● "; }
.mark.c-warn::before { content: "▲ "; }
.mark.c-bad::before { content: "✕ "; }
.ok-line::before { content: "● "; color: var(--blue-bright); }
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr)); gap: 0.75rem; }
.wcard { border: 1px solid var(--warn); border-radius: var(--radius); padding: 0.8rem 1rem; }
.wcard h4 { margin: 0 0 0.3rem; font-size: 1.05em; }
.wcard p { margin: 0.25rem 0; font-size: 0.9em; }
.wcard form { margin-top: 0.5rem; }
.testing { margin: 0.8rem 0 0; padding-left: 1.1rem; font-size: 0.88em; }
.boxes { font-size: 0.9em; }
.bx-row { display: grid; grid-template-columns: 1.7fr 0.5fr 0.7fr 1.2fr 1.1fr 1.3fr 1.2fr; gap: 0.6rem; align-items: start;
padding: 0.55rem 0.8rem; }
.bx-head { color: var(--text-2); font-size: 0.85em; border-bottom: 1px solid var(--line); }
details.bx { border-bottom: 1px solid var(--line); }
details.bx > summary { list-style: none; cursor: pointer; color: var(--text-1); font-size: inherit; }
details.bx > summary::-webkit-details-marker { display: none; }
details.bx > summary:hover { background: rgba(127,127,127,0.07); }
details.bx > summary .name::before { content: "▸ "; color: var(--text-2); }
details.bx[open] > summary .name::before { content: "▾ "; }
.bx-row > div { min-width: 0; overflow-wrap: anywhere; }
.bx-row .sub { display: block; color: var(--text-2); font-size: 0.88em; font-weight: normal; }
.bx-more { padding: 0.4rem 0.8rem 1rem 1.8rem; display: grid; grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); gap: 0.4rem 1.5rem; }
.bx-more h5 { margin: 0.6rem 0 0.3rem; font-size: 0.85em; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-2); }
.kv { display: grid; grid-template-columns: max-content 1fr; gap: 0.15rem 0.8rem; margin: 0; font-size: 0.92em; }
.kv dt { color: var(--text-2); }
.kv dd { margin: 0; overflow-wrap: anywhere; }
.kv form { display: inline; margin-left: 0.3rem; }
details.more > summary { cursor: pointer; font-weight: 600; font-size: 1.05rem; }
details.more h3 { margin-top: 1.3rem; }
.tbl-wrap { overflow-x: auto; }
.sys td, .sys th { font-size: 0.85em; vertical-align: top; }
@media (max-width: 760px) {
.nav-links { flex-wrap: wrap; gap: 0.3rem 1rem; }
.bx-head { display: none; }
.bx-row { grid-template-columns: 1fr 1fr; }
.bx-row > div:first-child { grid-column: 1 / -1; }
.bx-row > div[data-label]::before { content: attr(data-label); display: block; color: var(--text-2); font-size: 0.78em; font-weight: normal; }
.bx-more { padding-left: 0.8rem; }
}
</style>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <path d="M12 17h.01" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /> <rect width="20" height="8" x="2" y="14" rx="2" ry="2" /> <line x1="6" x2="6.01" y1="6" y2="6" /> <line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /> <circle cx="12" cy="12" r="3" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /> <path d="m6 6 12 12" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <path d="M12 8h.01" /></symbol>
<symbol id="i-hard-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 16h.01" /> <path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M21.946 12.013H2.054" /> <path d="M6 16h.01" /></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v2" /> <path d="M12 2v2" /> <path d="M17 20v2" /> <path d="M17 2v2" /> <path d="M2 12h2" /> <path d="M2 17h2" /> <path d="M2 7h2" /> <path d="M20 12h2" /> <path d="M20 17h2" /> <path d="M20 7h2" /> <path d="M7 20v2" /> <path d="M7 2v2" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <rect x="8" y="8" width="8" height="8" rx="1" /></symbol>
<symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /> <path d="M12 6v6l4 2" /></symbol>
<symbol id="i-boxes" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z" /> <path d="m7 16.5-4.74-2.85" /> <path d="m7 16.5 5-3" /> <path d="M7 16.5v5.17" /> <path d="M12 13.5V19l3.97 2.38a2 2 0 0 0 2.06 0l3-1.8a2 2 0 0 0 .97-1.71v-3.24a2 2 0 0 0-.97-1.71L17 10.5l-5 3Z" /> <path d="m17 16.5-5-3" /> <path d="m17 16.5 4.74-2.85" /> <path d="M17 16.5v5.17" /> <path d="M7.97 4.42A2 2 0 0 0 7 6.13v4.37l5 3 5-3V6.13a2 2 0 0 0-.97-1.71l-3-1.8a2 2 0 0 0-2.06 0l-3 1.8Z" /> <path d="M12 8 7.26 5.15" /> <path d="m12 8 4.74-2.85" /> <path d="M12 13.5V8" /></symbol>
<symbol id="i-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /> <path d="M16 3.128a4 4 0 0 1 0 7.744" /> <path d="M22 21v-2a4 4 0 0 0-3-3.87" /> <circle cx="9" cy="7" r="4" /></symbol>
</svg>
<script>
function felhomConfirm(el,question,onYes){
if(!el||el.dataset.fcOpen)return;
el.dataset.fcOpen='1';
var wrap=document.createElement('span');wrap.className='inline-confirm';
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-sm btn-danger';yes.textContent='Igen';
var no=document.createElement('button');no.type='button';no.className='btn btn-sm btn-outline';no.textContent='Mégse';
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
no.addEventListener('click',close);
yes.addEventListener('click',function(){close();onYes();});
}
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
if(!btn)return;
e.preventDefault();
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
var form=btn.closest('form');
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
});
});
</script>
<div class="container">
<header>
<h1>Felhom <span>Hub</span></h1>
<nav class="nav-links">
<a href="/" class="nav-link">Dashboard</a>
<a href="/configs" class="nav-link">Customers</a>
<a href="/apps" class="nav-link">Apps</a>
<a href="/hosts" class="nav-link">Hosts</a>
<a href="/system" class="nav-link active">System</a>
<a href="/offsite" class="nav-link">Offsite</a>
<a href="/configuration" class="nav-link">Configuration</a>
</nav>
</header>
<h2 style="margin-bottom: 1rem;">System — versions and OS updates</h2>
<section class="card sys-sec" id="attention">
<h3>Needs attention</h3>
<p class="why">One line per box that is amber or red, and why. Amber: worth a look. Red: an operator alarm fires (`08` §6.3).</p>
<ul class="att">
<li><span class="mark c-warn" title="worth a look">look</span>
<a href="/hosts/tester1-0f1e2d"><strong>tester1-0f1e2d</strong></a> <span class="text-muted">Tester 1</span>
<span class="rs"><span class="c-warn" title="1 minor release behind — sign an agent_update for this box">agent behind: 0.155.0 → 0.156.0 (since 2026-10-08)</span></span></li>
<li><span class="mark c-bad" title="an operator alarm fires for this box">alarm</span>
<a href="/hosts/tester2-9a8b7c"><strong>tester2-9a8b7c</strong></a> <span class="text-muted">Tester 2</span>
<span class="rs"><span class="c-bad" title="15 minor releases behind — sign an agent_update for this box">agent behind: 0.141.0 → 0.156.0 (since 2026-10-01)</span> · <span class="c-warn">no versions reported (agent older than v0.142.0)</span> · <span class="c-warn">crash guard not installed</span></span></li>
</ul>
</section>
<section class="card sys-sec" id="waiting">
<h3>Waiting for you</h3>
<p class="why">Update sets that only you approve: the kernel, the Docker engine and the Proxmox packages. Guest and host Debian updates approve themselves after 24 h and one night on the <span class="term" title="Ring 0 = the demo boxes. They install every new fix first; the other boxes (ring 1) install only what is approved.">ring-0</span> boxes.</p>
<p class="ok-line" style="margin: 0;">Nothing waits for your approval.</p>
<ul class="testing">
<li><strong>Docker engine 29.8.3-1</strong> — still being tested: <span class="text-muted">demo-hp-d4e5f6 has 1 of 2 healthy night Docker step(s) with this set</span></li>
</ul>
</section>
<section class="card sys-sec boxes" id="boxes" style="padding-left: 0; padding-right: 0;">
<h3 style="padding: 0 1rem;">Boxes</h3>
<p class="why" style="padding: 0 1rem;">Click a box to see every value it reports, and its ring and update switches. "unknown": the box could not read the value — never a guess.</p>
<div class="bx-row bx-head"><div>Box</div><div>Ring</div><div>Health</div><div>Controller · agent</div><div>OS updates</div><div>Kernel</div><div title="The newest OS update run">Last night</div></div>
<details class="bx" id="box-demo-felhom-a1b2c3">
<summary class="bx-row">
<div class="name"><a href="/hosts/demo-felhom-a1b2c3">demo-felhom-a1b2c3</a><span class="sub">Demo N100</span></div>
<div data-label="Ring"><span class="term" title="Ring 0: a demo box — it installs every new fix first">0</span></div>
<div data-label="Health"><span class="mark" title="nothing amber or red">fine</span></div>
<div data-label="Controller · agent"><span>0.232.0</span><span class="sub" title="current (vouched 0.156.0)">agent 0.156.0</span></div>
<div data-label="OS updates"><span title="runs the newest approved guest and host releases">on · up to date</span></div>
<div data-label="Kernel"><span>7.0.14-23-pve</span></div>
<div data-label="Last night"><span title="12 h ago · applied · 41 s — last successful leg: 12 h ago">ok · 12 h ago</span></div>
</summary>
<div class="bx-more sys">
<div>
<h5>Ring and updates</h5>
<dl class="kv">
<dt>Ring</dt><dd>ring 0
<form method="POST" action="/os/ring/demo-felhom-a1b2c3">
<input type="hidden" name="_csrf" value="csrf-fixture-token"><input type="hidden" name="return" value="/system">
<input type="hidden" name="ring" value="1"><button type="submit" class="btn btn-sm btn-outline" data-confirm="Make demo-felhom-a1b2c3 a normal (ring 1) box? It then installs only approved releases.">→ normal</button>
</form></dd>
<dt>OS updates</dt><dd>updates <strong>ON</strong>
<form method="POST" action="/os/enabled/demo-felhom-a1b2c3">
<input type="hidden" name="_csrf" value="csrf-fixture-token"><input type="hidden" name="return" value="/system">
<input type="hidden" name="on" value="0"><button type="submit" class="btn btn-sm btn-outline" data-confirm="Switch OS updates OFF for demo-felhom-a1b2c3? It keeps reporting and installs nothing.">switch off</button>
</form></dd>
<dt>Tunnel</dt><dd>running</dd>
<dt>Controller</dt><dd>0.232.0</dd>
<dt>Agent</dt><dd title="current (vouched 0.156.0)">0.156.0</dd>
<dt>Last OS leg</dt><dd title="last successful leg: 12 h ago">12 h ago · applied · 41 s</dd>
</dl>
<h5>Docker engine</h5>
<dl class="kv">
<dt>Docker</dt><dd>29.8.2</dd>
<dt>containerd</dt><dd>2.3.6-1~debian.13~trixie</dd>
<dt>live-restore</dt><dd>on</dd>
<dt>Docker release</dt><dd>os-docker-20261001-031500</dd>
</dl>
</div>
<div>
<h5>Host</h5>
<dl class="kv">
<dt>Proxmox</dt><dd>9.0.11</dd>
<dt>Kernel (running)</dt><dd>7.0.14-23-pve</dd>
<dt>Kernel (next boot)</dt><dd>7.0.14-23-pve</dd>
<dt title="The kernel GRUB boots normally (R-836). Amber: a one-shot flag names another kernel for the next boot only.">Kernel (default)</dt><dd>7.0.14-23-pve</dd>
<dt title="The kernel lane (R-836): the newest step, the kernel the box is due, and whether its household was told for tonight (no mail, no step).">Kernel step</dt><dd>7.0.14-23-pve applied 30 h ago</dd>
<dt>Debian</dt><dd>13.7</dd>
<dt>Felhom release</dt><dd>os-host-20261009-031500</dd>
<dt>Pending</dt><dd>0</dd>
<dt>Not covered</dt><dd>0</dd>
<dt>Held</dt><dd>none</dd>
<dt>Reboot needed</dt><dd>no</dd>
<dt>kernel.panic</dt><dd>10 s</dd>
<dt>Oops</dt><dd>no</dd>
<dt>Crash restarts 24 h</dt><dd>0</dd>
<dt>Crash guard</dt><dd>armed</dd>
<dt title="The root-owned config bundle: sudoers, wrappers, units (R-840)">Root files</dt><dd>0.155.0</dd>
</dl>
</div>
<div>
<h5>Guest</h5>
<dl class="kv">
<dt>Guest Debian</dt><dd>13.7</dd>
<dt>Felhom release</dt><dd>os-guest-20261009-031500</dd>
<dt>Pending</dt><dd>0</dd>
<dt>Restart needed</dt><dd>0</dd>
<dt title="The newest pct fstrim of the guest's disks, and what it freed (R-444). The boxes trim weekly.">Last disk trim</dt><dd title="schedule: weekly">2 days ago · 3.0 GiB</dd>
</dl>
</div>
</div>
</details>
<details class="bx" id="box-demo-hp-d4e5f6">
<summary class="bx-row">
<div class="name"><a href="/hosts/demo-hp-d4e5f6">demo-hp-d4e5f6</a><span class="sub">Demo HP</span></div>
<div data-label="Ring"><span class="term" title="Ring 0: a demo box — it installs every new fix first">0</span></div>
<div data-label="Health"><span class="mark" title="nothing amber or red">fine</span></div>
<div data-label="Controller · agent"><span>0.232.0</span><span class="sub" title="current (vouched 0.156.0)">agent 0.156.0</span></div>
<div data-label="OS updates"><span title="runs the newest approved guest and host releases">on · up to date</span></div>
<div data-label="Kernel"><span>7.0.14-23-pve</span></div>
<div data-label="Last night"><span title="12 h ago · applied · 41 s — last successful leg: 12 h ago">ok · 12 h ago</span></div>
</summary>
<div class="bx-more sys">
<div>
<h5>Ring and updates</h5>
<dl class="kv">
<dt>Ring</dt><dd>ring 0
<form method="POST" action="/os/ring/demo-hp-d4e5f6">
<input type="hidden" name="_csrf" value="csrf-fixture-token"><input type="hidden" name="return" value="/system">
<input type="hidden" name="ring" value="1"><button type="submit" class="btn btn-sm btn-outline" data-confirm="Make demo-hp-d4e5f6 a normal (ring 1) box? It then installs only approved releases.">→ normal</button>
</form></dd>
<dt>OS updates</dt><dd>updates <strong>ON</strong>
<form method="POST" action="/os/enabled/demo-hp-d4e5f6">
<input type="hidden" name="_csrf" value="csrf-fixture-token"><input type="hidden" name="return" value="/system">
<input type="hidden" name="on" value="0"><button type="submit" class="btn btn-sm btn-outline" data-confirm="Switch OS updates OFF for demo-hp-d4e5f6? It keeps reporting and installs nothing.">switch off</button>
</form></dd>
<dt>Tunnel</dt><dd>running</dd>
<dt>Controller</dt><dd>0.232.0</dd>
<dt>Agent</dt><dd title="current (vouched 0.156.0)">0.156.0</dd>
<dt>Last OS leg</dt><dd title="last successful leg: 12 h ago">12 h ago · applied · 41 s</dd>
</dl>
<h5>Docker engine</h5>
<dl class="kv">
<dt>Docker</dt><dd>29.8.2</dd>
<dt>containerd</dt><dd>2.3.6-1~debian.13~trixie</dd>
<dt>live-restore</dt><dd>on</dd>
<dt>Docker release</dt><dd>os-docker-20261001-031500</dd>
</dl>
</div>
<div>
<h5>Host</h5>
<dl class="kv">
<dt>Proxmox</dt><dd>9.0.11</dd>
<dt>Kernel (running)</dt><dd>7.0.14-23-pve</dd>
<dt>Kernel (next boot)</dt><dd>7.0.14-23-pve</dd>
<dt title="The kernel GRUB boots normally (R-836). Amber: a one-shot flag names another kernel for the next boot only.">Kernel (default)</dt><dd>7.0.14-23-pve</dd>
<dt title="The kernel lane (R-836): the newest step, the kernel the box is due, and whether its household was told for tonight (no mail, no step).">Kernel step</dt><dd>7.0.14-23-pve applied 30 h ago</dd>
<dt>Debian</dt><dd>13.7</dd>
<dt>Felhom release</dt><dd>os-host-20261009-031500</dd>
<dt>Pending</dt><dd>0</dd>
<dt>Not covered</dt><dd>0</dd>
<dt>Held</dt><dd>none</dd>
<dt>Reboot needed</dt><dd>no</dd>
<dt>kernel.panic</dt><dd>10 s</dd>
<dt>Oops</dt><dd>no</dd>
<dt>Crash restarts 24 h</dt><dd>0</dd>
<dt>Crash guard</dt><dd>armed</dd>
<dt title="The root-owned config bundle: sudoers, wrappers, units (R-840)">Root files</dt><dd>0.155.0</dd>
</dl>
</div>
<div>
<h5>Guest</h5>
<dl class="kv">
<dt>Guest Debian</dt><dd>13.7</dd>
<dt>Felhom release</dt><dd>os-guest-20261009-031500</dd>
<dt>Pending</dt><dd>0</dd>
<dt>Restart needed</dt><dd>0</dd>
<dt title="The newest pct fstrim of the guest's disks, and what it freed (R-444). The boxes trim weekly.">Last disk trim</dt><dd title="schedule: weekly">3 days ago · 3.0 GiB</dd>
</dl>
</div>
</div>
</details>
<details class="bx" id="box-tester1-0f1e2d">
<summary class="bx-row">
<div class="name"><a href="/hosts/tester1-0f1e2d">tester1-0f1e2d</a><span class="sub">Tester 1</span></div>
<div data-label="Ring"><span class="term" title="Ring 1: a normal box — it installs only approved releases">1</span></div>
<div data-label="Health"><span class="mark c-warn" title="worth a look">look</span></div>
<div data-label="Controller · agent"><span>0.231.0</span><span class="sub c-warn" title="1 minor release behind — sign an agent_update for this box">agent 0.155.0 → 0.156.0 (since 2026-10-08)</span></div>
<div data-label="OS updates"><span title="not on the newest approved release yet — a box takes it at its next night run">on · guest behind</span></div>
<div data-label="Kernel"><span>7.0.14-20-pve</span></div>
<div data-label="Last night"><span title="12 h ago · nothing · 41 s — last successful leg: 36 h ago">ok · 12 h ago</span></div>
</summary>
<div class="bx-more sys">
<div>
<h5>Ring and updates</h5>
<dl class="kv">
<dt>Ring</dt><dd>ring 1
<form method="POST" action="/os/ring/tester1-0f1e2d">
<input type="hidden" name="_csrf" value="csrf-fixture-token"><input type="hidden" name="return" value="/system">
<input type="hidden" name="ring" value="0"><button type="submit" class="btn btn-sm btn-outline" data-confirm="Make tester1-0f1e2d a DEMO (ring 0) box? It then installs every new fix first and takes unsigned Docker steps if its root-owned ring-0 mark allows.">→ demo</button>
</form></dd>
<dt>OS updates</dt><dd>updates <strong>ON</strong>
<form method="POST" action="/os/enabled/tester1-0f1e2d">
<input type="hidden" name="_csrf" value="csrf-fixture-token"><input type="hidden" name="return" value="/system">
<input type="hidden" name="on" value="0"><button type="submit" class="btn btn-sm btn-outline" data-confirm="Switch OS updates OFF for tester1-0f1e2d? It keeps reporting and installs nothing.">switch off</button>
</form></dd>
<dt>Tunnel</dt><dd>running</dd>
<dt>Controller</dt><dd>0.231.0</dd>
<dt>Agent</dt><dd class="c-warn" title="1 minor release behind — sign an agent_update for this box">0.155.0 → 0.156.0 (since 2026-10-08)</dd>
<dt>Last OS leg</dt><dd title="last successful leg: 36 h ago">12 h ago · nothing · 41 s</dd>
</dl>
<h5>Docker engine</h5>
<dl class="kv">
<dt>Docker</dt><dd>29.8.2</dd>
<dt>containerd</dt><dd>2.3.6-1~debian.13~trixie</dd>
<dt>live-restore</dt><dd>on</dd>
<dt>Docker release</dt><dd>—</dd>
</dl>
</div>
<div>
<h5>Host</h5>
<dl class="kv">
<dt>Proxmox</dt><dd>9.0.11</dd>
<dt>Kernel (running)</dt><dd>7.0.14-20-pve</dd>
<dt>Kernel (next boot)</dt><dd>7.0.14-20-pve</dd>
<dt title="The kernel GRUB boots normally (R-836). Amber: a one-shot flag names another kernel for the next boot only.">Kernel (default)</dt><dd>7.0.14-20-pve</dd>
<dt title="The kernel lane (R-836): the newest step, the kernel the box is due, and whether its household was told for tonight (no mail, no step).">Kernel step</dt><dd>due 7.0.14-23-pve — not told yet (mails 09–20 h)</dd>
<dt>Debian</dt><dd>13.7</dd>
<dt>Felhom release</dt><dd>os-host-20261009-031500</dd>
<dt>Pending</dt><dd>0</dd>
<dt>Not covered</dt><dd>0</dd>
<dt>Held</dt><dd>none</dd>
<dt>Reboot needed</dt><dd>no</dd>
<dt>kernel.panic</dt><dd>10 s</dd>
<dt>Oops</dt><dd>no</dd>
<dt>Crash restarts 24 h</dt><dd>0</dd>
<dt>Crash guard</dt><dd>armed</dd>
<dt title="The root-owned config bundle: sudoers, wrappers, units (R-840)">Root files</dt><dd>0.155.0</dd>
</dl>
</div>
<div>
<h5>Guest</h5>
<dl class="kv">
<dt>Guest Debian</dt><dd>13.7</dd>
<dt>Felhom release</dt><dd>os-guest-20261008-031500</dd>
<dt>Pending</dt><dd>0</dd>
<dt>Restart needed</dt><dd>0</dd>
<dt title="The newest pct fstrim of the guest's disks, and what it freed (R-444). The boxes trim weekly.">Last disk trim</dt><dd title="schedule: weekly">4 days ago · 3.0 GiB</dd>
</dl>
</div>
</div>
</details>
<details class="bx" id="box-tester2-9a8b7c">
<summary class="bx-row">
<div class="name"><a href="/hosts/tester2-9a8b7c">tester2-9a8b7c</a><span class="sub">Tester 2</span></div>
<div data-label="Ring"><span class="term" title="Ring 1: a normal box — it installs only approved releases">1</span></div>
<div data-label="Health"><span class="mark c-bad" title="an operator alarm fires for this box">alarm</span></div>
<div data-label="Controller · agent"><span>0.229.0</span><span class="sub c-bad" title="15 minor releases behind — sign an agent_update for this box">agent 0.141.0 → 0.156.0 (since 2026-10-01)</span></div>
<div data-label="OS updates"><span title="runs the newest approved guest and host releases">on · up to date</span></div>
<div data-label="Kernel"><span class="c-warn">unknown</span></div>
<div data-label="Last night"><span title="12 h ago · nothing · 41 s — last successful leg: 12 h ago">ok · 12 h ago</span></div>
</summary>
<div class="bx-more sys">
<div>
<p class="c-warn" style="font-weight: normal; margin: 0.6rem 0 0;">no versions reported (agent older than v0.142.0)</p>
<h5>Ring and updates</h5>
<dl class="kv">
<dt>Ring</dt><dd>ring 1
<form method="POST" action="/os/ring/tester2-9a8b7c">
<input type="hidden" name="_csrf" value="csrf-fixture-token"><input type="hidden" name="return" value="/system">
<input type="hidden" name="ring" value="0"><button type="submit" class="btn btn-sm btn-outline" data-confirm="Make tester2-9a8b7c a DEMO (ring 0) box? It then installs every new fix first and takes unsigned Docker steps if its root-owned ring-0 mark allows.">→ demo</button>
</form></dd>
<dt>OS updates</dt><dd>updates <strong>ON</strong>
<form method="POST" action="/os/enabled/tester2-9a8b7c">
<input type="hidden" name="_csrf" value="csrf-fixture-token"><input type="hidden" name="return" value="/system">
<input type="hidden" name="on" value="0"><button type="submit" class="btn btn-sm btn-outline" data-confirm="Switch OS updates OFF for tester2-9a8b7c? It keeps reporting and installs nothing.">switch off</button>
</form></dd>
<dt>Tunnel</dt><dd>running</dd>
<dt>Controller</dt><dd>0.229.0</dd>
<dt>Agent</dt><dd class="c-bad" title="15 minor releases behind — sign an agent_update for this box">0.141.0 → 0.156.0 (since 2026-10-01)</dd>
<dt>Last OS leg</dt><dd title="last successful leg: 12 h ago">12 h ago · nothing · 41 s</dd>
</dl>
<h5>Docker engine</h5>
<dl class="kv">
<dt>Docker</dt><dd class="c-warn" title="the box could not read it (agent older than v0.142.0, or the guest is down)">unknown</dd>
<dt>containerd</dt><dd class="c-warn" title="the box could not read it (agent older than v0.142.0, or the guest is down)">unknown</dd>
<dt>live-restore</dt><dd class="c-warn" title="the box could not read it (agent older than v0.142.0, or the guest is down)">unknown</dd>
<dt>Docker release</dt><dd>—</dd>
</dl>
</div>
<div>
<h5>Host</h5>
<dl class="kv">
<dt>Proxmox</dt><dd class="c-warn" title="the box could not read it (agent older than v0.142.0, or the guest is down)">unknown</dd>
<dt>Kernel (running)</dt><dd class="c-warn" title="the box could not read it (agent older than v0.142.0, or the guest is down)">unknown</dd>
<dt>Kernel (next boot)</dt><dd class="c-warn" title="the box could not read it (agent older than v0.142.0, or the guest is down)">unknown</dd>
<dt title="The kernel GRUB boots normally (R-836). Amber: a one-shot flag names another kernel for the next boot only.">Kernel (default)</dt><dd class="c-warn" title="the box could not read it (agent older than v0.142.0, or the guest is down)">unknown</dd>
<dt title="The kernel lane (R-836): the newest step, the kernel the box is due, and whether its household was told for tonight (no mail, no step).">Kernel step</dt><dd title="the box reports no kernel lane (agent older than v0.152.0)">—</dd>
<dt>Debian</dt><dd class="c-warn" title="the box could not read it (agent older than v0.142.0, or the guest is down)">unknown</dd>
<dt>Felhom release</dt><dd>os-host-20261009-031500</dd>
<dt>Pending</dt><dd>0</dd>
<dt>Not covered</dt><dd>0</dd>
<dt>Held</dt><dd class="c-warn" title="the box could not read it (agent older than v0.142.0, or the guest is down)">unknown</dd>
<dt>Reboot needed</dt><dd>no</dd>
<dt>kernel.panic</dt><dd class="c-warn" title="the box could not read it (agent older than v0.142.0, or the guest is down)">unknown</dd>
<dt>Oops</dt><dd>no</dd>
<dt>Crash restarts 24 h</dt><dd class="c-warn" title="the box could not read it (agent older than v0.142.0, or the guest is down)">unknown</dd>
<dt>Crash guard</dt><dd class="c-warn">not installed</dd>
<dt title="The root-owned config bundle: sudoers, wrappers, units (R-840)">Root files</dt><dd class="c-warn" title="the box could not read it (agent older than v0.142.0, or the guest is down)">unknown</dd>
</dl>
</div>
<div>
<h5>Guest</h5>
<dl class="kv">
<dt>Guest Debian</dt><dd class="c-warn" title="the box could not read it (agent older than v0.142.0, or the guest is down)">unknown</dd>
<dt>Felhom release</dt><dd>os-guest-20261009-031500</dd>
<dt>Pending</dt><dd>0</dd>
<dt>Restart needed</dt><dd>0</dd>
<dt title="The newest pct fstrim of the guest's disks, and what it freed (R-444). The boxes trim weekly.">Last disk trim</dt><dd title="the agent reports no disk trim (an agent without the weekly trim)">—</dd>
</dl>
</div>
</div>
</details>
</section>
<details class="card more" id="details">
<summary>Details</summary>
<p class="why text-muted" style="font-size: 0.85em;">Release ids, cancelled approvals, what ring 0 runs, the version floors, and every box's crash guard and root files.</p>
<h3>Approved releases</h3>
<p class="text-muted" style="font-size: 0.85em; margin-top: 0;">The newest approved set of each layer. A <span class="term" title="Approved while a TEST wait override was active. It is cancelled when the hub starts without the override (`11` §5.3.1).">TEST approval</span> is amber.</p>
<div class="rel-grid">
<div><strong>guest</strong>: <code>os-guest-20261009-031500</code><br>
<span class="text-muted">214 packages · 2026-10-09 09:17 UTC · by auto</span>
</div>
<div><strong>host</strong>: <code>os-host-20261009-031500</code><br>
<span class="text-muted">389 packages · 2026-10-09 09:17 UTC · by auto</span>
</div>
<div><strong>docker</strong>: <code>os-docker-20261001-031500</code><br>
<span class="text-muted">4 packages · 2026-10-09 09:17 UTC · by auto</span>
</div>
<div><strong>pve</strong>: <code>os-pve-20261007-090000</code><br>
<span class="text-muted">31 packages · 2026-10-09 09:17 UTC · by auto</span>
</div>
<div><strong>kernel</strong>: <code>os-kernel-20261010-091200</code><br>
<span class="text-muted">2 packages · 2026-10-09 09:17 UTC · by operator</span>
</div>
</div>
<form method="POST" action="/os/approve-now" style="margin-top: 0.8rem;">
<input type="hidden" name="_csrf" value="csrf-fixture-token"><input type="hidden" name="return" value="/system">
<button type="submit" class="btn btn-sm btn-danger" data-confirm="Approve the guest and host sets now, without the usual 24 h and one night? Every ring-1 box installs them at its next night run.">Approve now (guest + host)</button>
<span class="text-muted" style="font-size: 0.85em;">An urgent fix only — normally the hub approves after 24 h and one night.</span>
</form>
<h3>Cancelled approvals (last 7 days)</h3>
<div class="rel-grid">
<div><strong>guest</strong>: <code>os-guest-20261006-100000</code><br>
<span class="c-warn">cancelled 2026-10-07 08:00:00 UTC — a TEST approval</span><br>
<span class="text-muted">no further box installs it; boxes that installed it keep it</span></div>
<div><strong>host</strong>: <code>os-host-20261006-110000</code><br>
<span class="c-warn">cancelled 2026-10-07 08:00:00 UTC — a TEST approval</span><br>
<span class="text-muted">no further box installs it; boxes that installed it keep it</span></div>
<div><strong>docker</strong>: <code>os-docker-20261006-120000</code><br>
<span class="c-warn">cancelled 2026-10-07 08:00:00 UTC — a TEST approval</span><br>
<span class="text-muted">no further box installs it; boxes that installed it keep it</span></div>
<div><strong>pve</strong>: <code>os-pve-20261006-130000</code><br>
<span class="c-warn">cancelled 2026-10-07 08:00:00 UTC — a TEST approval</span><br>
<span class="text-muted">no further box installs it; boxes that installed it keep it</span></div>
</div>
<h3>What ring 0 runs now</h3>
<div class="rel-grid">
<div><strong>guest</strong>:
214 packages, first seen 2026-10-09 03:17 UTC<br>
<span class="text-muted">approved as os-guest-20261009-031500</span>
</div>
<div><strong>host</strong>:
389 packages, first seen 2026-10-09 03:17 UTC<br>
<span class="text-muted">approved as os-host-20261009-031500</span>
</div>
<div><strong>docker</strong>:
4 packages, first seen 2026-10-09 19:17 UTC<br>
<span class="text-muted">demo-hp-d4e5f6 has 1 of 2 healthy night Docker step(s) with this set</span>
</div>
<div><strong>pve</strong>:
31 packages, first seen 2026-10-06 15:17 UTC<br>
<span class="text-muted">approved as os-pve-20261007-090000</span>
</div>
<div><strong>kernel</strong>:
2 packages, first seen 2026-10-09 13:17 UTC<br>
<span class="text-muted">approved as os-kernel-20261010-091200</span>
</div>
</div>
<h3 id="version-floors">Version floors</h3>
<p>Global controller floor: <strong>0.229.0</strong> · vouched agent: <strong>0.156.0</strong></p>
<p class="text-muted" style="font-size: 0.85em; margin-top: -0.5rem;"><span class="term" title="A floor is the lowest controller version a box must run; the hub moves a box below it up.">What is a floor?</span> · <span class="term" title="The agent version the operator checked and signed off for the fleet. Agents update only by a per-box signed job.">What is a vouched agent?</span></p>
<div class="tbl-wrap">
<table class="data-table">
<thead><tr><th>Customer</th><th>Own floor</th><th>Set</th><th>Global floor moves it?</th></tr></thead>
<tbody>
<tr>
<td><a href="/customers/c-tester1">c-tester1</a><br><span class="text-muted">Tester 1</span></td>
<td>0.231.0</td>
<td class="" title="">5 days ago (2026-10-05)</td>
<td>no — its own floor applies (at or above the global)</td>
</tr>
</tbody>
</table>
</div>
<h3>Crash guard and root files</h3>
<div class="tbl-wrap">
<table class="data-table sys">
<thead><tr><th>Box</th><th>kernel.panic</th><th>Oops</th><th>Crash restarts 24 h</th><th>Crash guard</th><th title="The root-owned config bundle: sudoers, wrappers, units (R-840)">Root files</th></tr></thead>
<tbody>
<tr><td><a href="/hosts/demo-felhom-a1b2c3">demo-felhom-a1b2c3</a></td><td>10 s</td><td>no</td><td>0</td><td>armed</td><td>0.155.0</td></tr>
<tr><td><a href="/hosts/demo-hp-d4e5f6">demo-hp-d4e5f6</a></td><td>10 s</td><td>no</td><td>0</td><td>armed</td><td>0.155.0</td></tr>
<tr><td><a href="/hosts/tester1-0f1e2d">tester1-0f1e2d</a></td><td>10 s</td><td>no</td><td>0</td><td>armed</td><td>0.155.0</td></tr>
<tr><td><a href="/hosts/tester2-9a8b7c">tester2-9a8b7c</a></td><td class="c-warn" title="the box could not read it (agent older than v0.142.0, or the guest is down)">unknown</td><td>no</td><td class="c-warn" title="the box could not read it (agent older than v0.142.0, or the guest is down)">unknown</td><td class="c-warn">not installed</td><td class="c-warn" title="the box could not read it (agent older than v0.142.0, or the guest is down)">unknown</td></tr>
</tbody>
</table>
</div>
</details>
<footer style="margin-top: 2rem; color: var(--text-muted); font-size: 0.8rem; text-align: center;">
Felhom Hub <span style="font-family: var(--font-mono)">test</span>
</footer>
</div>
<script>
(function(){var h=location.hash&&document.getElementById(location.hash.slice(1));if(!h)return;var d=h.closest('details');while(d){d.open=true;d=d.parentElement&&d.parentElement.closest('details');}h.scrollIntoView();})();
</script>
</body>
</html>