633505e7dc
- Add quick modal, table, service status, and confirmation dialog components - Refactor all pages (certs, dhcp, proxy, etc.) to use new component patterns - Introduce refactor load utility and render guard for consistent UX - Add hoover documentation and update AGENTS.md, architecture, overview
147 lines
6.5 KiB
JavaScript
147 lines
6.5 KiB
JavaScript
import { h, PageHeader, Badge, StatusDot, Empty, Table, ServiceStatus, renderGuard, esc, enc, $val, apiFetch, toast, openModal, closeModal, formModal, definePage, apiSubmit, refactorLoad, ActionButton, ActionCell, MonoText, ActionGroup, QuickModal, downloadBlob } from '/static/hoover/index.js?v=6';
|
|
|
|
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',
|
|
},
|
|
reload: (s) => load(s),
|
|
});
|
|
|
|
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');
|
|
}
|
|
},
|
|
},
|
|
],
|
|
);
|
|
});
|
|
}
|
|
|
|
async function load(state, abortController, entry) {
|
|
await refactorLoad(state,
|
|
s => s.peers?.length,
|
|
async (s, sig, isAborted) => {
|
|
const stR = await apiFetch('/api/wireguard/status', { signal: sig });
|
|
if (isAborted()) return;
|
|
if (stR.ok) s.status = stR.data || {};
|
|
else s.error = stR.error;
|
|
const pR = await apiFetch('/api/wireguard/peers', { signal: sig });
|
|
if (isAborted()) return;
|
|
if (pR.ok) s.peers = pR.data || [];
|
|
else if (!s.error) s.error = pR.error;
|
|
const cfgR = await apiFetch('/api/wireguard/config', { signal: sig });
|
|
if (isAborted()) return;
|
|
if (cfgR.ok) s.config = cfgR.data || {};
|
|
else if (!s.error) s.error = cfgR.error;
|
|
},
|
|
{ entry, abortController },
|
|
);
|
|
}
|
|
|
|
export default definePage({
|
|
init() {
|
|
return { status: {}, peers: [], config: {} };
|
|
},
|
|
subscribe: ['wireguard'],
|
|
load,
|
|
render(state) {
|
|
const guard = renderGuard(state, 'WireGuard', 'Tunnel: ' + ((state.status || {}).state || 'unknown') + ', Listen: ' + ((state.config?.interface || {}).listen_port || '-'), state.peers);
|
|
if (guard) return guard;
|
|
|
|
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', 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.config, state),
|
|
removeUrl: '/api/wireguard/peers/' + enc(p.name),
|
|
removeMessage: 'Remove peer ' + p.name + '?',
|
|
removeSuccess: 'Peer removed',
|
|
removeReload: () => load(state),
|
|
}),
|
|
);
|
|
});
|
|
|
|
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'),
|
|
reload: () => load(state),
|
|
}),
|
|
ActionButton({
|
|
url: '/api/wireguard/apply',
|
|
successMsg: 'Config applied',
|
|
label: 'Apply',
|
|
reload: () => load(state),
|
|
}),
|
|
);
|
|
|
|
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.' }),
|
|
];
|
|
},
|
|
});
|