refactor: extract shared utilities and standardize page patterns

- Add fmtBytes() and csvToArr() helpers to hoover/helpers.js
- Replace inline async patterns with ActionButton/ConfirmDelete in wireguard.js
- Convert addDomain/editDomain to QuickModal + apiSubmit in proxy.js
- Convert settingsModal handlers to formAction in certs.js
- Remove redundant synced handling from dhcp.js apply button
- Add onComplete callback to ConfirmDelete (fixes users.js onRefresh bug)
- Fix passkeys.js ActionCell/Table usage (invalid component API)
- Remove duplicate fmtBytes from dashboard.js
This commit is contained in:
2026-07-28 17:32:51 +00:00
parent 244576b8eb
commit 8bb3619ddc
10 changed files with 156 additions and 220 deletions
+16 -50
View File
@@ -1,5 +1,5 @@
/** WireGuard page — tunnel & peer management. */
import { html, PageHeader, Badge, StatusDot, Empty, Table, ServiceStatus, renderGuard, esc, enc, $val, apiFetch, toast, openModal, closeModal, formModal, definePage, getModel, modelFetch, ActionButton, ActionCell, MonoText, ActionGroup, QuickModal, downloadBlob, formAction, ApplyConfirm, qrSVG } from '/static/hoover/index.js';
import { html, PageHeader, Badge, StatusDot, Empty, Table, ServiceStatus, renderGuard, esc, enc, $val, apiFetch, toast, openModal, closeModal, formModal, definePage, getModel, modelFetch, ActionButton, ActionCell, ConfirmDelete, MonoText, ActionGroup, QuickModal, downloadBlob, formAction, ApplyConfirm, qrSVG, csvToArr } from '/static/hoover/index.js';
/* ── LAN detection helper ────────────────────────────────────── */
function getLanSubnets() {
@@ -22,11 +22,7 @@ function getLanSubnets() {
}
}
/* ── Allowed IPs helper ──────────────────────────────────────── */
function parseAllowedIps(value) {
if (!value || !value.trim()) return [];
return value.split(',').map(s => s.trim()).filter(Boolean);
}
/* ── Color helpers ────────────────────────────────────────────── */
function classColor(classKey) {
@@ -63,7 +59,7 @@ const addPeer = QuickModal({
} else if (preset === 'none') {
allowed_ips = [];
} else if (preset === 'custom') {
allowed_ips = parseAllowedIps($val('wg-allowed'));
allowed_ips = csvToArr($val('wg-allowed'));
} else {
allowed_ips = ['0.0.0.0/0'];
}
@@ -241,7 +237,7 @@ function settingsModal(wireguardData, state) {
handler: formAction(async () => {
const port = parseInt($val('wg-port'), 10);
if (isNaN(port) || port < 1 || port > 65535) throw 'Invalid port';
const addresses = ($val('wg-addrs') || '').split(',').map(s => s.trim()).filter(Boolean);
const addresses = csvToArr($val('wg-addrs'));
if (!addresses.length) throw 'At least one address required';
const body = {
interface: {
@@ -342,43 +338,6 @@ function editClassModal(key, cls, peerCount) {
});
}
async function initClassKeys(classKey) {
const resp = await apiFetch('/api/wireguard/classes/keys/' + enc(classKey), {
method: 'POST',
});
if (!resp.ok) {
toast(resp.error || 'Failed to generate keys', 'error');
return;
}
toast('Keys generated for class "' + classKey + '"', 'success');
modelFetch('wireguard');
}
async function deleteAccessClass(key) {
if (!confirm(`Delete access class '${key}'?`)) return;
const resp = await apiFetch('/api/wireguard/classes', {
method: 'DELETE',
body: { key },
});
if (!resp.ok) {
toast(resp.error || 'Failed to delete class', 'error');
return;
}
toast('Class deleted', 'success');
modelFetch('wireguard');
}
async function toggleClassTunnel(classKey, isUp) {
const url = '/api/wireguard/classes/' + enc(classKey) + '/' + (isUp ? 'down' : 'up');
const resp = await apiFetch(url, { method: 'POST' });
if (!resp.ok) {
toast(resp.error || 'Failed', 'error');
return;
}
toast('Tunnel ' + (isUp ? 'stopped' : 'started'), 'success');
modelFetch('wireguard');
}
/* ── Access Classes Section ──────────────────────────────────── */
function renderAccessClasses(config, status) {
const classes = config?.access_classes || {};
@@ -414,13 +373,18 @@ function renderAccessClasses(config, status) {
</td>
<td>
${!hasKeys
? html`<button class="btn btn-sm btn-warning" onClick=${() => initClassKeys(k)} title="Generate keys">Keys</button>`
? html`<${ActionButton} url=${'/api/wireguard/classes/keys/' + enc(k)} label="Keys"
cls="btn btn-sm btn-warning" successMsg=${'Keys generated for ' + esc(k)} refresh="wireguard" />`
: ''}
<button class="btn btn-sm btn-outline" onClick=${() => editClassModal(k, v, pCount)}>Edit</button>
<button class="btn btn-sm btn-outline" onClick=${() => toggleClassTunnel(k, isUp)}>${isUp ? 'Stop' : 'Start'}</button>
<${ActionButton} url=${'/api/wireguard/classes/' + enc(k) + '/' + (isUp ? 'down' : 'up')}
cls="btn btn-sm btn-outline" labelOn="Stop" labelOff="Start" condition=${isUp}
successMsg=${isUp ? 'Tunnel stopped' : 'Tunnel started'} refresh="wireguard" />
${(pCount > 0)
? html`<button class="btn btn-sm btn-outline" disabled title="Peers reference this class">Delete</button>`
: html`<button class="btn btn-sm btn-outline" onClick=${() => deleteAccessClass(k)}>Delete</button>`}
: html`<${ConfirmDelete} url=${'/api/wireguard/classes'} body=${{ key: k }}
deleteKey=${k} message=${'Delete access class ' + esc(k) + '?'} success="Class deleted"
refresh="wireguard" label="Delete" />`}
</td>
</tr>`;
});
@@ -528,10 +492,12 @@ export default definePage({
<div class="d-flex justify-content-between">
<span>Subnet: ${esc(v.subnet || '-')}</span>
<span>LAN: ${v.lan_access ? 'Yes' : 'No'}</span>
<span>Keys: ${classHasKeys(v) ? 'Ready' : html`<button class="btn btn-xs btn-warning" onClick=${() => initClassKeys(k)}>Generate</button>`}</span>
<span>Keys: ${classHasKeys(v) ? 'Ready' : html`<${ActionButton} url=${'/api/wireguard/classes/keys/' + enc(k)} label="Generate" cls="btn btn-xs btn-warning" successMsg=${'Keys generated'} refresh="wireguard" />`}</span>
</div>
<div style="margin-top: 4px;">
<button class="btn btn-xs btn-outline" onClick=${() => toggleClassTunnel(k, isUp)}>${isUp ? 'Stop' : 'Start'}</button>
<${ActionButton} url=${'/api/wireguard/classes/' + enc(k) + '/' + (isUp ? 'down' : 'up')}
cls="btn btn-xs btn-outline" labelOn="Stop" labelOff="Start" condition=${isUp}
successMsg=${isUp ? 'Stopped' : 'Started'} refresh="wireguard" />
</div>
</div>
</div>`;