From 93a13c988561b8bab944c43b3273d10c23fa227a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 07:29:18 +1000 Subject: [PATCH] Control: add 'Add applicant' form to Applicants tab Owner can add a tester directly (email/label) via the existing POST /api/control/admin/applicants proxy route, instead of relying on the not-yet-built public /register page. Co-Authored-By: Claude Opus 4.8 --- public/views/control.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/public/views/control.js b/public/views/control.js index 3e532fc..733fac1 100644 --- a/public/views/control.js +++ b/public/views/control.js @@ -75,6 +75,23 @@ async function renderApplicants(panel) { try { rows = await api.get(`${A}/applicants?status=pending`); } catch (e) { return mount(panel, errBox(e)); } + // 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)' }); + const labelI = el('input', { class: 'lk-url', placeholder: 'label / name' }); + const addMsg = el('span', { class: 'muted', style: { fontSize: '0.78rem' } }, ''); + const addBtn = btn('Add applicant', async () => { + if (!emailI.value.trim() && !labelI.value.trim()) return notify(addMsg, 'email or label required', false); + try { + await api.post(`${A}/applicants`, { email: emailI.value.trim(), label: labelI.value.trim() }); + emailI.value = ''; labelI.value = ''; + renderApplicants(panel); + } catch (e) { notify(addMsg, e.message || 'add failed', false); } + }, 'primary'); + const addCard = el('div', { class: 'card', style: { display: 'grid', gap: '0.5rem', marginBottom: '0.9rem', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', alignItems: 'end' } }, + field('Email', emailI), field('Label', labelI), + 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' } }, ''); const groupSel = select([{ value: '', label: '(default group)' }, ...groupsCache.map(g => ({ value: g.id, label: g.name }))]); @@ -104,6 +121,7 @@ async function renderApplicants(panel) { mount(panel, el('div', { class: 'term-bar' }, el('span', { class: 'term-title' }, '◆ Pending applicants'), btn('Refresh', () => renderApplicants(panel), 'ghost')), + addCard, rows.length ? table(['Applicant', 'Note', 'Status', 'Action', ''], body) : el('p', { class: 'muted' }, 'No pending applicants.')); }