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(
{{ t('pages.inbounds.expireDate')
- }}
+ }}
@@ -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(
-
@@ -1423,7 +1421,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: {