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 TIERS = ['lock', 'uninstall-keep', 'wipe'];
|
||||||
const A = '/api/control/admin';
|
const A = '/api/control/admin';
|
||||||
// Public, Google-gated self-service registration page testers are sent to.
|
// 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 -------------------------------------------------------
|
// ---- small UI helpers -------------------------------------------------------
|
||||||
|
|
||||||
@@ -104,17 +126,16 @@ async function renderApplicants(panel) {
|
|||||||
el('div', { style: { display: 'flex', alignItems: 'center', gap: '0.5rem' } }, addBtn, addMsg));
|
el('div', { style: { display: 'flex', alignItems: 'center', gap: '0.5rem' } }, addBtn, addMsg));
|
||||||
|
|
||||||
const body = rows.map(a => {
|
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 groupSel = select([{ value: '', label: '(default group)' }, ...groupsCache.map(g => ({ value: g.id, label: g.name }))]);
|
||||||
const approve = btn('Approve', async () => {
|
const approve = btn('Approve', async () => {
|
||||||
try {
|
try {
|
||||||
const r = await api.post(`${A}/applicants/${a.id}/approve`, groupSel.value ? { group_id: groupSel.value } : {});
|
const r = await api.post(`${A}/applicants/${a.id}/approve`, groupSel.value ? { group_id: groupSel.value } : {});
|
||||||
clear(msg);
|
clear(msg);
|
||||||
const code = r.claim_code || r.code || '';
|
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)', fontSize: '0.72rem' } }, '✓ Approved'));
|
||||||
msg.appendChild(el('span', { style: { color: 'var(--accent,#5ec27a)' } }, 'Claim code: '));
|
msg.appendChild(deployPanel(code));
|
||||||
msg.appendChild(codeEl);
|
|
||||||
msg.appendChild(btn('Copy', () => navigator.clipboard?.writeText(code), 'ghost'));
|
|
||||||
} catch (e) { notify(msg, e.message || 'approve failed', false); }
|
} catch (e) { notify(msg, e.message || 'approve failed', false); }
|
||||||
}, 'primary');
|
}, 'primary');
|
||||||
const deny = btn('Deny', async () => {
|
const deny = btn('Deny', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user