v0.20.0:0 - per-spark ssh-key copy + wireguard status badge
This commit is contained in:
+44
-4
@@ -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
|
||||
|
||||
@@ -244,6 +244,24 @@
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog id="sshkey-dialog" class="modal">
|
||||
<form method="dialog" class="modal-form">
|
||||
<h3 id="sshkey-title">SSH public key</h3>
|
||||
<p id="sshkey-intro" class="muted small"></p>
|
||||
<div class="sshkey-row">
|
||||
<pre id="sshkey-value" class="snippet copyable" data-copy-self title="Click to copy"></pre>
|
||||
<button type="button" class="icon-btn" data-copy="#sshkey-value" title="Copy public key" aria-label="Copy 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>
|
||||
<p class="muted small">To let this Spark log in to another machine (e.g. your Mac), run this in a terminal <em>on that machine</em>:</p>
|
||||
<pre id="sshkey-install" class="snippet copyable" data-copy-self title="Click to copy"></pre>
|
||||
<div class="modal-actions">
|
||||
<button type="button" id="sshkey-close" class="btn">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog id="advanced-dialog" class="modal">
|
||||
<form method="dialog" class="modal-form" id="advanced-form">
|
||||
<h3 id="adv-title">Advanced settings</h3>
|
||||
|
||||
@@ -374,6 +374,12 @@ main {
|
||||
}
|
||||
.hw-card .head .name { font-weight: 600; font-size: 15px; }
|
||||
.hw-card .head .meta { color: var(--muted); font-size: 12px; margin-left: auto; }
|
||||
/* WireGuard "VPN <ip>" badge in the meta line — accent (green) = on a tunnel. */
|
||||
.hw-card .head .meta .wg-badge { color: var(--accent); font-weight: 600; cursor: help; }
|
||||
/* Copy-this-Spark's-ssh-key button pins to the top-right corner; meta keeps
|
||||
its margin-left:auto so name/meta/button read left→right→corner. */
|
||||
.hw-card .head .ssh-key-btn { align-self: flex-start; padding: 3px 6px; }
|
||||
.hw-card .head .ssh-key-btn svg { width: 13px; height: 13px; }
|
||||
.hw-card.unreachable { border-color: rgba(239, 68, 68, 0.4); }
|
||||
.hw-card.unreachable .name { color: var(--error); }
|
||||
.hw-card.unreachable ol { color: var(--muted); }
|
||||
@@ -387,6 +393,10 @@ main {
|
||||
}
|
||||
.hw-card .wol-row .btn { padding: 5px 10px; font-size: 12px; }
|
||||
.hw-card .mac-display { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
||||
/* SSH-key dialog: key line beside its copy button; long key wraps rather than scrolls. */
|
||||
.sshkey-row { display: flex; align-items: flex-start; gap: 8px; }
|
||||
.sshkey-row .snippet { flex: 1; margin: 0; white-space: pre-wrap; word-break: break-all; }
|
||||
#sshkey-install { white-space: pre-wrap; word-break: break-all; }
|
||||
|
||||
.connectivity-content {
|
||||
max-height: 360px;
|
||||
|
||||
Reference in New Issue
Block a user