* feat(notifications): event bus architecture with Telegram and SMTP subscribers
- Event bus core with buffered channel, fan-out, panic recovery
- Telegram subscriber with HTML formatting and rate limiting
- Email subscriber with SMTP/TLS/STARTTLS support and stage diagnostics
- 5 event types: outbound.down/up, xray.crash, cpu.high, login.attempt
- CPU threshold checks per subscriber (tgCpu for TG, smtpCpu for Email)
- SystemMetricData struct for raw metric values in events
- i18n keys for en-US, ru-RU, and English defaults for other locales
* fix
* fix(notifications): repair crash/CPU alerts, harden secrets, add node alerts
Bug fixes:
- Xray crash notifications were permanently suppressed after the first crash:
XrayStateTracker latched state="down" with no reset and no recovery event,
so only the first crash per process lifetime ever notified. Removed the
tracker; the existing 1/min rate limiter already dedupes crash-loop spam.
- Email CPU alerts could never fire unless Telegram was also enabled, because
the CPU job was registered only inside the tgbot block. Register it whenever
either Telegram or SMTP wants cpu.high (new cpuAlarmWanted gate) and relax
the cadence to @every 1m (cpu.Percent already samples over a full minute).
- SMTP password (and, pre-existing, all other secrets) were shipped to the
browser in plaintext: GetAllSettingView was dead code and /setting/all
returned the raw model. Wire getAllSetting -> GetAllSettingView, redact
smtpPassword with a hasSmtpPassword presence flag, and preserve it on blank
save. Closes the leak for tgBotToken/ldapPassword/2FA token too.
Polish:
- email Send: use nil SMTP auth when no credentials (Go refuses PlainAuth over
the unencrypted "none" transport).
- Remove unused EventClientDepleted; fix inaccurate bus.go doc comments; drop
stale tgBotLoginNotify from the frontend schema; gofmt alignment.
Feature - node online/offline alerts:
- Emit node.down/node.up from the heartbeat job on a real status transition
(with a startup-spam guard), reusing NodeHealthData. Formatted by both the
Telegram and email subscribers and selectable in the settings UI.
Regenerated frontend types (hasSmtpPassword). New i18n keys added to en-US;
other locales fall back to English (bundle default) until translated.
* fix(settings): use antd Space orientation instead of deprecated direction
Ant Design 6 deprecated Space's `direction` prop in favor of `orientation`,
which logged a console warning from the Telegram/Email notification tabs. Brings
these two tabs in line with the rest of the codebase, which already uses
`orientation`.
* i18n(notifications): translate the notification feature into all locales
The notifications PR shipped ~99 new strings (SMTP settings, event labels,
Telegram/email message templates) as English placeholders in every non-English
locale. Translate them — plus the node-alert keys added during this review —
into all 12 locales: Arabic, Spanish, Persian, Indonesian, Japanese,
Portuguese-BR, Russian, Turkish, Ukrainian, Vietnamese, and Simplified/
Traditional Chinese.
Go-template placeholders ({{ .Tag }}, {{ .Name }}, etc.) are preserved exactly;
tgbot message values carry no leading status emoji (the bot/email code adds
those, so an emoji in the value would duplicate it); product/protocol names
(SMTP, STARTTLS, TLS, CPU, Xray, Telegram) are kept as-is.
---------
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
Xray counts client traffic globally per email, so a client attached to
several of a node's inbounds has its single shared counter copied onto
every inbound by the node's enriched inbound list. When those copies
diverge (legacy per-inbound rows surviving a v3.2.x->v3.3.x upgrade, or
any drift) the per-inbound delta loop read the lower sibling as a
node-counter reset and re-added its full value, inflating the client far
past real usage (#5274).
Fold each email to its per-field node-wide max before the delta loop so
every occurrence is equal: the per-email baseline dedup then holds and
the reset clamp never misfires.
SlugRemark stripped every non-ASCII character, so tags generated from
remarks like Cyrillic names collapsed to just their digits, making
imported outbounds hard to identify. Keep Unicode letters and digits in
the slug regex while still collapsing punctuation into dashes.
Deleting a client attached to a remote-node inbound could silently fail
to reach the node, so the node's next traffic snapshot resurrected the
client once the 90s delete tombstone expired.
Two paths in the single-client delete (Delete -> DelInboundClientByEmail):
- A disabled client was skipped entirely: the node-propagation and
mark-dirty block sat behind the client's enable flag (needApiDel), so a
disabled client on a node never detached and never marked the node
dirty. The bulk and multi-client delete paths already handle the node
case independently of enable state; mirror that structure here.
- Remote.DeleteUser returned nil when resolveRemoteID failed, hiding the
failure from the caller so the node was never marked dirty. Surface the
error like AddClient/UpdateUser do, so the caller marks the node dirty
and the next reconcile converges.
Add a regression test asserting a disabled node client's deletion marks
the node dirty.
The internal API inbound (tag "api", default port 62789 on 127.0.0.1) lives in
the Xray config template, not the inbounds table, so checkPortConflict never
caught a local user inbound reusing it — Xray then bound the port twice and
served requests unpredictably. Now reject a local TCP inbound whose listen
overlaps loopback on the reserved API port, read from the template (fallback
62789). Nodes are unaffected since they run their own Xray.
Inbound XMUX and other client-side xHTTP knobs were written into
bin/config.json even though xray-core's server listener ignores them.
Strip them in GenXrayInboundConfig while leaving the DB row intact so
buildXhttpExtra still pushes defaults to clients via share links.
* fix(subscriptions): avoid shared mutable state during generation
* fix(subscriptions): serve external-link-only subs in JSON/Clash; load remark settings per request
The ForRequest refactor added an early `len(inbounds) == 0` return to
GetJson/GetClash that fired before external links were fetched, so a
subscription whose only entries are external links (or whose inbounds are
all disabled) rendered empty in the JSON and Clash formats. Drop the
premature check — the existing inbounds+externalLinks empty guard already
covers the truly-empty case.
Also load datepicker/emailInRemark in PrepareForRequest rather than only in
getSubs, so JSON and Clash remarks honor these settings instead of seeing
the zero values (emailInRemark previously depended on the shared-state leak
this PR fixes).
Add a regression test covering an external-link-only sub across both formats.
---------
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
* feat(web): cap request body size on state-changing routes
* fix(web): exempt importDB from request body size cap
The 10 MiB body cap was applied globally, which would break database
restore (/panel/api/server/importDB) on any panel whose SQLite backup
exceeds the limit. Make MaxBodyBytes accept exempt path suffixes and
pass importDB through uncapped; the cap still covers all other
state-changing routes. Add a test for the skip-suffix behavior.
---------
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
* fix(nodes): stop un-activated nodes from resetting "start after first connect" expiry
In a multi-node setup a client is attached to inbounds on several nodes, but
its `client_traffics` row is shared per-email (the column is `gorm:"unique"`).
With "start expiry after first connect", the expiry is stored as a negative
duration and each node converts it to an absolute deadline (now+duration) the
first time the client connects *there*.
The master's per-node traffic merge wrote `expiry_time = ?` unconditionally for
every node sync. So a node where the client never connected keeps reporting the
un-activated negative duration and clobbers the absolute deadline that the node
where the client *did* connect had already activated — last writer wins. The
shared row flip-flops and usually lands back on the negative value, so the main
panel shows the timer "not started" while the active node counts down, and the
subscription (which reads this row and recomputes negative as now+duration on
every fetch) reports a perpetually-resetting, wrong expiry and usage.
Guard the merge so an un-activated (<= 0) value reported by a node can never
reset an already-activated absolute deadline. A positive node value is still
adopted, so a node that legitimately moves the deadline forward (traffic reset /
auto-renew) still propagates. The rule lives in both the SQL CASE used by the
merge and a small `mergeActivationExpiry` helper (kept in lockstep) that the
structural-change check reuses so the guard does not trigger spurious config
re-pushes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(nodes): cast expiry merge params to BIGINT for Postgres
The "start after first connect" merge guard introduced the comparison
`? <= 0` in the client_traffics expiry_time CASE. There Postgres infers
the parameter type as int4 from the literal 0, so binding a real expiry
value — a negative start-after-connect duration or a positive absolute
deadline (~1.7e12 ms) — overflows int4 and the whole setRemoteTrafficLocked
transaction fails, breaking node traffic and expiry sync on Postgres.
SQLite (dynamic typing) was unaffected.
Wrap both params in CAST(? AS BIGINT) (portable across SQLite and
Postgres) so the parameter is typed bigint, matching the explicit casts
the sibling GreatestExpr/ClientTrafficEnableMergeExpr helpers already use.
Verified against Postgres 16: TestNodeFirstConnectExpiry_NotClobbered
failed before this change and passes after; SQLite suite unchanged.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
* feat: add enable/disable toggle for xray routing rules
* fix(routing): never let the internal api rule be disabled
The Enable/Disable toggle could strip the stats api rule: its table
switch was locked, but the rule-form modal's Enable dropdown was not,
and stripDisabledRules had no api-rule guard (EnsureStatsRouting's
delete only runs when the api rule isn't already first). A disabled
api rule then dropped out of the generated config and broke traffic
accounting.
- stripDisabledRules now always keeps the api rule, even if marked
disabled, and strips the panel-only enabled key from every rule
- extract isApiRule helper (backend + frontend) and reuse it across
the table switch, card switch, and form modal
- disable the form-modal Enable dropdown for the api rule
- add stripDisabledRules tests covering the api-rule survival path
---------
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
* feat(utils): add speedFormat utility and tests
* feat(inbounds): add InboundSpeedEntry type
* feat(inbounds): add speed column to inbound list
* feat(inbounds): show speed in inbound stats modal
* feat(inbounds): compute inbound speed from traffic deltas
* feat(inbounds): wire inbound speed through page
* feat(i18n): add speed translation for all locales
* refactor(inbounds): dedupe live-speed UI and harden formatting
Extract a shared InboundSpeedTag component and isActiveSpeed guard used by the speed column and stats modal, unify InboundSpeedEntry into a single type, and route speedFormat through sizeFormat.
Also guard sizeFormat against non-finite input (no more "NaN PB/s") and clear stale per-inbound speeds when a traffic poll returns no deltas.
---------
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
* feat(docker): support XUI_PORT runtime override
Allow deployments to select the panel listener port without mutating the persisted webPort setting. Invalid values fall back to the database-backed port and are covered by parser boundary tests.
* docs: describe XUI_PORT deployment usage
Add commented local and Compose examples, explain runtime precedence, and call out matching Docker bridge port mappings.
Add a Links tab to the client form for attaching third-party share
links and remote subscription URLs per client. They are merged into
the client's raw/JSON/Clash subscription output: links are emitted
verbatim and parsed for JSON/Clash; subscription URLs are fetched
(cached, with a short timeout) and their configs merged in.
i18n keys added across all 13 locales.
Test All only iterated the editable template outbounds, so subscription
outbounds (the read-only "from subscriptions" table) were never probed in
bulk. They are now queued too, keyed by tag in subscriptionTestStates so
their rows light up live; the template and subscription HTTP lanes run
serially to respect the backend's single-batch lock (TCP runs alongside).
Also stop testing freedom ("direct") and dns outbounds: they aren't
proxies, so an HTTP probe through them only measures the host's own
reachability, not a tunnel. They are now untestable in every mode -- the
per-row button is disabled and Test All skips them -- with a matching
backend guard so a direct API caller can't HTTP-test them either.
The node probe honored the per-node TlsVerifyMode (skip/pin) but
runtime.Remote used a shared client with no TLSClientConfig, so traffic
sync and every other remote op fell back to system-CA verification and
failed against self-signed nodes even after the operator set skip/pin.
Move the TLS client builder into the runtime layer (HTTPClientForNode /
DecodeCertPin) as the single source of truth, have Remote build and cache
its per-node client through it, and delegate the service probe to the same
builder so the two paths can no longer diverge.
v3.3.1 removed the Panel Proxy URL field from the UI but left the stored
panelProxy/tgBotProxy values in the DB. The Telegram bot still reads
tgBotProxy directly, so a stale value masked the panelOutbound egress
fallback. Add a one-off seeder to drop both rows.
Closes#5266
The #5035 change tagged node-hosted entries with the node name to
disambiguate multi-node subscriptions, but the node name is
panel-internal and leaked into the profile names end users see in
their client apps. Drop the suffix entirely — remarks are the
admin-set inbound remark again.
The vlessenc fix (#5185) enabled flow on XHTTP only in the security=none
branch of genVlessLink, and the Clash builder still gated flow on
network==tcp. With XHTTP+REALITY+vlessenc the panel accepts and stores
the flow (inboundCanEnableTlsFlow passes), but subscriptions dropped it,
so clients received configs without xtls-rprx-vision.
Add vlessFlowAllowed mirroring inboundCanEnableTlsFlow — tcp with
tls/reality, or xhttp with vlessenc regardless of security layer — and
use it in both the vless:// link generator and the Clash proxy builder.
- Split the Happ and Clash/Mihomo routing sections out of Information into
their own dedicated tabs.
- Extract the profile/branding fields (title, support URL, profile page,
announcement, theme dir) out of the mislabeled "Subscription Title"
divider into a new Profile tab.
- Move the Update Interval setting into Information and drop the
single-field Intervals tab.
- Add the "profile" tab label across all locales.
- Replace the Telegram "Notification Time" free-text field with a guided
cron builder: @every + number + unit (s/m/h), the @hourly/@daily/@weekly/
@monthly macros, and a Custom option that seeds a valid 6-field crontab
(cron runs with seconds enabled) as an escape hatch.
- Move "Restart Xray After Auto Disable" from the External Traffic tab to
Panel Settings, where it belongs.
- Add a "Template guide" link to the Sub Theme Directory setting pointing at
docs/custom-subscription-templates.md.
- Localize all new strings across every locale.
Add a per-inbound "Route through Xray" toggle (off by default) plus an
optional outbound picker on MTProto inbounds. mtg only supports a SOCKS5
upstream, so when enabled the panel injects a loopback SOCKS bridge into
the generated Xray config — tagged with the inbound's own tag — and mtg
dials Telegram through it via a [network] proxies upstream. The router
then governs Telegram egress: matchable in the Routing tab, or forced to a
chosen outbound/balancer via the picker.
- mtproto: Instance carries RouteThroughXray + XrayRoutePort (in the
fingerprint); InstanceFromInbound parses them; renderConfig emits the
socks5 [network] upstream; freeLocalPort exported as FreeLocalPort.
- xray.go: injectMtprotoEgress appends the loopback SOCKS bridge and
prepends an optional inboundTag->outbound/balancer rule, hot-appliable
like injectPanelEgress.
- inbound.go: backend-owned egress port persisted in settings, allocated
once and carried across edits (stored value wins); stripped with the
inert outboundTag when routing is off; allocation failure fails the save;
routed add/update/del force a config regen.
- mtproto_job: skip folding mtg metrics for routed inbounds (the bridge,
carrying the inbound tag, is metered by xray_traffic_job) to avoid
double-counting.
- frontend: toggle + outbound/balancer Select (useOutboundTags) on the
MTProto form; i18n keys for all locales.
Replace the per-outbound burstObservatory polling (one temp xray spawn +
up to 15s of /debug/vars polling per outbound, serialised) with one
shared temp xray instance per batch: every tested outbound gets its own
loopback SOCKS inbound plus an inboundTag->outboundTag routing rule, and
the panel times a real HTTP request through each one in parallel. The
probe returns as soon as the response lands and records the HTTP status
plus an httptrace breakdown (proxy connect / TLS via outbound / first
byte) shown in the result popover.
New POST /panel/api/xray/testOutbounds endpoint (array in, results in
input order, max 50); the legacy /testOutbound endpoint now delegates to
the same engine. Test All chunks HTTP probes 16 per request, and a batch
whose shared process never comes up (one structurally-broken outbound
poisons the config) retries each item in an isolated instance so the
broken outbound reports xray's real error while the rest still test.
Split the group traffic summary into two inbound-style cards: a "Total
upload / download" card with up/down arrow icons and a "Total Usage" card
with the pie icon. Add the totalUpDown label across all locales.
Add per-group up/down to GroupSummary (backend + schema), surface them
as Upload/Download columns in the groups table, and fold upload/download
into the Total traffic summary card. Rename the group "Clients in group"
column to just "Clients" across all locales.
Accept the @-prefixed abstract socket form (e.g. @xray/in.sock) for an
inbound listen address, not just path-based sockets. The form now allows
Port 0 for both, and the Address help text documents the @ form across
all locales. The backend already treated both prefixes as unix sockets.
@
An authenticated admin could set xrayTemplateConfig.log.access/error to an
arbitrary path (via the raw Xray editor or a wholesale DB import), making the
supervised Xray process write its log there — an arbitrary file write as the
Xray user (root in many deployments). resolveXrayLogPaths now reduces any log
path to its base filename under config.GetLogFolder(), so absolute paths and
".." traversal can no longer escape the log folder; "" and "none" still
disable logging.
Importing a second inbound whose clients overlap an already-imported inbound
failed with "UNIQUE constraint failed: client_traffics.email". The import path
carries exported ClientStats, and tx.Save(inbound) cascaded that has-many
association as INSERTs whose ON CONFLICT targets only the primary key, so a
shared email (already owning a row from the first import) tripped the global
unique constraint.
Omit the ClientStats association on save and insert the carried stats ourselves
with the same OnConflict{email, DoNothing} guard AddClientStat already uses:
new clients keep their imported counters, shared emails reuse the existing row.
Then run an idempotent AddClientStat pass over all clients so any client present
in settings but missing from the stats payload still gets a traffic row (else it
would escape quota/expiry accounting), and propagate insert errors so the tx
rolls back instead of committing a partial state.
Add a subSortIndex field to inbounds that controls the order of links
in subscription output only: the raw sub body, the HTML sub page, and
the JSON/Clash formats (all served from the same query). Lower values
come first; ties keep id order. The panel inbound list is unaffected.
The value is editable in the inbound form next to the share-address
fields, propagates to nodes via wireInbound, and follows the usual
node-sync rules (copied on import, mirrored while not dirty, never a
structural change).
Rescoped from #5214 by @Ponywka.
- Rename tabs: "Basic" → "Basics", "Config" → "Credentials"
- Move reverseTag field from Credentials tab to Basics tab
- Move IP log button inline with limitIp field (tooltip button, edit mode only)
- Hide random email button when editing an existing client
- Add tooltips to totalGB and limitIp fields with descriptive hints
- Rename labels: "Total Sent/Received (GB)" → "Traffic Limit (GB)", "Duration" → "Duration (days)"
- Add renewDays translation key for auto-renew label with unit hint
- Remove redundant filterOption and width style from AutoComplete group selectors
- Update all 15 locale files with new and renamed translation keys
Multi-node panels had no way to narrow the inbounds or clients lists to
a single node. Add a node filter to both pages:
- Inbounds: a toolbar select (All / Local / each node) that filters the
list client-side; shown only when the panel has nodes or node-attached
inbounds.
- Clients: a Nodes multi-select in the filter drawer. Node selections
are mapped onto inbound IDs client-side and fed through the existing
inbound CSV paging parameter, so the paging backend is untouched; an
impossible id (-1) is sent when no inbound matches so the filter
yields an honest empty result. InboundOption now carries nodeId to
make the mapping possible.
The local panel is selectable via a 0 sentinel (inbounds without a
nodeId). New i18n keys in all 13 locales.
GET /panel/api/clients/get/:email returned the quota (totalGB) but not
the bytes the client has actually used, forcing API consumers to scrape
it elsewhere. Add a sibling "usedTraffic" field (up+down, including the
cross-node global overlay) next to "client" and "inboundIds".
The panel seeded xhttp configs with scMaxEachPostBytes=1000000 and
scMinPostsIntervalMs=30 — xray-core''s own defaults — and emitted them
into every generated config and share link. The literal
scMinPostsIntervalMs=30 is a stable DPI fingerprint that Russia''s TSPU
keys on to block connections on mobile networks.
New configs no longer seed these values (empty schema/template defaults,
so xray-core applies its internal defaults). For configs already stored
with the old defaults, the link/subscription builders now drop values
equal to xray-core''s defaults instead of advertising them — covering
panel share links, the raw subscription, and the JSON subscription
without requiring every inbound to be re-saved. Non-default values the
user set deliberately are still emitted.
applyClientFieldByEmail patched only the first inbound that the
client_traffics row pointed at. For a multi-inbound client the sibling
inbounds kept the old expiryTime/totalGB/limitIp in their settings JSON,
and the next SyncInbound over a stale sibling reverted the edit in the
normalized records — the Telegram bot's expiry change appeared to apply
and then sprang back. Patch the field on every inbound linked to the
email, falling back to the legacy single-inbound lookup for clients that
were never normalized.
An inbound pushed to nodes keeps the same remark on every copy, so a
multi-node subscription (and the panel's per-client link view) listed
several identically-named entries differing only by address. Append the
node name to the remark of node-hosted inbounds unless the admin already
included it.
The fallbacks card only renders for VLESS/Trojan over RAW with TLS or
Reality security, and a new inbound starts at security=none — so the Add
Inbound page looked like it had lost fallback support entirely. Show an
inline hint in that state pointing at the Security tab.
The panel egress is injected as a routing rule, so a routing balancer is
a valid target for it (unlike the geodata download, which dials a forced
outbound tag and bypasses the router). Surface routing balancers in the
panel outbound picker as a separate group, and emit balancerTag instead
of outboundTag in the injected egress rule when the configured tag names
a balancer, so the panel's own traffic load-balances across its members.
The v3 outbound test spins up a temp xray that probes the outbound via
burstObservatory. Two regressions made it report "Failed" for healthy
outbounds on high-latency / tunnel-routed boxes (e.g. default route over
an OpenVPN tun device to a remote proxy), even though client traffic over
the same outbound works:
- Each probe disables keep-alive, so every attempt is a cold round-trip
(redial + re-handshake). The 5s per-probe timeout was too tight for such
paths and every probe timed out. Restore the ~10s budget the pre-v3
SOCKS-based test gave a cold connection (timeout 5s -> 10s) and widen the
poll window 12s -> 15s so one full probe can complete and surface alive.
- The temp config set log error to "none", discarding the real failure
reason, so "Failed" was undiagnosable. Route error logs to stderr ("")
like the production template does, so the probe error (DNS lookup
failure, connection refused, deadline exceeded, TLS error, ...) is
captured into the panel/Xray log, and point the operator there in the
generic timeout messages.
Multi-node sync/import drift can leave the same client twice inside an
inbound's legacy settings.clients JSON while the normalized
client_inbounds table stays clean (SyncInbound dedupes the rows it
writes but never rewrites the JSON). All three subscription builders
iterated that JSON verbatim, so every duplicate entry became a
duplicate profile in the raw, Clash, and JSON output.
Filter and dedupe by email in one shared helper (link generation keys
purely on inbound + email, so same-email entries are pure duplicates
and dropping them is lossless). The clash/json services' own
inboundService copies became unused and are removed.
Subscriptions resolved a node-managed inbound's address to the node's
panel address unconditionally, so an inbound bound to a specific public
IP advertised an endpoint clients could not reach. The shareAddrStrategy
field added in #5162 only applied to panel share/QR links by design.
resolveInboundAddress now follows the same order as the panel's link
builder: 'listen' prefers a routable bind, 'custom' prefers shareAddr,
and the default 'node' keeps the existing node-first behavior, so output
is unchanged for inbounds that never set the field. Applies to raw,
JSON, and Clash subscriptions, which all resolve through this path.
Help text in all locales updated to drop the 'subscriptions are not
affected' caveat.
The orphan sweeps in setRemoteTrafficLocked deleted the (node, email)
baseline row unconditionally whenever an email was missing from one
inbound's snapshot stats — even though baselines are keyed per node, not
per inbound. For a client attached to two inbounds of the same node whose
stats the node reports under only one of them, the sweep for the other
inbound deleted the baseline at the end of every sync cycle. Depending on
inbound order, the baseline written earlier in the same transaction was
wiped each time, so the next cycle computed delta against a missing
baseline (zero) and the client's traffic froze permanently.
Scope both sweeps to the union of emails across the whole snapshot: a
baseline is only dropped when the email left the node entirely.
* feat: select node inbounds for synchronization
Allow node owners to import either all remote inbounds or an explicit tag-based selection. Add remote inbound discovery, persistence, snapshot filtering, API documentation, tests, and localized UI labels.
* fix
* fix: scope node reconcile and orphan sweep to selected inbound tags
In 'selected' sync mode unselected inbounds never enter the panel DB, so
ReconcileNode treated them as undesired and deleted them from the node the
first time it went config-dirty. Reconcile now only sweeps remote tags that
are part of the selection; everything else on the node is unmanaged.
Panel-created or renamed inbounds on a selected-mode node also vanished:
their tag was outside the selection, so the next traffic pull filtered them
out of the snapshot and the orphan sweep silently dropped the central row.
AddInbound/UpdateInbound now allow the tag on the node before committing.
---------
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
* fix: derive JSON/Clash subscription URLs from configured subURI
When subURI is explicitly configured (reverse-proxy setup) but subJsonURI
or subClashURI are not, BuildSubURIBase generates URLs with the raw sub-
server port (2096) and the wrong scheme (http), producing broken links
on the subscription page (e.g. http://domain:2096/json/SUB_ID).
Fix: in BuildURLs, when subURI is set, extract its scheme+host and use
that as the base for all unconfigured sibling URLs instead of calling
BuildSubURIBase. This ensures JSON and Clash Copy URLs match the reverse-
proxy endpoint.
Fixes: JSON/Clash subscription URLs shown on the subscription info page
now correctly inherit the configured subURI's scheme and host.
* fix(sub): fall back to request base when configured subURI is unparseable
Harden the JSON/Clash URL derivation added for the reverse-proxy fix:
extractBaseFromURI now returns "" when the configured subURI has no
scheme/host, and BuildURLs falls back to the request-derived base in
that case instead of emitting a broken value (e.g. ":///json/ABC").
Add a regression test covering a scheme-less subURI.
---------
Co-authored-by: w3struk <w3struk@gmail.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>