v0.20.0:0 - per-spark ssh-key copy + wireguard status badge

This commit is contained in:
Keysat
2026-06-15 09:53:40 -05:00
parent 5341fcc506
commit e87158c492
7 changed files with 133 additions and 10 deletions
+44 -4
View File
@@ -305,6 +305,32 @@ async function wakeSpark(name) {
}
}
// Generate-if-missing + copy this Spark's OUTBOUND ssh public key (the key the
// Spark uses to log in to other machines, e.g. the Mac). Distinct from the
// package's own key in the StartOS "Show Public Key" action.
async function copySparkSshKey(name, btn) {
if (btn) btn.disabled = true;
try {
const r = await fetchJSON(`/api/spark/${name}/ssh-key`, { method: 'POST' });
// Best-effort clipboard copy; on plain-HTTP this no-ops, but the dialog
// below always shows the key for manual selection.
await copyText(r.pubkey, btn);
const label = r.host ? `${name} (${r.host})` : name;
el('#sshkey-title').textContent = `${name} — SSH public key`;
el('#sshkey-intro').textContent = r.created
? `Generated a new SSH key on ${label} and copied it to your clipboard. This is the key ${name} uses to log in to OTHER machines.`
: `${label} already had an SSH key; copied its public key to your clipboard. This is the key ${name} uses to log in to OTHER machines.`;
el('#sshkey-value').textContent = r.pubkey;
el('#sshkey-install').textContent =
`mkdir -p ~/.ssh && echo '${r.pubkey}' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys`;
el('#sshkey-dialog').showModal();
} catch (e) {
alert(`Couldn't get the SSH key for ${name}: ${e.message}`);
} finally {
if (btn) btn.disabled = false;
}
}
function renderHardware() {
const panel = el('#hardware-panel');
const grid = el('#hardware-grid');
@@ -358,11 +384,21 @@ function renderHardware() {
if (s.gpu_temp_c != null) gpuExtras.push(`${s.gpu_temp_c}°C`);
if (s.gpu_power_w != null) gpuExtras.push(`${s.gpu_power_w.toFixed(0)}W`);
const gpuExtrasStr = gpuExtras.length ? ` · ${gpuExtras.join(' · ')}` : '';
// Read-only WireGuard badge: shown only when the Spark has a wg interface up.
// "VPN <ip>" means it's a peer on that tunnel (reachable off-LAN when the
// tunnel is up); it reflects interface presence, not live peer reachability.
const wgIp = s.wg_addr ? String(s.wg_addr).split('/')[0] : '';
const wgBadge = s.wg_iface
? ` · <span class="wg-badge" title="On WireGuard tunnel '${escapeHtml(s.wg_iface)}'${wgIp ? ' as ' + escapeHtml(wgIp) : ''} — reachable off-LAN while the tunnel is up">VPN${wgIp ? ' ' + escapeHtml(wgIp) : ''}</span>`
: '';
card.className = 'hw-card';
card.innerHTML = `
<div class="head">
<span class="name">${escapeHtml(s.hostname || key)}</span>
<span class="meta">${escapeHtml(key)} · ${escapeHtml(s.gpu_name || '')} · ${escapeHtml(s.uptime || '')}</span>
<span class="meta">${escapeHtml(key)} · ${escapeHtml(s.gpu_name || '')} · ${escapeHtml(s.uptime || '')}${wgBadge}</span>
<button class="icon-btn ssh-key-btn" data-ssh-key="${escapeHtml(key)}" title="Copy this Spark's SSH public key (creates one if it doesn't have one) — e.g. to let it log in to your Mac" aria-label="Copy SSH public key">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
</button>
</div>
<div class="hw-metric">
<span class="label">CPU</span>
@@ -1849,11 +1885,15 @@ async function init() {
el('#nim-prog-close').addEventListener('click', () => el('#nim-progress-dialog').close());
el('#open-connectivity').addEventListener('click', openConnectivityDialog);
el('#connectivity-close').addEventListener('click', () => el('#connectivity-dialog').close());
// Wake-on-LAN buttons live on unreachable hardware cards; delegate.
// Hardware-card buttons (Wake-on-LAN on unreachable cards; SSH-key copy on
// reachable ones) are rendered dynamically, so delegate from the grid.
el('#hardware-grid').addEventListener('click', (e) => {
const btn = e.target.closest('[data-wake]');
if (btn) wakeSpark(btn.dataset.wake);
const wbtn = e.target.closest('[data-wake]');
if (wbtn) { wakeSpark(wbtn.dataset.wake); return; }
const kbtn = e.target.closest('[data-ssh-key]');
if (kbtn) { copySparkSshKey(kbtn.dataset.sshKey, kbtn); return; }
});
el('#sshkey-close').addEventListener('click', () => el('#sshkey-dialog').close());
setupCatalogDialog();
setupAdvancedDialog();
// Open WebUI link from /api/config