@seatlayer/js 0.37.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
@@ -1,11 +1,9 @@
1
- var __typeError = (msg) => {
2
- throw TypeError(msg);
3
- };
4
- var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
5
- var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
6
- var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
7
- var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
8
- var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
1
+ import {
2
+ __privateAdd,
3
+ __privateGet,
4
+ __privateMethod,
5
+ __privateSet
6
+ } from "./chunk-HMLY7DHA.js";
9
7
 
10
8
  // src/SeatingChart.ts
11
9
  import {
@@ -120,6 +118,41 @@ var PubApi = class {
120
118
  body: { holdId, ...ttlMs ? { ttlMs } : {} }
121
119
  });
122
120
  }
121
+ /**
122
+ * Which gateways this event can actually take money through — the question
123
+ * `checkout: 'hosted'` has to answer BEFORE it shows a buyer a Pay button, so
124
+ * the answer is never discovered by failing a payment.
125
+ *
126
+ * Anonymous, and it discloses no account, key, mode or currency for a gateway
127
+ * that did not match.
128
+ */
129
+ paymentOptions(key) {
130
+ return this.request(`/pub/events/${encodeURIComponent(key)}/payment-options`);
131
+ }
132
+ /**
133
+ * Turn a live hold into an order and start a payment.
134
+ *
135
+ * The amount is NOT sent: the server recomputes it from the hold's own items,
136
+ * which is the only reason a browser cannot alter what it pays. Nor is the
137
+ * PROVIDER — the event row decides which gateway charges, and a `provider` in
138
+ * the body is checked rather than obeyed (409 `provider_mismatch`). Omitting
139
+ * it is the shape that cannot disagree.
140
+ */
141
+ startCheckout(key, input) {
142
+ return this.request(`/pub/events/${encodeURIComponent(key)}/checkout`, {
143
+ method: "POST",
144
+ body: input
145
+ });
146
+ }
147
+ /**
148
+ * Poll an order while its gateway webhook lands. The order id is an
149
+ * unguessable token the buyer already holds, so it acts as the capability —
150
+ * which is also why a buyer returning from a gateway page can be told what
151
+ * happened with nothing but the id in the return URL.
152
+ */
153
+ orderStatus(orderId) {
154
+ return this.request(`/pub/orders/${encodeURIComponent(orderId)}/status`);
155
+ }
123
156
  /**
124
157
  * Mint a one-use subscribe ticket for the next socket attempt (protocol doc
125
158
  * §3). The bearer travels here, over ordinary HTTPS where CORS and Origin
@@ -1673,7 +1706,8 @@ var EmbeddedDesigner = class {
1673
1706
  }
1674
1707
  buildSkeleton(overlay) {
1675
1708
  const style = document.createElement("style");
1676
- style.textContent = `
1709
+ style.textContent = /* @sl-css */
1710
+ `
1677
1711
  @media (prefers-reduced-motion: no-preference) {
1678
1712
  @keyframes seatlayer-designer-shimmer {
1679
1713
  0% { background-position: -320px 0; }
@@ -1799,7 +1833,6 @@ import {
1799
1833
  PickerController as PickerController2,
1800
1834
  ACCESSIBILITY_TYPES,
1801
1835
  expandChart,
1802
- generateSeatPanorama,
1803
1836
  generateSeatThumb,
1804
1837
  loadLocale as loadLocale2,
1805
1838
  setStringOverrides as setStringOverrides2,
@@ -1862,20 +1895,45 @@ function hasWebGL2() {
1862
1895
  }
1863
1896
  return _webgl2Cache;
1864
1897
  }
1898
+ function cdnChunkUrl(fileName) {
1899
+ const base = SEATLAYER_MODULE_URL ?? (typeof location !== "undefined" ? location.href : void 0);
1900
+ if (!base) throw new Error(`seatlayer: cannot resolve the ${fileName} chunk URL`);
1901
+ return new URL(`./${fileName}`, base).href;
1902
+ }
1865
1903
  async function loadVenue3d() {
1866
1904
  if (typeof __SEATLAYER_CDN__ !== "undefined" && __SEATLAYER_CDN__) {
1867
- const base = SEATLAYER_MODULE_URL ?? (typeof location !== "undefined" ? location.href : void 0);
1868
- if (!base) throw new Error("seatlayer: cannot resolve the 3D view chunk URL");
1869
- const url = new URL("./seatlayer-view3d.mjs", base).href;
1870
1905
  return import(
1871
1906
  /* @vite-ignore */
1872
- url
1907
+ cdnChunkUrl("seatlayer-view3d.mjs")
1873
1908
  );
1874
1909
  }
1875
1910
  return import("@seatlayer/core/view3d");
1876
1911
  }
1912
+ async function loadPanorama() {
1913
+ if (typeof __SEATLAYER_CDN__ !== "undefined" && __SEATLAYER_CDN__) {
1914
+ return import(
1915
+ /* @vite-ignore */
1916
+ cdnChunkUrl("seatlayer-panorama.mjs")
1917
+ );
1918
+ }
1919
+ return import("@seatlayer/core");
1920
+ }
1921
+ async function loadHostedCheckout() {
1922
+ if (typeof __SEATLAYER_CDN__ !== "undefined" && __SEATLAYER_CDN__) {
1923
+ return import(
1924
+ /* @vite-ignore */
1925
+ cdnChunkUrl("seatlayer-checkout.mjs")
1926
+ );
1927
+ }
1928
+ return import("./hostedCheckout-PLHKPT6W.js");
1929
+ }
1930
+ function paymentsOffReason(reason) {
1931
+ return reason === "unavailable_for_event" || reason === "payments_off_for_event" ? reason : "not_configured";
1932
+ }
1877
1933
  var STYLE_ID = "seatlayer-picker-style";
1878
- var CSS = `
1934
+ var CSS = (
1935
+ /* @sl-css */
1936
+ `
1879
1937
  .sl-picker{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:420px;overflow:hidden;
1880
1938
  background:var(--sl-bg);color:var(--sl-text);font-family:var(--sl-font);border-radius:var(--sl-radius);
1881
1939
  --sl-r-sm:calc(var(--sl-radius) * .55);
@@ -2612,7 +2670,8 @@ var CSS = `
2612
2670
  .sl-modal-scrim{position:fixed;inset:0;z-index:2147483000;background:rgba(5,7,12,.66);display:flex;align-items:center;justify-content:center;padding:18px}
2613
2671
  .sl-modal-frame{width:min(1200px,100%);height:min(820px,100%);border-radius:16px;overflow:hidden;box-shadow:0 40px 120px -30px rgba(0,0,0,.8)}
2614
2672
  @media(max-width:640px){.sl-modal-scrim{padding:0}.sl-modal-frame{width:100%;height:100%;border-radius:0}}
2615
- `;
2673
+ `
2674
+ );
2616
2675
  function ensureStyle() {
2617
2676
  if (document.getElementById(STYLE_ID)) return;
2618
2677
  const el = document.createElement("style");
@@ -2677,6 +2736,14 @@ var SeatPicker = class _SeatPicker {
2677
2736
  this.handedOff = false;
2678
2737
  /** Guards single onBooked + single success overlay per hold. */
2679
2738
  this.bookedShown = false;
2739
+ /**
2740
+ * In-flight or settled `payment-options` for this event, started at render in
2741
+ * hosted mode. One request, kicked off while the buyer is still choosing, so
2742
+ * pressing Pay does not wait on a lookup whose answer never changes mid-session.
2743
+ */
2744
+ this.paymentOptions = null;
2745
+ /** The mounted payment card, while one is up. */
2746
+ this.checkoutPanel = null;
2680
2747
  this.extendEl = null;
2681
2748
  this.bookedEl = null;
2682
2749
  this.gaQty = /* @__PURE__ */ new Map();
@@ -2791,6 +2858,12 @@ var SeatPicker = class _SeatPicker {
2791
2858
  onObjectUnavailable: (event) => this.opts.onSelectedObjectUnavailable?.(event)
2792
2859
  });
2793
2860
  this.api = options.transport ?? this.pubApi;
2861
+ if (options.checkout === "hosted" && !this.pubApi) {
2862
+ console.warn(
2863
+ 'seatlayer: checkout: "hosted" needs the widget\'s own transport \u2014 a custom `transport` owns its backend, so the picker is staying on onCheckout for this mount.'
2864
+ );
2865
+ }
2866
+ this.checkoutMode = options.checkout === "hosted" && this.pubApi ? "hosted" : "handoff";
2794
2867
  this.maxTickets = Math.max(1, Math.floor(options.maxSelection ?? DEFAULT_MAX_SELECTION2));
2795
2868
  this.cbSafe = readStoredColorblind() ?? !!options.colorblindSafe;
2796
2869
  this.controller = new PickerController2({
@@ -3154,6 +3227,7 @@ var SeatPicker = class _SeatPicker {
3154
3227
  ensureStyle();
3155
3228
  await loadLocale2(this.opts.locale);
3156
3229
  if (this.opts.messages) setStringOverrides2(this.opts.messages);
3230
+ if (this.checkoutMode === "hosted") this.paymentOptions = this.pubApi.paymentOptions(this.opts.event);
3157
3231
  const mount = resolveContainer3(this.opts.container);
3158
3232
  const root = document.createElement("div");
3159
3233
  root.className = "sl-picker";
@@ -3451,8 +3525,40 @@ var SeatPicker = class _SeatPicker {
3451
3525
  if (this.salesClosed) this.applySalesClosed();
3452
3526
  this.syncPrices();
3453
3527
  this.syncTray();
3528
+ this.resumeHostedOrder();
3454
3529
  return this;
3455
3530
  }
3531
+ /**
3532
+ * A hosted gateway returned this buyer to a page that runs the widget, with
3533
+ * `?order=…&status=…` in the URL. Pick the order up and finish the story.
3534
+ *
3535
+ * Only `success` resumes. `cancelled` means the buyer backed out at the
3536
+ * gateway and their seats are still held — the map they are looking at IS the
3537
+ * right screen, and opening a card to say "you cancelled" would be noise.
3538
+ *
3539
+ * The two parameters are then stripped with `replaceState`, because they are a
3540
+ * one-shot instruction: leaving them in place would re-open the confirmation
3541
+ * on every later navigation, and would carry an order id into browser history
3542
+ * and any Referer this page later sends. `status` is only ever removed
3543
+ * alongside an `order` we actually consumed, so a host page that uses a
3544
+ * `status` parameter of its own keeps it.
3545
+ */
3546
+ resumeHostedOrder() {
3547
+ if (this.checkoutMode !== "hosted" || typeof location === "undefined") return;
3548
+ const params = new URLSearchParams(location.search);
3549
+ const orderId = params.get("order");
3550
+ if (!orderId) return;
3551
+ const status = params.get("status");
3552
+ params.delete("order");
3553
+ params.delete("status");
3554
+ try {
3555
+ const query = params.toString();
3556
+ history.replaceState(history.state, "", `${location.pathname}${query ? `?${query}` : ""}${location.hash}`);
3557
+ } catch {
3558
+ }
3559
+ if (status !== "success") return;
3560
+ void this.openCheckoutPanel({ kind: "resume", orderId });
3561
+ }
3456
3562
  /**
3457
3563
  * Move layout-dependent chrome between its wide dock (map regions / zoom
3458
3564
  * column) and its narrow dock (the sheet's consolidated Filters row), and
@@ -4432,7 +4538,7 @@ var SeatPicker = class _SeatPicker {
4432
4538
  this.els.map.appendChild(el);
4433
4539
  this.confirmEl = el;
4434
4540
  this.reanchorConfirm();
4435
- el.querySelector(".sl-confirm-view")?.addEventListener("click", () => this.openSeatView(seat));
4541
+ el.querySelector(".sl-confirm-view")?.addEventListener("click", () => void this.openSeatView(seat));
4436
4542
  el.querySelector(".sl-confirm-3d")?.addEventListener("click", () => {
4437
4543
  if (this.buyerView === "venue3d") {
4438
4544
  void this.view3dHandle?.flyToSeat(seat.id);
@@ -4503,10 +4609,13 @@ var SeatPicker = class _SeatPicker {
4503
4609
  * uploaded photo (seat.viewUrl) when present, else a panorama generated from
4504
4610
  * the chart geometry — the stage placed at this seat's true bearing + size.
4505
4611
  * Zero extra dependencies: an equirectangular image panned with `repeat-x`.
4612
+ *
4613
+ * Async only because the generator is a lazy chunk (see `loadPanorama`); an
4614
+ * organizer photo needs no generator and never waits on it. The two callers
4615
+ * are click handlers, so nothing observes the promise.
4506
4616
  */
4507
- openSeatView(seat) {
4617
+ async openSeatView(seat) {
4508
4618
  if (!this.root || !this.seatViewEnabled()) return;
4509
- this.closeSeatView();
4510
4619
  const doc = this.controller.doc;
4511
4620
  const activeId = this.controller.getActiveFloorId();
4512
4621
  const focal = seat.focalPoint ?? doc?.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc?.focalPoint ?? { x: 0, y: 0 };
@@ -4518,10 +4627,19 @@ var SeatPicker = class _SeatPicker {
4518
4627
  caption = t2("picker.panorama360");
4519
4628
  real = true;
4520
4629
  } else {
4521
- const pano2 = generateSeatPanorama(seat, focal, this.allSeats());
4630
+ let pano2;
4631
+ try {
4632
+ const { generateSeatPanorama } = await loadPanorama();
4633
+ pano2 = generateSeatPanorama(seat, focal, this.allSeats());
4634
+ } catch (err) {
4635
+ this.opts.onError?.(err);
4636
+ return;
4637
+ }
4638
+ if (!this.root || !this.seatViewEnabled()) return;
4522
4639
  panoUrl = pano2.url;
4523
4640
  caption = t2("picker.illustrationCaption", { m: pano2.distanceM });
4524
4641
  }
4642
+ this.closeSeatView();
4525
4643
  const el = document.createElement("div");
4526
4644
  el.className = "sl-view";
4527
4645
  el.setAttribute("role", "dialog");
@@ -4887,7 +5005,7 @@ var SeatPicker = class _SeatPicker {
4887
5005
  this.els.tray.querySelectorAll(".sl-chip .view[data-view-label]").forEach((btn) => {
4888
5006
  btn.addEventListener("click", () => {
4889
5007
  const seat = this.controller.seatByLabel(btn.dataset.viewLabel);
4890
- if (seat) this.openSeatView(seat);
5008
+ if (seat) void this.openSeatView(seat);
4891
5009
  });
4892
5010
  });
4893
5011
  this.els.tray.querySelectorAll(".sl-chip[data-locate]").forEach((chip) => {
@@ -5030,7 +5148,7 @@ var SeatPicker = class _SeatPicker {
5030
5148
  const seats = this.hold.seats ?? committed;
5031
5149
  this.handedOff = true;
5032
5150
  this.setCtaPhase("checkout");
5033
- this.opts.onCheckout?.(this.hold, seats, this.buildHandoff(this.hold));
5151
+ this.checkoutHandoff(this.hold, seats);
5034
5152
  return;
5035
5153
  }
5036
5154
  this.holdingLabels = new Set(committed.map((seat) => seat.label));
@@ -5059,7 +5177,7 @@ var SeatPicker = class _SeatPicker {
5059
5177
  this.flashHeldSeats(hold);
5060
5178
  this.setCtaPhase("checkout");
5061
5179
  this.emitHoldChange();
5062
- this.opts.onCheckout?.(hold, hold.seats ?? chosenSeats, this.buildHandoff(hold));
5180
+ this.checkoutHandoff(hold, hold.seats ?? chosenSeats);
5063
5181
  } catch (err) {
5064
5182
  this.opts.onError?.(err);
5065
5183
  const problem = err;
@@ -5164,6 +5282,109 @@ var SeatPicker = class _SeatPicker {
5164
5282
  this.bookedEl?.classList.add("on");
5165
5283
  this.opts.onBooked?.(handoff);
5166
5284
  }
5285
+ /**
5286
+ * The seats are held. Send the buyer wherever this picker's `checkout` option
5287
+ * says they go.
5288
+ *
5289
+ * The default branch is the literal call that stood here before hosted
5290
+ * checkout existed, unchanged, so nothing about an existing integration moves.
5291
+ */
5292
+ checkoutHandoff(hold, seats) {
5293
+ if (this.checkoutMode === "hosted") {
5294
+ void this.startHostedCheckout(hold, seats);
5295
+ return;
5296
+ }
5297
+ this.opts.onCheckout?.(hold, seats, this.buildHandoff(hold));
5298
+ }
5299
+ /**
5300
+ * Take the money ourselves, through the organizer's own gateway.
5301
+ *
5302
+ * Order of operations matters: ASK FIRST, load second. `payment-options` is
5303
+ * already in flight from render, and its answer decides whether any payment
5304
+ * code is fetched at all — an event that cannot charge never downloads the
5305
+ * card that would have charged it.
5306
+ *
5307
+ * An empty list is not a failure and never dead-ends the buyer. It routes them
5308
+ * to whatever the host has: `onCheckoutUnavailable` (with the server's reason,
5309
+ * so the host can say the right one of three very different sentences), then
5310
+ * `onCheckout` with the ordinary handoff. A host that supplied neither gets
5311
+ * the widget's own honest card instead of a press that did nothing.
5312
+ */
5313
+ async startHostedCheckout(hold, seats) {
5314
+ const handoff = this.buildHandoff(hold);
5315
+ let options = null;
5316
+ try {
5317
+ options = await (this.paymentOptions ??= this.pubApi.paymentOptions(this.opts.event));
5318
+ } catch (err) {
5319
+ this.opts.onError?.(err);
5320
+ }
5321
+ if (this.destroyed) return;
5322
+ const provider = options?.providers?.[0];
5323
+ if (!provider) {
5324
+ const reason = paymentsOffReason(options?.reason);
5325
+ const handled = !!this.opts.onCheckoutUnavailable || !!this.opts.onCheckout;
5326
+ this.opts.onCheckoutUnavailable?.({ reason, handoff });
5327
+ this.opts.onCheckout?.(hold, seats, handoff);
5328
+ if (!handled) {
5329
+ void this.openCheckoutPanel({
5330
+ kind: "unavailable",
5331
+ reason,
5332
+ seatCount: handoff.lineItems.reduce((sum, item) => sum + item.quantity, 0)
5333
+ });
5334
+ }
5335
+ return;
5336
+ }
5337
+ await this.openCheckoutPanel({
5338
+ kind: "pay",
5339
+ provider,
5340
+ order: {
5341
+ holdId: handoff.holdId,
5342
+ expiresAt: handoff.expiresAt,
5343
+ currency: handoff.currency,
5344
+ total: handoff.total,
5345
+ labels: handoff.lineItems.map((item) => item.displayLabel ?? item.label)
5346
+ }
5347
+ });
5348
+ }
5349
+ /**
5350
+ * Fetch the checkout chunk and put its card over the map.
5351
+ *
5352
+ * Every failure here lands the buyer back on a map with their seats still
5353
+ * held, which is a place they can act from — a blocked chunk request must not
5354
+ * leave them staring at a CTA that no longer does anything.
5355
+ */
5356
+ async openCheckoutPanel(state) {
5357
+ let mountCheckout;
5358
+ try {
5359
+ ({ mountCheckout } = await loadHostedCheckout());
5360
+ } catch (err) {
5361
+ this.opts.onError?.(err);
5362
+ this.toast("Checkout could not be opened. Your seats are still held \u2014 please try again.", "error");
5363
+ this.setCtaPhase("idle");
5364
+ return;
5365
+ }
5366
+ if (this.destroyed || !this.root) return;
5367
+ this.closeCheckoutPanel();
5368
+ this.checkoutPanel = mountCheckout({
5369
+ root: this.root,
5370
+ state,
5371
+ startSession: (input) => this.pubApi.startCheckout(this.opts.event, input),
5372
+ orderStatus: (orderId) => this.pubApi.orderStatus(orderId),
5373
+ onCancel: () => {
5374
+ this.checkoutPanel = null;
5375
+ if (!this.hold) this.setCtaPhase("idle");
5376
+ },
5377
+ onConfirmed: (order) => {
5378
+ this.opts.onOrderConfirmed?.(order);
5379
+ void this.controller.refresh();
5380
+ },
5381
+ onError: (err) => this.opts.onError?.(err)
5382
+ });
5383
+ }
5384
+ closeCheckoutPanel() {
5385
+ this.checkoutPanel?.destroy();
5386
+ this.checkoutPanel = null;
5387
+ }
5167
5388
  /** Assemble the stable {@link CheckoutHandoff} from a hold's server line items. */
5168
5389
  buildHandoff(hold) {
5169
5390
  const items = hold.items ?? [];
@@ -5398,7 +5619,7 @@ var SeatPicker = class _SeatPicker {
5398
5619
  }
5399
5620
  /** Build the view-from-seat panorama the cinematic dissolves into — reuses the
5400
5621
  * exact input path as the 2D `openSeatView` (organizer photo, else generated). */
5401
- seatViewFor3d(seatId) {
5622
+ async seatViewFor3d(seatId) {
5402
5623
  const seat = this.allSeats().find((s) => s.id === seatId);
5403
5624
  if (!seat) return null;
5404
5625
  if (seat.viewUrl) return { url: seat.viewUrl };
@@ -5407,6 +5628,7 @@ var SeatPicker = class _SeatPicker {
5407
5628
  const activeId = this.controller.getActiveFloorId();
5408
5629
  const focal = seat.focalPoint ?? doc.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc.focalPoint ?? { x: 0, y: 0 };
5409
5630
  try {
5631
+ const { generateSeatPanorama } = await loadPanorama();
5410
5632
  return { url: generateSeatPanorama(seat, focal, this.allSeats()).url };
5411
5633
  } catch {
5412
5634
  return null;
@@ -5546,9 +5768,10 @@ var SeatPicker = class _SeatPicker {
5546
5768
  // in orbit instead of dissolving into an empty overlay.
5547
5769
  getSeatView: (id) => new Promise((resolve, reject) => {
5548
5770
  const run = () => {
5549
- const view = this.seatViewFor3d(id);
5550
- if (view) resolve(view);
5551
- else reject(new Error("seat_view_unavailable"));
5771
+ void this.seatViewFor3d(id).then((view) => {
5772
+ if (view) resolve(view);
5773
+ else reject(new Error("seat_view_unavailable"));
5774
+ });
5552
5775
  };
5553
5776
  const ric = globalThis.requestIdleCallback;
5554
5777
  if (typeof ric === "function") ric(run, { timeout: 1500 });
@@ -5826,6 +6049,7 @@ var SeatPicker = class _SeatPicker {
5826
6049
  this.closeConfirm();
5827
6050
  this.dismissTableDialog(false);
5828
6051
  this.closeSeatView();
6052
+ this.closeCheckoutPanel();
5829
6053
  this.exit3d();
5830
6054
  this.stopHoldTimer();
5831
6055
  if (this.toastTimer) clearTimeout(this.toastTimer);
@@ -5941,6 +6165,8 @@ import {
5941
6165
  SeatmapRenderer,
5942
6166
  expandChart as expandChart2,
5943
6167
  computeSections,
6168
+ gaAreasOf,
6169
+ gaUnitLabels,
5944
6170
  UNGROUPED_ID
5945
6171
  } from "@seatlayer/core";
5946
6172
 
@@ -6552,7 +6778,9 @@ var PREVIEW_ELIGIBLE_STROKE = "#b9c0ff";
6552
6778
  var PREVIEW_UNAVAILABLE_FILL = "#303846";
6553
6779
  var PREVIEW_UNAVAILABLE_STROKE = "#4b5669";
6554
6780
  var ALLOCATION_STROKE = "#101723";
6555
- var CHANNELS_CSS = `
6781
+ var CHANNELS_CSS = (
6782
+ /* @sl-css */
6783
+ `
6556
6784
  .slm{--slm-mo-instant:80ms;--slm-mo-quick:140ms;--slm-mo-base:200ms;--slm-mo-slow:320ms;--slm-mo-ambient:2000ms;
6557
6785
  --slm-mo-out:cubic-bezier(.2,.8,.2,1);--slm-mo-in-out:cubic-bezier(.4,0,.2,1);--slm-mo-exit:cubic-bezier(.4,0,1,1);
6558
6786
  --slm-mo-spring:cubic-bezier(.34,1.3,.64,1)}
@@ -6741,7 +6969,8 @@ var CHANNELS_CSS = `
6741
6969
  .slm-ch-staged.shake{outline:2px solid #e5484d;outline-offset:2px}
6742
6970
  .slm-ch-staged.done{outline:2px solid #5bd39b;outline-offset:2px}
6743
6971
  }
6744
- `;
6972
+ `
6973
+ );
6745
6974
  function bucketRowsHtml(rows) {
6746
6975
  return rows.map((row, index) => `
6747
6976
  <div class="slm-ch-bucket" style="animation-delay:${Math.min(index, 4) * 30}ms">
@@ -8639,7 +8868,9 @@ var LEGEND = [
8639
8868
  { key: "booked", label: "Booked", color: "#22a06b" },
8640
8869
  { key: "blocked", label: "Blocked", color: "#8b94ac" }
8641
8870
  ];
8642
- var MANAGER_CSS = `
8871
+ var MANAGER_CSS = (
8872
+ /* @sl-css */
8873
+ `
8643
8874
  .slm{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:480px;overflow:hidden;
8644
8875
  background:var(--slm-bg);color:var(--slm-text);font-family:var(--slm-font);border-radius:var(--slm-radius);
8645
8876
  /* Motion tokens (motion-system \xA72), declared by the cockpit ROOT rather than
@@ -8881,7 +9112,8 @@ var MANAGER_CSS = `
8881
9112
  transition:none!important;
8882
9113
  scroll-behavior:auto!important}
8883
9114
  }
8884
- ${CHANNELS_CSS}`;
9115
+ ${CHANNELS_CSS}`
9116
+ );
8885
9117
  function injectStyle() {
8886
9118
  if (typeof document === "undefined" || document.getElementById(STYLE_ID2)) return;
8887
9119
  const el = document.createElement("style");
@@ -8930,13 +9162,39 @@ var SeatManager = class {
8930
9162
  this.labelToId = /* @__PURE__ */ new Map();
8931
9163
  this.labelToSeat = /* @__PURE__ */ new Map();
8932
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();
8933
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;
8934
9179
  this.currency = "USD";
8935
9180
  this.authoritativeGrossRevenue = 0;
8936
9181
  this.revenueStatus = "loading";
8937
9182
  this.revenueRequest = 0;
8938
- this.revenueRefreshTimer = null;
8939
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;
8940
9198
  this.trendWindowMinutes = 15;
8941
9199
  this.heatEnabled = false;
8942
9200
  this.lastKpiValues = /* @__PURE__ */ new Map();
@@ -9034,12 +9292,7 @@ var SeatManager = class {
9034
9292
  const res = await this.api.chart(this.key);
9035
9293
  this.doc = res.doc;
9036
9294
  this.currency = res.event.currency ?? this.opts.currency ?? this.currency;
9037
- const seats = expandChart2(res.doc);
9038
- for (const s of seats) {
9039
- this.labelToId.set(s.label, s.id);
9040
- this.labelToSeat.set(s.label, s);
9041
- this.allIds.push(s.id);
9042
- }
9295
+ this.buildUnitUniverse(res.doc);
9043
9296
  this.buildRenderer();
9044
9297
  this.buildSectionOptions();
9045
9298
  const [, controlRoom] = await Promise.all([
@@ -9400,7 +9653,10 @@ var SeatManager = class {
9400
9653
  if (this.followLiveTimer) clearTimeout(this.followLiveTimer);
9401
9654
  if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
9402
9655
  if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
9403
- if (this.revenueRefreshTimer) clearTimeout(this.revenueRefreshTimer);
9656
+ if (this.paintHandle !== null && typeof cancelAnimationFrame === "function") {
9657
+ cancelAnimationFrame(this.paintHandle);
9658
+ }
9659
+ this.paintHandle = null;
9404
9660
  this.channels?.destroy();
9405
9661
  this.channels = null;
9406
9662
  if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
@@ -9483,6 +9739,37 @@ var SeatManager = class {
9483
9739
  }
9484
9740
  this.syncSelection();
9485
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
+ }
9486
9773
  repaintAll() {
9487
9774
  const r = this.renderer;
9488
9775
  if (!r) return;
@@ -9532,7 +9819,7 @@ var SeatManager = class {
9532
9819
  ws.onopen = () => {
9533
9820
  this.attempt = 0;
9534
9821
  this.setLive(true);
9535
- void this.resnapshot().then(() => this.scheduleRevenueRefresh(0));
9822
+ void this.resnapshot().then(() => this.refreshControlRoom()).catch((err) => this.opts.onError?.(err));
9536
9823
  void this.refreshAvailability();
9537
9824
  };
9538
9825
  ws.onmessage = (e) => this.onMessage(e);
@@ -9570,15 +9857,18 @@ var SeatManager = class {
9570
9857
  this.updateEffectiveAvailability(m.hidden, m.closed);
9571
9858
  }
9572
9859
  if (m.type === "presence") {
9573
- if (this.controlRoomSnapshot && typeof m.shoppingSessions === "number" && typeof m.activeHolds === "number") {
9574
- this.controlRoomSnapshot = {
9575
- ...this.controlRoomSnapshot,
9576
- 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 }
9577
9864
  };
9865
+ if (this.controlRoomSnapshot) {
9866
+ this.controlRoomSnapshot = { ...this.controlRoomSnapshot, presence: this.livePresence.value };
9867
+ this.opts.onControlRoom?.(this.controlRoomSnapshot);
9868
+ }
9578
9869
  this.lastSyncedAt = Date.now();
9579
9870
  this.recomputeTallies();
9580
9871
  this.paintMonitorInsights();
9581
- this.opts.onControlRoom?.(this.controlRoomSnapshot);
9582
9872
  }
9583
9873
  return;
9584
9874
  }
@@ -9592,7 +9882,7 @@ var SeatManager = class {
9592
9882
  const st = ["free", "held", "booked", "blocked"].includes(ch.status) ? ch.status : "free";
9593
9883
  const prev = this.status.get(ch.label) ?? "free";
9594
9884
  if (prev === st) continue;
9595
- this.status.set(ch.label, st);
9885
+ this.setStatusLabel(ch.label, st, prev);
9596
9886
  const id = this.labelToId.get(ch.label);
9597
9887
  if (id) {
9598
9888
  this.renderer?.setStatus([id], toRenderStatus(st));
@@ -9612,10 +9902,38 @@ var SeatManager = class {
9612
9902
  this.lastSyncedAt = Date.now();
9613
9903
  this.afterPaint();
9614
9904
  }
9905
+ if (typeof m.revenue?.gross === "number" && Number.isFinite(m.revenue.gross)) {
9906
+ this.applyLiveGross(m.revenue.gross);
9907
+ }
9615
9908
  this.recomputeTallies();
9616
- if (ids.length) this.scheduleRevenueRefresh();
9617
9909
  }
9618
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
+ }
9619
9937
  async resnapshot() {
9620
9938
  try {
9621
9939
  const objs = await this.api.objects(this.key);
@@ -9638,23 +9956,31 @@ var SeatManager = class {
9638
9956
  const next = /* @__PURE__ */ new Map();
9639
9957
  if (fallback !== void 0) {
9640
9958
  const base = known(fallback);
9641
- for (const label of this.labelToId.keys()) next.set(label, base);
9959
+ for (const label of this.knownLabels()) next.set(label, base);
9642
9960
  }
9643
9961
  for (const [label, st] of Object.entries(seats)) {
9644
9962
  next.set(label, known(st));
9645
9963
  }
9646
9964
  this.status = next;
9965
+ this.modelVersion += 1;
9966
+ this.recountAll();
9647
9967
  this.lastSyncedAt = Date.now();
9648
9968
  this.repaintAll();
9649
9969
  this.afterPaint();
9650
9970
  this.recomputeTallies();
9651
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
+ }
9652
9978
  /** Optimistic local write shared by organizer actions. Paint and tally once,
9653
9979
  * even when an arena-sized operation changes hundreds of seats. */
9654
9980
  setSeatsLocal(labels, st) {
9655
9981
  const ids = [];
9656
9982
  for (const label of labels) {
9657
- this.status.set(label, st);
9983
+ this.setStatusLabel(label, st);
9658
9984
  const id = this.labelToId.get(label);
9659
9985
  if (id) ids.push(id);
9660
9986
  }
@@ -9780,12 +10106,33 @@ var SeatManager = class {
9780
10106
  this.revenueStatus = "current";
9781
10107
  this.recomputeTallies();
9782
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
+ */
9783
10117
  async refreshControlRoom() {
9784
10118
  const request = ++this.revenueRequest;
10119
+ const requestedAt = Date.now();
9785
10120
  try {
9786
- 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;
9787
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
+ }
9788
10134
  this.controlRoomSnapshot = snapshot;
10135
+ this.rebaseServerTotals(snapshot);
9789
10136
  this.lastSyncedAt = Date.now();
9790
10137
  this.authoritativeGrossRevenue = snapshot.revenue.gross;
9791
10138
  this.currency = snapshot.currency;
@@ -9804,37 +10151,80 @@ var SeatManager = class {
9804
10151
  throw err;
9805
10152
  }
9806
10153
  }
9807
- scheduleRevenueRefresh(delay = 140) {
9808
- this.revenueStatus = "stale";
9809
- this.recomputeTallies();
9810
- if (this.revenueRefreshTimer) clearTimeout(this.revenueRefreshTimer);
9811
- this.revenueRefreshTimer = setTimeout(() => {
9812
- this.revenueRefreshTimer = null;
9813
- void this.refreshControlRoom().catch((err) => this.opts.onError?.(err));
9814
- }, 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
+ };
9815
10168
  }
9816
- 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;
9817
10187
  const t3 = {
9818
- free: 0,
9819
- held: 0,
9820
- booked: 0,
9821
- blocked: 0,
9822
- 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(),
9823
10193
  capacityPct: 0,
9824
10194
  sellThroughPct: 0,
9825
10195
  grossRevenue: this.authoritativeGrossRevenue,
9826
10196
  revenueStatus: this.revenueStatus,
9827
10197
  currency: this.currency
9828
10198
  };
9829
- let nonFree = 0;
9830
- for (const st of this.status.values()) {
9831
- t3[st] += 1;
9832
- if (st !== "free") nonFree += 1;
9833
- }
9834
- t3.free = Math.max(0, t3.total - nonFree);
9835
10199
  t3.capacityPct = t3.total ? Math.round(t3.booked / t3.total * 100) : 0;
9836
10200
  const sellable = t3.total - t3.blocked;
9837
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();
9838
10228
  this.paintKpis(t3);
9839
10229
  if (this.mode === "view") {
9840
10230
  this.paintLegend(t3);
@@ -10121,16 +10511,16 @@ var SeatManager = class {
10121
10511
  paintKpis(t3) {
10122
10512
  if (!this.els.kpis) return;
10123
10513
  const rev = t3.revenueStatus === "current" ? fmtMoney(t3.grossRevenue, t3.currency) : "\u2014";
10124
- const presence = this.controlRoomSnapshot?.presence;
10514
+ const presence = this.presenceCounts();
10125
10515
  const items = [
10126
- { key: "sold-seats", raw: t3.booked, n: t3.booked.toLocaleString(), l: "Sold seats", dot: "#22a06b" },
10127
- { key: "held-seats", raw: t3.held, n: t3.held.toLocaleString(), l: "Held seats", dot: "#f4b740" },
10128
- { key: "buyers", raw: presence?.shoppingSessions ?? null, n: presence ? presence.shoppingSessions.toLocaleString() : "\u2014", l: "Buyers" },
10129
- { key: "active-holds", raw: presence?.activeHolds ?? null, n: presence ? presence.activeHolds.toLocaleString() : "\u2014", l: "Active holds" },
10130
- { key: "free-seats", raw: t3.free, n: t3.free.toLocaleString(), l: "Free seats", dot: "#6e7bff" },
10131
- { key: "blocked", raw: t3.blocked, n: t3.blocked.toLocaleString(), l: "Blocked", dot: "#8b94ac" },
10132
- { key: "sold-pct", raw: t3.capacityPct, n: `${t3.capacityPct}%`, l: "Sold" },
10133
- { 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" }
10134
10524
  ];
10135
10525
  let hasChanges = false;
10136
10526
  this.els.kpis.innerHTML = items.map((item) => {
@@ -10146,7 +10536,7 @@ var SeatManager = class {
10146
10536
  }
10147
10537
  if (item.raw != null) this.lastKpiValues.set(item.key, item.raw);
10148
10538
  const activeDelta = this.activeKpiDeltas.get(item.key);
10149
- 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)}">
10150
10540
  <b>${item.dot ? `<span class="dot" style="background:${item.dot}"></span>` : ""}${item.n}</b><span>${item.l}</span>
10151
10541
  ${activeDelta ? `<span class="slm-kpidelta${activeDelta.down ? " down" : ""}">${activeDelta.text}</span>` : ""}
10152
10542
  </div>`;
@@ -10204,15 +10594,21 @@ var SeatManager = class {
10204
10594
  this.paintMomentumHelp();
10205
10595
  this.paintFeed();
10206
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
+ }
10207
10602
  paintMonitorInsights() {
10208
10603
  if (this.mode !== "view") return;
10209
10604
  const snapshot = this.controlRoomSnapshot;
10210
10605
  if (this.els.presence) {
10211
10606
  const connected = this.root?.classList.contains("live");
10212
10607
  const sync = this.lastSyncedAt ? relTime(this.lastSyncedAt, Date.now()) : "waiting";
10608
+ const presence = this.presenceCounts();
10213
10609
  this.els.presence.innerHTML = `
10214
- <div class="slm-healthitem"><b>${snapshot ? snapshot.presence.shoppingSessions.toLocaleString() : "\u2014"}</b><span>Buyer sessions</span></div>
10215
- <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>
10216
10612
  <div class="slm-healthitem"><b>${connected ? "Healthy" : "Reconnecting"}</b><span>Live connection</span></div>
10217
10613
  <div class="slm-healthitem"><b>${sync}</b><span>Last sync</span></div>`;
10218
10614
  }
@@ -10823,7 +11219,7 @@ var SeatManager = class {
10823
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;
10824
11220
  if (activity) this.paintSpatialActivity(activity);
10825
11221
  }
10826
- if (action !== "setHoldTtl") this.scheduleRevenueRefresh(0);
11222
+ if (action !== "setHoldTtl") void this.refreshControlRoom().catch((err) => this.opts.onError?.(err));
10827
11223
  this.opts.onActionComplete?.({ action, labels, count: labels.length });
10828
11224
  }
10829
11225
  toastOk(msg) {