client_traffics.inbound_id is a legacy single-inbound pointer that goes stale when an inbound is deleted and recreated: the email-keyed traffic row survives but references a missing inbound. Code that resolved the owning inbound from it broke several client operations.
- adjustTraffics: 'Start After First Use' (negative expiry) never converted to an absolute deadline on first traffic, so the countdown never started. Now resolves inbounds via the client_inbounds link and computes the new expiry once per email so multi-inbound clients stay consistent.
- GetClientInboundByEmail / GetClientInboundByTrafficID: fall back to client_inbounds when the pointer is dead, fixing reset traffic ('record not found'), client info, and Telegram set-tgId.
- autoRenewClients: resolve renew targets via client_inbounds so scheduled renews are not silently skipped.
- clients page: allow resetting a client with no inbound attachment (the backend already zeroes counters by email).
Add regression test for the delayed-start conversion under a stale inbound_id.
When an inbound is deleted and recreated it gets a new id, but the shared-by-email client_traffics row keeps the old (now deleted) inbound_id because AddClientStat's OnConflict-DoNothing never refreshes it. The traffic updater matched rows with inbound_id IN (local inbounds), so those orphaned rows were dropped: client traffic and online status stopped updating and auto-renew skipped them, while inbound-level traffic (matched by tag) kept working and the client count still showed (matched by email).
Match by email and exclude only rows owned by a node inbound (inbound_id NOT IN (node inbounds)) in addClientTraffic and autoRenewClients. The local Xray only reports local-client emails, so a stale local pointer no longer hides the row, while genuine node-owned rows stay protected. Verified against a real affected dump: visible rows went from 4/668 to 668/668.