ClientsPage now subscribes to traffic / client_stats / invalidate
WebSocket events instead of polling /onlines every 10s. Per-row
traffic counters refresh in place, online state stays current, and
list-level mutations elsewhere trigger a refresh.
The client roll-up summary moves from InboundsPage to ClientsPage
where it belongs, restructured into six labeled stat tiles
(Total / Online / Ended / Expiring / Disabled / Active) with email
popovers on the ones with issues.
Auto-disabled clients (traffic exhausted or expiry passed) now
classify as 'depleted' even though clients.enable=false, so they
show up under the Ended filter and render a red Ended tag instead
of looking indistinguishable from an operator-disabled row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Search/filter relocation:
- Remove the search/filter toolbar (search switch + filter radio +
protocol/node selects + the visibleInbounds projection +
inboundsFilterState localStorage + filter CSS + the SearchOutlined/
FilterOutlined/ObjectUtil/Inbound imports it required) from
InboundList. The filters were all client-oriented buckets bolted
onto the inbound row.
- Add a search/filter toolbar to ClientsPage with the same shape:
switch between deep-text search and bucket filter (active /
deactive / depleted / expiring / online) + protocol filter that
matches clients attached to at least one inbound with the chosen
protocol. State persists in clientsFilterState localStorage.
filteredClients drives both the desktop table and the mobile card
list, and select-all / allSelected / someSelected only span the
visible subset.
- useClients now also fetches expireDiff and trafficDiff from
/panel/setting/defaultSettings (used to detect the expiring
bucket); ClientsPage threads them into the client-bucket helper.
Loose fixes folded in:
- Add Client: email field is auto-filled with a random handle on
open, matching uuid/subId/password/auth.
- Inbound clone: parse and reuse the source settings JSON (with
clients reset to []) instead of building a fresh defaulted
Settings, so VLESS Encryption/Decryption and other non-client
fields survive the clone.
- en-US.json: add the ipLog string used by the edit-client modal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The PortFallback protocol type now covers the common
VLESS-master-plus-children case with auto-wired dests, so the manual
Fallbacks editor (showFallbacks block in the Protocol tab) is mostly
redundant. Removed:
- the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows)
- the showFallbacks computed
- the addFallback / delFallback helpers
- the .fallbacks-header / .fallbacks-title styles
- the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP
no longer shows an empty Protocol tab)
Power users who need a non-inbound fallback dest (nginx, static site)
can still author settings.fallbacks via the Advanced JSON tab.
Inbound modal rework (InboundFormModal.vue + inbound.js):
- Drop the embedded Client subform in the Protocol tab. Multi-inbound
clients are managed exclusively from the Clients page now; a fresh
inbound is created with zero clients (settings constructors default
to []) and the user attaches clients afterwards.
- Hide the Protocol tab entirely when it has nothing to render
(VMESS, Trojan without fallbacks, Hysteria). Auto-switches active
tab to Basic when the tab disappears while focused.
- Move the Security section (Security selector + TLS block with certs
and ECH + Reality block) out of the Stream tab into its own
Security tab, sharing the canEnableStream gate.
Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue):
- Flow select (xtls-rprx-vision / -udp443) appears only when the
panel actually has a Vision-capable inbound (VLESS or PortFallback
on TCP with TLS or Reality). Hidden otherwise, and cleared when
it disappears.
- IP Limit input is disabled when the panel-level ipLimitEnable
setting is off, fetched into useClients alongside subSettings and
threaded through ClientsPage to both modals.
- Edit modal now shows an "IP Log" section listing IPs that have
connected with the client's credentials, with refresh and clear
buttons (calls the renamed /panel/api/clients/ips and /clearIps
endpoints).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two related cleanups in the new /clients surface:
1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic +
last_traffic_reset_time, with node-runtime propagation) from
InboundService to ClientService. PeriodicTrafficResetJob now holds
a clientService and calls
j.clientService.ResetAllClientTraffics(&j.inboundService, id).
The last client-mutation method on InboundService is gone.
2. Shorten redundantly-named routes/handlers under /panel/api/clients:
- /clientIps/:email -> /ips/:email (handler getIps)
- /clearClientIps/:email -> /clearIps/:email (handler clearIps)
The "client" prefix was redundant inside the clients namespace.
Frontend (InboundInfoModal) and api-docs updated to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
After the multi-inbound client migration, client state belongs to the
client API surface, not the inbound one. Twelve routes that were
crammed under /panel/api/inbounds/* now live where they belong, under
/panel/api/clients/*.
Moved (route, handler, doc):
POST /clientIps/:email
POST /clearClientIps/:email
POST /onlines
POST /lastOnline
POST /updateClientTraffic/:email
POST /resetAllClientTraffics/:id
POST /delDepletedClients/:id
POST /:id/resetClientTraffic/:email
GET /getClientTraffics/:email
GET /getClientTrafficsById/:id
GET /getSubLinks/:subId
GET /getClientLinks/:id/:email
Their /clients/* counterparts are:
POST /clients/clientIps/:email
POST /clients/clearClientIps/:email
POST /clients/onlines
POST /clients/lastOnline
POST /clients/updateTraffic/:email
POST /clients/resetTraffic/:email (email-only, fans out)
GET /clients/traffic/:email
GET /clients/traffic/byId/:id
GET /clients/subLinks/:subId
GET /clients/links/:id/:email
per-inbound resetAllClientTraffics and delDepletedClients are dropped
entirely — the Clients page already exposes global Reset All Traffic
and Delete depleted actions, and per-inbound resets are meaningless
once a client can be attached to many inbounds.
ClientService.ResetTrafficByEmail is the new email-only reset path:
it looks up every inbound the client is attached to and pushes the
counter reset + Xray re-add through inboundService.ResetClientTraffic
for each one, so depleted users come back online instantly.
Frontend callers (ClientsPage, useClients, ClientQrModal,
ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all
switched to the new paths. The Inbounds page drops its per-inbound
"Reset client traffic" and "Delete depleted clients" dropdown items —
users do those at the client level now. api-docs is rebuilt to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Now that clients live as first-class rows attached to one or many
inbounds, the per-inbound client UI on the inbounds page is dead
weight — every client action either has a global equivalent on the
Clients page or makes no sense in a many-to-many world.
Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and
ClientRowTable from inbounds/. Strips the matching emits, refs,
handlers, and dropdown menu items from InboundList and InboundsPage,
and removes the dead mobile expand-chevron state and the desktop
expanded-row plumbing that drove the inline client table.
The InboundFormModal Clients tab still works in add-mode (one inline
client at inbound creation) — that flow goes through ClientService.
SyncInbound on save and remains useful.
Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit
in ClientsPage that broke the template parser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Removes the AllTime field from Inbound and ClientTraffic and migrates
existing DBs by dropping the all_time columns on startup. The counter
duplicated up+down without adding signal, and the per-event accumulator
ran on every traffic write.
Frontend: drop the All-time column from the inbound list and the
client-row table, the All-time row from the client info modal, and the
All-Time Total Usage tile from the inbounds summary card. The
allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every
locale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.
Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.
Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Collapsed repeated stream/sniffing/settings handling in InboundFormModal
into shared helpers (stampAdvancedTextFor, parseAdvancedSliceWithLabel,
compactAdvancedJson, withSaving) plus a wrapped-config factory for the
single-key editors. Cuts ~120 lines from the script section with no
behavior change.
The advanced-panel subtitle and editor-meta text used a fixed dark color
that was unreadable on the dark and ultra-dark modal backgrounds.
Switched both to opacity-on-inherit so they pick up AntD's theme-aware
foreground color, the same pattern .section-heading already uses.
- Frontend: Only include streamSettings in toJson() for vmess, vless, trojan, shadowsocks, and hysteria protocols
- Frontend: Hide Stream tab in Advanced section for unsupported protocols
- Frontend: Clear streamSettings in Advanced tab when switching to unsupported protocols
- Frontend: Add CodeMirror JSON editor to config view in index page with mobile responsive design
- Backend: Add normalizeStreamSettings() to clear streamSettings for tunnel, mixed, http, tun, and wireguard protocols
- Backend: Apply normalization in AddInbound() and UpdateInbound()
- Backend: Add omitempty JSON tag to StreamSettings field to exclude null values from Xray config
The Obfs password field in the Hysteria2 stream settings tab was incorrectly
labeled. It binds to hysteriaSettings.auth (the server-wide authentication
password), not to the salamander obfuscation password. Per Xray-core docs,
Hysteria2 salamander obfuscation belongs in finalmask.udp[].salamander.password,
which is correctly handled by the FinalMaskForm (UDP Masks section).
Fixed the label to Auth password with an accurate tooltip explaining that
salamander obfuscation is configured via the UDP Masks section below.
- Move 'Edit' button from dropdown to the table since it's the most used action. Only for desktop.
- Increase column widths for action keys in Inbounds, Balancers, Outbounds and Routing tables.
- Slightly enhance layout for consistency.
AntD's <a-qrcode> defaults the module color to the active theme's
text token. Under the dark and ultra-dark themes that text is a light
gray, so the QR rendered low-contrast on the white canvas background
and phones could not lock onto it. Pinned color="#000000" and
bg-color="#ffffff" on every <a-qrcode> usage (share links in
QrPanel, 2FA enrollment in TwoFactorModal, sub/json/clash codes on
SubPage) so the contrast stays high regardless of panel theme.
Two bugs combined to leave per-client traffic / remained / all-time
columns stuck at stale numbers while only the inbound-level row and
the online badge refreshed:
1. Backend (xray + node sync traffic jobs) only included the per-client
array in the client_stats broadcast when activeEmails / touched
was non-empty. Cycles with no client deltas — or any node sync that
failed to fetch a snapshot — shipped only the inbound summary, so
the frontend had nothing to merge for clients. Replaced both code
paths with a single GetAllClientTraffics() snapshot per cycle; the
broadcast now always carries the full client list.
2. Frontend mutated dbInbound.clientStats[i] in place. DBInbound is a
plain class instance (not wrapped in reactive()), so Vue could not
see the field-level changes and ClientRowTable's statsMap computed
stayed cached forever. Added a statsVersion tick bumped on every
merge and read inside statsMap so the computed re-evaluates and the
template pulls fresh up/down/allTime/expiryTime each push.
Removed the now-dead emailSet helper from node_traffic_sync_job and
the activeEmails filter from xray_traffic_job.
A new JsonEditor.vue component wraps CodeMirror 6 + lang-json with
line numbers, JSON syntax highlighting, bracket matching, code
folding, search (Ctrl+F), undo/redo, lint (red squiggle and gutter
icon on invalid JSON), tab indent, and line wrapping. It is wired
into the four raw-JSON spots that previously used <a-textarea
class="json-editor">: the Xray Advanced Template tab, the Outbound
JSON tab, the Balancer Observatory pane, and the Inbound Advanced
tab (settings / streamSettings / sniffing).
Chrome colors are driven by EditorView.theme so they win the
specificity fight cleanly against CodeMirror's own injected styles.
A single buildDarkTheme() factory yields a Dark+ palette (#1e1e1e
background, #252526 active line, #2d2d30 panels) for the regular
dark mode and a near-black variant (#0a0a0a / #141414 / #1f1f1f
border) for ultra-dark — both pair with oneDarkHighlightStyle for
the syntax colors. Light mode stays on basicSetup's default.
CodeMirror lazy-loads as a ~17 kB gzipped chunk that only appears
on the Xray/Inbounds bundles.
Mobile inbound cards now show only #id and remark; mobile client cards
show only the status badge and email. The full stat grid (protocol,
port, node, traffic, all-time, clients, expiry — and per-client
remained/online/expiry) moves behind a new info icon that opens an
a-modal, so the list stays scannable on small screens.
* tunnel: rename settings to Xray's current schema (address →
rewriteAddress, port → rewritePort, network → allowedNetwork) in
the model, form modal, info modal, and the bundled API inbound
template; expose portMap so per-port forwarding can be configured
from the panel.
* tun: add the full TUN protocol form and read-only info blocks
(name, mtu, gateway, dns, userLevel, autoSystemRoutingTable,
autoOutboundsInterface) — previously the protocol was selectable
but the form rendered blank.
* hysteria: surface the stream-level version, obfs password, and
udpIdleTimeout fields that the model already supported.
Refs https://xtls.github.io/config/inbounds/tunnel.html
Refs https://xtls.github.io/config/inbounds/tun.html
Refs https://xtls.github.io/config/transports/hysteria.html
InboundFormModal: switching out of the Advanced tab now parses the three
JSON textareas and rebuilds the structured Inbound via Inbound.fromJson,
so the Basic tab reflects what was pasted. Invalid JSON keeps the user
on Advanced with a specific parse error.
XrayPage: Save now parses xraySetting upfront and snaps the user back to
the Advanced tab on invalid JSON instead of letting the backend reject a
generic blob.
The Deploy-to selector, node column, node stat row, and node filter all
appeared whenever a node row existed in the DB. Local-only deployments
with no nodes (or only disabled nodes) saw a dropdown that only had
"Local Panel" and a filter that did nothing.
useNodeList now exposes hasActive (any node with enable === true).
Inbounds form and list gate node UI on hasActive instead of map size.
* refactor(session): store user ID in session instead of full struct
Replaces storing the full User object in the session cookie with just
the user ID. GetLoginUser now re-fetches the user from the database on
every request so credential/permission changes take effect immediately
without requiring a re-login. Includes a backward-compatible migration
path for existing sessions that still carry the old struct payload.
* feat(auth): block panel with default admin/admin credentials and guide credential change
checkLogin middleware now detects default admin/admin credentials and
redirects every panel route to /panel/settings until they are changed.
The settings page auto-opens the Authentication tab, shows a
non-dismissible error banner, and lists 'Default credentials' first in
the security checklist. Login response includes mustChangeCredentials
so the login page can redirect directly. Logout is now POST-only.
Password must be at least 10 characters and cannot be admin/admin.
* feat(settings): redact secrets in AllSettingView and add TrustedProxyCIDRs
Introduces AllSettingView which strips tgBotToken, twoFactorToken,
ldapPassword, apiToken and warp/nord secrets before sending them to
the browser, replacing them with boolean hasFoo presence flags. A new
/panel/setting/secret endpoint allows updating individual secrets by
key. Secrets that arrive blank on a save are preserved from the DB
rather than overwritten. Adds TrustedProxyCIDRs as a configurable
setting (defaults to localhost CIDRs). URL fields are validated before
save.
* fix(security): SSRF prevention, trusted-proxy header gating, CSP nonce, HTTP timeouts
Adds SanitizeHTTPURL / SanitizePublicHTTPURL to reject private-range
and loopback targets before any outbound HTTP request (node probe,
xray download, outbound test, external traffic inform, tgbot API
server, panel updater). Forwarded headers (X-Real-IP, X-Forwarded-For,
X-Forwarded-Host) are now only trusted when the direct connection
arrives from a CIDR in TrustedProxyCIDRs. CSP policy is tightened with
a per-request nonce. HTTP server gains read/write/idle timeouts. Panel
updater downloads the script to a temp file instead of piping curl into
shell. Xray archive download adds a size cap and response-code check.
backuptotgbot is changed from GET to POST.
* feat(nodes): add allow-private-address toggle per node
Adds AllowPrivateAddress to the Node model (DB default false). When
enabled it bypasses the SSRF private-range check for that node's probe
URL, allowing nodes hosted on RFC-1918 or loopback addresses (e.g.
a private VPN or LAN setup).
* chore: frontend UX improvements, CI pipeline, and dev tooling
- AppSidebar: logout via POST /logout instead of navigating to GET
- InboundList: persist filter state (search, protocol, node) to
localStorage across page reloads; add protocol and node filter dropdowns
- IndexPage: add health status strip (Xray, CPU, Memory, Update) with
quick-action buttons
- dependabot: weekly go mod and npm update schedule
- ci.yml: add GitHub Actions workflow for build and vet
- .nvmrc: pin Node 22 for local development
- frontend: bump package.json and package-lock.json
- SubPage, DnsPresetsModal, api-docs: minor fixes
* fix(ci): stub web/dist before go list to satisfy go:embed at compile time
* chore(ui): remove health-strip bar from dashboard top
* Revert "feat(auth): block panel with default admin/admin credentials and guide credential change"
This reverts commit 56ce6073ce.
* fix(auth): make logout POST+CSRF and propagate session loss to other tabs
- Switch /logout from GET to POST with CSRFMiddleware so it matches the
SPA's existing HttpUtil.post('/logout') call (previously 404'd silently)
and blocks GET-based logout via image tags or link prefetchers. Handler
now returns JSON; the SPA already navigates client-side.
- Return 401 (instead of 404) from /panel/api/* when the caller is a
browser XHR (X-Requested-With: XMLHttpRequest) so the axios interceptor
redirects to the login page on logout-in-another-tab, cookie expiry,
and server restart. Anonymous callers still get 404 to keep endpoints
hidden from casual scanners.
- One-shot the 401 redirect in axios-init.js and hang the rejected
promise so queued polls don't stack reloads or surface error toasts
while the browser is navigating away.
- Add the CSP nonce to the runtime-injected <script> in dist.go so the
panel loads under the existing script-src 'nonce-...' policy.
- Update api-docs endpoints.js: GET /logout doc entry was missing.
* fix(settings): POST /logout after credential change
* fix(auth): invalidate other sessions when credentials change
When the admin changes username/password from one machine, sessions
on every other machine kept working until they manually logged out
because session storage is a signed client-side cookie — there is
no server-side session list to revoke.
Add a per-user LoginEpoch counter stamped into the session at login
and re-verified on every authenticated request. UpdateUser and
UpdateFirstUser bump the epoch (UpdateUser via gorm.Expr so a single
update statement is atomic), so any cookie issued before the change
no longer matches the user's current epoch and GetLoginUser returns
nil — the SPA's 401 interceptor then redirects to the login page.
Backward compatible: the column defaults to 0 and missing cookie
values are treated as 0, so sessions issued before this change
remain valid until the first credential update.
---------
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
When searching for a user, the projected DBInbound only contains the
matching clients, so isRemovable evaluated to alse (since a single
match made clients.value.length === 1), hiding the Delete button.
Pass the original total client count from the parent's clientCount
prop and use it in the isRemovable check instead of the projected
clients array length.
* fix(hysteria2): restore missing masquerade config in inbound form
Fixes#4303
The Hysteria2 Masquerade option was missing from the Stream settings
tab after the v3.0.0 form rewrite. Added the UI form and ensured the
masquerade block is passed through in subscription JSON generation.
The menu item, backend endpoint (POST /panel/api/inbounds/:id/copyClients),
and i18n keys were already in place after the Vue3 migration, but the modal
itself was never ported — clicking the menu just toasted "coming soon".
Adds CopyClientsModal.vue: source inbound dropdown (multi-user inbounds
except the target), per-client checkbox selection via a-table row-selection,
optional Flow override when the target supports TLS flow, and result toasts
for added/skipped/errors.
* feat(traffic_writer): enhance traffic writer with concurrency safety and state management
* Revert "feat(traffic_writer): enhance traffic writer with concurrency safety and state management"
This reverts commit e6760ae396.
* feat(vless): clarify VLESS encryption auth selection and enhance parsing logic
DelClient rejects the removal that would leave an inbound with zero
clients (the constraint exists because Xray protocols need at least
one client to keep the inbound functional). The bulk-delete flow
fired one DelClient call per picked client in a loop, so picking
every client meant the final iteration always errored out with
"no client remained in Inbound" and surfaced as a red toast even
though N-1 deletions had already gone through.
Now confirmBulkDelete detects the "all selected" case up front,
drops the last client from the request, and surfaces the partial
operation in the confirm dialog ("N-1 / N — last selected will
remain. Delete the inbound to remove all."). The pre-existing
single-row delete path and partial-selection bulk delete paths are
untouched. If the only client in the inbound is selected, a
Modal.warning explains the constraint instead of asking for confirm.
- ClientRowTable now applies the General-Settings pageSize to its
expanded client list. The 3.0 rewrite dropped pagination, so users
with thousands of clients per inbound hit a 30-60s browser hang on
expand (#4233).
- ID column was marked responsive: ['xs'] so it was hidden on desktop;
removed the restriction so it shows as the first column everywhere.
- Remark column is now omitted entirely when no inbound has a non-empty
remark, matching the existing Node-column pattern.
- ClientBulkModal: add `comment` and VLESS `reverseTag` fields so the
bulk-add modal can set them on every generated client (matching the
single-client form)
- ClientRowTable: add multi-select checkboxes (desktop + mobile) with a
tri-state select-all and a sticky bulk-action bar; emits a new
`delete-clients` event so the parent can wipe the picked clients in
one go. Hidden entirely when the inbound has only one client (the
last one must stay)
- ClientRowTable: new "Remained" column shows live remaining quota
per client (∞ for unlimited, red when depleted)
- InboundInfoModal: Remained cell now shows the ∞ tag when the client
has no totalGB limit, matching how Total Usage already renders it
- InboundsPage: add Online tag (+ per-bucket popovers listing client
emails) to the summary card so it mirrors the per-inbound row, and
wire an `onDeleteClients` handler that loops the existing single-
delete endpoint then refreshes once
- InboundList: forward the `delete-clients` event; hide empty remarks
on both the desktop table (custom #bodyCell) and the mobile card
- useInbounds: aggregate an `online` email list across all inbounds
so the summary popover has data to render
- Migrate SubPage, QrPanel and TwoFactorModal from a QRious canvas to
<a-qrcode type="svg">, which renders the QR matrix as crispEdges
SVG rectangles — pixel-perfect at any display size or DPR, no more
white scan-line artifacts from non-integer canvas scaling
- Drop the now-unused qrious dependency and its manualChunks entry
- Default the panel to ultra-dark on first load (existing user
preferences in localStorage are preserved)
- Let the sub controller read subpage.html from web/dist/ first and
fall back to the embedded copy, so Vite rebuilds in dev no longer
require a Go recompile to refresh the asset hashes
- Swap navy dark palette for VS Code Dark+ neutrals (#1e1e1e/#252526/
#2d2d30) across theme tokens, page backgrounds and DateTimePicker
- Add brand header to the mobile drawer and desktop sider, and recolor
the drawer body so it reads as one panel with the menu
- Redesign login page with a centered card, cycling Hello/Welcome
headline and per-theme animated gradient-blob backgrounds
Rename the SPA globals injected by Go to drop the ad-hoc dunder shape
and free up the bare `webBasePath` name (still the DB setting key)
from colliding with the JS global it used to share:
window.__X_UI_BASE_PATH__ -> window.X_UI_BASE_PATH
window.__X_UI_CUR_VER__ -> window.X_UI_CUR_VER
Also rework the QR-Code modal to fold every QR (subscription + JSON
sub URL, share links, WireGuard config/peer links) into a single
a-collapse with one panel per QR. Subscription panels are listed
first and open by default; everything else stays collapsed so a
multi-link inbound no longer scrolls forever.
- DNS server modal: rename expectIPs -> expectedIPs (per docs); add
per-server tag, clientIP, serveStale, serveExpiredTTL, timeoutMs;
flip skipFallback default to false; hydration still accepts legacy
expectIPs for back-compat.
- DNS tab: add hosts editor (domain -> IP/array), serveStale +
serveExpiredTTL controls, "Use Preset" button bringing back the
legacy preset gallery (Google / Cloudflare / AdGuard + Family
variants — fixed AdGuard Family IPs that were wrong in legacy),
and a "Delete All" button to wipe the server list at once.
- i18n: add 15 new dns.* keys across all 13 locales.
- Frontend-wide formatter pass on Vue components (whitespace and
attribute layout only, no behavior changes).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Traffic-writer single-consumer queue (web/service/traffic_writer.go)
serialises every DB write that touches up/down/all_time/last_online
(AddTraffic, SetRemoteTraffic, Reset*, UpdateClientTrafficByEmail) so
overlapping goroutines can no longer clobber each other's column-scoped
Updates with a stale tx.Save.
- DB pool: WAL + busy_timeout=10s + synchronous=NORMAL + _txlock=
immediate, MaxOpenConns=8 / MaxIdleConns=4. The immediate-tx PRAGMA
fixes residual "database is locked [0ms]" cases where deferred-tx
writer-upgrade conflicts bypass busy_timeout.
- SetRemoteTraffic full-mirrors node-authoritative state into central:
settings JSON, remark, listen, port, total, expiry, all_time, enable,
plus per-client total/expiry/reset/all_time. Inbounds and
client_traffics rows present on node but missing from central are
created; rows missing from snap are deleted (with cascading
client_traffics removal).
- NodeTrafficSyncJob detects structural changes from the mirror and
broadcasts invalidate(inbounds) so open central UIs re-fetch via REST
on node-side add/del/edit without manual refresh.
- XrayTrafficJob broadcasts invalidate(inbounds) when auto-disable flips
client_traffics.enable so the per-client toggle reflects depletion
without manual refresh.
- Frontend: inbounds page now subscribes to the BroadcastInbounds 'inbounds'
WS event (full-list pushes from add/del/update controllers were silently
dropped). Fixes invalidate payload field (dataType -> type). Restart-
panel modal switched from Promise-wrap to onOk-only so Cancel actually
cancels.
- Node files trimmed of stale prose-comments; cron cadence dropped
10s -> 5s to match the inbounds page UX.
- README badges and Go module path bumped v2 -> v3 to match module rename.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Switch the inbounds-page modals, login page's theme switch, and the
Persian date picker to defineAsyncComponent. They're not needed on
first paint, so deferring them shrinks the initial bundle and lets
the LCP element render sooner.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Show the per-client pane as the default tab and fold the subscription
URLs into its bottom (under a divider) so the modal has two tabs
instead of three. Inbound details move to the second tab and remain
the default fallback for protocol-only entries (HTTP/Mixed/Tunnel/
WireGuard) that have no per-client view.
Replace the cramped <a-table> on <768px with a stacked card list for
both inbounds and the per-client expanded rows. Each card surfaces
protocol, port, node, traffic, all-time traffic, client count and
expiry inline as labeled rows instead of hiding them behind popovers,
fixes the 0px gutter that made cards visually merge, and softens the
in-quota green from #52c41a to #389e0a (Ant green-7) so traffic tags
are no longer blinding on dark themes.