Skip to content

feat: opt-in node fanout for apiv2 save - #99

Open
shenaba wants to merge 1 commit into
mainfrom
feat/apiv2-save-sync
Open

feat: opt-in node fanout for apiv2 save#99
shenaba wants to merge 1 commit into
mainfrom
feat/apiv2-save-sync

Conversation

@shenaba

@shenaba shenaba commented Aug 7, 2026

Copy link
Copy Markdown
Owner

POST <panel path>apiv2/save now honours a sync form value. Without it a
client/inbound change converges through the hourly reconcile; with it the nodes
are pushed immediately, the way the web UI always has. Malformed values are
rejected rather than silently degrading to "no fanout".

Loop safety was never this flag's job — expectedClients' node_id scoping is
what keeps a pushed client out of every outgoing set — so the comments claiming
otherwise are corrected.

Cluster bugs fixed along the way

Reviewing that surface turned up a number of pre-existing problems:

  • Every @cluster client was re-pushed every reconcile round. clientDiffers
    compared a config the node's clients projection never sent, so the compare
    always failed. Each spurious push appended a credential-bearing changes row,
    and nothing prunes that table. Config now ships only to the reconcile read
    (full=1) — keeping it off the websocket payload and out of browser sessions —
    and is compared only when both sides carry one, which also covers
    mixed-version clusters and configless clients.
  • Node pushes wiped the node's traffic history. A push omits the counters and
    Save wrote them back as zero. They are now preserved unless the request
    actually carries them, which is how the per-client reset expresses itself: the
    drawer sends them only after Reset is clicked, so an edit from a drawer left
    open no longer rolls back what the stats job recorded meanwhile.
  • editbulk silently switched off periodic auto-reset, writing the list
    projection's omitted columns back as zeros.
  • A stale tab stripped every cluster route from a client's subscription.
    Node-owned links were taken from the request payload; they now come from the
    stored row, scoped to the replica inbounds the client still references so
    revoking access takes effect even while that node is offline. The drawer
    renders them read-only.
  • Resetting traffic left paid-up users rejected for up to an hour — it
    re-enables depleted clients but never told the nodes.
  • The [node] link marker had four independent encodings. Now one; inbound
    deletion strips the prefixed form, and node names may no longer contain
    brackets that would make two nodes' prefixes overlap.
  • ConfigService.Save's unknown-object branch left the named err nil, so
    the deferred commit, hub notify and core start ran for a failed request.

The hostname baked into generated links and the advertised subscription URI is
now resolved in one place, preferring the configured web domain and normalising
it the same way a request Host is.

UI

Links in the client drawer are no longer truncated, and a copy-all button hands
the whole set over for bulk import into a proxy client.

Verification

go vet, the full tagged build and vue-tsc all pass. Functionally verified on
a real panel:

  • edit without Reset preserves counters; Reset + save zeroes up/down and
    rolls the totals correctly
  • apiv2/clients omits config, apiv2/clients?full=1 includes it
  • node links render read-only and separate from user-owned external links
  • copy-all copies the whole set

No automated tests — the api package has no test scaffolding today, which is
tracked separately.

apiv2 save now honours a `sync` form value: without it a client/inbound
change converges through the hourly reconcile, with it the nodes are pushed
immediately, the way the web UI always has. Malformed values are rejected
rather than silently degrading to "no fanout". Loop safety was never this
flag's job — expectedClients' node_id scoping is what keeps a pushed client
out of every outgoing set — so the comments claiming otherwise are corrected.

Reviewing that surface turned up several cluster bugs, fixed here:

- clientDiffers compared a config the node's clients projection never sent,
  so every @cluster client was re-pushed every round, each push appending a
  credential-bearing changes row that nothing prunes. Config now ships only
  to the reconcile read (full=1), keeping it off the websocket payload and
  out of browser sessions, and is compared only when both sides carry one —
  which also covers mixed-version clusters and configless clients.
- A node push omits the traffic counters, and Save wrote them back as zero,
  wiping the node's history. They are preserved unless the request actually
  carries them, which is how the per-client reset expresses itself: the
  drawer sends them only after Reset is clicked, so an edit from a drawer
  left open no longer rolls back everything the stats job recorded.
- editbulk submits the list projection, whose omitted columns were written
  back as zeros, silently switching off periodic auto-reset.
- Node-owned links were taken from the request payload, so a tab loaded
  before the last reconcile stripped every cluster route from a client's
  subscription. They now come from the stored row, scoped to the replica
  inbounds the client still references so revoking access takes effect even
  while that node is offline, and the drawer renders them read-only.
- Resetting traffic re-enables depleted clients but never told the nodes,
  leaving paid-up users rejected for up to an hour.
- The "[node] " link marker had four independent encodings; it now has one,
  inbound deletion strips the prefixed form, and node names may no longer
  contain brackets that would make two nodes' prefixes overlap.
- ConfigService.Save's unknown-object branch left the named err nil, so the
  deferred commit, hub notify and core start ran for a failed request.

The hostname baked into generated links and the advertised subscription URI
is now resolved in one place, preferring the configured web domain and
normalising it the same way a request Host is.

In the client drawer, links are no longer truncated and a copy-all button
hands the whole set over for bulk import into a proxy client.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant