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:
@@ -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);
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}
|
||||
@@ -48,3 +48,6 @@ export { ApplyConfirm } from './components/applyconfirm.js?v=9';
|
||||
|
||||
/* ── UI Components: Toast ────────────────────────────────────── */
|
||||
export { ToastContainer } from './components/toast.js?v=9';
|
||||
|
||||
/* ── UI Components: QR Code ──────────────────────────────────── */
|
||||
export { qrSVG, QRCodeVNode, LogoUpload } from './components/qr.js?v=9';
|
||||
|
||||
Reference in New Issue
Block a user