Control: Deploy panel (one-liner/download/docker/landing) replaces raw claim code

On approve, show the tester's zero-touch deploy options with Copy buttons;
manual code kept as fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-06-15 10:58:34 +10:00
parent cd4fee92dd
commit 0c7d5c7382

View File

@@ -9,7 +9,29 @@ import { api } from '../api.js';
const TIERS = ['lock', 'uninstall-keep', 'wipe'];
const A = '/api/control/admin';
// Public, Google-gated self-service registration page testers are sent to.
const REGISTER_URL = 'https://ivctl.hynesy.com/register';
const IVCTL_BASE = 'https://ivctl.hynesy.com';
const REGISTER_URL = `${IVCTL_BASE}/register`;
// A label + monospace value + Copy button row (used by the Deploy panel).
function copyRow(label, value) {
return el('div', { style: { display: 'flex', gap: '0.4rem', alignItems: 'center', margin: '0.12rem 0' } },
el('span', { class: 'muted', style: { fontSize: '0.66rem', minWidth: '74px' } }, label),
el('code', { style: { userSelect: 'all', wordBreak: 'break-all', fontSize: '0.7rem', flex: '1' } }, value),
btn('Copy', () => navigator.clipboard?.writeText(value), 'ghost'));
}
// Zero-touch deploy options for a freshly approved instance. The tester runs ONE
// of these; the artifact is already keyed, so there's no claim code to enter.
function deployPanel(code) {
return el('div', { class: 'card', style: { display: 'grid', gap: '0.15rem', marginTop: '0.3rem', padding: '0.5rem 0.6rem' } },
el('div', { class: 'term-title', style: { fontSize: '0.7rem' } }, '◆ Deploy — send the tester ONE of these'),
copyRow('One-liner', `curl -fsSL ${IVCTL_BASE}/provision/install/${code} | sudo bash`),
copyRow('Download', `${IVCTL_BASE}/provision/get/${code}`),
copyRow('Docker', `${IVCTL_BASE}/provision/docker/${code}`),
copyRow('Landing', `${IVCTL_BASE}/provision/${code}`),
el('div', { class: 'muted', style: { fontSize: '0.64rem', marginTop: '0.2rem' } },
'Manual fallback code: ', el('code', { style: { userSelect: 'all' } }, code)));
}
// ---- small UI helpers -------------------------------------------------------
@@ -104,17 +126,16 @@ async function renderApplicants(panel) {
el('div', { style: { display: 'flex', alignItems: 'center', gap: '0.5rem' } }, addBtn, addMsg));
const body = rows.map(a => {
const msg = el('span', { class: 'muted', style: { fontSize: '0.75rem' } }, '');
// A div (not span) so the post-approve Deploy panel can hold block content.
const msg = el('div', { class: 'muted', style: { fontSize: '0.75rem' } }, '');
const groupSel = select([{ value: '', label: '(default group)' }, ...groupsCache.map(g => ({ value: g.id, label: g.name }))]);
const approve = btn('Approve', async () => {
try {
const r = await api.post(`${A}/applicants/${a.id}/approve`, groupSel.value ? { group_id: groupSel.value } : {});
clear(msg);
const code = r.claim_code || r.code || '';
const codeEl = el('code', { style: { fontWeight: '700', userSelect: 'all' } }, code);
msg.appendChild(el('span', { style: { color: 'var(--accent,#5ec27a)' } }, 'Claim code: '));
msg.appendChild(codeEl);
msg.appendChild(btn('Copy', () => navigator.clipboard?.writeText(code), 'ghost'));
msg.appendChild(el('span', { style: { color: 'var(--accent,#5ec27a)', fontSize: '0.72rem' } }, '✓ Approved'));
msg.appendChild(deployPanel(code));
} catch (e) { notify(msg, e.message || 'approve failed', false); }
}, 'primary');
const deny = btn('Deny', async () => {