From f7e5bb8186cc82e42b349fff1a663b06a735c623 Mon Sep 17 00:00:00 2001 From: T-Crypt <132922589+T-Crypt@users.noreply.github.com> Date: Fri, 30 Oct 2026 21:52:53 -0600 Subject: [PATCH 1/2] feat(harbor): rewrite the About tab and fix header clipping The About tab now leads with the node's version and links to the docs, changelog, issues and source, covers machines with several cards, and drops planned features that shipped or changed. Its sections are cards in two columns on a wide screen, with the same section rail as the other tabs. The header no longer clips the ADVANCED switch or cuts its tagline off, and a phone alert wraps instead of ending in an ellipsis. A section link such as #sec-library opened the card tab on a reload. It now opens the tab that holds the section. --- CHANGELOG.md | 15 +++ docs/harbor.md | 6 +- internal/harbor/web/harbor.css | 79 +++++++++--- internal/harbor/web/harbor.js | 16 ++- internal/harbor/web/index.html | 212 +++++++++++++++++++-------------- 5 files changed, 220 insertions(+), 108 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d956ae1..72dac4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,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, diff --git a/docs/harbor.md b/docs/harbor.md index 5ce41b7..a348cd8 100644 --- a/docs/harbor.md +++ b/docs/harbor.md @@ -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 diff --git a/internal/harbor/web/harbor.css b/internal/harbor/web/harbor.css index 96ea9f1..f3477c2 100644 --- a/internal/harbor/web/harbor.css +++ b/internal/harbor/web/harbor.css @@ -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; @@ -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; } @@ -1606,16 +1609,48 @@ 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); @@ -1623,14 +1658,17 @@ dialog.sheet::backdrop { background: var(--scrim); backdrop-filter: blur(3px); } 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); @@ -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); } } @@ -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 */ @@ -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 ---------- */ diff --git a/internal/harbor/web/harbor.js b/internal/harbor/web/harbor.js index 30d7b69..b0aae4b 100644 --- a/internal/harbor/web/harbor.js +++ b/internal/harbor/web/harbor.js @@ -483,8 +483,14 @@ 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) requestAnimationFrame(() => target.scrollIntoView({ block: "start" })); } let pendingTicket = location.hash.startsWith("#ticket=") ? decodeURIComponent(location.hash.slice(8)) : null; @@ -502,6 +508,12 @@ 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); @@ -509,6 +521,7 @@ function wireChrome() { } catch (e) { state.collapsed = []; } buildSubnav($("subnav-harbor"), HARBOR_SECTIONS); buildSubnav($("subnav-engines"), ENGINES_SECTIONS); + buildSubnav($("subnav-about"), ABOUT_SECTIONS); wireCollapse(); wireShortcuts(); wireRailMenu(); @@ -1185,6 +1198,7 @@ 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 (pendingTicket) { diff --git a/internal/harbor/web/index.html b/internal/harbor/web/index.html index ceae2f3..ac19e45 100644 --- a/internal/harbor/web/index.html +++ b/internal/harbor/web/index.html @@ -281,100 +281,132 @@

STORAGE

From d35e1c981ffc18d0545474e821bb1a99fed737ef Mon Sep 17 00:00:00 2001 From: T-Crypt <132922589+T-Crypt@users.noreply.github.com> Date: Fri, 30 Oct 2026 21:59:47 -0600 Subject: [PATCH 2/2] fix(harbor): scroll to a linked section after the first status The scroll ran before the first status rendered its alerts and panels, so a linked section landed under the header or far below it. --- internal/harbor/web/harbor.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/internal/harbor/web/harbor.js b/internal/harbor/web/harbor.js index b0aae4b..1d9873b 100644 --- a/internal/harbor/web/harbor.js +++ b/internal/harbor/web/harbor.js @@ -490,9 +490,12 @@ function wireTabs() { const target = h ? document.getElementById(h) : null; const key = target ? Object.keys(panels).find((k) => panels[k].contains(target)) : undefined; show(key); - if (key) requestAnimationFrame(() => target.scrollIntoView({ block: "start" })); + 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 @@ -1201,6 +1204,11 @@ function onStatus(s) { 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;