- 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).
- 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.
- 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).
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).
- 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.
NodeView carried only the category counts, so the nodes page chips
could hover to nothing but the category title. GetAll/recountByGuid
now emit online/active/disabled/depleted email lists from the same
bucketing pass as the counts (statuses gain the client email;
the online list is the counted set), and the chips pop the list
over with the inbounds page's popover pattern.
The client form, bulk-add form and the inbound-side attach entry all
filtered attachable inbounds by a protocol whitelist that lacked
socks/mixed (inherited from upstream), so SK5 inbounds could never be
picked as a client's associated inbound even though the backend
accepts the association. Add 'socks' and 'mixed' to all three gates.
The client record's existing password field is the socks credential;
no new credential shape is introduced.
Rows whose stored ForwardingSpec is enabled get a volcano chip after
the protocol tags: '转发 → <target remark>' for managed landings
(resolved against the full row set), '转发 → <address>:<port>' for
manual endpoints, '转发 → SK5' when the target row is gone. Text is
ellipsised at 180px with a tooltip so narrow rows never overflow.
Data rides the slim list's existing forwarding field — no API change.
The edit modal seeds from the list row wrapped in DBInbound, whose
cloneProps only copies keys the instance already owns. DBInbound had
no forwarding field, so the stored ForwardingSpec JSON was silently
dropped on every row wrap and rawInboundToFormValues fell back to
direct — saved forwarding compiled and worked, but never echoed back
into the form. Backend/API carried the field correctly all along.
Regression test covers managed/manual/direct through the exact
row -> DBInbound -> form-values chain.
Design as decided 2026-10-21 (replaces the raw-JSON egress editor idea):
the forwarding form lives on the inbound, and the entry agent's egress
entries are compiled from it on every config pull — never snapshotted.
- model: Inbound.Forwarding (JSON text) via AutoMigrate; panel-side
metadata only, never reaches xray itself
- service/forwarding.go: ForwardingSpec (mode direct/forward; landing
managed|manual), save-time validation (managed target must exist and
be a SOCKS-family landing — the fork's SK5 inbound is 'mixed'; no
self-forward; manual endpoint/protocol sanity), live resolution
(managed: concrete listen IP wins, else hosting node's address;
credentials from the target's first account), and the compiler that
appends one socks/http outbound (tag fwd-<inboundId>) plus a prepended
inboundTag rule per forwarding inbound
- buildXrayConfig agent path: compile runs after applyAgentEgress, so
entries ride on the live template (or stored egress override) and
template edits keep flowing; unresolvable entries skip with a warning
instead of failing the whole config
- UpdateInbound's field-merge now carries Forwarding (its absence was
caught by the smoke: adds worked, updates silently kept the old spec)
- GET /panel/api/inbounds/forwardTargets: managed-landing candidates
(id/remark/tag/listen/port/node name) — credentials never exposed
- frontend: new Forwarding tab in the inbound form (mode, landing type,
managed picker, manual endpoint with link import incl. base64 form),
adapter round-trips the spec as a JSON string like settings; en/zh
strings; generated API types refreshed
Acceptance: unit tests for parse/validate/resolve/compile incl. the
dynamic-recompile proof (template edit + spec edit both reflected), and
an end-to-end smoke (real binary, two agent nodes, all inbound writes
in browser form-urlencoded format): 22/22 PASS — see P9-REPORT.md.
Per user decision 2026-10-21 (reverses the 2026-10-20 drop-Telegram
call): port the upstream 3x-ui v3.7.0-generation Telegram bot wholesale
instead of rewriting a minimal set, adapted to this fork.
- new internal/web/service/tgbot package (telego): bot lifecycle,
commands, reports, callback router, send helpers; token validation via
telego; custom API server URL passes the fork's SSRF sanitizer
(loopback/private bases fall back to the default endpoint, logged)
- settings: full upstream key set (tgBotEnable/Token/Proxy/APIServer/
ChatId, tgRunTime, tgBotBackup, tgCpu/tgMemory, tgLang,
tgEnabledEvents) with getters/setters; settings page gains a Telegram
tab + sidebar entry; token shown masked, clearTgBotToken honored
- wiring (web.go): stats-notify cron registered from tgRunTime when the
bot is enabled; CheckHashStorage job; CPU/memory alarm jobs gated on
tgEnabledEvents + thresholds; login notifications via event bus;
POST /panel/api/setting/testTgBot and /panel/api/backuptotgbot
- lite adaptation: StatsNotifyJob no longer gates on a local Xray core
(this panel never runs one) so the periodic report and the scheduled
DB backup (sendDocument) can actually fire
- translations: upstream TG keys restored across all locales
End-to-end smoke (real binary + fake Telegram API over HTTPS at a
public-looking address, browser form wire format): bot starts on
settings change (getMe), testTgBot delivers, panel restart re-arms the
bot, tgRunTime='@every 1m' cron really fires sendDocument carrying the
panel SQLite DB (magic verified) plus the report sendMessage, login
notification delivered, dead-endpoint failure is logged not silent.
13/13 PASS — see P9-REPORT.md.
Per user decision 2026-10-21 (reverses the earlier keep call in
PANEL-LITE-SPEC.md, now amended): the lite panel never runs a local
core, so measuring/probing from the panel host is meaningless.
Backend:
- delete service/outbound probe implementation (probe_http.go and its
tests) and the TestOutbound/TestOutbounds batch prober; outbound
traffic accounting (AddTraffic/GetOutboundsTraffic/Reset) unchanged
- drop POST /panel/api/xray/testOutbound + testOutbounds routes and the
outboundTestUrl setting (get/update plumbing removed; stored rows in
existing DBs are inert)
- drop POST /panel/api/server/restartXrayService + stopXrayService
routes/handlers and the node-sync allowlist entry; the service-level
Stop/RestartXrayService stay for internal import/restore flows
- node-sync allowlist contract test updated to the new inventory
Frontend:
- remove test buttons/mode switch/test-all from outbounds tab, card
list and table columns; delete TestResultPopover and CountryPill;
drop the egress/country/latency columns fed by probe results
- remove outboundTestUrl state and save plumbing from useXraySetting
and the Basics tab field; drop OutboundTestResult schemas
- delete the 15 now-dead i18n keys from all 13 locales (incl. the
restart/stop keys orphaned since P6/P7 — i18n-dead-keys test green)
- tests: drop the test-URL hook test, re-probe the loopback index
mapping through Move-to-top, regenerate API types (gen)
An inbound's 部署到 node can now be changed after creation; the row,
clients, client_traffics and subscription output stay untouched (same
inbound id throughout).
Contract: the update payload's nodeId counts only when the key was
explicitly sent — the controller probes the raw JSON body and sets
Inbound.NodeIDProvided; the wire adapter now always emits nodeId (null =
local) so clearing the picker is an explicit move-to-local, while
callers that omit nodeId keep the stored assignment (legacy behaviour).
UpdateInbound resolves the target before the port-conflict check (so
conflicts are scoped to the target node), rejects moves to a missing
node, re-derives auto tags under the target node's prefix, validates
the tag against the target node's namespace, and marks BOTH nodes
dirty: the source node's next reconcile sweeps the old tag (no silent
ghost — the flag persists until a reconcile succeeds, so an offline
side just defers), the target's pushes the inbound. Agent-bound nodes
converge via GetXrayConfigForAgent on the agent's next config poll.
A local side has no reconcile loop, so it is applied directly
post-commit (Del/Add on the local runtime, restart fallback).
Frontend: the deploy-to Select is no longer disabled in edit mode
(protocol stays locked); candidate rules unchanged (online nodes only).
Tests: panel->panel move with reconcile sweep/push assertions,
auto-tag prefix regeneration, agent-config flip in both directions,
omitted-nodeId legacy contract, same-node no-op, missing-node
rejection, node->local move.
Backend: new overviewMetrics store — a single-tier (2s x 360 = 12min)
in-memory ring fed by ServerService.RefreshStatus for exactly the eight
overview series (cpu/mem/swap/diskUsage/netUp/netDown/tcpCount/udpCount).
No persistence, no gob snapshot, no system-history page: the only surface
is GET /panel/api/server/history/:metric/:bucket restricted to those
keys, which seeds the frontend curves. ~46KB bounded by construction;
the ring starts empty on every panel start.
Frontend: restore useOverviewHistory + the VitalTile/ThroughputCard/
ConnectionsCard sparklines and the IndexPage wiring exactly as they
were before P7 removed them along with the history module.
- nodes: port node-type (traditional/agent) picker back from web-src into
the embedded frontend; backend annotates NodeView.nodeType from
agent_nodes bindings; fix agent-node update path (skip reachability
probe and apiToken requirement for agent-managed nodes); restore i18n
keys in all 13 locales; install command now uses the Gitea mirror
- jobs: expire agent-bound nodes whose agent_nodes.last_seen is older
than max(3x heartbeat interval, 120s) = 180s; last_seen=0 (never
registered) is left untouched; transitions reuse UpdateHeartbeat +
node event broadcast
- config: GetBaseVersion honors ldflags-injected buildVersion; release
builds inject 3.7.0-agent.1 so the panel no longer reports a phantom
update against the Gitea release tag
- logger/server: remove the three log viewers (panel/xray/amneziawg),
their API endpoints and the 10240-entry in-memory log buffer; file
logging via lumberjack unchanged; tests assert via a new optional
logger hook instead of the removed buffer
- server: remove system history and xray metrics modules end to end
(endpoints, sampling appends in RefreshStatus, per-minute persist
cron, gob snapshot/restore, XrayMetricsService); realtime status and
per-node history are untouched
- settings: subPort may be 0/unset - subscription links are generated
without a port while the subscription service keeps binding the
default 2096; the settings form declares the cleared state via
onNumber's new onClear option instead of snapping back to 1