3x-ui/internal/web/service
n0ctal e940f30bb8
feat(clients): cap how many times a client may auto-renew (#6238)
* feat(clients): cap how many times a client may auto-renew

Auto-renew today runs forever: a prepaid or fixed-term client keeps being
handed new periods until an operator remembers to switch it off. There is no
way to say "renew this three times, then let it lapse".

Add a per-client maximum. Zero keeps today's behaviour, so nothing changes for
anyone who does not set one. When the count is reached the client is simply
left to expire, like any client without auto-renew.

Catching up several missed periods spends one allowance per period. A client
that was away for three cycles must not receive three of them free of the cap,
and the catch-up stops at the last period the cap paid for rather than jumping
to the present.

* fix(clients): persist the auto-renew cap and stop the capped churn

resetMax lived only in the inbound settings JSON and client_traffics, so
every path that rebuilds a client from the clients table wrote it back as
zero. The edit dialog showed 0 for a capped client, and saving an
unrelated comment change lifted the cap; an attach or a traffic reset did
the same with no operator action at all.

Adds reset_max to ClientRecord and threads it through ToRecord, ToClient,
applyClientRecordMerge, the record update map and ClientSlim, so the cap
survives the round trip.

When the cap truncates a catch-up the client is still expired, but the
renewal side effects fired anyway: counters were zeroed for periods it
can never use, and it was enabled and pushed to xray only for
disableInvalidClients to undo both in the same transaction. Those are now
skipped when the new expiry has not reached the present.

Also makes any non-positive resetMax mean unlimited instead of silently
meaning "never renew again", rejects a negative one at the service layer,
surfaces renewals used against allowed in the client info modal so the
operator can see what to raise, adds the field to the bulk-add modal,
translates the labels in all 13 locales, and drops the stray
internal/web/dist/.gitkeep build stub.

* fix(clients): let the renewal cap be changed after creation

ClientService.Update writes the record columns directly only for a client
with no inbounds. The normal path goes through SyncInbound and
applyClientRecordMerge, which this change had not extended, so raising a
cap from 3 to 6 — the natural action when a customer buys another block
of periods — updated the inbound settings JSON while clients.reset_max
kept the old value and the renewal query kept enforcing it.

The existing test did not catch it: it asserted the cap survived an
unrelated edit, and it survived precisely because nothing on that path
ever wrote it. TestClientEditChangesTheRenewalCap raises the cap and then
lifts it entirely; removing the record write turns it red.

* chore: drop the accidentally committed dist build stub

internal/web/dist/.gitkeep is what make dist-stub creates locally. Committing
it changes fresh-clone behaviour for everyone: today a bare go build fails
loudly on //go:embed all:dist, which is the documented signal to run the stub
target; with the file present the build succeeds and the panel serves an empty
dist instead.

---------

Co-authored-by: n0ctal <n0ctal@users.noreply.github.com>
2026-08-18 11:53:11 +02:00
..
email
integration fix(warp): preserve WARP Plus license key when changing IP (#6218) 2026-08-16 14:00:16 +02:00
outbound fix(migration): stop a half-applied startup migration from committing silently (#6182) 2026-08-14 20:10:39 +02:00
panel feat(api): scoped, optionally expiring API tokens (#6201) 2026-08-15 15:31:49 +02:00
tgbot test(tgbot): detect open-coded keypad transitions (#6214) 2026-08-15 15:27:02 +02:00
access_log_parse_test.go
api_scale_postgres_test.go feat(sub): add per-client subscription HWID limits (#5802) 2026-08-15 16:50:20 +02:00
backup_filename_test.go
bulk_clients_test.go
bulk_traffic_test.go
client.go feat(sub): add per-client subscription HWID limits (#5802) 2026-08-15 16:50:20 +02:00
client_add_dedupe_test.go
client_apply_field_test.go
client_bulk.go feat(inbound): DisableFlow — opt an inbound out of auto XTLS Vision (#5689) (#5698) 2026-08-15 23:09:16 +02:00
client_bulk_flow_test.go
client_bulk_reenable_test.go
client_bulk_reset_reenable_test.go
client_create_fanout_test.go
client_create_reuse_test.go
client_crud.go feat(clients): cap how many times a client may auto-renew (#6238) 2026-08-18 11:53:11 +02:00
client_delete_continue_test.go
client_delete_node_baseline_test.go
client_delete_node_full_test.go
client_delete_tombstone_test.go
client_effective_flow_test.go
client_email_validation_test.go
client_external_link.go
client_flow_isolation_test.go
client_group_bulk_test.go fix(groups): report changed bulk moves without restarting xray (#6199) 2026-08-14 19:50:06 +02:00
client_group_node_sync_test.go feat(sub): add per-client subscription HWID limits (#5802) 2026-08-15 16:50:20 +02:00
client_group_reset_test.go
client_groups.go fix(groups): report changed bulk moves without restarting xray (#6199) 2026-08-14 19:50:06 +02:00
client_hwid.go feat(sub): add per-client subscription HWID limits (#5802) 2026-08-15 16:50:20 +02:00
client_hwid_test.go feat(sub): add per-client subscription HWID limits (#5802) 2026-08-15 16:50:20 +02:00
client_inbound_apply.go
client_link.go feat(clients): cap how many times a client may auto-renew (#6238) 2026-08-18 11:53:11 +02:00
client_locks.go
client_locks_test.go
client_lookup.go
client_lookup_test.go
client_paging.go feat(clients): cap how many times a client may auto-renew (#6238) 2026-08-18 11:53:11 +02:00
client_paging_test.go
client_portable.go feat(sub): add per-client subscription HWID limits (#5802) 2026-08-15 16:50:20 +02:00
client_stat_reuse_test.go feat(sub): add per-client subscription HWID limits (#5802) 2026-08-15 16:50:20 +02:00
client_sync_mtproto_test.go
client_sync_multiprotocol_test.go
client_sync_orphan.go
client_sync_orphan_test.go
client_test.go
client_traffic.go feat(sub): add per-client subscription HWID limits (#5802) 2026-08-15 16:50:20 +02:00
client_update_enable_test.go feat(sub): add per-client subscription HWID limits (#5802) 2026-08-15 16:50:20 +02:00
client_update_no_inbound_test.go feat(sub): add per-client subscription HWID limits (#5802) 2026-08-15 16:50:20 +02:00
client_update_rename_test.go feat(sub): add per-client subscription HWID limits (#5802) 2026-08-15 16:50:20 +02:00
client_wireguard.go
client_wireguard_crud_test.go
client_wireguard_test.go
config.json
del_shared_email_runtime_test.go
fallback.go
finalmask_rand_packet_test.go
geodata.go feat(xray): browse geosite/geoip categories from routing rules (#6165) 2026-08-15 17:12:59 +02:00
global_traffic_test.go fix(traffic): apply maintenance side effects only after the commit lands (#6200) 2026-08-14 19:42:09 +02:00
golden_fixtures_xray_test.go
host.go
host_test.go
inbound.go fix(inbounds): close the port check-and-claim race on the serial writer (#6225) 2026-08-18 11:48:40 +02:00
inbound_autorenew_maxcount_test.go feat(clients): cap how many times a client may auto-renew (#6238) 2026-08-18 11:53:11 +02:00
inbound_autorenew_shadowsocks_test.go fix(traffic): apply maintenance side effects only after the commit lands (#6200) 2026-08-14 19:42:09 +02:00
inbound_autorenew_test.go fix(traffic): apply maintenance side effects only after the commit lands (#6200) 2026-08-14 19:42:09 +02:00
inbound_client_ips.go fix(clients): stop a stale IP row from blocking a client edit 2026-08-02 12:32:45 +02:00
inbound_client_ips_merge_test.go
inbound_client_move_test.go
inbound_client_traffic_test.go
inbound_clients.go feat(inbound): DisableFlow — opt an inbound out of auto XTLS Vision (#5689) (#5698) 2026-08-15 23:09:16 +02:00
inbound_create_race_test.go fix(inbounds): close the port check-and-claim race on the serial writer (#6225) 2026-08-18 11:48:40 +02:00
inbound_disable.go fix(traffic): apply maintenance side effects only after the commit lands (#6200) 2026-08-14 19:42:09 +02:00
inbound_disable_flow_test.go feat(inbound): DisableFlow — opt an inbound out of auto XTLS Vision (#5689) (#5698) 2026-08-15 23:09:16 +02:00
inbound_durable_postgres_test.go
inbound_fallback_runtime_test.go
inbound_finalmask_reality_test.go
inbound_finalmask_xmc_test.go
inbound_flow_restore.go feat(inbound): DisableFlow — opt an inbound out of auto XTLS Vision (#5689) (#5698) 2026-08-15 23:09:16 +02:00
inbound_flow_restore_test.go
inbound_import_external_proxy_test.go
inbound_import_shared_clients_test.go
inbound_migration.go feat(inbound): DisableFlow — opt an inbound out of auto XTLS Vision (#5689) (#5698) 2026-08-15 23:09:16 +02:00
inbound_migration_test.go fix(migration): stop a half-applied startup migration from committing silently (#6182) 2026-08-14 20:10:39 +02:00
inbound_mtproto.go
inbound_mtproto_apply_test.go
inbound_mtproto_client_test.go
inbound_mtproto_desired_test.go
inbound_mtproto_test.go
inbound_mtproto_txfail_test.go
inbound_node.go fix(node): keep disabled inbounds the node snapshot cannot report (#6221) 2026-08-18 11:50:09 +02:00
inbound_node_ips.go
inbound_node_ips_test.go
inbound_node_reconcile_test.go fix(node): adopt a matching deployed inbound instead of recreating it (#6197) 2026-08-14 20:01:35 +02:00
inbound_node_sweep_log_test.go fix(nodes): log the inbound the node snapshot removes centrally (#6219) 2026-08-16 14:02:50 +02:00
inbound_options_node_address_test.go
inbound_protocol.go
inbound_protocol_test.go
inbound_settings_clients.go
inbound_settings_clients_test.go
inbound_sub_sort_test.go
inbound_sublink.go
inbound_subsort_test.go feat(inbounds): add a narrow endpoint for subscription sort order (#6179) 2026-08-15 21:48:24 +02:00
inbound_tgbot_lookup_test.go
inbound_traffic.go feat(clients): cap how many times a client may auto-renew (#6238) 2026-08-18 11:53:11 +02:00
inbound_traffic_apply.go fix(traffic): apply maintenance side effects only after the commit lands (#6200) 2026-08-14 19:42:09 +02:00
inbound_traffic_global.go
inbound_traffic_reset_test.go
inbound_update_tag_test.go
inbound_util.go
key_gen_parse_test.go
metric_history.go
metric_history_test.go
mtproto_fake_test.go
node.go feat(nodes): opt-in encryption at rest for the outbound node API token (#6186) 2026-08-15 16:48:50 +02:00
node_bulk_dispatch_test.go feat(inbounds): add a narrow endpoint for subscription sort order (#6179) 2026-08-15 21:48:24 +02:00
node_client_breakdown_test.go
node_client_expiry_sync_test.go
node_client_renew_sync_test.go
node_client_traffic_sum_test.go
node_contract.go
node_credentials_writeonly_test.go
node_delete_orphan_test.go
node_dirty_test.go fix(node): keep disabled inbounds the node snapshot cannot report (#6221) 2026-08-18 11:50:09 +02:00
node_host_adopt_test.go
node_mtls.go fix(nodes): validate every certificate in the node mTLS trust bundle (#6188) 2026-08-15 21:31:31 +02:00
node_mtls_test.go fix(nodes): apply a rotated master mTLS certificate without restarting the panel (#6194) 2026-08-15 16:03:42 +02:00
node_netmetrics_test.go
node_origin_guid_test.go feat(inbounds): improve multi-node online attribution (#6164) 2026-08-15 17:40:35 +02:00
node_push_payload_test.go
node_shared_guid_test.go
node_sweep_guard_test.go
node_tag_sync_test.go fix(node): adopt a matching deployed inbound instead of recreating it (#6197) 2026-08-14 20:01:35 +02:00
node_test.go fix(node): adopt a matching deployed inbound instead of recreating it (#6197) 2026-08-14 20:01:35 +02:00
node_token_encryption_test.go feat(nodes): opt-in encryption at rest for the outbound node API token (#6186) 2026-08-15 16:48:50 +02:00
node_tree.go
node_tree_test.go
outbound_subscription.go fix(outbounds): propagate allocation query failures (#6208) 2026-08-14 19:41:24 +02:00
outbound_subscription_ssrf_test.go
outbound_subscription_test.go fix(outbounds): propagate allocation query failures (#6208) 2026-08-14 19:41:24 +02:00
port_conflict.go fix(inbounds): close the port check-and-claim race on the serial writer (#6225) 2026-08-18 11:48:40 +02:00
port_conflict_test.go
reality_scan.go
reality_scan_test.go
reset_writer_test.go
scale_helpers_test.go
server.go
server_import_sniff_test.go
server_import_stage_test.go
server_pg_restore_test.go
server_vlessenc_test.go
server_xray_checksum_test.go
setting.go fix(web): fallback to default secret when database setting is empty (#6189) 2026-08-15 16:40:22 +02:00
setting_clash_test.go
setting_factory_defaults_test.go fix(nodes): persist the master mTLS credential atomically and stop silent reissue (#6195) 2026-08-14 20:03:54 +02:00
setting_mtls.go fix(nodes): validate every certificate in the node mTLS trust bundle (#6188) 2026-08-15 21:31:31 +02:00
setting_mtls_bundle_test.go fix(nodes): validate every certificate in the node mTLS trust bundle (#6188) 2026-08-15 21:31:31 +02:00
setting_mtls_test.go fix(nodes): persist the master mTLS credential atomically and stop silent reissue (#6195) 2026-08-14 20:03:54 +02:00
setting_security_test.go fix(web): fallback to default secret when database setting is empty (#6189) 2026-08-15 16:40:22 +02:00
setting_sub_identity_test.go
shadowsocks_client_key_test.go
stream_network_alias_test.go
sub_uri_base_test.go
sync_scale_postgres_test.go
traffic_commit_test.go
traffic_poll_scale_test.go
traffic_runtime_apply_test.go fix(traffic): apply maintenance side effects only after the commit lands (#6200) 2026-08-14 19:42:09 +02:00
traffic_writer.go
traffic_writer_test.go
url_safety.go
ws_payload_scale_test.go
xray.go feat(inbound): DisableFlow — opt an inbound out of auto XTLS Vision (#5689) (#5698) 2026-08-15 23:09:16 +02:00
xray_config_clients_test.go
xray_config_inject_test.go
xray_config_scale_test.go
xray_lifecycle_test.go
xray_metrics.go
xray_metrics_test.go
xray_restart_test.go
xray_setting.go
xray_setting_dns_routing.go
xray_setting_dns_routing_test.go
xray_setting_routing_sync.go
xray_setting_routing_sync_test.go
xray_setting_test.go
xray_strip_rules_test.go
xray_wireguard_config_test.go
xray_xhttp_session_test.go