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 @@
Berth is the VRAM leasing service for every GPU you run. Engines ask the berthd node for a lease on the card; the node queues the request, asks the current holder to drain, waits until the driver shows the memory free, and only then grants the berth. Nothing is ever granted on process exit alone, and nothing is claimed as reserved unless the driver measured it. Every stat on this page is collected by the node itself, from the driver, the host, and the engines' own endpoints.
- -A section rail under the header jumps to any panel on the tab and marks the one in view. Every panel folds from the chevron in its head and stays folded between visits. The card tab counts voyages and the engines tab counts engines, so the header says which tab wants attention. On a phone the nav sits along the bottom edge with the tab you are on marked, and the menu button opens the node's versions, the last successful read, the detail level, the retry and this list. Times read relative, with the exact clock in the title. When a read goes stale a retry appears on the conn pill that reads the node now instead of waiting out the poll.
-The log's filter box matches the kind word, the engine and the detail; the chips carry live counts and the count line names what is shown. LATEST jumps back to the newest row. The board's headers sort by vessel, hold, status and age, and the foot names the order. Alert cards dismiss until they clear or change level. The ticket copies its JSON or a deep link to this exact voyage.
- -berth. the VRAM leasing service for every GPU you run
+Engines ask the berthd node for a lease on a card. The node queues the request, asks the current holder to drain, waits until the driver shows the memory free, and only then grants the berth. Nothing is granted on a process exit alone, and nothing is called reserved unless the driver measured it. Every number on this page comes from the node: the driver, the host and the engines' own endpoints.
+Every state shows as a shape and its word together, the same on every panel: a dot is working now, a ring is waiting or passive, a half-filled circle is in transition, a triangle needs attention, a square is not running or not live. A magenta CONTESTED flag on a docked row means the driver saw other VRAM holders on the card at grant time, so the measured hold and the free space disagree. A green WARM flag marks a parked engine, and its hover text carries the estimated RAM it holds and how long it has been parked; a boarding row carries a Q flag with its place in the queue, and the node's completion estimate when it sent one. The row title, the berth panel and the ticket stamp carry how much each holds, and the ticket names the tier the weights live in, HOT on the card or WARM in RAM, beside the exact terminal order for it as a copy button. CARD RANKING on the engines tab shows how the node orders engines when two at one priority ask for the card inside the juggle window: the config order, or card hours over the last seven days.
- -Every count is an event the node actually saw. Berth never estimates what might have happened.
- -Context residency draws the engine's own per-request token figures - cached, fresh prefill and generated - as columns against the model's context window, so a request that would not fit its window is visible at a glance; below the columns sit the engines' live KV sessions. The KV pool is each engine's live KV slots (the llama.cpp family's /slots): how many context tokens each session holds against its window, with 75 and 90 percent marks. Stored ranges show the per-minute peak slot fill the harbor folded from the node's samples; the live slots stay live, whatever the range says. An engine without these endpoints reports no window and no slots, and the panels say so rather than guess.
The scheduled card reports its power draw against its power limit, temperature, utilisation, and in advanced mode its SM and memory clocks, fan, and PCIe generation and width. A reading of n/a means the driver did not report it, not zero. The machine panel is the host itself: CPU, cores, used and total memory, swap, and the one, five and fifteen minute load averages. Its socket line counts event-stream write timeouts and dropped slow readers, so a stalled log on a live node starts there.
- -The library lists the model file roots this machine has, the same rows berth models prints: each model's shelf, quant, size, and its CACHED share with the tier it reads as, COOL at half the model's bytes in the kernel page cache or better, COLD below. The kernel takes those pages back whenever the machine needs the RAM, so the share says a reload may skip the disk; it is an estimate of reload speed, never a statement about RAM held. SERVED BY names each engine that reports the model resident now, and EST VRAM is that engine's learned footprint from its past runs, an estimate of what a run needs and never a reservation. Each row's copy buttons give the exact commands: berth prefetch NAME warms the files, berth models show NAME reads the full header.
A stowaway is any process holding VRAM without a lease. Berth reports it and never kills it. A "no ticket" run is an engine started while the node was down, so it holds the card outside any lease. Both show here read only.
- -The harbor shows the node. Ordering work is coming in later phases, and these rows are marked coming, not working:
-Until then, everything the page shows is also on the terminal:
-berth status +
One berthd governs every card in the machine. Each card it schedules has its own berth panel with only that card's leases and stowaways, and a card it only reads is marked INVENTORY ONLY. A board row, a stowaway and a lease line name their card (gpu1), and a lease on several cards lists them. The tide draws the memory of all scheduled cards together. A card passed to a VM is marked on its own panel, and the others keep serving.
+Each state shows as a shape and its word together on every panel. A dot is working, a ring is waiting, a half-filled circle is in transition, a triangle needs attention, and a square is stopped. A magenta CONTESTED flag on a docked row means the driver saw other VRAM holders at grant time. A green WARM flag marks a parked engine, and its title gives the RAM it holds and how long it has been parked. A boarding row carries a Q flag with its place in the queue and the node's estimate when it sent one. A ticket names the tier the weights live in, HOT or WARM, and gives the exact terminal command as a copy button. CARD RANKING on the engines tab shows how the node orders two engines at one priority inside the juggle window: the config order, or card hours over seven days.
+Every count is an event the node saw. Berth never estimates what might have happened.
+Context residency draws each request's cached, fresh and generated tokens against the model's context window, so a request that would not fit is plain to see. The KV pool shows each engine's live slots (the llama.cpp family's /slots) against their windows, with marks at 75% and 90%. Longer ranges show the per-minute peak fill; the live slots stay live. An engine without these endpoints reports no window and no slots, and the panels say so.
Each card reports power against its limit, temperature and utilisation, and in advanced mode its SM and memory clocks, fan and PCIe link. n/a means the driver did not report a value; it never means zero. The machine panel is the host: CPU, cores, memory, swap and load. Its socket line counts event-stream write timeouts and dropped slow readers, so a stalled log starts there.
+The library lists the model files on this machine, the same rows berth models prints: shelf, quant, size, and the CACHED share in the kernel page cache. COOL means half the file or more is cached, COLD less. The kernel takes those pages back when it needs the RAM, so CACHED predicts a faster reload and says nothing about RAM held. SERVED BY names the engine that has the model loaded, and EST VRAM is that engine's learned footprint, an estimate and never a reservation. The copy buttons give berth prefetch NAME and berth models show NAME.
A game that a hold claim matches holds the card under a claim:NAME lease, so engines drain and wait until it exits. berth hold does the same by hand. A stowaway is any process holding VRAM without a lease: Berth reports it and never stops it. A NO TICKET run is an engine started while the node was down, so it holds the card outside any lease.
The harbor reads the node and changes nothing. Switching and stopping engines from here will need a paired device (berth pair), and that is not built yet. The gateway has its own page at /gui/ on the gateway's port, where an engine with a start command can be launched. Everything this page shows is also on the terminal:
berth status berth events -f -berth run +berth claims +berth hold --reason game berth config check+
The roadmap says what comes next.
+