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 addPeerModal(state) { openModal((inner, idx) => { formModal(inner, 'Add WireGuard Peer', [ { label: 'Name', id: 'wg-name', placeholder: 'client-name' }, { label: 'Endpoint (optional)', id: 'wg-endpoint', placeholder: '1.2.3.4:51820' }, { label: 'Allowed IPs (optional)', id: 'wg-allowed', placeholder: '10.0.0.2/32' }, { label: 'Persistent Keepalive (optional)', id: 'wg-keepalive', type: 'number', placeholder: '25' }, ], [ { label: 'Cancel', cls: 'btn-outline', action: 'c', handler: () => closeModal(idx) }, { label: 'Add', cls: 'btn-primary', action: 's', handler: async () => { const body = { name: ($val('wg-name') || '').trim(), endpoint: ($val('wg-endpoint') || '').trim() || undefined, allowed_ips: ($val('wg-allowed') || '').trim() ? [($val('wg-allowed') || '').trim()] : [], persistent_keepalive: $val('wg-keepalive') ? parseInt($val('wg-keepalive')) : undefined, }; if (!body.name) { toast('Name is required', 'error'); return; } const resp = await apiFetch('/api/wireguard/peers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (resp.ok) { toast('Peer added', 'success'); closeModal(idx); await load(state); } else { toast(resp.error || 'Failed', 'error'); } }, }, ], ); }); } function downloadConfigModal(peerName, config, state) { openModal((inner, idx) => { formModal(inner, 'Download Config for ' + peerName, [{ label: 'Server Endpoint', id: 'wg-srv-endpoint', placeholder: 'vpn.example.com:51820' }], [ { label: 'Cancel', cls: 'btn-outline', action: 'c', handler: () => closeModal(idx) }, { label: 'Generate', cls: 'btn-primary', action: 's', handler: async () => { const endpoint = ($val('wg-srv-endpoint') || '').trim(); if (!endpoint) { toast('Server endpoint is required', 'error'); return; } const resp = await apiFetch('/api/wireguard/generate-client', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: peerName, server_endpoint: endpoint }), }); if (resp.ok && resp.data?.config) { const blob = new Blob([resp.data.config], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = peerName + '.conf'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); toast('Config downloaded', 'success'); closeModal(idx); } else { toast(resp.error || 'Failed', 'error'); } }, }, ], ); }); } async function load(state) { try { const stR = await apiFetch('/api/wireguard/status'); if (stR.ok) state.status = stR.data || {}; const pR = await apiFetch('/api/wireguard/peers'); if (pR.ok) state.peers = pR.data || []; const cfgR = await apiFetch('/api/wireguard/config'); if (cfgR.ok) state.config = cfgR.data || {}; } catch (e) { state.error = String(e); } state.loading = false; } export default definePage({ init() { return { status: {}, peers: [], config: {}, loading: true, error: null }; }, subscribe: ['wireguard'], load, render(state) { if (state.loading) { return [ PageHeader({ title: 'WireGuard' }), h('div', { class: 'card', key: 'loading' }, h('div', { class: 'card-body loading' }, 'Loading...'), ), ]; } if (state.error) { return [ PageHeader({ title: 'WireGuard' }), h('div', { class: 'card', key: 'error' }, h('div', { class: 'card-body error-msg' }, state.error), ), ]; } const st = state.status || {}; const isUp = st.state === 'up'; const listenPort = (state.config?.interface || {}).listen_port || '-'; const peerRows = state.peers.map(p => { const hasHandshake = !!p.latest_handshake; return h('tr', { key: p.name }, h('td', null, StatusDot({ status: hasHandshake ? 'success' : 'danger' }), h('strong', null, esc(p.name || 'unnamed')), ), h('td', { style: 'font-family:monospace;font-size:11px;' }, esc((p.public_key || 'N/A').substring(0, 20)) + (p.public_key && p.public_key.length > 20 ? '...' : ''), ), h('td', { class: 'text-sm' }, esc(p.allowed_ips || '-')), h('td', { class: 'text-sm' }, esc(p.endpoint || '-')), h('td', { class: 'text-sm' }, esc(p.latest_handshake || 'Never')), h('td', { class: 'text-sm' }, 'Recv: ' + esc(p.transfer_recv || '0'), h('br'), 'Sent: ' + esc(p.transfer_sent || '0'), ), h('td', null, h('button', { class: 'btn btn-sm btn-outline', style: 'margin-right:4px;', 'on:click': () => downloadConfigModal(p.name, state.config, state) }, 'Config'), h('button', { class: 'btn btn-sm btn-danger', 'on:click': async () => { if (!confirm('Remove peer ' + p.name + '?')) return; const resp = await apiFetch('/api/wireguard/peers/' + enc(p.name), { method: 'DELETE' }); if (resp.ok) { toast('Peer removed', 'success'); await load(state); } else { toast(resp.error || 'Failed', 'error'); } }}, 'Remove'), ), ); }); const actions = h('div', { style: 'display:flex;gap:8px;' }, h('button', { class: 'btn btn-primary', 'on:click': () => addPeerModal(state) }, 'Add Peer'), h('button', { class: 'btn btn-outline', 'on:click': async () => { const resp = await apiFetch('/api/wireguard/' + (isUp ? 'down' : 'up'), { method: 'POST' }); if (resp.ok) { toast('Tunnel ' + (isUp ? 'stopped' : 'started'), 'success'); await load(state); } else { toast(resp.error || 'Failed', 'error'); } }}, isUp ? 'Stop' : 'Start'), h('button', { class: 'btn btn-outline', 'on:click': async () => { const resp = await apiFetch('/api/wireguard/apply', { method: 'POST' }); if (resp.ok) { toast('Config applied', 'success'); await load(state); } else { toast(resp.error || 'Failed', 'error'); } }}, 'Apply'), ); return [ PageHeader({ title: 'WireGuard', subtitle: 'Tunnel: ' + (st.state || 'unknown') + ', Listen: ' + listenPort, actions, }), h('div', null, StatusDot({ status: isUp ? 'success' : 'danger' }), ' ', Badge({ text: st.state || 'down', variant: isUp ? 'success' : 'danger' }), ), peerRows.length ? h('div', { class: 'card' }, h('table', { class: 'table' }, h('thead', null, h('tr', null, h('th', null, 'Peer'), h('th', null, 'Public Key'), h('th', null, 'Allowed IPs'), h('th', null, 'Endpoint'), h('th', null, 'Handshake'), h('th', null, 'Transfer'), h('th', { style: 'width:120px;' }, 'Actions'), ), ), h('tbody', null, ...peerRows), )) : Empty({ text: 'No peers configured. Add a peer above.' }), ]; }, });