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
4 changes: 3 additions & 1 deletion frontend/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ function applyDashboardSettings(settings: DashboardSettings): void {
tickerBar.hidden = false;
if (!panels.tickers) tickerBar.innerHTML = '<span class="ticker-empty">Tickers hidden</span>';
}
document.body.classList.toggle('expand-lists', !!panels.expandLists);
}

async function loadDashboardSettings(): Promise<void> {
Expand Down Expand Up @@ -67,7 +68,7 @@ function openSettingsEditor(focus?: 'tickers'): void {
overlay.className = 'links-editor-modal settings-editor-modal';
overlay.id = 'settingsEditor';
const panels = dashboardSettings.panels;
overlay.innerHTML = `<form class="links-editor-box settings-editor-box"><div class="links-editor-head"><div><h2>Configure</h2><p class="muted">Sidebar widgets and stock/crypto tickers are saved in dashboard-config.json.</p></div><button type="button" class="ghost" data-close-settings>Cancel</button></div><div class="settings-grid"><label><input type="checkbox" name="machine" ${panels.machine ? 'checked' : ''}> Machine</label><label class="settings-subsetting"><input type="checkbox" name="machineSensors" ${panels.machineSensors ? 'checked' : ''}> Thermal sensors</label><label><input type="checkbox" name="remoteHosts" ${panels.remoteHosts ? 'checked' : ''}> Remote hosts</label><label><input type="checkbox" name="containers" ${panels.containers ? 'checked' : ''}> Local containers</label><label><input type="checkbox" name="links" ${panels.links ? 'checked' : ''}> Links</label><label><input type="checkbox" name="tickers" ${panels.tickers ? 'checked' : ''}> Ticker bar</label></div><label for="settingsTickers">Tickers</label><textarea id="settingsTickers" spellcheck="false" placeholder="MSFT, NVDA, BTC-USD"></textarea><div class="links-editor-actions"><button type="submit" class="primary">${icon('settings')}<span>Save config</span></button></div></form>`;
overlay.innerHTML = `<form class="links-editor-box settings-editor-box"><div class="links-editor-head"><div><h2>Configure</h2><p class="muted">Sidebar widgets and stock/crypto tickers are saved in dashboard-config.json.</p></div><button type="button" class="ghost" data-close-settings>Cancel</button></div><div class="settings-grid"><label><input type="checkbox" name="machine" ${panels.machine ? 'checked' : ''}> Machine</label><label class="settings-subsetting"><input type="checkbox" name="machineSensors" ${panels.machineSensors ? 'checked' : ''}> Thermal sensors</label><label><input type="checkbox" name="remoteHosts" ${panels.remoteHosts ? 'checked' : ''}> Remote hosts</label><label><input type="checkbox" name="containers" ${panels.containers ? 'checked' : ''}> Local containers</label><label><input type="checkbox" name="links" ${panels.links ? 'checked' : ''}> Links</label><label><input type="checkbox" name="tickers" ${panels.tickers ? 'checked' : ''}> Ticker bar</label><label><input type="checkbox" name="expandLists" ${panels.expandLists ? 'checked' : ''}> Expand lists (no scrollbars)</label></div><label for="settingsTickers">Tickers</label><textarea id="settingsTickers" spellcheck="false" placeholder="MSFT, NVDA, BTC-USD"></textarea><div class="links-editor-actions"><button type="submit" class="primary">${icon('settings')}<span>Save config</span></button></div></form>`;
document.body.appendChild(overlay);
const form = overlay.querySelector<HTMLFormElement>('form')!;
const textarea = overlay.querySelector<HTMLTextAreaElement>('#settingsTickers')!;
Expand All @@ -87,6 +88,7 @@ function openSettingsEditor(focus?: 'tickers'): void {
containers: data.has('containers'),
links: data.has('links'),
tickers: data.has('tickers'),
expandLists: data.has('expandLists'),
},
}).then(close).catch((error: Error) => toast(error.message));
});
Expand Down
3 changes: 2 additions & 1 deletion frontend/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ interface PanelSettings {
remoteHosts: boolean;
links: boolean;
tickers: boolean;
expandLists: boolean;
}

interface DashboardSettings {
Expand Down Expand Up @@ -90,7 +91,7 @@ let summaryLoading = false;
let shellsLoading = false;
let dashboardSettings: DashboardSettings = {
tickers: [],
panels: { machine: true, machineSensors: true, containers: true, remoteHosts: true, links: true, tickers: true },
panels: { machine: true, machineSensors: true, containers: true, remoteHosts: true, links: true, tickers: true, expandLists: false },
};
const SHELL_LABEL_ALIASES_KEY = 'sdShellLabelAliases';
const SHELL_AUTO_TITLES_KEY = 'sdShellAutoTitles';
Expand Down
61 changes: 53 additions & 8 deletions frontend/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ interface MetricTemp {

interface MachineMetrics {
hostname: string;
ip?: string;
cpu_pct: number;
cpu_cores: number;
cpu_mhz: number;
Expand All @@ -28,6 +29,8 @@ interface ContainerInfo {
name: string;
image: string;
status: string;
cpu?: string | null;
mem?: string | null;
}

interface RemoteMetrics {
Expand All @@ -53,10 +56,50 @@ interface RemoteHostStatus {
checked_at: string;
containers: ContainerInfo[];
container_total?: number;
ip?: string | null;
metrics?: RemoteMetrics | null;
error?: string | null;
}

// running = "Up ..."; flag unhealthy/restarting; everything else (Exited/Created) = stopped.
function containerState(status: string): 'running' | 'unhealthy' | 'stopped' {
const s = (status || '').toLowerCase();
if (s.includes('unhealthy')) return 'unhealthy';
if (s.startsWith('restarting')) return 'unhealthy';
if (s.startsWith('up')) return 'running';
return 'stopped';
}

// "23 running · 2 stopped · 1 unhealthy" — omits zero buckets.
function containerHealth(containers: ContainerInfo[]): string {
let running = 0; let stopped = 0; let unhealthy = 0;
for (const c of containers) {
const st = containerState(c.status);
if (st === 'unhealthy') unhealthy += 1;
else if (st === 'stopped') stopped += 1;
else running += 1;
}
const parts: string[] = [];
if (running) parts.push(`${running} running`);
if (stopped) parts.push(`${stopped} stopped`);
if (unhealthy) parts.push(`${unhealthy} unhealthy`);
return parts.join(' · ') || 'no containers';
}

function containerStatsText(c: ContainerInfo): string {
const bits: string[] = [];
if (c.cpu) bits.push(`${c.cpu} CPU`);
if (c.mem) bits.push(c.mem);
return bits.join(' · ');
}

// Shared row for local + remote container lists. Stopped/unhealthy get a state class for greying.
function containerRowHtml(c: ContainerInfo, extraClass = ''): string {
const stats = containerStatsText(c);
const statsHtml = stats ? `<small class="container-stats">${escapeHtml(stats)}</small>` : '';
return `<div class="container-item ${extraClass} state-${containerState(c.status)}"><div><b>${escapeHtml(c.name)}</b><span>${escapeHtml(c.image)}</span></div><small>${escapeHtml(c.engine)}</small><em>${escapeHtml(c.status)}</em>${statsHtml}</div>`;
}

const SENSOR_LABEL_ALIASES_KEY = 'sdSensorLabelAliases';
let latestMachineMetrics: MachineMetrics | null = null;

Expand Down Expand Up @@ -144,7 +187,8 @@ function renderMetrics(m: MachineMetrics): void {
latestMachineMetrics = m;
const host = document.getElementById('metricsHost');
const mhz = m.cpu_mhz ? ` · ${Math.round(m.cpu_mhz).toLocaleString()} MHz` : '';
if (host) host.textContent = `${m.hostname} · CPU ${m.cpu_cores} cores${mhz} · uptime ${fmtUptime(m.uptime_secs)}`;
const ip = m.ip ? ` · ${m.ip}` : '';
if (host) host.textContent = `${m.hostname}${ip} · CPU ${m.cpu_cores} cores${mhz} · uptime ${fmtUptime(m.uptime_secs)}`;

setMeter('cpu', m.cpu_pct, `${m.cpu_pct.toFixed(0)}%`);
setMeter('mem', m.mem_pct, `${fmtGiB(m.mem_used_kb)} / ${fmtGiB(m.mem_total_kb)} GiB`);
Expand Down Expand Up @@ -176,10 +220,11 @@ function renderContainers(containers: ContainerInfo[]): void {
const list = document.getElementById('containerList');
if (!list) return;
if (!containers.length) {
list.innerHTML = '<div class="muted container-empty">No running containers</div>';
list.innerHTML = '<div class="muted container-empty">No containers</div>';
return;
}
list.innerHTML = containers.map((container) => `<div class="container-item"><div><b>${escapeHtml(container.name)}</b><span>${escapeHtml(container.image)}</span></div><small>${escapeHtml(container.engine)}</small><em>${escapeHtml(container.status)}</em></div>`).join('');
const summary = `<div class="container-health">${escapeHtml(containerHealth(containers))}</div>`;
list.innerHTML = summary + containers.map((c) => containerRowHtml(c)).join('');
}

function remoteProbeText(host: RemoteHostStatus): string {
Expand Down Expand Up @@ -225,14 +270,14 @@ function renderRemoteHosts(hosts: RemoteHostStatus[]): void {
list.innerHTML = hosts.map((host) => {
const containers = host.containers || [];
const total = typeof host.container_total === 'number' ? host.container_total : containers.length;
const countLabel = total === 1 ? '1 container' : `${total} containers`;
const shownNote = total > containers.length ? ` · showing ${containers.length}` : '';
const shownNote = total > containers.length ? ` · showing ${containers.length} of ${total}` : '';
const containerHtml = containers.length
? `<div class="remote-count">${escapeHtml(countLabel)}${escapeHtml(shownNote)}</div><div class="remote-containers">${containers.map((container) => `<div class="container-item remote-container"><div><b>${escapeHtml(container.name)}</b><span>${escapeHtml(container.image)}</span></div><small>${escapeHtml(container.engine)}</small><em>${escapeHtml(container.status)}</em></div>`).join('')}</div>`
: `<div class="muted remote-empty">${host.online ? 'No running containers' : escapeHtml(host.error || 'Remote host is offline')}</div>`;
? `<div class="remote-count">${escapeHtml(containerHealth(containers))}${escapeHtml(shownNote)}</div><div class="remote-containers">${containers.map((c) => containerRowHtml(c, 'remote-container')).join('')}</div>`
Comment on lines 271 to +275
: `<div class="muted remote-empty">${host.online ? 'No containers' : escapeHtml(host.error || 'Remote host is offline')}</div>`;
const error = host.error && host.online ? `<div class="remote-error">${escapeHtml(host.error)}</div>` : '';
const metricsHtml = host.metrics ? remoteMetricsHtml(host.metrics) : '';
return `<div class="remote-host-card ${host.online ? 'online' : 'offline'}"><div class="remote-head"><span class="dot ${host.online ? 'on' : ''}"></span><div><b>${escapeHtml(host.label || host.id)}</b><small title="${escapeHtml(host.target)}">${host.online ? 'Online' : 'Offline'} · ${escapeHtml(remoteCheckedText(host))}</small></div></div>${metricsHtml}${containerHtml}${error}</div>`;
const ipText = host.ip ? ` · ${host.ip}` : '';
return `<div class="remote-host-card ${host.online ? 'online' : 'offline'}"><div class="remote-head"><span class="dot ${host.online ? 'on' : ''}"></span><div><b>${escapeHtml(host.label || host.id)}</b><small title="${escapeHtml(host.target)}">${host.online ? 'Online' : 'Offline'}${escapeHtml(ipText)} · ${escapeHtml(remoteCheckedText(host))}</small></div></div>${metricsHtml}${containerHtml}${error}</div>`;
}).join('');
}

Expand Down
4 changes: 3 additions & 1 deletion public/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ function applyDashboardSettings(settings) {
if (!panels.tickers)
tickerBar.innerHTML = '<span class="ticker-empty">Tickers hidden</span>';
}
document.body.classList.toggle('expand-lists', !!panels.expandLists);
}
async function loadDashboardSettings() {
const response = await fetch('/api/ui-config', { cache: 'no-store', credentials: 'same-origin' });
Expand Down Expand Up @@ -66,7 +67,7 @@ function openSettingsEditor(focus) {
overlay.className = 'links-editor-modal settings-editor-modal';
overlay.id = 'settingsEditor';
const panels = dashboardSettings.panels;
overlay.innerHTML = `<form class="links-editor-box settings-editor-box"><div class="links-editor-head"><div><h2>Configure</h2><p class="muted">Sidebar widgets and stock/crypto tickers are saved in dashboard-config.json.</p></div><button type="button" class="ghost" data-close-settings>Cancel</button></div><div class="settings-grid"><label><input type="checkbox" name="machine" ${panels.machine ? 'checked' : ''}> Machine</label><label class="settings-subsetting"><input type="checkbox" name="machineSensors" ${panels.machineSensors ? 'checked' : ''}> Thermal sensors</label><label><input type="checkbox" name="remoteHosts" ${panels.remoteHosts ? 'checked' : ''}> Remote hosts</label><label><input type="checkbox" name="containers" ${panels.containers ? 'checked' : ''}> Local containers</label><label><input type="checkbox" name="links" ${panels.links ? 'checked' : ''}> Links</label><label><input type="checkbox" name="tickers" ${panels.tickers ? 'checked' : ''}> Ticker bar</label></div><label for="settingsTickers">Tickers</label><textarea id="settingsTickers" spellcheck="false" placeholder="MSFT, NVDA, BTC-USD"></textarea><div class="links-editor-actions"><button type="submit" class="primary">${icon('settings')}<span>Save config</span></button></div></form>`;
overlay.innerHTML = `<form class="links-editor-box settings-editor-box"><div class="links-editor-head"><div><h2>Configure</h2><p class="muted">Sidebar widgets and stock/crypto tickers are saved in dashboard-config.json.</p></div><button type="button" class="ghost" data-close-settings>Cancel</button></div><div class="settings-grid"><label><input type="checkbox" name="machine" ${panels.machine ? 'checked' : ''}> Machine</label><label class="settings-subsetting"><input type="checkbox" name="machineSensors" ${panels.machineSensors ? 'checked' : ''}> Thermal sensors</label><label><input type="checkbox" name="remoteHosts" ${panels.remoteHosts ? 'checked' : ''}> Remote hosts</label><label><input type="checkbox" name="containers" ${panels.containers ? 'checked' : ''}> Local containers</label><label><input type="checkbox" name="links" ${panels.links ? 'checked' : ''}> Links</label><label><input type="checkbox" name="tickers" ${panels.tickers ? 'checked' : ''}> Ticker bar</label><label><input type="checkbox" name="expandLists" ${panels.expandLists ? 'checked' : ''}> Expand lists (no scrollbars)</label></div><label for="settingsTickers">Tickers</label><textarea id="settingsTickers" spellcheck="false" placeholder="MSFT, NVDA, BTC-USD"></textarea><div class="links-editor-actions"><button type="submit" class="primary">${icon('settings')}<span>Save config</span></button></div></form>`;
document.body.appendChild(overlay);
const form = overlay.querySelector('form');
const textarea = overlay.querySelector('#settingsTickers');
Expand All @@ -87,6 +88,7 @@ function openSettingsEditor(focus) {
containers: data.has('containers'),
links: data.has('links'),
tickers: data.has('tickers'),
expandLists: data.has('expandLists'),
},
}).then(close).catch((error) => toast(error.message));
});
Expand Down
12 changes: 11 additions & 1 deletion public/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,12 @@ body.preview-fullscreen-open{overflow:hidden}
.container-item span{font-size:11px;color:var(--muted);margin-top:2px}
.container-item small{align-self:start;border:1px solid rgba(139,246,255,.22);border-radius:999px;padding:1px 6px;color:#c9fff3;font-size:10px;text-transform:uppercase}
.container-item em{grid-column:1 / -1;color:var(--muted);font-style:normal;font-size:11px}
.container-item .container-stats{grid-column:1 / -1;border:0;padding:0;text-transform:none;color:#9fe9dd;font-size:10.5px;font-variant-numeric:tabular-nums;letter-spacing:0}
.container-item.state-stopped{opacity:.5}
.container-item.state-stopped em{color:#9aa6b2}
.container-item.state-unhealthy{border-color:rgba(255,106,122,.45)}
.container-item.state-unhealthy em{color:#ffb3bd}
.container-health{font-size:11px;color:var(--muted);font-variant-numeric:tabular-nums;margin-bottom:2px}
/* Remote host monitor widget */
.remote-list{display:flex;flex-direction:column;gap:8px}
.remote-empty{font-size:12px}
Expand All @@ -441,7 +447,11 @@ body.preview-fullscreen-open{overflow:hidden}
.remote-head b{font-size:12.5px;color:var(--text)}
.remote-head small{font-size:11px;color:var(--muted);margin-top:2px}
.remote-count{font-size:11px;color:var(--muted);margin:1px 0 2px}
.remote-containers{display:flex;flex-direction:column;gap:6px;max-height:300px;overflow-y:auto}
.remote-containers{display:flex;flex-direction:column;gap:6px;max-height:300px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:rgba(139,246,255,.25) transparent}
.remote-containers::-webkit-scrollbar{width:6px}
.remote-containers::-webkit-scrollbar-thumb{background:rgba(139,246,255,.25);border-radius:6px}
/* "Expand lists (no scrollbars)" — let the lists grow to full height instead of scrolling. */
.expand-lists .remote-containers{max-height:none;overflow:visible}
.remote-container{padding:7px;background:#071017}
.remote-error{color:#ffc2cd;font-size:11px;line-height:1.35;overflow-wrap:anywhere}
.remote-metrics{display:grid;gap:5px}
Expand Down
2 changes: 1 addition & 1 deletion public/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ let summaryLoading = false;
let shellsLoading = false;
let dashboardSettings = {
tickers: [],
panels: { machine: true, machineSensors: true, containers: true, remoteHosts: true, links: true, tickers: true },
panels: { machine: true, machineSensors: true, containers: true, remoteHosts: true, links: true, tickers: true, expandLists: false },
};
const SHELL_LABEL_ALIASES_KEY = 'sdShellLabelAliases';
const SHELL_AUTO_TITLES_KEY = 'sdShellAutoTitles';
Expand Down
Loading