import { h, PageHeader, Badge, StatusDot, Empty, Card, esc, enc, att_esc, $val, apiFetch, toast, dismissToast, openModal, closeModal, formModal, definePage, hComp, connect, reactive, requestUpdate, Link, createRouter, ToastContainer, render, parseZones } from '/static/hoover/index.js'; 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', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(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) { let done = false; const timer = setInterval(async () => { if (done) return clearInterval(timer); const r = await apiFetch('/api/certs/issue/' + enc(rid)); if (r.ok && r.data) { if (r.data.status === 'completed') { done = true; clearInterval(timer); toast('Certificate issued for ' + (r.data.domain || rid), 'success'); await load(state); } else if (r.data.status === 'failed') { done = true; clearInterval(timer); toast('Issuance failed: ' + (r.data.error || 'unknown'), 'error'); } } }, 2000); } async function load(state) { try { const r = await apiFetch('/api/certs/list'); if (r.ok) state.certs = r.data || []; else state.error = r.error; } catch (e) { state.error = String(e); } state.loading = false; } export default definePage({ init() { return { certs: [], loading: true, error: null }; }, subscribe: ['acme'], load, render(state) { if (state.loading) { return [ PageHeader({ title: 'Certificates' }), h('div', { class: 'card', key: 'loading' }, h('div', { class: 'card-body loading' }, 'Loading...'), ), ]; } if (state.error) { return [ PageHeader({ title: 'Certificates' }), h('div', { class: 'card', key: 'error' }, h('div', { class: 'card-body error-msg' }, state.error), ), ]; } const rows = state.certs.map(c => { const days = c.days_remaining; let badge; if (c.expired || (days !== undefined && days <= 0)) { badge = Badge({ text: 'Expired', variant: 'danger' }); } else if (days !== undefined && days <= 30) { badge = Badge({ text: days + 'd left', variant: 'warning' }); } else { badge = Badge({ text: days !== undefined ? days + 'd left' : 'N/A', variant: 'success' }); } 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), h('td', null, h('button', { class: 'btn btn-sm btn-outline', style: 'margin-right:4px;', 'on:click': 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'); }}, 'Renew'), h('button', { class: 'btn btn-sm btn-danger', 'on:click': async () => { if (!confirm('Remove certificate for ' + c.domain + '?')) return; const resp = await apiFetch('/api/certs/' + enc(c.domain), { method: 'DELETE' }); if (resp.ok) { toast('Certificate removed', 'success'); await load(state); } else { toast(resp.error || 'Failed', 'error'); } }}, 'Remove'), ), ); }); return [ PageHeader({ title: 'Certificates', subtitle: 'ACME certificate management', actions: h('button', { class: 'btn btn-primary', 'on:click': () => issueCertModal(state) }, 'Issue Certificate'), }), rows.length ? h('div', { class: 'card' }, h('table', { class: 'table' }, h('thead', null, h('tr', null, h('th', null, 'Domain'), h('th', null, 'Issuer'), h('th', null, 'Expiry'), h('th', null, 'Status'), h('th', { style: 'width:120px;' }, 'Actions'), ), ), h('tbody', null, ...rows), )) : Empty({ text: 'No certificates found. Issue a certificate to get started.' }), ]; }, });