GetLink had no socks-family case, so an attached client's
subscription contained no link for the inbound at all, and
getInboundsBySubId's protocol list omitted socks/mixed entirely, so
such a subscription 404'd when the SK5 inbound was the client's
only one. genSocksLink emits socks://user:pass@host:port with the
same credential pair the core config carries — user is the client
email, pass is the client password, both percent-encoded. The
inbound's own fixed account is an operator credential and is never
emitted.
Socks-family inbounds authenticate against settings.accounts, so the
clients attached to them never reached the core: the generated config
carried the settings' "clients" mirror (ignored by xray for these
protocols, passwords and all) and only the inbound's own fixed
account. Emit one account per enabled attached client instead —
user = client email, pass = client password — appended after the
inbound's stored accounts, which keep their order and win name
collisions (managed-forwarding resolution reads the fixed account
from the stored settings, untouched). Drop the clients mirror from
the generated settings, and force auth=password when clients exist,
since accounts are inert under noauth.
Accounting needs no new plumbing: xray-core 26.6.27 reports these
accounts under user>>><account user> counters (probe-verified with
two accounts on both mixed and socks), and the agent snapshot ->
report -> AddTraffic chain already keys client traffic by that exact
string, so making the account name the client email aligns booking
end to end. Clientless socks-family inbounds pass through verbatim.
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)
updateInbound detected an explicit nodeId by probing the raw request
body as JSON only (requestHasJSONKey). The panel frontend, however,
posts application/x-www-form-urlencoded by default (http-init.ts
form-encodes every POST without a declared JSON Content-Type), so the
probe missed on every real browser save and UpdateInbound silently kept
the stored assignment — the edit dialog's 部署到 picker never moved
anything, while JSON API callers (and the P8 E2E, which posted JSON)
worked.
requestHasKey is now content-type aware: JSON bodies are probed as
before; urlencoded bodies are parsed with url.ParseQuery, where the
browser's null encoding (a bare "nodeId=") also counts as provided —
it binds as *int(0) and the existing 0->nil normalisation turns it
into the move-to-local target.
Regression tests post the exact browser wire format through the real
handler stack: form move between nodes, form post without nodeId
(assignment kept), form post with nodeId= (move to local, no binding
error), plus the JSON move at HTTP level. The two form-move tests
fail on the unfixed code and pass with the fix.
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