From 84b155698bb804470327cebb48f4f59d94dc7db4 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Fri, 8 May 2026 22:51:42 +0200 Subject: [PATCH] fix(frontend): inbound stream tidy-up + QR sizing + dev proxy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 --- .../src/pages/inbounds/InboundFormModal.vue | 25 ++++----- frontend/src/pages/inbounds/QrPanel.vue | 34 ++++++------ frontend/vite.config.js | 52 ++++++++++++++++++- 3 files changed, 79 insertions(+), 32 deletions(-) diff --git a/frontend/src/pages/inbounds/InboundFormModal.vue b/frontend/src/pages/inbounds/InboundFormModal.vue index a3145f6a..12b1ba77 100644 --- a/frontend/src/pages/inbounds/InboundFormModal.vue +++ b/frontend/src/pages/inbounds/InboundFormModal.vue @@ -570,7 +570,7 @@ watch( @@ -1023,7 +1023,7 @@ watch( - + - + - + @@ -1169,7 +1169,7 @@ watch( - + - + @@ -1312,7 +1312,6 @@ watch( - Security none @@ -1329,7 +1328,7 @@ watch( Auto {{ label - }} + }} @@ -1364,7 +1363,6 @@ watch( - - @@ -1514,7 +1511,6 @@ watch( - - @@ -1747,4 +1742,10 @@ watch( .wg-peer { margin-top: 4px; } + +.section-heading { + font-weight: 500; + margin: 12px 0 6px; + opacity: 0.85; +} diff --git a/frontend/src/pages/inbounds/QrPanel.vue b/frontend/src/pages/inbounds/QrPanel.vue index 6ac160f8..1f29a9d9 100644 --- a/frontend/src/pages/inbounds/QrPanel.vue +++ b/frontend/src/pages/inbounds/QrPanel.vue @@ -20,8 +20,12 @@ const props = defineProps({ remark: { type: String, default: '' }, // Optional download filename — when set, surfaces a download button. downloadName: { type: String, default: '' }, - // QR pixel size (drawn into a square canvas). - size: { type: Number, default: 180 }, + // Final on-screen QR size in CSS pixels. The canvas drawing buffer + // is rounded down to a multiple of the QR matrix width (so the QR + // fills it edge-to-edge) and CSS then scales the canvas to exactly + // this size — so a denser QR (e.g. WireGuard config) and a sparser + // one (its link) display at identical dimensions. + size: { type: Number, default: 240 }, // Toggle the QR rendering off when callers only want the "row of buttons" // styling (used when the legacy panel rendered links without QRs). showQr: { type: Boolean, default: true }, @@ -104,9 +108,12 @@ function download() {
- +
- {{ value }} @@ -142,20 +149,11 @@ function download() { cursor: pointer; display: block; border-radius: 4px; + /* Drawing buffer is matrix-snapped (smaller than display size for + * dense QRs); scale up crisply so dense and sparse QRs share the + * same on-screen footprint without blurring. */ + image-rendering: pixelated; + image-rendering: crisp-edges; } -.qr-panel-link { - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; - font-size: 11px; - word-break: break-all; - white-space: pre-wrap; - padding: 6px 8px; - background: rgba(0, 0, 0, 0.04); - border-radius: 4px; - user-select: all; -} - -:global(body.dark) .qr-panel-link { - background: rgba(255, 255, 255, 0.05); -} diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 1eddd9ef..663770ff 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -48,8 +48,30 @@ function makeBackendProxy(target, patterns) { return undefined; }, configure(proxy) { - proxy.on('error', (err) => { - if (err.code === 'ECONNREFUSED') return; + let warned = false; + proxy.on('error', (err, req) => { + // Node wraps connection failures in an AggregateError when DNS + // returns multiple addresses (e.g. ::1 + 127.0.0.1) and all + // refuse — the code lands on the inner errors, not the outer. + const codes = new Set(); + if (err && err.code) codes.add(err.code); + if (err && Array.isArray(err.errors)) { + for (const inner of err.errors) { + if (inner && inner.code) codes.add(inner.code); + } + } + const offline = codes.has('ECONNREFUSED') || codes.has('ECONNRESET'); + if (offline) { + // Print a single friendly hint the first time, then stay quiet. + if (!warned) { + warned = true; + // eslint-disable-next-line no-console + console.warn( + `[proxy] backend ${target} is not reachable — start the Go server (e.g. \`go run main.go\`) to forward ${req?.url || 'requests'}.`, + ); + } + return; + } // eslint-disable-next-line no-console console.error('[proxy]', err); }); @@ -71,6 +93,12 @@ export default defineConfig({ emptyOutDir: true, sourcemap: true, target: 'es2020', + // ant-design-vue is intentionally bundled as one chunk (its + // components share internals — splitting it breaks Modal/Form/ + // Select interop). Minified it lands ~1.4MB but gzips to ~410kB, + // so the actual transfer is fine and caches across every page. + // Bump the warning past that ceiling so the build stays quiet. + chunkSizeWarningLimit: 1500, // Multiple HTML entries — one per legacy page we migrate. // As pages get ported in later phases, add their entrypoints here. rollupOptions: { @@ -82,6 +110,26 @@ export default defineConfig({ xray: path.resolve(__dirname, 'xray.html'), subpage: path.resolve(__dirname, 'subpage.html'), }, + output: { + // Split vendor deps into stable chunks so each page only pulls + // what it needs and the browser caches them across versions. + // Without this, ant-design-vue + vue + icons all end up in one + // 1.6MB blob attached to whichever page consumed them first. + manualChunks(id) { + if (!id.includes('node_modules')) return undefined; + if (id.includes('ant-design-vue')) return 'vendor-antd'; + if (id.includes('@ant-design/icons-vue')) return 'vendor-icons'; + if (id.includes('vue-i18n')) return 'vendor-i18n'; + if ( + id.includes('/node_modules/vue/') + || id.includes('/node_modules/@vue/') + ) return 'vendor-vue'; + if (id.includes('dayjs')) return 'vendor-dayjs'; + if (id.includes('qrious')) return 'vendor-qrious'; + if (id.includes('axios')) return 'vendor-axios'; + return 'vendor'; + }, + }, }, }, server: {