Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@ has a matching GitHub release whose body comes from its section below.
`berth config check` refuses a slice name no unit can take. An entry with no slice starts its scope exactly as
before, on every platform.

### Changed

- The harbor's About tab is rewritten for v0.4. It leads with the node's version and links to the docs,
changelog, issues and source, adds a section on machines with several cards, and drops the list of planned
features that had shipped or changed. Its sections are cards in two columns on a wide screen, with the same
section rail as the other tabs.
- The harbor header no longer clips the ADVANCED switch or cuts its tagline off: below 1600 px the tagline
and the "& REQUESTS" half of the engines tab are hidden. On a phone an alert's message wraps instead of
ending in an ellipsis, with its time on its own line.

### Fixed

- A harbor link to a section, such as `#sec-library` from the engines tab's rail, opened the card tab on a
reload. It now opens the tab that holds the section and scrolls to it.

## [v0.4.0] - 2026-10-29

One berthd now governs every card on a machine. `berth run --gpu gpu1` puts an engine on one card,
Expand Down
6 changes: 4 additions & 2 deletions docs/harbor.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,10 @@ question an operator actually asks.
recent requests and context residency** answer the questions behind the request
log, and the storage panel (advanced) says what the harbor's own store keeps.

**The ABOUT tab** is this page's own documentation: the modes, the shortcut keys,
and what every state word and figure means.
**The ABOUT tab** is this page's own documentation: the node's version, links to these docs and the
changelog, several cards, the modes, the shortcut keys, and what every state word and figure means. A
section rail jumps between its sections, as on the other tabs. A section link, from any tab's rail,
opens the tab that holds the section when you share or reload it.

## Telemetry (built in, no other tool needed)
- **GPU:** memory, utilisation, temperature, power against its limit, fan, SM and memory
Expand Down
79 changes: 64 additions & 15 deletions internal/harbor/web/harbor.css
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,10 @@ body.mode-basic .adv-only { display: none !important; }
.mode-btn + .mode-btn { border-left: 1px solid var(--hair-2); }
.mode-btn[aria-pressed="true"] { color: var(--cyan); background: var(--cyan-wash); }
.mode-btn:hover { color: var(--ink); }
.tabs { display: flex; gap: 4px; margin-left: auto; }
.tabs { display: flex; gap: 4px; margin-left: auto; flex: none; }
/* the mode switch and the tab words never shrink: the descriptor gives way first */
.app-header .mode { flex: none; }
.tab-label { white-space: nowrap; }
.tab {
appearance: none;
background: transparent;
Expand Down Expand Up @@ -339,7 +342,7 @@ body.mode-basic .adv-only { display: none !important; }
padding: 0 6px; line-height: 1.6;
}
.tab[aria-current="page"] .count { color: var(--amber); border-color: var(--amber-dim); }
.header-status { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; }
.header-status { display: flex; align-items: center; justify-content: flex-end; gap: var(--s2) var(--s3); flex-wrap: wrap; }
.node-version { font-family: var(--mono); font-size: var(--fs-small); color: var(--dim); }
.update-notice { display: inline-flex; align-items: center; gap: 6px; font-family: var(--mono); font-size: var(--fs-small); }
.update-notice .update-link { color: var(--cyan); text-decoration: none; }
Expand Down Expand Up @@ -1606,31 +1609,66 @@ dialog.sheet::backdrop { background: var(--scrim); backdrop-filter: blur(3px); }
.sheet-hint { margin-left: auto; color: var(--dim-2); font-size: var(--fs-micro); letter-spacing: 0; }

/* ---------- about ---------- */
.about {
border: 1px solid var(--hair);
/* a lead card, then the reference sections as cards that flow down two
columns on a wide screen; the rail above jumps between them */
.about-hero {
border: 1px solid var(--hair-2);
border-radius: var(--r2);
background: linear-gradient(180deg, var(--panel-hi) 0, var(--panel) 132px);
box-shadow: var(--shadow), inset 0 1px 0 rgba(255, 255, 255, 0.035);
background: linear-gradient(180deg, var(--panel-hi) 0, var(--panel) 160px);
box-shadow: var(--e2), inset 0 1px 0 rgba(255, 255, 255, 0.035);
padding: var(--s6);
display: flex;
flex-direction: column;
gap: var(--s4);
max-width: 920px;
margin-bottom: var(--s5);
}
.about-hero p { margin: 0; color: var(--ink); font-size: var(--fs-body); line-height: 1.6; max-width: 88ch; }
.about-hero .about-word { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s2) var(--s3); color: var(--dim); font-size: var(--fs-label); }
.about-facts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 4px var(--s4); margin: 0; font-family: var(--mono); font-size: var(--fs-small); }
.about-facts dt { color: var(--dim-2); letter-spacing: 1px; text-transform: uppercase; }
.about-facts dd { margin: 0; color: var(--ink); overflow-wrap: anywhere; }
.about-links { display: flex; flex-wrap: wrap; gap: var(--s2); margin: 0; padding: 0; list-style: none; }
.about-links a, .about-sec a {
color: var(--cyan); text-decoration: underline; text-decoration-color: var(--cyan-dim);
text-underline-offset: 3px;
}
.about-links a {
display: inline-flex; align-items: center; min-height: 36px; padding: 6px var(--s3);
border: 1px solid var(--hair-2); border-radius: var(--r1); background: var(--panel-2);
font-family: var(--mono); font-size: var(--fs-small); letter-spacing: 1px; text-decoration: none;
}
.about-links a:hover, .about-sec a:hover { color: var(--ink-strong); border-color: var(--cyan-dim); }
.about-links a:focus-visible, .about-sec a:focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; }
.about { columns: 2 520px; column-gap: var(--s5); }
.about-sec {
break-inside: avoid;
border: 1px solid var(--hair);
border-radius: var(--r2);
background: var(--panel);
box-shadow: var(--e1);
padding: var(--s5);
margin: 0 0 var(--s5);
display: flex;
flex-direction: column;
gap: var(--s3);
}
.about .section-title {
font-family: var(--mono);
font-size: var(--fs-label);
letter-spacing: 2px;
color: var(--amber);
font-weight: 600;
margin: var(--s3) 0 0 0;
}
.about p { margin: 0; color: var(--ink); font-size: var(--fs-body); max-width: 80ch; }
.about-dl { display: grid; grid-template-columns: 170px minmax(0, 1fr); gap: 6px var(--s4); margin: 0; }
.about-dl dt { font-family: var(--mono); font-size: var(--fs-small); letter-spacing: 1px; color: var(--amber); padding-top: 2px; }
.about-dl dd { margin: 0; font-size: var(--fs-body); color: var(--dim); }
.about-ul { margin: 0; padding-left: 18px; color: var(--dim); font-size: var(--fs-body); }
.about-ul li { margin: 4px 0; }
margin: 0;
}
.about p { margin: 0; color: var(--ink); font-size: var(--fs-body); line-height: 1.6; }
.about-dl { display: grid; grid-template-columns: minmax(96px, max-content) minmax(0, 1fr); gap: 8px var(--s4); margin: 0; }
.about-dl dt { font-family: var(--mono); font-size: var(--fs-small); letter-spacing: 1px; color: var(--amber); padding-top: 2px; white-space: nowrap; }
.about-dl dd { margin: 0; font-size: var(--fs-body); color: var(--dim); line-height: 1.5; }
.about-keys kbd {
display: inline-block; min-width: 22px; padding: 1px 6px; text-align: center;
font-family: var(--mono); font-size: var(--fs-small); color: var(--ink);
border: 1px solid var(--hair-2); border-bottom-width: 2px; border-radius: var(--r1); background: var(--panel-3);
}
.about-pre {
margin: 0;
font-family: var(--mono);
Expand Down Expand Up @@ -1658,6 +1696,11 @@ dialog.sheet::backdrop { background: var(--scrim); backdrop-filter: blur(3px); }
}

/* ---------- responsive: one query per breakpoint ---------- */
@media (max-width: 1599px) {
/* below this the descriptor only fits cut off; the wordmark names the app
and the about tab leads with the same line */
.tab-long, .descriptor { display: none; }
}
@media (max-width: 1279px) {
.harbor-grid { grid-template-columns: minmax(0, 7fr) minmax(300px, 5fr); }
}
Expand Down Expand Up @@ -1805,6 +1848,11 @@ dialog.sheet::backdrop { background: var(--scrim); backdrop-filter: blur(3px); }
should not have to aim at a 24 px glyph */
.toast-close, .conn .retry, .alert-card .alert-dismiss { min-width: var(--touch); min-height: var(--touch); }
.alert-card .alert-dismiss { margin: -8px -6px -8px 0; }
/* an alert's message is the warning itself: on a phone it wraps instead of
ending at "2 waitin..." */
.alert-card { flex-wrap: wrap; row-gap: 2px; }
.alert-card .alert-msg { white-space: normal; overflow-wrap: anywhere; flex: 1 1 0; }
.alert-card .alert-when { order: 9; flex-basis: 100%; }
.conn .retry { margin: -6px -8px -6px 0; }
/* every chart's shape, not its words, takes the phone's minimum height: an
axis label has had room to be 13 px since the frame stopped scaling text */
Expand Down Expand Up @@ -1846,6 +1894,7 @@ dialog.sheet::backdrop { background: var(--scrim); backdrop-filter: blur(3px); }
.ticket-stub { display: none; }
.ticket-paper { --fold-x: 100%; }
.about-dl { grid-template-columns: 1fr; }
.about-hero, .about-sec { padding: var(--s4); }
}

/* ---------- motion preferences and other media ---------- */
Expand Down
24 changes: 23 additions & 1 deletion internal/harbor/web/harbor.js
Original file line number Diff line number Diff line change
Expand Up @@ -483,10 +483,19 @@ function wireTabs() {
show(key);
});
}
// A hash is a tab, a ticket, or a section the rail wrote: a section opens
// the tab that holds it, so a shared or reloaded link lands where it says.
const h = location.hash.slice(1);
show(h.startsWith("ticket=") ? "harbor" : h);
if (tabs[h] || h.startsWith("ticket=")) return show(h);
const target = h ? document.getElementById(h) : null;
const key = target ? Object.keys(panels).find((k) => panels[k].contains(target)) : undefined;
show(key);
if (key) pendingSection = target;
}
let pendingTicket = location.hash.startsWith("#ticket=") ? decodeURIComponent(location.hash.slice(8)) : null;
// A linked section waits for the first status, whose alerts and panels move
// it down the page.
let pendingSection = null;

// ---------------- page chrome ----------------
// The sub-navigation, the collapsible panels, the keyboard shortcuts, the
Expand All @@ -502,13 +511,20 @@ const ENGINES_SECTIONS = [
["sec-throughput", "Throughput"], ["sec-ledger", "Ledger"], ["sec-lb", "Leaderboard"],
["sec-recent", "Recent"], ["sec-residency", "Residency"], ["sec-storage", "Storage"],
];
const ABOUT_SECTIONS = [
["about-top", "Berth"], ["about-cards", "Cards"], ["about-modes", "Modes"], ["about-nav", "Getting around"],
["about-keys", "Keyboard"], ["about-states", "Status words"], ["about-kpis", "KPIs"], ["about-counters", "Counters"],
["about-kv", "Context and KV"], ["about-telemetry", "GPU and machine"], ["about-library", "Library"],
["about-holders", "Games and stowaways"], ["about-next", "Terminal"],
];
function wireChrome() {
try {
const stored = localStorage.getItem(COLLAPSE_KEY);
if (stored) state.collapsed = JSON.parse(stored) || [];
} catch (e) { state.collapsed = []; }
buildSubnav($("subnav-harbor"), HARBOR_SECTIONS);
buildSubnav($("subnav-engines"), ENGINES_SECTIONS);
buildSubnav($("subnav-about"), ABOUT_SECTIONS);
wireCollapse();
wireShortcuts();
wireRailMenu();
Expand Down Expand Up @@ -1185,8 +1201,14 @@ function demoInsights(range) {
}
function onStatus(s) {
nodeVersion.textContent = "node " + (s.version || "") + (s.harbor_version ? " / harbor " + s.harbor_version : "");
setText($("about-version"), nodeVersion.textContent);
renderAll(s);
paintOverall();
if (pendingSection) {
const target = pendingSection;
pendingSection = null;
requestAnimationFrame(() => target.scrollIntoView({ block: "start" }));
}
if (pendingTicket) {
const key = pendingTicket;
pendingTicket = null;
Expand Down
Loading
Loading