diff --git a/frontend/src/pages/inbounds/InboundFormModal.vue b/frontend/src/pages/inbounds/InboundFormModal.vue index 49e9bb84..a3145f6a 100644 --- a/frontend/src/pages/inbounds/InboundFormModal.vue +++ b/frontend/src/pages/inbounds/InboundFormModal.vue @@ -59,7 +59,7 @@ const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL); const inbound = ref(null); const dbForm = ref(null); const saving = ref(false); -const advancedJson = ref({ stream: '', sniffing: '' }); +const advancedJson = ref({ stream: '', sniffing: '', settings: '' }); // Cached default cert/key paths from /panel/setting/defaultSettings — // powers the "Set default cert" button on the TLS form. const defaultCert = ref(''); @@ -211,8 +211,15 @@ function freshDbForm() { function primeAdvancedJson() { if (!inbound.value) return; - advancedJson.value.stream = JSON.stringify(JSON.parse(inbound.value.stream.toString()), null, 2); - advancedJson.value.sniffing = JSON.stringify(JSON.parse(inbound.value.sniffing.toString()), null, 2); + try { + advancedJson.value.stream = JSON.stringify(JSON.parse(inbound.value.stream.toString()), null, 2); + } catch (_e) { /* keep prior text */ } + try { + advancedJson.value.sniffing = JSON.stringify(JSON.parse(inbound.value.sniffing.toString()), null, 2); + } catch (_e) { /* keep prior text */ } + try { + advancedJson.value.settings = JSON.stringify(JSON.parse(inbound.value.settings.toString()), null, 2); + } catch (_e) { /* keep prior text */ } } watch(() => props.open, (next) => { @@ -431,6 +438,7 @@ async function submit() { // transports — both go to wire as serialized JSON. let streamSettings; let sniffing; + let settings; try { streamSettings = canEnableStream.value ? JSON.stringify(JSON.parse(advancedJson.value.stream)) @@ -441,6 +449,9 @@ async function submit() { try { sniffing = JSON.stringify(JSON.parse(advancedJson.value.sniffing || inbound.value.sniffing.toString())); } catch (e) { message.error(`Sniffing JSON invalid: ${e.message}`); return; } + try { + settings = JSON.stringify(JSON.parse(advancedJson.value.settings || inbound.value.settings.toString())); + } catch (e) { message.error(`Settings JSON invalid: ${e.message}`); return; } // The structured form mutates `inbound.stream` directly when the // user edits TCP/WS/gRPC/HTTPUpgrade fields, but if they touched @@ -459,7 +470,7 @@ async function submit() { listen: inbound.value.listen, port: inbound.value.port, protocol: inbound.value.protocol, - settings: inbound.value.settings.toString(), + settings: settings, streamSettings: streamSettings, sniffing: sniffing, }; @@ -486,17 +497,35 @@ const okText = computed(() => props.mode === 'edit' ? t('pages.client.submitEdit') : t('create'), ); -// Whenever the structured stream form mutates the model, refresh the -// Advanced JSON tab so it reflects the latest state. Use a deep watch -// on the parsed JSON of the stream. +// Whenever the structured form mutates stream / sniffing / settings, +// refresh the matching slice of the Advanced JSON tab so the user +// always sees the live state — flipping a switch in Sniffing or +// editing encryption in Protocol now reflects in Advanced. watch( () => inbound.value && JSON.stringify(inbound.value.stream?.toJson?.() || {}), - (next) => { - if (next) { - try { - advancedJson.value.stream = JSON.stringify(JSON.parse(inbound.value.stream.toString()), null, 2); - } catch (_e) { /* leave as is */ } - } + () => { + if (!inbound.value?.stream) return; + try { + advancedJson.value.stream = JSON.stringify(JSON.parse(inbound.value.stream.toString()), null, 2); + } catch (_e) { /* leave as is */ } + }, +); +watch( + () => inbound.value && JSON.stringify(inbound.value.sniffing?.toJson?.() || {}), + () => { + if (!inbound.value?.sniffing) return; + try { + advancedJson.value.sniffing = JSON.stringify(JSON.parse(inbound.value.sniffing.toString()), null, 2); + } catch (_e) { /* leave as is */ } + }, +); +watch( + () => inbound.value && JSON.stringify(inbound.value.settings?.toJson?.() || {}), + () => { + if (!inbound.value?.settings) return; + try { + advancedJson.value.settings = JSON.stringify(JSON.parse(inbound.value.settings.toString()), null, 2); + } catch (_e) { /* leave as is */ } }, ); @@ -1631,6 +1660,10 @@ watch( message="Edit raw stream JSON to access advanced fields we don't yet expose through the form." class="mb-12" /> + + + diff --git a/frontend/src/pages/inbounds/QrPanel.vue b/frontend/src/pages/inbounds/QrPanel.vue index 07a3b960..6ac160f8 100644 --- a/frontend/src/pages/inbounds/QrPanel.vue +++ b/frontend/src/pages/inbounds/QrPanel.vue @@ -29,17 +29,42 @@ const props = defineProps({ const canvas = ref(null); +// Byte-mode capacities (level M) for QR versions 1..40 — used to pick +// the matrix width up front so we can size the canvas as a multiple +// of pixelSize. Without this, QRious renders at floor(size/matrix) +// and centers, leaving a white margin whenever size isn't divisible. +const QR_M_BYTE_CAPACITY = [ + 14, 26, 42, 62, 84, 106, 122, 152, 180, 213, + 251, 287, 331, 362, 412, 450, 504, 560, 624, 666, + 711, 779, 857, 911, 997, 1059, 1125, 1190, 1264, 1370, + 1452, 1538, 1628, 1722, 1809, 1911, 1989, 2099, 2213, 2331, +]; + +function pickQrMatrixWidth(value) { + const byteLen = new TextEncoder().encode(value).length; + for (let i = 0; i < QR_M_BYTE_CAPACITY.length; i++) { + if (byteLen <= QR_M_BYTE_CAPACITY[i]) return 17 + 4 * (i + 1); + } + return 17 + 4 * 40; // version 40 (177 modules) +} + function paint() { if (!props.showQr || !canvas.value || !props.value) return; + // Canvas size = matrixWidth × pixelSize, so the QR fills it edge-to- + // edge. pixelSize is floored against the requested size so the QR + // never grows past the host's expected box. + const matrixWidth = pickQrMatrixWidth(props.value); + const pixelSize = Math.max(1, Math.floor(props.size / matrixWidth)); + const exactSize = matrixWidth * pixelSize; // eslint-disable-next-line no-new new QRious({ element: canvas.value, - size: props.size, + size: exactSize, value: props.value, background: 'white', backgroundAlpha: 1, foreground: 'black', - padding: 2, + padding: 0, level: 'M', }); } @@ -115,7 +140,7 @@ function download() { .qr-panel-canvas canvas { cursor: pointer; - background: #fff; + display: block; border-radius: 4px; }