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:
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user