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
7 changes: 4 additions & 3 deletions webview/hugo/layouts/shortcodes/workstation-probe-panel.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,11 @@

{{- $data := $raw | transform.Unmarshal -}}
<div class="workstation-probe-panel" data-workstation-probe-config='{{ $data | jsonify }}'>
<div class="workstation-probe-panel-header">
<div class="workstation-probe-panel-header" data-workstation-probe-header>
<h3>{{ $data.name | default $data.api }}</h3>
<span class="workstation-probe-panel-header-uri" data-workstation-probe-uri="{{ $data.api }}">{{ $data.api }}</span>
<span class="workstation-probe-panel-header-facts" data-workstation-probe-header-facts>loading</span>
<span class="workstation-probe-panel-header-fact workstation-probe-panel-header-uri" data-workstation-probe-header-fact data-workstation-probe-header-fact-uri data-wsp-header-priority="3" data-workstation-probe-uri="{{ $data.api }}" title="{{ $data.api }}">{{ $data.api }}</span>
<span class="workstation-probe-panel-header-fact workstation-probe-panel-header-time" data-workstation-probe-header-fact data-workstation-probe-header-fact-server-time data-wsp-header-priority="2" data-wsp-empty="true"></span>
<span class="workstation-probe-panel-header-fact workstation-probe-panel-header-uptime" data-workstation-probe-header-fact data-workstation-probe-header-fact-uptime data-wsp-header-priority="1" data-wsp-empty="true"></span>
<div class="workstation-probe-panel-meta">
<span class="workstation-probe-status" data-state="connecting">
<span class="workstation-probe-status-dot" data-workstation-probe-status-dot></span>
Expand Down
79 changes: 48 additions & 31 deletions webview/hugo/static/workstation-probe/css/panel.css
Original file line number Diff line number Diff line change
Expand Up @@ -94,28 +94,30 @@
.workstation-probe-panel-header {
display: flex;
align-items: center;
flex-wrap: wrap;
flex-wrap: nowrap;
gap: 0.75rem;
padding: 0.875rem 1.25rem;
background: linear-gradient(180deg, var(--wsp-card-bg-hover) 0%, var(--wsp-card-bg) 100%);
border-bottom: 1px solid var(--wsp-border);
overflow: hidden;
}

.workstation-probe-panel-header h3 {
min-width: 0;
max-width: min(20rem, 34vw);
flex: 0 0 auto;
margin: 0;
font-size: 1rem;
font-weight: 600;
font-family: var(--wsp-mono);
color: var(--wsp-fg);
letter-spacing: -0.01em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.workstation-probe-panel-header-uri {
font-size: 0.75rem;
color: var(--wsp-fg-dim);
font-family: var(--wsp-mono);
}

.workstation-probe-panel-header-fact,
.workstation-probe-panel-header-facts {
min-width: 0;
overflow: hidden;
Expand All @@ -124,6 +126,16 @@
font-size: 0.75rem;
color: var(--wsp-fg-dim);
font-family: var(--wsp-mono);
flex: 0 0 auto;
}

.workstation-probe-panel-header-uri {
max-width: min(36rem, 45vw);
}

.workstation-probe-panel-header-fact[data-wsp-empty="true"],
.workstation-probe-panel-header-fact[data-wsp-header-hidden] {
display: none;
}

.workstation-probe-panel-meta {
Expand All @@ -134,6 +146,7 @@
font-family: var(--wsp-mono);
font-size: 0.75rem;
color: var(--wsp-fg-dim);
flex: 0 0 auto;
}

.workstation-probe-status {
Expand Down Expand Up @@ -212,20 +225,14 @@
display: inline-flex;
align-items: center;
gap: 0.25rem;
white-space: nowrap;
}

@media (max-width: 720px) {
.workstation-probe-panel-header-uri {
display: none;
}

.workstation-probe-panel-header-facts {
flex-basis: 100%;
order: 3;
}

.workstation-probe-panel-meta {
margin-left: auto;
.workstation-probe-panel-header {
gap: 0.625rem;
padding-left: 1rem;
padding-right: 1rem;
}
}

Expand Down Expand Up @@ -523,21 +530,34 @@
* scanned at a glance to spot the disk that's about to fill up.
*/
.workstation-probe-sub-storage {
display: flex;
flex-direction: column;
}

.workstation-probe-sub-row {
display: flex;
display: grid;
grid-template-columns: max-content minmax(80px, 160px) minmax(96px, 1fr) max-content;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 1.25rem;
column-gap: 0.75rem;
font-family: var(--wsp-mono);
font-size: 0.8125rem;
line-height: 1.3;
}

.workstation-probe-sub-row + .workstation-probe-sub-row {
.workstation-probe-sub-row {
display: contents;
}

.workstation-probe-sub-row > * {
min-width: 0;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}

.workstation-probe-sub-row > :first-child {
padding-left: 1.25rem;
}

.workstation-probe-sub-row > :last-child {
padding-right: 1.25rem;
}

.workstation-probe-sub-row + .workstation-probe-sub-row > * {
border-top: 1px solid var(--wsp-border-soft);
}

Expand All @@ -547,21 +567,18 @@
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--wsp-fg);
min-width: 56px;
white-space: nowrap;
}

.workstation-probe-sub-row-path {
color: var(--wsp-fg-dim);
font-size: 0.75rem;
min-width: 80px;
max-width: 160px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.workstation-probe-sub-row-bar {
flex: 1;
position: relative;
height: 10px;
display: flex;
Expand Down
60 changes: 56 additions & 4 deletions webview/hugo/static/workstation-probe/js/boot.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,16 @@ const BOOTED_ATTR = 'data-workstation-probe-booted';
const CONFIG_ATTR = 'data-workstation-probe-config';
const REFRESH_SEL = '[data-workstation-probe-refresh]';
const URI_SEL = '[data-workstation-probe-uri]';
const HEADER_FACTS_SEL = '[data-workstation-probe-header-facts]';
const HEADER_SEL = '[data-workstation-probe-header]';
const HEADER_FACT_SEL = '[data-workstation-probe-header-fact]';
const HEADER_FACT_URI_SEL = '[data-workstation-probe-header-fact-uri]';
const HEADER_FACT_TIME_SEL = '[data-workstation-probe-header-fact-server-time]';
const HEADER_FACT_UPTIME_SEL = '[data-workstation-probe-header-fact-uptime]';
const HEADER_FACT_PRIORITY_ATTR = 'data-wsp-header-priority';
const HEADER_FACT_HIDDEN_ATTR = 'data-wsp-header-hidden';

const STALE_MULTIPLIER = 2.5; // panel dims when last fetch is older than refresh * N
const headerResizeObservers = new WeakMap();

function init() {
const panels = document.querySelectorAll(`.${PANEL_CLASS}:not([${BOOTED_ATTR}])`);
Expand All @@ -42,10 +49,11 @@ function bootOne(panel) {
// Render the static shell. Inner DOM is rebuilt here so multiple panels
// on the same page don't interfere with each other.
panel.innerHTML = `
<div class="${PANEL_CLASS}-header">
<div class="${PANEL_CLASS}-header" ${HEADER_SEL.replace(/[\[\]]/g, '')}>
<h3>${escapeHtml(cfg.name || cfg.api)}</h3>
<span class="${PANEL_CLASS}-header-uri" ${URI_SEL.replace(/[\[\]]/g, '')}="${escapeHtml(cfg.api || '')}">${escapeHtml(cfg.api || '')}</span>
<span class="${PANEL_CLASS}-header-facts" ${HEADER_FACTS_SEL.replace(/[\[\]]/g, '')}>loading</span>
<span class="${PANEL_CLASS}-header-fact ${PANEL_CLASS}-header-uri" ${HEADER_FACT_SEL.replace(/[\[\]]/g, '')} ${HEADER_FACT_URI_SEL.replace(/[\[\]]/g, '')} ${HEADER_FACT_PRIORITY_ATTR}="3" ${URI_SEL.replace(/[\[\]]/g, '')}="${escapeHtml(cfg.api || '')}" title="${escapeHtml(cfg.api || '')}">${escapeHtml(cfg.api || '')}</span>
<span class="${PANEL_CLASS}-header-fact ${PANEL_CLASS}-header-time" ${HEADER_FACT_SEL.replace(/[\[\]]/g, '')} ${HEADER_FACT_TIME_SEL.replace(/[\[\]]/g, '')} ${HEADER_FACT_PRIORITY_ATTR}="2" data-wsp-empty="true"></span>
<span class="${PANEL_CLASS}-header-fact ${PANEL_CLASS}-header-uptime" ${HEADER_FACT_SEL.replace(/[\[\]]/g, '')} ${HEADER_FACT_UPTIME_SEL.replace(/[\[\]]/g, '')} ${HEADER_FACT_PRIORITY_ATTR}="1" data-wsp-empty="true"></span>
<div class="${PANEL_CLASS}-meta">
<span class="${STATUS_CLASS}" data-state="connecting">
<span class="${STATUS_CLASS}-dot" data-workstation-probe-status-dot></span>
Expand All @@ -59,6 +67,7 @@ function bootOne(panel) {
${(cfg.modules || []).map((m) => renderSubShell(m)).join('')}
</div>
`;
setupHeaderAutoFit(panel);

// Subscribe once; the poller deduplicates by api+refresh+query.
subscribe(cfg, (state) => {
Expand Down Expand Up @@ -99,9 +108,52 @@ function bootOne(panel) {
if (state.data) {
renderPanel(panel, cfg, state.data, state.profile);
}
fitHeaderFacts(panel);
});
}

function setupHeaderAutoFit(panel) {
const header = panel.querySelector(HEADER_SEL);
if (!header) return;
const fit = () => fitHeaderFacts(panel);
if (typeof ResizeObserver !== 'undefined') {
const observer = new ResizeObserver(fit);
observer.observe(header);
headerResizeObservers.set(panel, observer);
} else {
window.addEventListener('resize', fit, { passive: true });
}
window.requestAnimationFrame(fit);
}

function fitHeaderFacts(panel) {
const header = panel.querySelector(HEADER_SEL);
if (!header || !header.clientWidth) return;

const facts = Array.from(header.querySelectorAll(HEADER_FACT_SEL))
.filter((el) => el.getAttribute('data-wsp-empty') !== 'true');
for (const el of facts) {
el.removeAttribute(HEADER_FACT_HIDDEN_ATTR);
}

const hideOrder = facts
.slice()
.sort((a, b) => headerFactPriority(a) - headerFactPriority(b));
for (const el of hideOrder) {
if (!headerOverflows(header)) break;
el.setAttribute(HEADER_FACT_HIDDEN_ATTR, 'true');
}
}

function headerFactPriority(el) {
const n = Number(el.getAttribute(HEADER_FACT_PRIORITY_ATTR));
return Number.isFinite(n) ? n : 0;
}

function headerOverflows(header) {
return header.scrollWidth > header.clientWidth + 1;
}

// renderSubShell produces the per-module shell. Each module picks a
// body layout that matches its data shape:
// cpu : per-core tile grid
Expand Down
36 changes: 24 additions & 12 deletions webview/hugo/static/workstation-probe/js/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ const STORAGE_SELECTOR = '[data-wsp-storage]';
const GPU_SELECTOR = '[data-wsp-gpu]';
const META_TEXT_SELECTOR = '[data-wsp-sub-meta-text]';
const MODULE_ATTR = 'data-workstation-probe-module';
const HEADER_FACTS_SELECTOR = '[data-workstation-probe-header-facts]';
const HEADER_FACT_TIME_SELECTOR = '[data-workstation-probe-header-fact-server-time]';
const HEADER_FACT_UPTIME_SELECTOR = '[data-workstation-probe-header-fact-uptime]';

export function renderPanel(panel, _cfg, data, profile = null) {
renderHeaderFacts(panel, data, profile);
Expand All @@ -34,21 +35,31 @@ export function renderPanel(panel, _cfg, data, profile = null) {
}

function renderHeaderFacts(panel, data, profile) {
const el = panel.querySelector(HEADER_FACTS_SELECTOR);
if (!el) return;
const facts = [];
const timeEl = panel.querySelector(HEADER_FACT_TIME_SELECTOR);
const uptimeEl = panel.querySelector(HEADER_FACT_UPTIME_SELECTOR);

let timeText = '';
if (data.server_time_local) {
const zone = formatServerZone(
profile?.server_timezone || data.server_timezone,
profile?.server_timezone_offset_seconds ?? data.server_timezone_offset_seconds,
);
facts.push(zone ? `${data.server_time_local} ${zone}` : data.server_time_local);
timeText = zone ? `${data.server_time_local} ${zone}` : data.server_time_local;
}
setHeaderFact(timeEl, timeText);

const uptime = hostUptimeSeconds(data, profile);
if (uptime > 0) {
facts.push(`up ${formatDuration(uptime)}`);
setHeaderFact(uptimeEl, uptime > 0 ? `up ${formatDuration(uptime)}` : '');
}

function setHeaderFact(el, text) {
if (!el) return;
el.textContent = text;
if (text) {
el.removeAttribute('data-wsp-empty');
} else {
el.setAttribute('data-wsp-empty', 'true');
}
el.textContent = facts.length > 0 ? facts.join(' · ') : '';
}

function renderSub(sub, mod, data, profile) {
Expand Down Expand Up @@ -621,10 +632,11 @@ function renderStorage(sub, _valueEl, metaEl, _canvas, storage) {
}

// renderStorageRows fills the storage sub-panel's data-wsp-storage
// element with one row per mount. Each row is a flex container with
// fixed-width label slots and a flex-1 bar; the bar itself contains a
// thick coloured "used" segment and a thin grey "free" segment that
// share the same horizontal extent.
// element with one row per mount. CSS makes every row participate in
// one shared grid so alias, path, bar, and size columns align to the
// widest value in the list. The bar itself contains a thick coloured
// "used" segment and a thin grey "free" segment that share the same
// horizontal extent.
function renderStorageRows(container, disks) {
const html = disks.map((d) => {
const pct = clamp(d.used_percent || 0, 0, 100);
Expand Down
7 changes: 4 additions & 3 deletions webview/hugo/test/expected/basic/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@
<h1>Local workstation webview test</h1><p>This page renders the local workstation-probe panel against
<code>http://localhost:19090</code>.</p>
<div class="workstation-probe-panel" data-workstation-probe-config='{&#34;api&#34;:&#34;http://localhost:19090&#34;,&#34;modules&#34;:[&#34;cpu&#34;,&#34;memory&#34;,&#34;gpu&#34;,&#34;storage&#34;],&#34;name&#34;:&#34;localhost&#34;,&#34;refresh&#34;:&#34;5s&#34;}'>
<div class="workstation-probe-panel-header">
<div class="workstation-probe-panel-header" data-workstation-probe-header>
<h3>localhost</h3>
<span class="workstation-probe-panel-header-uri" data-workstation-probe-uri="http://localhost:19090">http://localhost:19090</span>
<span class="workstation-probe-panel-header-facts" data-workstation-probe-header-facts>loading</span>
<span class="workstation-probe-panel-header-fact workstation-probe-panel-header-uri" data-workstation-probe-header-fact data-workstation-probe-header-fact-uri data-wsp-header-priority="3" data-workstation-probe-uri="http://localhost:19090" title="http://localhost:19090">http://localhost:19090</span>
<span class="workstation-probe-panel-header-fact workstation-probe-panel-header-time" data-workstation-probe-header-fact data-workstation-probe-header-fact-server-time data-wsp-header-priority="2" data-wsp-empty="true"></span>
<span class="workstation-probe-panel-header-fact workstation-probe-panel-header-uptime" data-workstation-probe-header-fact data-workstation-probe-header-fact-uptime data-wsp-header-priority="1" data-wsp-empty="true"></span>
<div class="workstation-probe-panel-meta">
<span class="workstation-probe-status" data-state="connecting">
<span class="workstation-probe-status-dot" data-workstation-probe-status-dot></span>
Expand Down
Loading
Loading