A forward node's exit target is now a choice: a managed panel server
(existing behavior) or a manual landing endpoint — address + port +
protocol (VLESS / SS2022 / SOCKS5) + operator-supplied credentials,
including third-party nodes the panel does not run. The panel only
builds the entry-side sing-box outbound and route rule; there is no
exit resource, no exit job, and the node is exit-ready by definition.
- model: Node.ExitMode/ExitAddress; manual exits force fixed port mode
and skip the exit-server port table entirely
- validation: address/port required; per-protocol credential presence;
SS2022 passwords must be base64 key material of the method's key
length (one PSK segment or serverKey:userKey) — the exact form
sing-box accepts, rejected up front instead of failing at deploy
- orchestration: resource building, deploy validation and rebuilds all
branch on manualExit; switching managed->manual queues removal of the
old exit resource, manual->managed mints a fresh exit resource id
(deploy validation previously dereferenced the nil exit server —
regression test locks the fix)
- UI: exit target segmented control, link paste box that parses
vless:// / ss:// / socks5:// links into the form, per-protocol
credential fields (blank keeps the stored value on edit)
- golden: new manualCreate/manualAssign/manualDeploy/manualJobs/
manualNodes sections — exactly one entry-side job whose outbound
targets the manual address; all prior sections unchanged
- the generated combined sing-box config passes the real sing-box
1.13.21 check
GET /api/customers gains a summary object (total/online/depleted/
depletingSoon/disabled/enabled) computed server-side; the customers page
renders it as a six-cell stats strip above the toolbar.
Predicates aligned verbatim with 3x-ui client_paging.go:
- depleted = (limit>0 && used>=limit) || (expiry set && expiry<=now),
counted regardless of status;
- depletingSoon = active && !depleted && (expiry within expireDiff days
|| remaining traffic < trafficDiff GB), thresholds from new settings
expireDiff/trafficDiff with 3x-ui's factory default 0 (nobody counts
until configured);
- enabled = status active; disabled = manual suspensions only.
Online reuses the existing 3-minute presence projection.
Golden diff confined to the additive summary keys; summary_test.go
covers every bucket with thresholds off (soon=0) and on (soon=2).