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: {