import { h, PageHeader, Badge, Empty, Table, renderGuard, esc, enc, $val, apiFetch, toast, openModal, closeModal, formModal, definePage, refactorLoad, ActionCell, certStatusBadge, poll } from '/static/hoover/index.js?v=6'; function issueCertModal(state) { openModal((inner, idx) => { formModal(inner, 'Issue Certificate', [ { label: 'Domain', id: 'ic-domain', placeholder: 'example.com' }, { label: 'Email (optional)', id: 'ic-email', placeholder: 'you@example.com' }, ], [ { label: 'Cancel', cls: 'btn-outline', action: 'c', handler: () => closeModal(idx) }, { label: 'Issue', cls: 'btn-primary', action: 's', handler: async () => { const domain = ($val('ic-domain') || '').trim(); if (!domain) { toast('Domain is required', 'error'); return; } const body = { domain, email: ($val('ic-email') || '').trim() || undefined }; const resp = await apiFetch('/api/certs/issue/start', { method: 'POST', body, }); if (resp.ok) { toast('Issuance started for ' + domain, 'success'); closeModal(idx); const rid = resp.data?.request_id; if (rid) pollCertIssue(rid, state); } else { toast(resp.error || 'Failed', 'error'); } }, }, ], ); }); } async function pollCertIssue(rid, state) { poll({ url: '/api/certs/issue/' + enc(rid), successKey: (d) => d.status === 'completed', onErrorKey: (d) => d.status === 'failed', onComplete: (d) => { toast('Certificate issued for ' + (d.domain || rid), 'success'); load(state); }, onError: (d) => { toast('Issuance failed: ' + (d?.error || 'unknown'), 'error'); }, }); } async function load(state, abortController, entry) { await refactorLoad(state, s => s.certs?.length, async (s, sig, isAborted) => { const r = await apiFetch('/api/certs/list', { signal: sig }); if (isAborted()) return; if (r.ok) s.certs = r.data || []; else s.error = r.error; }, { entry, abortController }, ); } export default definePage({ init() { return { certs: [] }; }, subscribe: ['acme'], load, render(state) { const guard = renderGuard(state, 'Certificates', 'ACME certificate management', state.certs); if (guard) return guard; const rows = state.certs.map(c => { const badge = certStatusBadge({ expired: c.expired, daysRemaining: c.days_remaining }); return h('tr', { key: c.domain }, h('td', null, h('strong', null, esc(c.domain || 'unknown'))), h('td', { class: 'text-sm' }, esc(c.issuer || '-')), h('td', null, esc(c.expiry || 'N/A')), h('td', null, badge), ActionCell({ editLabel: 'Renew', editClick: async () => { const resp = await apiFetch('/api/certs/' + enc(c.domain) + '/renew', { method: 'POST' }); if (resp.ok) toast('Renewal started for ' + c.domain, 'success'); else toast(resp.error || 'Failed', 'error'); }, removeUrl: '/api/certs/' + enc(c.domain), removeMessage: 'Remove certificate for ' + c.domain + '?', removeSuccess: 'Certificate removed', removeReload: () => load(state), }), ); }); return [ PageHeader({ title: 'Certificates', subtitle: 'ACME certificate management', actions: h('button', { class: 'btn btn-primary', 'on:click': () => issueCertModal(state) }, 'Issue Certificate'), }), rows.length ? Table({ columns: ['Domain', 'Issuer', 'Expiry', 'Status', 'Actions'], rows, }) : Empty({ text: 'No certificates found. Issue a certificate to get started.' }), ]; }, });