Keep usage tokens first-link-only while adding an opt-in setting for repeating EMAIL and USERNAME in subscription-body remarks.
Co-authored-by: x06579 <x06579@ai-dashboard>
Hysteria2 subscription links included a v2rayN-specific fm=<json>
finalmask dump alongside standard obfs/obfs-password. Clients such as
mihomo reject the unknown query param and fail to update the provider.
Emit only the standard salamander fields in genHysteriaLink, matching
the Clash generator.
Fixes#5982
Case-only email edits (test → Test) skipped the ClientRecord rename
because the gate used strings.EqualFold. SyncInbound then failed its
case-sensitive lookup and inserted a second row; the later fallback
rename hit UNIQUE constraint failed: clients.email.
Rename on any byte-level email difference so the same client is updated
in place.
Fixes#5951
expandEntry cleared Name for external subscriptions and single links
with empty remark, so Clash/JSON fell back to the client email. Keep
each link's original name (#fragment / vmess ps); use the row remark
when set. Pass remark from the client form for single external links.
Fixes#6032
The WireGuard client address is stored end-to-end as a string slice
(model.AllowedIPs []string, comma-split/joined in the DB), and the UI
hint documents comma-separated multi-value input. Three export paths
only read index [0], silently dropping every address after the first
(e.g. a dual-stack IPv4+IPv6 client never receives its second address):
- genWireguardLink share link address param (inbound-link.ts)
- genWireguardConfig .conf Address line (inbound-link.ts)
- SubService.genWireguardLink raw subscription address (service.go)
The sibling JSON and Clash subscriptions already emit the full slice,
and WireguardPeerFromClient passes the whole slice into the real Xray
peer config, so only the exported text was wrong. Join all entries,
matching the model's strings.Join(..., ",") convention for the link
params and the ', '-joined AllowedIPs line already used a few lines
below in genWireguardConfig.
Fixes#6031
changeIp() rebuilt the warp outbound from a fixed default template via
collectConfig and replaced the whole in-memory object through
onResetOutbound, so only secretKey/address/reserved and the first peer's
publicKey/endpoint survived — a user's custom mtu, domainStrategy,
noKernelTun, the first peer's keepAlive / allowedIPs / preSharedKey, and
any extra peers were silently dropped from the editor. Because the page
keeps rendering that in-memory copy and a later page Save persists it to
/panel/api/xray/update, the loss could become permanent, even though the
backend ChangeWarpIP already patches only the rotated fields and leaves
everything else intact in the stored template.
Mirror the server-side UpdateWarpXraySetting: merge only the rotated
fields (secretKey, address, reserved, first peer publicKey/endpoint)
into the existing outbound, preserving the rest. register() (first-time
creation, nothing to preserve) still uses collectConfig's full build.
Extracted as a pure module-level mergeWarpRotation so it is unit-testable
without rendering the modal.
Fixes#6019
ClientService.Update only wrote most editable fields to the clients table
inside the per-inbound loop (UpdateInboundClient -> SyncInbound), so a
client with no attached inbound — the external-links / remote-
subscription client — silently dropped subId, totalGB, expiryTime,
limitIp, tgId, comment, reset, flow, security and the credential fields
on edit. Update still returned success, so the panel showed a saved
toast while the row was untouched. email/enable/group_name/ad_tag/reverse
already had dedicated unconditional direct writes that covered the
no-inbound case; the rest did not.
This was a known failure shape: commit c4448f4e added the no-inbound
fallback for email only, and TestUpdate_PersistsRecordEnable_NoInbound
covered enable only — both stayed scoped to a single field.
Extract SyncInbound's per-row field merge into applyClientRecordMerge
(unconditional for scalar quota/lifecycle/subscription fields,
preserve-when-empty for credentials/identifiers, earliest CreatedAt)
and reuse it from Update. Update's new branch runs only when the client
has no inbound, so it fixes the gap without altering the inbound-attached
path (where flow is per-inbound gated via clientWithInboundFlow and
SyncInbound already persists every field). email/reverse/group/ad_tag/
enable keep their existing dedicated writes; the new write covers the
remaining columns.
Fixes#5981
* fix(balancer): pin loopback routing rules ahead of general rules
ensureBalancerLoopback appended a balancer's fallback loopback rule
({ inboundTag: ['_bl_<target>'], balancerTag: <target> }) to the END of
routing.rules via Array.push, and only updated balancerTag in place when
the rule already existed. Xray evaluates routing rules top-to-bottom,
first match wins, and a general domain rule (no inboundTag restriction)
matches every inbound including the loopback one. So once such a general
rule targeting the parent balancer ended up earlier in the array — added
before the fallback was configured, or recreated later by an edit or by
ensureMissingBalancerLoopbacks — the fallback re-entered routing through
the _bl_<target> loopback outbound, the general rule matched again, and
traffic routed straight back into the parent balancer: an unbounded loop
and the CPU spike in the report. detectBalancerCycles only catches
mutual fallback cycles, so the plain acyclic Balancer1 -> Balancer2
chain passed silently.
Insert the loopback rule before the first general (no-inboundTag) rule
instead of pushing to the end, and reposition any existing loopback rule
that already landed after a general rule (preserving its other fields,
mirroring the EnsureStatsRouting hoist used for the same class of bug).
Rules with an inboundTag restriction (api, real inbounds) are left in
place — they can never match loopback traffic, so only the unrestricted
rules are dangerous. ensureMissingBalancerLoopbacks runs on every Save
All and calls ensureBalancerLoopback per fallback, so this also repairs
configs loaded from the DB with the wrong order.
Fixes#5960
* ci: re-trigger checks after flaky FuzzDecodeCertPin timeout
---------
Co-authored-by: x06579 <x06579@ai-dashboard>
* fix(frontend): add shared stable speed-tag style
Give live up/down rate tags a fixed width, centered layout, nowrap,
and tabular numerals so digit/unit changes cannot reflow the Speed column.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* fix(frontend): stabilize InboundSpeedTag and ClientSpeedTag layout
Apply the shared speed-tag class/style to both live rate tags and lock
the behavior with a focused component test for small and large rates.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* fix(frontend): align speed columns with stable tag width
Widen inbound/client Speed columns to match the fixed tag and apply the
same stable style to idle dash cells so active/idle swaps do not jitter.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* fix(frontend): scope stable speed tags to table cells and fit content
---------
Co-authored-by: x06579 <x06579@ai-dashboard>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>