country/country_auto in buildProbeNodeView now come from a local
MMDB lookup of the node's public address (held v4, held v6, seen
v4, seen v6) instead of the pin alone / a hardcoded empty string.
Layered data: local file in the data folder wins, embedded factory
snapshot is the fallback, no database means the pre-flag empty
shape. A weekly job conditionally downloads a fresher copy,
validates it, atomically replaces the local file and hot-swaps the
reader; no URL configured (geoipCountryDefaultURL / the
geoip_country_url setting) means the updater stays silent. No
address ever leaves the master.
buildProbeNodeView now sources its 'name' from probeDisplayName:
the node's public remark (probe_meta.public_remark) when non-empty
after trimming, else the node name as before. Contract field name
unchanged, theme packages untouched, both audiences (anonymous and
admin frames share the builder); the panel's internal node list is
a different surface and keeps the plain node name.
Tests: display-name unit cases (remark / empty / whitespace) and
view-level assertions for both audiences; the public contract
test's seeded expectation updated to the remark (its fixture sets
public_remark='shown publicly' on node 'PubNode').
Unit unification: GB entries converted at 1e9 while every byte
formatter is binary (1024^3), so a 3000 GB limit rendered 2.73 TB
instead of 2.93 TB. calBytes and the form's limit conversion now
use 1024^3. Stored traffic_limits (bytes, typed x1e9) are rescaled
x1.073741824 exactly once at store open, guarded by the
probe_setting mark gb_1024_migrated_v1; calibration values are
stored in GB (no rescale — only their conversion changed) and
price/sort/reset-day are unitless.
One-shot calibration: the form's cal fields are deltas to apply
now. probe.MergeCalDelta folds a non-zero submitted value into the
stored standing correction and keeps the standing value on a
zero/absent submission; putMeta merges against the stored row
(SaveMeta/UpsertMeta keep plain replace — the notify sweep relies
on it). GET no longer echoes the standing correction into the form,
and the frontend keeps the cal inputs as local state that starts
empty and clears on successful save, so a later unrelated save
resubmits nothing.
Tests: calBytes(3000)=3000x2^30; migration 3.0e12 -> 3000x2^30 with
no second rescale on reopen; one-shot flow (save delta, save
unrelated with zero cal -> calibrated traffic unmoved, second delta
accumulates once). Existing TestTrafficCalibrationAppliedAndFloored
expectation updated to the binary figure. No vitest covers
ProbeMetaSection (disclosed in the report).
displayUpdateFields returned avail=false whenever the running
build's display version could not be compared (the VV2.1.0
double-letter builds), so the panel swallowed a real update prompt
and the native updater refused to run. displayUpdateFieldsFor now
takes the current display/raw versions as inputs and, when the
display comparison cannot judge, compares the latest tag against
the raw base version with the same tolerant display parser —
unparseable raw base yields false. (Not isNewerVersion: on this
fork's uppercase-V tags it degrades to string inequality, which
would phantom-report an update on form difference alone.)
Parseable display versions keep the segment rule untouched.
Regression tests cover the VV scenario in both raw forms.
NodeService.Probe now checks for a bound agent first: agent-managed
nodes synthesize the result from agent_nodes.last_seen (MAX over
bound agents, ms) — fresh within the same 180s window the heartbeat
job expires by = online with the heartbeat age as latency; stale or
never-seen = offline with LatencyMs -1. No dial, and LastError is
never written for agent nodes (the stored dial error from the
placeholder address https://agent:2053 is also healed on next ⚡,
since the synthesized patch carries an empty error through
UpdateHeartbeat). Traditional-node dial path unchanged; verdict is a
pure function with unit tests.
Frontend: the ⚡ tooltip/menu used t('pages.nodes.probe'), which
since the probe-metadata work resolves to the status-page SECTION
object, so i18next fell back to rendering the raw key. The existing
pages.nodes.testConnection key (present in all 13 locales) is the
right label — point the three usages at it. No new i18n keys.
Scope B (Windows agent):
- probe.go split into shared core + probe_linux.go (verbatim Linux
sampler, build-tagged) + probe_windows.go (Win32 sampler: GetSystemTimes
CPU diff, GlobalMemoryStatusEx, fixed-drive df totals, GetIfTable2
counted set, MachineGuid boot identity, registry facts; load and
TCP/UDP counts stay zero - x/sys v0.47.0 exports no wrapper)
- update intents are acknowledged and skipped on Windows (the release
assets are Linux binaries; Windows nodes update via install-agent.ps1)
- cmd/xui-agent-windows: SCM service host (svc.Run + mgr install/
uninstall/start/stop) mirroring main.go's agent lifecycle wiring
- install-agent.ps1: downloads the exe, writes probeOnly agent.json,
registers and starts the service, rerun-safe
Scope C (version display):
- config.GetAgentDisplayVersion (and the master getter) compose from
the bare number so a V-form injection no longer doubles the letter
(AV2.1.0 -> A2.1.0)
- node list view repairs stored AV-prefixed reports at assembly so
deployed agents read uniformly without an update
Scope D (production fix):
- share-link reality sni: when the inbound has a sing-box fragment,
sni is the fragment's tls.server_name - the only name the sing-box
REALITY server serves. The previous rule picked a uniformly random
serverNames entry per generation, which xray tolerated but sing-box
refuses as an invalid connection. sid keeps its random pick: the
converter carries the whole shortIds list into the fragment, so
every listed sid is served.
agent_nodes gains probe_only; getConfig always serves the flag and withholds the sing-box payload (null config, empty hash) for flagged nodes while probe tasks, fast interval, stat iface and update intents ride as usual. The agent gates both core branches when the local agent.json preset or the master flag is on, retires a running core once (best-effort tail flush, switchover bookkeeping untouched so turning the flag off self-heals through the existing hash-current-but-not-running path), and makes the traffic cycle an explicit no-op. Panel: node create/update accept probeOnly, the node view overlays it from the binding, the node form gains a switch (carried into the generated install command), the list shows a probe-only tag in the core-version cell, and install-agent.sh --probe-only writes the preset, skips the core download/self-check and inherits the flag on no-arg reruns.
- 0: sing-box gen overrides fragment listen/port from the inbound row
(empty listen -> "::"), Update/Add recompute the stored fragment;
non-loopback E2E against the real core binary.
- 1: agent fetches the sing-box core from the Gitea release when
missing, self-checks it, and only then latches the switch.
- 2: node list shows the sing-box core version (heartbeat field,
persisted on the node row), xray version as fallback.
- 3: public-page fallback returns 404 to anonymous visitors when the
page is disabled.
- 4: settings restart hint bar removed, dead i18n keys cleaned.
- 5: two PUT calls missing JSON headers fixed (audit: only those two),
batch probe-task creation endpoint + UI.
- 6: cf-probe retirement card and coverage pipeline removed.
- 7: generated vless links no longer carry spx (panel export + sub);
parser still accepts spx; golden fixtures updated.
- probe batch: dedicated probe settings tab (public-page settings,
tasks, per-node meta), retention_days editable, per-node traffic
calibration + stat-interface pinning, runtime metrics split into a
lightweight fast report (default 3s, 1-60s adjustable).
- 8: sing-box core processes start with GOMEMLIMIT=32MiB, GOGC=50.
- 9: dead /getTwoFactorEnable docs entries removed (frontend call and
contract entry already gone at base; verified by grep).
The B5 full-stack rehearsal caught a silent accounting killer:
with the production switch set (inbound on, outbound off) the
generated v2ray_api stats block omitted the outbounds list, and
sing-box then registers NO counters at all — QueryStats answered
zero entries while real traffic flowed (A/B against the real core:
adding the list produced full user/inbound/outbound counters).
- builder: whenever the stats block is generated, emit inbounds,
users and outbounds together; the gates now only decide whether
the block exists
- master: enforce the inbound category gate at recording time in
the agent traffic report (the whole-network accounting switch
semantics: off period is never counted); onlines stay ungated
- golden gate test updated to the corrected contract
- master getConfig serves singbox_config only; xray payload retired
(build failure is a hard 500, agent keeps its current state)
- agent: singbox_config arrival is the switch signal — flush xray
tail traffic, stop xray, activate sing-box; activation failure
restarts xray from the last payload and retries next poll; a
legacy master (no sing-box keys) keeps the xray path untouched
- sing-box log rotation: 8 MiB copy-truncate with one .1 kept,
checked on the traffic cycle
- install-agent.sh: xray/geo downloads and checks removed, agent
binary now from the V2.0.0 release, self-check is sing-box
binary + check on the live config
- display version default 2.0.0 (master V2.0.0 / agent A2.0.0)
- Delete the xray settings page (routes/menu/title), geodata UI/service,
XraySettingController/Service and routing-sync write paths; the stored
xrayTemplateConfig row loses its last readers (GetXrayConfigTemplate now
returns the embedded factory template; B1 already deleted the row).
- Settings GeneralTab gains the four trafficStatsSwitches switches
(settings.trafficStatsSwitches, B1) with the dual warning copy; entity
AllSetting carries the key end to end (TestTrafficStatsSwitchesPlumbing).
- Tag-directory consumers (panelOutbound, mtproto egress, node/host
outbound pickers) move from POST /xray/ to GET /server/outboundDirectory.
- Shared Mux/Sockopt forms and balancer helpers relocated to lib/xray;
converter preserves sniffing ips/domains exclusions into the panel
sidecar instead of dropping them (B2 gap closed by retention).
- Freedom final-rules DB fix seeders retired; getDefaultJsonConfig
endpoint removed (no in-repo consumer); locales pruned to referenced
keys (pages.xray 331 -> 68) plus the five new settings keys.
Admin endpoints under /panel/api/probe/* (list, preview, chunk
upload, repo install, update, select, delete, public settings).
saveThemeConfig gate aligned to hub: selectable, not disk-installed.
Session cookie Path=/ so the root public surface (theme config save,
/api/me authed) can see the operator session under a non-root panel
base path — with basePath-scoped cookies it was unreachable (401).
Install/remove/list/select with hub limits and refusal texts, chunked
upload receiver, site icons, public-surface settings; vendored
upstream default theme as the embedded fallback (MIT, stqfdyr).
The '.' root dir entry of ./-prefixed packages (guboysky LuminaPlus)
is the package root, not a path escape.
- ProbeTasksModal: self-loading/self-saving surface (B3 probe-meta
pattern), tasks grouped by region label (ungrouped last), editor
with name/region/target/interval/auto-join + agent-node multi
select snapshotting baseNodeIds for the base-diff save; delete
with confirm. Toolbar entry on NodeList (RadarChart icon), wired
in NodesPage; NodeList call sites in two component tests updated
for the new required prop.
- i18n pages.nodes.probeTasks.* (25 keys) in all 13 locales: zh-CN
translated, en-US English, others English placeholder per repo
convention; insertion verified pure-additive (351 insertions,
0 deletions) and the i18n dead-key failures are byte-identical
to the B3 baseline.
- tsc clean; oxlint 0/0 (baseline parity); oxfmt clean on touched
files (3 pre-existing drift files untouched).
Baseline note: TestMetricHistoryMinuteFold fails identically on
the pristine B3 tip in the same wall-clock window (minute-parity
sensitive seed), passes in others — pre-existing, untouched path.
- probe package: probe_ping_task/probe_ping_node tables; task CRUD
with hub save semantics (id high-water, base-diff assignment,
64-per-agent crowding refusal, auto_join cap); filing with
assignment-gated insert; PingHistory weighted-median chart read
over minute+hour tiers; auto-join on first Record (ensureRows).
- controller: /panel/api/agent/probe-tasks* management endpoints
(node id space, hub validation chain verbatim); series=ping now
serves real rows from probe.PingHistory; heartbeat ingests
probeResults (missing latency skipped, batch capped); config
response carries probe_tasks (always present, empty = clear).
- agent: per-task probe loops (immediate first probe, ticker,
900ms resolve/dial budgets, <=3 addresses, resolve overrun = no
sample), result buffer drained on heartbeat and requeued on
failure; task set applied before the config-hash early return.
- SingboxService builds the full sing-box config from the new model:
stored fragments re-projected against the identity layer (converter
projections factored into internal/singbox/project.go, shared by
converter and generator), agent singbox_outbounds/routing_rules
override, forwarding compiled to sing-box outbounds+route rules,
default private-CIDR reject rule (geoip:private replacement),
tun dns landing in the config dns block (closes the B1 'B2' pending).
- trafficStatsSwitches wiring: experimental.v2ray_api.stats lists are
generated per category only when both direction switches are on;
all-off yields no experimental block (no backfill, per the switches'
documented semantics).
- Agent /config endpoint additionally serves singbox_config +
singbox_config_hash; xray payload untouched, sing-box build failures
log-and-omit.
- SubService.GetLinkFromModel regenerates share links from identity +
singbox_config/singbox_preserved only (legacy generators reused over
rebuilt settings/stream views). Golden fixtures 26/26 byte-equal;
production backup DB: 107/107 links equal (55 byte-equal, 52 equal
modulo the legacy generator's own random Reality sni/sid pick),
generated config passes sing-box check.
- Converter preservation amendments (panel-side, for blocked/discarded
material link generation still needs): blockedTls for xhttp/kcp,
verbatim streamNetwork/streamSecurity, vlessEncryption presence,
ssPassword for removed ciphers, wireguardDns, socksAuth/socksAccounts
provenance for exact mixed/socks user refresh.
Co-Authored-By: Muse Spark <noreply@muse.dev>
Fix 1: InboundsPage hostOverrideFor read NodeView.address raw; agent
nodes store the literal placeholder "agent" there, so exported inbound
links came out as vless://...@agent:<port>. Route the pick through the
new shareHostForNode (lib/xray/inbound-link): resolved ipv4/ipv6 first,
raw address only as a non-placeholder fallback; the placeholder can
never reach a link and the fallback chain continues when an agent has
no telemetry yet. Traditional nodes (address only) are unchanged.
Fix 2: agent nodes showed no online users on the nodes page. The
attribution keys were NOT misaligned (write effectiveNodeKey ==
read effectiveNodeGuid == "node:{id}" for guid-less agent nodes,
pinned by TestAgentNodeOnlineEmailsInNodeList). The node traffic sync
job (every 5s) pull-syncs every enabled+online node as if it were a
panel; agent nodes have no panel API, so FetchTrafficSnapshot always
failed and its failure branch ClearNodeOnlineClients wiped the online
tree the agent had just pushed via SetAgentOnlineClients. syncOne now
skips agent-managed nodes (push-only), mirroring the heartbeat job.
Pinned by TestSyncOne_AgentNodeKeepsReportedOnlines (fails pre-fix
with byGuid emptied after one tick, passes post-fix).
New internal/web/service/probe package keeps probe history in its own
probe.db beside the panel DB (single connection, WAL), so history
writes never contend with the panel write lock.
- Minute rows per hub semantics: a held-reading rhythm books traffic
against each reading own arrival; minute buckets merge (means,
rounded int means, peaks) and the stored net rate derives from the
kernel counters between row marks, falling back to agent rates at
epoch breaks.
- probe_traffic ledger: accumulate is a line port of hub traffic.rs
(baseline alignment, same-boot shrink books 0, epoch change books 0,
unseen-boot exception books the whole counters-since-boot measured
against the master last-seen stamp, per-direction period resets,
saturating adds); reads gate stale day/month stamps to zero.
- probe_meta rows are ensured for every reporting agent (B3 edits
them in place); probe_ping_* tables are created now, written by B4.
- Hourly roll_up behind a watermark (hour-aligned, late grace,
pre-retention hours skipped unfoldered) and watermark-guarded
chunked prune (minute tier 7 days or the retention window when
shorter; hourly tier to retention_days, default 7, cap 365).
- Wiring: heartbeat files probe samples after the snapshot update
(last-seen captured before touch); housekeeping hangs off the
existing robfig/cron in web.startTask (@hourly + one pass a minute
after startup, mirroring the hub boot pass); Server.stop books held
readings. Every failure path logs and degrades - a broken probe.db
never fails a heartbeat or the panel.
New stored model (legacy columns untouched, read paths unchanged
until B2):
- inbounds.singbox_config (converted sing-box inbound/endpoint
fragment JSON; empty = blocked or external-runtime) and
inbounds.singbox_preserved (unmapped items verbatim + panel-side
material + notes)
- agent_nodes.singbox_outbounds / singbox_routing_rules
Converter (internal/singbox, pure functions, ITEM6-CONVERSION-SPEC):
all panel protocols mapped; unmapped constructs are preserved and
registered, never dropped. B0 golden fixtures: 26/26 semantic match
(fragment + unmapped concept/pending sets + disposition).
One-shot migrator (internal/database, InitDB hook after initModels):
marker-gated and idempotent; SQLite file backup before any write
(failure refuses migration/startup); stats switches migrate out of
the template BEFORE the xrayTemplateConfig row is discarded
(SCOPE 4.3.1 order); identity layer is the only client truth with
spec 7-1 mirror reconciliation (adopt/fill/conflict all reported);
fallbacks merged from table + embedded settings, deduped; agent
overrides converted with per-entry issues; everything in one
transaction, marker written last; JSON report written next to the
DB. Malformed rows/templates are hard failures (rollback, old DB
untouched).
settings.trafficStatsSwitches added with factory defaults
(inbound on, outbound off) in defaultValueMap.
Validation: synthetic agent-era DB (mirror drift, fallbacks, xhttp,
agent geo rule, non-default stats) converts as specified; old
production backup (25 inbounds) migrates with credentials, Reality
pbk derivation, traffic counters and reset cycles byte-identical;
idempotent re-run is a no-op; touched packages show zero new test
failures vs the 9-red baseline.
- i18n dead keys: drop 13 keys with no reference left (pages.xray.routeTester*
from the P9 feature removal, tgbot.online superseded by tgbot.messages.online,
tgbot.commands.restartSuccess/restartFailed from the rewritten restart handler)
- i18n key sets: add the P9 forwarding block (20 keys) to the 11 locales that
missed it, as English placeholders per repo convention; all 13 locales now
carry identical key sets
- subscription port test: the case predated P7 — clearing the port field is the
explicit subPort 0 (no port in generated links) opt-in via onNumber onClear,
so assert updateSetting is called with { subPort: 0 } instead of never called
No component code, UI behavior, or styles touched.
1. per-client links (LinkProvider.LinksForClient) now apply the
inbound's managed Host records with the same precedence as
subscriptions and export-all (host endpoints after fallback
projection); zero-host inbounds keep the legacy output.
2. agent node writes no longer store the form placeholder
address='agent', port=2053: CreateFromRequest/UpdateFromRequest
sanitize it to the node's resolved agent address (update) or
empty (create / unreported), port 0; normalize gains a lenient
endpoint mode for agent writes only.
3. runtime Remote.baseURL refuses the legacy 'agent' placeholder
with a clear no-panel-endpoint error instead of splicing it into
a dialable https://agent:2053 URL.
Agent nodes store the create form's placeholder in nodes.address
('agent', port 2053; NodeFormModal.buildPayload). The export fallback
chain treated it as a real node address, so every host-less inbound on
an agent node produced links like vless://...@agent:31324.
SubService now loads agent_nodes bindings alongside nodes (latest
last_seen per node, same rule as the node list) and resolves an
agent-managed node's export address through the single convergence
point agentNodeAddress(nodeID): telemetry IPv4, then IPv6, then ""
so the chain continues to listen/public/request host and can never
emit the placeholder. Host-bound inbounds and traditional nodes are
untouched. GetInboundOptions applies the same substitution via
agentAdvertisedAddresses so the frontend share/QR/WireGuard feed
never sees the placeholder either.