import { h, PageHeader, Badge, StatusDot, Empty, Table, ServiceStatus, renderGuard, esc, enc, $val, apiFetch, toast, openModal, closeModal, formModal, definePage, getModel, modelFetch, ActionButton, ActionCell, MonoText, ActionGroup, QuickModal, downloadBlob } from '/static/hoover/index.js?v=7'; const addPeer = QuickModal({ title: 'Add WireGuard Peer', fields: [ { 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' }, ], submit: { url: '/api/wireguard/peers', 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, }), validate: (b) => !b.name ? 'Name is required' : null, successMsg: 'Peer added', }, refresh: 'wireguard', }); 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', body: { name: peerName, server_endpoint: endpoint }, }); if (resp.ok && resp.data?.config) { downloadBlob(new Blob([resp.data.config], { type: 'text/plain' }), peerName + '.conf'); toast('Config downloaded', 'success'); closeModal(idx); } else { toast(resp.error || 'Failed', 'error'); } }, }, ], ); }); } export default definePage({ init() { return { wireguard: getModel('wireguard'), }; }, render(state) { const guard = renderGuard(state.wireguard, 'WireGuard', null, state.wireguard.data?.peers); if (guard) return guard; const st = state.wireguard.data?.status || {}; const isUp = st.state === 'up'; const listenPort = (state.wireguard.data?.config?.interface || {}).listen_port || '-'; const peerRows = (state.wireguard.data?.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', null, MonoText({ text: p.public_key || 'N/A', maxLength: 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'), ), ActionCell({ editLabel: 'Config', editClick: () => downloadConfigModal(p.name, state.wireguard.data?.config, state), removeUrl: '/api/wireguard/peers/' + enc(p.name), removeMessage: 'Remove peer ' + p.name + '?', removeSuccess: 'Peer removed', removeRefresh: 'wireguard', }), ); }); const actions = ActionGroup( h('button', { class: 'btn btn-primary', 'on:click': () => addPeer(state) }, 'Add Peer'), ActionButton({ url: '/api/wireguard/' + (isUp ? 'down' : 'up'), labelOn: 'Stop', labelOff: 'Start', condition: isUp, successMsg: 'Tunnel ' + (isUp ? 'stopped' : 'started'), refresh: 'wireguard', }), ActionButton({ url: '/api/wireguard/apply', successMsg: 'Config applied', label: 'Apply', refresh: 'wireguard', }), ); return [ PageHeader({ title: 'WireGuard', subtitle: 'Tunnel: ' + (st.state || 'unknown') + ', Listen: ' + listenPort, actions, }), ServiceStatus({ state: st.state || 'down' }), peerRows.length ? Table({ columns: ['Peer', 'Public Key', 'Allowed IPs', 'Endpoint', 'Handshake', 'Transfer', 'Actions'], rows: peerRows, }) : Empty({ text: 'No peers configured. Add a peer above.' }), ]; }, });