WireGuard access classes, firewall nftables fixes, network sync event refactor

- WireGuard: refactor to multi-interface 'access classes' model; extract config
  generation and helpers into lib/wireguard.py; add per-class up/down endpoints
  and API routes; update UI with class management pages and QR code component
- Firewall: fix zone creation with --new-zone before --set-target; skip
  masquerade on public zone; add masquerade propagation for nftables backend
  so NAT works when internal zones exit via public
- Network: rename sync event subsystem 'network' -> 'networkd'; always stamp
  config hash even when deployment fails (fixes pending-changes detection)
- DHCP: add new API endpoint and update frontend page
- State/Sync: update state collectors and sync buses for new subsystems
- Docs: update API and config documentation for new endpoints and schemas
This commit is contained in:
2026-07-20 03:57:16 +00:00
parent dadabd7954
commit 04417cf05c
19 changed files with 2688 additions and 455 deletions
+154
View File
@@ -0,0 +1,154 @@
/**
* Hoover — components/qr.js
*
* QR code SVG renderer with optional logo overlay.
* Uses qrcode-svg library for generation.
*/
import { h } from '../vdom.js?v=9';
import { esc } from '../helpers.js?v=9';
import QRCode from '../../../vendor/qrcode-svg-1.1.0.js';
/**
* Generate a QR code SVG string from text content.
*
* @param {object} props
* @param {string} props.text - Text to encode
* @param {number} [props.size=200] - QR code size in px
* @param {number} [props.margin=2] - Quiet zone margin
* @param {string} [props.ecLevel='Q'] - Error correction level (L/M/Q/H)
* @param {string} [props.logo] - Base64 data URL for center logo
* @param {number} [props.logoSize=40] - Logo size in px (when overlaying)
* @param {string} [props.color] - Foreground color (default: #000000)
* @param {string} [props.background] - Background color (default: #ffffff)
* @returns {string} SVG markup string
*/
export function qrSVG(props = {}) {
const {
text,
size = 200,
margin = 2,
ecLevel = 'Q',
logo,
logoSize = 40,
color = '#000000',
background = '#ffffff',
} = props;
if (!text) return '';
const qr = new QRCode({
content: text,
container: 'svg',
margin: margin,
padding: 0,
width: size,
height: size,
color: color,
background: background,
ecl: ecLevel,
creambo: false,
prettyprint: false,
});
let svg = qr.svg();
// Add center logo overlay if provided
if (logo) {
const halfSize = size / 2;
const halfLogo = logoSize / 2;
const ns = 'http://www.w3.org/2000/svg';
const parser = new DOMParser();
const doc = parser.parseFromString(svg, 'image/svg+xml');
const rootSvg = doc.documentElement;
const viewBox = rootSvg.getAttribute('viewBox') || `0 0 ${size} ${size}`;
const newSvg = doc.createElementNS(ns, 'svg');
newSvg.setAttribute('xmlns', ns);
newSvg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
newSvg.setAttribute('width', size);
newSvg.setAttribute('height', size);
newSvg.setAttribute('viewBox', viewBox);
// Clone original content
const clone = rootSvg.cloneNode(true);
while (clone.firstChild) {
newSvg.appendChild(clone.firstChild);
}
// White background behind logo
const bgRect = doc.createElementNS(ns, 'rect');
bgRect.setAttribute('x', halfSize - halfLogo - 4);
bgRect.setAttribute('y', halfSize - halfLogo - 4);
bgRect.setAttribute('width', logoSize + 8);
bgRect.setAttribute('height', logoSize + 8);
bgRect.setAttribute('fill', background);
newSvg.appendChild(bgRect);
// Logo image overlay
const img = doc.createElementNS(ns, 'image');
img.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', logo);
img.setAttribute('x', halfSize - halfLogo);
img.setAttribute('y', halfSize - halfLogo);
img.setAttribute('width', logoSize);
img.setAttribute('height', logoSize);
newSvg.appendChild(img);
const serializer = new XMLSerializer();
svg = serializer.serializeToString(newSvg);
}
return svg;
}
/**
* Render a QR code as a VNode with innerHTML for the SVG.
*
* @param {object} props
* @param {string} props.text - Text to encode
* @param {number} [props.size=200] - QR code size
* @param {string} [props.logo] - Base64 data URL for logo
* @param {number} [props.logoSize=40] - Logo overlay size
* @returns {object} VNode
*/
export function QRCodeVNode(props = {}) {
const svgString = qrSVG(props);
if (!svgString) {
return h('div', {}, h('span', { class: 'text-muted' }, 'No content'));
}
return h('div', {
class: 'qr-code-container text-center',
innerHTML: svgString,
});
}
/**
* Logo upload widget — file input that produces base64 data URL.
*
* @param {object} props
* @param {string} props.id - Input element ID
* @param {function} props.onChange - Callback(logoBase64) on file select
*/
export function LogoUpload(props = {}) {
const inputId = props.id || 'qr-logo-input';
return h('div', { class: 'mb-2' }, [
h('label', { class: 'form-label' }, 'Logo (optional)'),
h('input', {
type: 'file',
id: inputId,
accept: 'image/*',
class: 'form-control',
onChange: function (e) {
const file = e.target.files?.[0];
if (!file || !props.onChange) return;
const reader = new FileReader();
reader.onload = function (ev) {
props.onChange(ev.target.result);
};
reader.readAsDataURL(file);
},
}),
]);
}