@seatlayer/js 0.38.0 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6165,6 +6165,8 @@ import {
6165
6165
  SeatmapRenderer,
6166
6166
  expandChart as expandChart2,
6167
6167
  computeSections,
6168
+ gaAreasOf,
6169
+ gaUnitLabels,
6168
6170
  UNGROUPED_ID
6169
6171
  } from "@seatlayer/core";
6170
6172
 
@@ -9160,13 +9162,39 @@ var SeatManager = class {
9160
9162
  this.labelToId = /* @__PURE__ */ new Map();
9161
9163
  this.labelToSeat = /* @__PURE__ */ new Map();
9162
9164
  this.allIds = [];
9165
+ /**
9166
+ * GA inventory units — real sellable labels the server counts, with NO seat
9167
+ * geometry and therefore no renderer binding. They live here rather than in
9168
+ * `labelToId`/`allIds` so every paint path keeps addressing paintable nodes
9169
+ * only, while the tally denominator finally covers the same universe the
9170
+ * numerator does. Without them a GA sale hit `booked` but not `total`:
9171
+ * Free under-reported by GA capacity and SOLD% could exceed 100%.
9172
+ */
9173
+ this.gaUnitLabelSet = /* @__PURE__ */ new Set();
9163
9174
  this.status = /* @__PURE__ */ new Map();
9175
+ /** Live non-free counters, moved by each delta rather than re-walked. */
9176
+ this.counts = { held: 0, booked: 0, blocked: 0 };
9177
+ /** Bumped whenever the seat model is replaced wholesale (a full snapshot). */
9178
+ this.modelVersion = 0;
9164
9179
  this.currency = "USD";
9165
9180
  this.authoritativeGrossRevenue = 0;
9166
9181
  this.revenueStatus = "loading";
9167
9182
  this.revenueRequest = 0;
9168
- this.revenueRefreshTimer = null;
9169
9183
  this.controlRoomSnapshot = null;
9184
+ /**
9185
+ * The server's own totals, pinned to the client model they were read against.
9186
+ * Display = server baseline + (client now − client then), so the authoritative
9187
+ * numbers land exactly on arrival and deltas still move them between reads.
9188
+ * A wholesale model replacement invalidates the pairing (`model`), and the
9189
+ * client tallies — themselves a fresh authenticated read — take over.
9190
+ */
9191
+ this.serverBaseline = null;
9192
+ /** Latest presence frame, held whether or not a snapshot has landed yet. */
9193
+ this.livePresence = null;
9194
+ /** Latest cumulative booked gross pushed on a delta frame. */
9195
+ this.liveGross = null;
9196
+ /** Coalesces a burst of deltas into one KPI/rail repaint. */
9197
+ this.paintHandle = null;
9170
9198
  this.trendWindowMinutes = 15;
9171
9199
  this.heatEnabled = false;
9172
9200
  this.lastKpiValues = /* @__PURE__ */ new Map();
@@ -9264,12 +9292,7 @@ var SeatManager = class {
9264
9292
  const res = await this.api.chart(this.key);
9265
9293
  this.doc = res.doc;
9266
9294
  this.currency = res.event.currency ?? this.opts.currency ?? this.currency;
9267
- const seats = expandChart2(res.doc);
9268
- for (const s of seats) {
9269
- this.labelToId.set(s.label, s.id);
9270
- this.labelToSeat.set(s.label, s);
9271
- this.allIds.push(s.id);
9272
- }
9295
+ this.buildUnitUniverse(res.doc);
9273
9296
  this.buildRenderer();
9274
9297
  this.buildSectionOptions();
9275
9298
  const [, controlRoom] = await Promise.all([
@@ -9630,7 +9653,10 @@ var SeatManager = class {
9630
9653
  if (this.followLiveTimer) clearTimeout(this.followLiveTimer);
9631
9654
  if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
9632
9655
  if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
9633
- if (this.revenueRefreshTimer) clearTimeout(this.revenueRefreshTimer);
9656
+ if (this.paintHandle !== null && typeof cancelAnimationFrame === "function") {
9657
+ cancelAnimationFrame(this.paintHandle);
9658
+ }
9659
+ this.paintHandle = null;
9634
9660
  this.channels?.destroy();
9635
9661
  this.channels = null;
9636
9662
  if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
@@ -9713,6 +9739,37 @@ var SeatManager = class {
9713
9739
  }
9714
9740
  this.syncSelection();
9715
9741
  }
9742
+ /**
9743
+ * Build the client's inventory universe from the chart.
9744
+ *
9745
+ * `expandChart` yields SEATS — it has no output for a GA area, whose capacity
9746
+ * is sold as N synthetic unit labels. The server's seat map keys, its deltas
9747
+ * and its `totals` all speak those labels, so a client that only knows seats
9748
+ * counts GA sales in the numerator (every key of the snapshot is written into
9749
+ * `status`) while leaving them out of the denominator. Registering the GA
9750
+ * units here — labels only, never a render binding — is what makes the two
9751
+ * agree.
9752
+ */
9753
+ buildUnitUniverse(doc) {
9754
+ for (const seat of expandChart2(doc)) {
9755
+ this.labelToId.set(seat.label, seat.id);
9756
+ this.labelToSeat.set(seat.label, seat);
9757
+ this.allIds.push(seat.id);
9758
+ }
9759
+ for (const area of gaAreasOf(doc)) {
9760
+ for (const label of gaUnitLabels(area)) {
9761
+ if (!this.labelToId.has(label)) this.gaUnitLabelSet.add(label);
9762
+ }
9763
+ }
9764
+ }
9765
+ /** Every sellable unit the client knows: seats + GA capacity. */
9766
+ unitTotal() {
9767
+ return this.allIds.length + this.gaUnitLabelSet.size;
9768
+ }
9769
+ /** Every label the client models, whether or not it can be painted. */
9770
+ knownLabels() {
9771
+ return [...this.labelToId.keys(), ...this.gaUnitLabelSet];
9772
+ }
9716
9773
  repaintAll() {
9717
9774
  const r = this.renderer;
9718
9775
  if (!r) return;
@@ -9762,7 +9819,7 @@ var SeatManager = class {
9762
9819
  ws.onopen = () => {
9763
9820
  this.attempt = 0;
9764
9821
  this.setLive(true);
9765
- void this.resnapshot().then(() => this.scheduleRevenueRefresh(0));
9822
+ void this.resnapshot().then(() => this.refreshControlRoom()).catch((err) => this.opts.onError?.(err));
9766
9823
  void this.refreshAvailability();
9767
9824
  };
9768
9825
  ws.onmessage = (e) => this.onMessage(e);
@@ -9800,15 +9857,18 @@ var SeatManager = class {
9800
9857
  this.updateEffectiveAvailability(m.hidden, m.closed);
9801
9858
  }
9802
9859
  if (m.type === "presence") {
9803
- if (this.controlRoomSnapshot && typeof m.shoppingSessions === "number" && typeof m.activeHolds === "number") {
9804
- this.controlRoomSnapshot = {
9805
- ...this.controlRoomSnapshot,
9806
- presence: { shoppingSessions: m.shoppingSessions, activeHolds: m.activeHolds }
9860
+ if (typeof m.shoppingSessions === "number" && typeof m.activeHolds === "number") {
9861
+ this.livePresence = {
9862
+ at: Date.now(),
9863
+ value: { shoppingSessions: m.shoppingSessions, activeHolds: m.activeHolds }
9807
9864
  };
9865
+ if (this.controlRoomSnapshot) {
9866
+ this.controlRoomSnapshot = { ...this.controlRoomSnapshot, presence: this.livePresence.value };
9867
+ this.opts.onControlRoom?.(this.controlRoomSnapshot);
9868
+ }
9808
9869
  this.lastSyncedAt = Date.now();
9809
9870
  this.recomputeTallies();
9810
9871
  this.paintMonitorInsights();
9811
- this.opts.onControlRoom?.(this.controlRoomSnapshot);
9812
9872
  }
9813
9873
  return;
9814
9874
  }
@@ -9822,7 +9882,7 @@ var SeatManager = class {
9822
9882
  const st = ["free", "held", "booked", "blocked"].includes(ch.status) ? ch.status : "free";
9823
9883
  const prev = this.status.get(ch.label) ?? "free";
9824
9884
  if (prev === st) continue;
9825
- this.status.set(ch.label, st);
9885
+ this.setStatusLabel(ch.label, st, prev);
9826
9886
  const id = this.labelToId.get(ch.label);
9827
9887
  if (id) {
9828
9888
  this.renderer?.setStatus([id], toRenderStatus(st));
@@ -9842,10 +9902,38 @@ var SeatManager = class {
9842
9902
  this.lastSyncedAt = Date.now();
9843
9903
  this.afterPaint();
9844
9904
  }
9905
+ if (typeof m.revenue?.gross === "number" && Number.isFinite(m.revenue.gross)) {
9906
+ this.applyLiveGross(m.revenue.gross);
9907
+ }
9845
9908
  this.recomputeTallies();
9846
- if (ids.length) this.scheduleRevenueRefresh();
9847
9909
  }
9848
9910
  }
9911
+ /**
9912
+ * Adopt the cumulative booked gross a delta frame carried.
9913
+ *
9914
+ * Stashed with its arrival time so an in-flight control-room read can decide
9915
+ * whether it is holding the newer number: a frame that landed after the
9916
+ * request started is newer than the response, one that landed before is not.
9917
+ */
9918
+ applyLiveGross(gross) {
9919
+ this.liveGross = { at: Date.now(), value: gross };
9920
+ this.authoritativeGrossRevenue = gross;
9921
+ this.revenueStatus = "current";
9922
+ if (this.controlRoomSnapshot) {
9923
+ this.controlRoomSnapshot = {
9924
+ ...this.controlRoomSnapshot,
9925
+ revenue: { ...this.controlRoomSnapshot.revenue, gross }
9926
+ };
9927
+ this.opts.onControlRoom?.(this.controlRoomSnapshot);
9928
+ }
9929
+ }
9930
+ /** The single writer for a label's status, so the counters never drift. */
9931
+ setStatusLabel(label, next, prev = this.status.get(label) ?? "free") {
9932
+ this.status.set(label, next);
9933
+ if (prev === next) return;
9934
+ if (prev !== "free") this.counts[prev] -= 1;
9935
+ if (next !== "free") this.counts[next] += 1;
9936
+ }
9849
9937
  async resnapshot() {
9850
9938
  try {
9851
9939
  const objs = await this.api.objects(this.key);
@@ -9868,23 +9956,31 @@ var SeatManager = class {
9868
9956
  const next = /* @__PURE__ */ new Map();
9869
9957
  if (fallback !== void 0) {
9870
9958
  const base = known(fallback);
9871
- for (const label of this.labelToId.keys()) next.set(label, base);
9959
+ for (const label of this.knownLabels()) next.set(label, base);
9872
9960
  }
9873
9961
  for (const [label, st] of Object.entries(seats)) {
9874
9962
  next.set(label, known(st));
9875
9963
  }
9876
9964
  this.status = next;
9965
+ this.modelVersion += 1;
9966
+ this.recountAll();
9877
9967
  this.lastSyncedAt = Date.now();
9878
9968
  this.repaintAll();
9879
9969
  this.afterPaint();
9880
9970
  this.recomputeTallies();
9881
9971
  }
9972
+ /** The one O(n) walk left: a wholesale model replacement re-bases the counters. */
9973
+ recountAll() {
9974
+ const counts = { held: 0, booked: 0, blocked: 0 };
9975
+ for (const st of this.status.values()) if (st !== "free") counts[st] += 1;
9976
+ this.counts = counts;
9977
+ }
9882
9978
  /** Optimistic local write shared by organizer actions. Paint and tally once,
9883
9979
  * even when an arena-sized operation changes hundreds of seats. */
9884
9980
  setSeatsLocal(labels, st) {
9885
9981
  const ids = [];
9886
9982
  for (const label of labels) {
9887
- this.status.set(label, st);
9983
+ this.setStatusLabel(label, st);
9888
9984
  const id = this.labelToId.get(label);
9889
9985
  if (id) ids.push(id);
9890
9986
  }
@@ -10010,12 +10106,33 @@ var SeatManager = class {
10010
10106
  this.revenueStatus = "current";
10011
10107
  this.recomputeTallies();
10012
10108
  }
10109
+ /**
10110
+ * Read the server's own control-room projection.
10111
+ *
10112
+ * Called on mount, on every socket (re)connect and after an organizer action —
10113
+ * never on a timer and never per delta frame. Presence and gross that arrived
10114
+ * on the socket AFTER this request started are newer than the response, so
10115
+ * they survive it; anything older defers to the read.
10116
+ */
10013
10117
  async refreshControlRoom() {
10014
10118
  const request = ++this.revenueRequest;
10119
+ const requestedAt = Date.now();
10015
10120
  try {
10016
- const snapshot = await this.api.controlRoom(this.key, this.trendWindowMinutes);
10121
+ const fetched = await this.api.controlRoom(this.key, this.trendWindowMinutes);
10122
+ let snapshot = fetched;
10017
10123
  if (request === this.revenueRequest) {
10124
+ if (this.livePresence && this.livePresence.at >= requestedAt) {
10125
+ snapshot = { ...snapshot, presence: this.livePresence.value };
10126
+ } else {
10127
+ this.livePresence = null;
10128
+ }
10129
+ if (this.liveGross && this.liveGross.at >= requestedAt) {
10130
+ snapshot = { ...snapshot, revenue: { ...snapshot.revenue, gross: this.liveGross.value } };
10131
+ } else {
10132
+ this.liveGross = null;
10133
+ }
10018
10134
  this.controlRoomSnapshot = snapshot;
10135
+ this.rebaseServerTotals(snapshot);
10019
10136
  this.lastSyncedAt = Date.now();
10020
10137
  this.authoritativeGrossRevenue = snapshot.revenue.gross;
10021
10138
  this.currency = snapshot.currency;
@@ -10034,37 +10151,80 @@ var SeatManager = class {
10034
10151
  throw err;
10035
10152
  }
10036
10153
  }
10037
- scheduleRevenueRefresh(delay = 140) {
10038
- this.revenueStatus = "stale";
10039
- this.recomputeTallies();
10040
- if (this.revenueRefreshTimer) clearTimeout(this.revenueRefreshTimer);
10041
- this.revenueRefreshTimer = setTimeout(() => {
10042
- this.revenueRefreshTimer = null;
10043
- void this.refreshControlRoom().catch((err) => this.opts.onError?.(err));
10044
- }, delay);
10154
+ /** Pin the server's totals to the client model they were read against. */
10155
+ rebaseServerTotals(snapshot) {
10156
+ const totals = snapshot.totals;
10157
+ if (!totals || ["free", "held", "booked", "blocked"].some(
10158
+ (key) => !Number.isFinite(totals[key])
10159
+ )) {
10160
+ this.serverBaseline = null;
10161
+ return;
10162
+ }
10163
+ this.serverBaseline = {
10164
+ model: this.modelVersion,
10165
+ server: { free: totals.free, held: totals.held, booked: totals.booked, blocked: totals.blocked },
10166
+ client: this.clientTallies()
10167
+ };
10045
10168
  }
10046
- recomputeTallies() {
10169
+ /** What the client's own model says — GA units included since `render()`. */
10170
+ clientTallies() {
10171
+ const { held, booked, blocked } = this.counts;
10172
+ return { held, booked, blocked, free: Math.max(0, this.unitTotal() - held - booked - blocked) };
10173
+ }
10174
+ /**
10175
+ * The numbers the KPI bar and rail render.
10176
+ *
10177
+ * The server is the authority: its totals land exactly as read, and the
10178
+ * delta-driven client model carries them forward until the next read. Before
10179
+ * the first snapshot — and after a wholesale model replacement invalidates the
10180
+ * pairing — the client model stands alone.
10181
+ */
10182
+ buildTallies() {
10183
+ const client = this.clientTallies();
10184
+ const baseline = this.serverBaseline?.model === this.modelVersion ? this.serverBaseline : null;
10185
+ const of = (key) => baseline ? Math.max(0, baseline.server[key] + (client[key] - baseline.client[key])) : client[key];
10186
+ const seatTotal = this.controlRoomSnapshot?.event?.seatTotal;
10047
10187
  const t3 = {
10048
- free: 0,
10049
- held: 0,
10050
- booked: 0,
10051
- blocked: 0,
10052
- total: this.allIds.length,
10188
+ free: of("free"),
10189
+ held: of("held"),
10190
+ booked: of("booked"),
10191
+ blocked: of("blocked"),
10192
+ total: Number.isFinite(seatTotal) ? seatTotal : this.unitTotal(),
10053
10193
  capacityPct: 0,
10054
10194
  sellThroughPct: 0,
10055
10195
  grossRevenue: this.authoritativeGrossRevenue,
10056
10196
  revenueStatus: this.revenueStatus,
10057
10197
  currency: this.currency
10058
10198
  };
10059
- let nonFree = 0;
10060
- for (const st of this.status.values()) {
10061
- t3[st] += 1;
10062
- if (st !== "free") nonFree += 1;
10063
- }
10064
- t3.free = Math.max(0, t3.total - nonFree);
10065
10199
  t3.capacityPct = t3.total ? Math.round(t3.booked / t3.total * 100) : 0;
10066
10200
  const sellable = t3.total - t3.blocked;
10067
10201
  t3.sellThroughPct = sellable > 0 ? Math.round(t3.booked / sellable * 100) : 0;
10202
+ return t3;
10203
+ }
10204
+ /**
10205
+ * Queue one KPI/rail repaint for this burst of changes.
10206
+ *
10207
+ * A delta frame can carry hundreds of seats and `paintKpis` rebuilds eight
10208
+ * nodes from scratch, so painting per change is what made an arena-sized
10209
+ * frame expensive. Coalescing on a frame keeps the burst to a single rebuild;
10210
+ * without `requestAnimationFrame` (SSR, an older test env) it paints inline
10211
+ * rather than dropping the update.
10212
+ */
10213
+ recomputeTallies() {
10214
+ if (this.closed) return;
10215
+ if (typeof requestAnimationFrame !== "function") {
10216
+ this.flushTallies();
10217
+ return;
10218
+ }
10219
+ if (this.paintHandle !== null) return;
10220
+ this.paintHandle = requestAnimationFrame(() => {
10221
+ this.paintHandle = null;
10222
+ this.flushTallies();
10223
+ });
10224
+ }
10225
+ flushTallies() {
10226
+ if (this.closed) return;
10227
+ const t3 = this.buildTallies();
10068
10228
  this.paintKpis(t3);
10069
10229
  if (this.mode === "view") {
10070
10230
  this.paintLegend(t3);
@@ -10351,16 +10511,16 @@ var SeatManager = class {
10351
10511
  paintKpis(t3) {
10352
10512
  if (!this.els.kpis) return;
10353
10513
  const rev = t3.revenueStatus === "current" ? fmtMoney(t3.grossRevenue, t3.currency) : "\u2014";
10354
- const presence = this.controlRoomSnapshot?.presence;
10514
+ const presence = this.presenceCounts();
10355
10515
  const items = [
10356
- { key: "sold-seats", raw: t3.booked, n: t3.booked.toLocaleString(), l: "Sold seats", dot: "#22a06b" },
10357
- { key: "held-seats", raw: t3.held, n: t3.held.toLocaleString(), l: "Held seats", dot: "#f4b740" },
10358
- { key: "buyers", raw: presence?.shoppingSessions ?? null, n: presence ? presence.shoppingSessions.toLocaleString() : "\u2014", l: "Buyers" },
10359
- { key: "active-holds", raw: presence?.activeHolds ?? null, n: presence ? presence.activeHolds.toLocaleString() : "\u2014", l: "Active holds" },
10360
- { key: "free-seats", raw: t3.free, n: t3.free.toLocaleString(), l: "Free seats", dot: "#6e7bff" },
10361
- { key: "blocked", raw: t3.blocked, n: t3.blocked.toLocaleString(), l: "Blocked", dot: "#8b94ac" },
10362
- { key: "sold-pct", raw: t3.capacityPct, n: `${t3.capacityPct}%`, l: "Sold" },
10363
- { key: "gross-sales", raw: t3.revenueStatus === "current" ? t3.grossRevenue : null, n: rev, l: "Gross sales" }
10516
+ { key: "sold-seats", raw: t3.booked, n: t3.booked.toLocaleString(), l: "Sold seats", dot: "#22a06b", title: "Seats booked" },
10517
+ { key: "held-seats", raw: t3.held, n: t3.held.toLocaleString(), l: "Held seats", dot: "#f4b740", title: "Seats held in a checkout right now" },
10518
+ { key: "free-seats", raw: t3.free, n: t3.free.toLocaleString(), l: "Free seats", dot: "#6e7bff", title: "Seats on sale and unsold" },
10519
+ { key: "blocked", raw: t3.blocked, n: t3.blocked.toLocaleString(), l: "Blocked", dot: "#8b94ac", title: "Seats withheld from sale" },
10520
+ { key: "buyers", raw: presence?.shoppingSessions ?? null, n: presence ? presence.shoppingSessions.toLocaleString() : "\u2014", l: "Buyers", title: "People on the map right now" },
10521
+ { key: "carts", raw: presence?.activeHolds ?? null, n: presence ? presence.activeHolds.toLocaleString() : "\u2014", l: "Carts", title: "Checkouts holding seats right now \u2014 sessions, not seats" },
10522
+ { key: "sold-pct", raw: t3.capacityPct, n: `${t3.capacityPct}%`, l: "Sold", title: "Sold seats as a share of the whole event" },
10523
+ { key: "gross-sales", raw: t3.revenueStatus === "current" ? t3.grossRevenue : null, n: rev, l: "Gross sales", title: "Exact booked gross" }
10364
10524
  ];
10365
10525
  let hasChanges = false;
10366
10526
  this.els.kpis.innerHTML = items.map((item) => {
@@ -10376,7 +10536,7 @@ var SeatManager = class {
10376
10536
  }
10377
10537
  if (item.raw != null) this.lastKpiValues.set(item.key, item.raw);
10378
10538
  const activeDelta = this.activeKpiDeltas.get(item.key);
10379
- return `<div class="slm-kpi${activeDelta ? " changed" : ""}" data-kpi="${item.key}">
10539
+ return `<div class="slm-kpi${activeDelta ? " changed" : ""}" data-kpi="${item.key}" title="${esc2(item.title)}">
10380
10540
  <b>${item.dot ? `<span class="dot" style="background:${item.dot}"></span>` : ""}${item.n}</b><span>${item.l}</span>
10381
10541
  ${activeDelta ? `<span class="slm-kpidelta${activeDelta.down ? " down" : ""}">${activeDelta.text}</span>` : ""}
10382
10542
  </div>`;
@@ -10434,15 +10594,21 @@ var SeatManager = class {
10434
10594
  this.paintMomentumHelp();
10435
10595
  this.paintFeed();
10436
10596
  }
10597
+ /** Live presence wins over the snapshot's copy — it is the fresher channel,
10598
+ * and it exists from the first frame rather than the first fetch. */
10599
+ presenceCounts() {
10600
+ return this.livePresence?.value ?? this.controlRoomSnapshot?.presence ?? null;
10601
+ }
10437
10602
  paintMonitorInsights() {
10438
10603
  if (this.mode !== "view") return;
10439
10604
  const snapshot = this.controlRoomSnapshot;
10440
10605
  if (this.els.presence) {
10441
10606
  const connected = this.root?.classList.contains("live");
10442
10607
  const sync = this.lastSyncedAt ? relTime(this.lastSyncedAt, Date.now()) : "waiting";
10608
+ const presence = this.presenceCounts();
10443
10609
  this.els.presence.innerHTML = `
10444
- <div class="slm-healthitem"><b>${snapshot ? snapshot.presence.shoppingSessions.toLocaleString() : "\u2014"}</b><span>Buyer sessions</span></div>
10445
- <div class="slm-healthitem"><b>${snapshot ? snapshot.presence.activeHolds.toLocaleString() : "\u2014"}</b><span>Active holds</span></div>
10610
+ <div class="slm-healthitem" title="People on the map right now"><b>${presence ? presence.shoppingSessions.toLocaleString() : "\u2014"}</b><span>Buyers</span></div>
10611
+ <div class="slm-healthitem" title="Checkouts holding seats right now \u2014 sessions, not seats"><b>${presence ? presence.activeHolds.toLocaleString() : "\u2014"}</b><span>Carts</span></div>
10446
10612
  <div class="slm-healthitem"><b>${connected ? "Healthy" : "Reconnecting"}</b><span>Live connection</span></div>
10447
10613
  <div class="slm-healthitem"><b>${sync}</b><span>Last sync</span></div>`;
10448
10614
  }
@@ -11053,7 +11219,7 @@ var SeatManager = class {
11053
11219
  const activity = action === "block" ? this.pushActivity(labels, "blocked", "blocked") : action === "unblock" || action === "unblockAll" ? this.pushActivity(labels, "unblocked", "free") : action === "cancelBooking" ? this.pushActivity(labels, "cancelled", "free") : null;
11054
11220
  if (activity) this.paintSpatialActivity(activity);
11055
11221
  }
11056
- if (action !== "setHoldTtl") this.scheduleRevenueRefresh(0);
11222
+ if (action !== "setHoldTtl") void this.refreshControlRoom().catch((err) => this.opts.onError?.(err));
11057
11223
  this.opts.onActionComplete?.({ action, labels, count: labels.length });
11058
11224
  }
11059
11225
  toastOk(msg) {