Files
3x-ui/internal/web/service
Sanaei f2cf589947 fix(node): flag every hosting node before a client edit applies
A client edit fans out one transaction per inbound, and each one renames the
single shared clients row but calls MarkNodeDirtyTx for only its OWN node. So
between the first and the last commit the record already carries the new email
while every other node hosting that client is still config_dirty = false.

setRemoteTrafficLocked gates the snapshot merge on that flag, so a merge landing
in the gap is accepted, sees a pre-rename snapshot, finds no record for the old
email and inserts one through syncInboundClients' CreateInBatches — the only
place in the panel that creates a client record. The ghost is never in any later
merge's perInboundOld, so markSyncOrphan never fires and ReapSyncOrphans never
collects it: the operator is left with a permanent second client under the old
name. The same stale merge reverts an expiry-only edit instead of duplicating it.

Mark every node hosting the client dirty in one serialized write before the
fanout starts, so a merge queued behind it skips the node instead of merging a
half-applied edit. The nodes were going to be marked by their own applies
anyway; doing it up front only moves it earlier, and a client on local-only
inbounds never reaches the writer at all.

The set is the client's FULL attachment list, taken before the inboundIds
filter narrows it: the rename rewrites the one shared record, so an inbound the
filter excluded goes stale too.

Two tests, both red without the change. The first pins the ordering rather than
the end state — it reads the watched node's flag from inside another inbound's
push, so moving the marking after the fanout turns it red. The second pins that
the filtered path still covers the excluded node.

This narrows the window rather than closing it everywhere. A reconcile tick can
still clear the flag mid-fanout, and on a filtered edit the excluded inbound
keeps the old email in its settings for good, so its next merge duplicates
again. The case-drift path — a node reporting another case of a known email —
is untouched and still duplicates.
2026-09-07 01:51:18 +02:00
..