From 55406eec23169b4142ff4cf768141c8f5d8068e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 08:01:07 +1000 Subject: [PATCH] Control: show shareable /register invite link in Applicants tab Co-Authored-By: Claude Opus 4.8 --- public/views/control.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/public/views/control.js b/public/views/control.js index 733fac1..73f4134 100644 --- a/public/views/control.js +++ b/public/views/control.js @@ -8,6 +8,8 @@ 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'; // ---- small UI helpers ------------------------------------------------------- @@ -75,6 +77,15 @@ async function renderApplicants(panel) { try { rows = await api.get(`${A}/applicants?status=pending`); } catch (e) { return mount(panel, errBox(e)); } + // Shareable invite link — the public, Google-gated registration page. Hand + // this to prospective testers; their submission lands here as a pending row. + const inviteLink = el('a', { href: safeHref(REGISTER_URL), target: '_blank', rel: 'noopener', + style: { color: 'var(--accent,#5ec27a)', wordBreak: 'break-all' } }, REGISTER_URL); + const inviteCard = el('div', { class: 'card', style: { display: 'flex', alignItems: 'center', gap: '0.6rem', flexWrap: 'wrap', marginBottom: '0.9rem' } }, + el('span', { class: 'muted', style: { fontSize: '0.8rem' } }, 'Invite testers:'), + inviteLink, + btn('Copy link', () => navigator.clipboard?.writeText(REGISTER_URL), 'ghost')); + // Add-applicant form (owner adds a tester directly; the public /register page // is a later addition). Posts to POST /admin/applicants {email,label}. const emailI = el('input', { class: 'lk-url', type: 'email', placeholder: 'email (optional)' }); @@ -121,6 +132,7 @@ async function renderApplicants(panel) { mount(panel, el('div', { class: 'term-bar' }, el('span', { class: 'term-title' }, '◆ Pending applicants'), btn('Refresh', () => renderApplicants(panel), 'ghost')), + inviteCard, addCard, rows.length ? table(['Applicant', 'Note', 'Status', 'Action', ''], body) : el('p', { class: 'muted' }, 'No pending applicants.'));