@seatlayer/js 0.11.0 → 0.12.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.cjs CHANGED
@@ -64,6 +64,7 @@ async function request(base, path, init = {}) {
64
64
  var PubApi = class {
65
65
  constructor(base) {
66
66
  this.base = base;
67
+ this.viewerId = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `viewer_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
67
68
  }
68
69
  chart(key) {
69
70
  return request(this.base, `/pub/events/${encodeURIComponent(key)}/chart`);
@@ -99,7 +100,8 @@ var PubApi = class {
99
100
  }
100
101
  socketUrl(key) {
101
102
  const wsBase = this.base.replace(/^http/, "ws");
102
- return `${wsBase}/pub/events/${encodeURIComponent(key)}/subscribe`;
103
+ const params = new URLSearchParams({ surface: "picker", viewerId: this.viewerId });
104
+ return `${wsBase}/pub/events/${encodeURIComponent(key)}/subscribe?${params}`;
103
105
  }
104
106
  };
105
107
 
@@ -2205,7 +2207,7 @@ var ManageApi = class {
2205
2207
  return this.pub(`/pub/events/${encodeURIComponent(key)}/objects`);
2206
2208
  }
2207
2209
  socketUrl(key) {
2208
- return `${this.base.replace(/^http/, "ws")}/pub/events/${encodeURIComponent(key)}/subscribe`;
2210
+ return `${this.base.replace(/^http/, "ws")}/pub/events/${encodeURIComponent(key)}/subscribe?surface=manager`;
2209
2211
  }
2210
2212
  // ---- inventory writes (token) ----
2211
2213
  /** Take FREE seats off sale in one batched call. Optional `releaseAt` (epoch
@@ -2238,6 +2240,9 @@ var ManageApi = class {
2238
2240
  report(key) {
2239
2241
  return this.auth(`/v1/events/${encodeURIComponent(key)}/report`);
2240
2242
  }
2243
+ controlRoom(key, windowMinutes = 15) {
2244
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/control-room?window=${windowMinutes}`);
2245
+ }
2241
2246
  log(key, opts = {}) {
2242
2247
  const params = new URLSearchParams();
2243
2248
  if (opts.limit != null) params.set("limit", String(opts.limit));
@@ -2367,7 +2372,31 @@ var CSS2 = `
2367
2372
  .slm-toast.err{background:#c0392b;color:#fff;border-color:#c0392b}
2368
2373
  .slm-toast.ok{background:#1f7a4d;color:#fff;border-color:#1f7a4d}
2369
2374
 
2370
- @media (max-width:720px){.slm-rail{width:100%;border-left:0;border-top:1px solid var(--slm-line);height:44%}.slm-body{flex-direction:column}}
2375
+ /* control-room actions + insights */
2376
+ .slm-bar-actions{display:flex;align-items:center;gap:7px}
2377
+ .slm-barbtn.on{background:rgba(244,183,64,.13);border-color:#f4b740;color:#f7ca6b}
2378
+ .slm-sectionlist{display:flex;flex-direction:column;gap:8px;margin-top:4px}
2379
+ .slm-sectionlist + .slm-eyebrow{margin-top:18px}
2380
+ .slm-sectionrow{padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface)}
2381
+ .slm-sectiontop,.slm-sectionmeta{display:flex;align-items:center;justify-content:space-between;gap:10px}
2382
+ .slm-sectiontop{font-size:12.5px;font-weight:800}.slm-sectionmeta{margin-top:5px;color:var(--slm-muted);font-size:11px}
2383
+ .slm-trend{font-size:10px;text-transform:uppercase;letter-spacing:.08em}.slm-trend.rising{color:#22a06b}.slm-trend.cooling{color:#f4b740}
2384
+ .slm-health{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px}
2385
+ .slm-healthitem{padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface)}
2386
+ .slm-healthitem b{display:block;font-size:17px;font-variant-numeric:tabular-nums}.slm-healthitem span{display:block;margin-top:2px;color:var(--slm-muted);font-size:10px;text-transform:uppercase;letter-spacing:.08em}
2387
+ .slm-sectionhead{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;margin-top:18px}
2388
+ .slm-windows{display:flex;gap:3px;padding:2px;border:1px solid var(--slm-line);border-radius:8px;background:var(--slm-surface)}
2389
+ .slm-window{padding:4px 6px;border-radius:6px;font-size:10px;font-weight:800;color:var(--slm-muted)}.slm-window.on{background:var(--slm-accent);color:var(--slm-accent-ink)}
2390
+ .slm-inspect-card{padding:12px;border:1px solid var(--slm-line);border-radius:12px;background:var(--slm-surface)}
2391
+ .slm-inspect-label{font-size:24px;font-weight:850;letter-spacing:-.02em}.slm-inspect-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:14px}
2392
+ .slm-inspect-grid span{display:block;color:var(--slm-muted);font-size:10px;text-transform:uppercase;letter-spacing:.08em}.slm-inspect-grid b{display:block;margin-top:3px;font-size:12.5px}
2393
+ .slm:fullscreen{border-radius:0;min-height:100vh;background:var(--slm-bg)}
2394
+ .slm:fullscreen .slm-bar{padding:14px 22px}.slm:fullscreen .slm-kpi b{font-size:21px}.slm:fullscreen .slm-rail{width:360px}
2395
+
2396
+ .slm.compact .slm-rail{width:100%;border-left:0;border-top:1px solid var(--slm-line);height:44%}
2397
+ .slm.compact .slm-body{flex-direction:column}
2398
+ .slm.compact .slm-bar{gap:8px;padding:8px}.slm.compact .slm-barbtn{padding:6px 9px}
2399
+ .slm.compact .slm-kpis{gap:9px}.slm.compact .slm-kpi:nth-child(n+5){display:none}
2371
2400
  `;
2372
2401
  function injectStyle() {
2373
2402
  if (typeof document === "undefined" || document.getElementById(STYLE_ID2)) return;
@@ -2405,6 +2434,9 @@ function fmtMoney(amount, currency) {
2405
2434
  return `${currency} ${Math.round(amount).toLocaleString()}`;
2406
2435
  }
2407
2436
  }
2437
+ function esc(value) {
2438
+ return String(value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
2439
+ }
2408
2440
  var SeatManager = class {
2409
2441
  constructor(options) {
2410
2442
  this.els = {};
@@ -2415,8 +2447,14 @@ var SeatManager = class {
2415
2447
  this.labelToSeat = /* @__PURE__ */ new Map();
2416
2448
  this.allIds = [];
2417
2449
  this.status = /* @__PURE__ */ new Map();
2418
- this.priceByCat = /* @__PURE__ */ new Map();
2419
2450
  this.currency = "USD";
2451
+ this.authoritativeGrossRevenue = 0;
2452
+ this.revenueStatus = "loading";
2453
+ this.revenueRequest = 0;
2454
+ this.revenueRefreshTimer = null;
2455
+ this.controlRoomSnapshot = null;
2456
+ this.trendWindowMinutes = 15;
2457
+ this.heatEnabled = false;
2420
2458
  // realtime socket
2421
2459
  this.ws = null;
2422
2460
  this.reconnectTimer = null;
@@ -2427,12 +2465,37 @@ var SeatManager = class {
2427
2465
  this.feedTimer = null;
2428
2466
  this.toastTimer = null;
2429
2467
  this.releaseAt = null;
2468
+ this.layoutObserver = null;
2469
+ this.tokenExpiresAt = null;
2470
+ this.tokenRefreshTimer = null;
2471
+ this.tokenRefreshInFlight = false;
2472
+ this.sectionByObject = /* @__PURE__ */ new Map();
2473
+ this.sectionLabelById = /* @__PURE__ */ new Map();
2474
+ this.lastSyncedAt = null;
2475
+ this.onFullscreenChange = () => {
2476
+ this.paintFullscreenButton();
2477
+ this.updateContainerLayout();
2478
+ this.renderer?.forceDraw();
2479
+ };
2480
+ this.onKeyDown = (event) => {
2481
+ if (event.metaKey || event.ctrlKey || event.altKey) return;
2482
+ const target = event.target;
2483
+ if (target?.matches('input,select,textarea,[contenteditable="true"]')) return;
2484
+ const key = event.key.toLowerCase();
2485
+ if (key === "m") this.setMode("view");
2486
+ else if (key === "i") this.setMode("inspect");
2487
+ else if (key === "b") this.setMode("block");
2488
+ else if (key === "f") this.toggleFullscreen();
2489
+ else return;
2490
+ event.preventDefault();
2491
+ };
2430
2492
  this.sectionOptions = [];
2431
2493
  this.opts = options;
2432
2494
  this.key = options.eventKey;
2433
2495
  this.mode = options.mode ?? "view";
2434
2496
  this.keepLive = options.keepLiveWhileHidden ?? true;
2435
2497
  this.currency = options.currency ?? "USD";
2498
+ this.tokenExpiresAt = options.tokenExpiresAt ?? null;
2436
2499
  this.api = new ManageApi(options.apiBase ?? DEFAULT_API_BASE3, options.token);
2437
2500
  this.host = resolveContainer4(options.container);
2438
2501
  }
@@ -2444,7 +2507,6 @@ var SeatManager = class {
2444
2507
  const res = await this.api.chart(this.key);
2445
2508
  this.doc = res.doc;
2446
2509
  this.currency = res.event.currency ?? this.opts.currency ?? this.currency;
2447
- for (const cat of res.doc.categories) this.priceByCat.set(cat.key, cat.price ?? 0);
2448
2510
  const seats = (0, import_core3.expandChart)(res.doc);
2449
2511
  for (const s of seats) {
2450
2512
  this.labelToId.set(s.label, s.id);
@@ -2453,13 +2515,17 @@ var SeatManager = class {
2453
2515
  }
2454
2516
  this.buildRenderer();
2455
2517
  this.buildSectionOptions();
2456
- await this.resnapshot();
2518
+ await Promise.all([
2519
+ this.resnapshot(),
2520
+ this.refreshControlRoom().catch((err) => this.opts.onError?.(err))
2521
+ ]);
2457
2522
  this.api.log(this.key, { limit: 24 }).then((page) => this.seedFeed(page.entries)).catch(() => {
2458
2523
  });
2459
2524
  this.connect();
2460
2525
  this.startFeedClock();
2461
2526
  this.ready = true;
2462
2527
  this.setMode(this.mode);
2528
+ this.scheduleTokenRefresh();
2463
2529
  this.opts.onReady?.();
2464
2530
  } catch (err) {
2465
2531
  this.fail(err);
@@ -2468,11 +2534,82 @@ var SeatManager = class {
2468
2534
  }
2469
2535
  // ---- public API -----------------------------------------------------------
2470
2536
  setMode(mode) {
2471
- const changed = mode !== this.mode || !this.renderer;
2537
+ const changed = mode !== this.mode;
2472
2538
  this.mode = mode;
2473
- if (changed && this.doc) this.buildRenderer();
2539
+ if (!this.renderer && this.doc) this.buildRenderer();
2540
+ else this.updateRendererInteraction();
2541
+ if (changed) this.renderer?.clearSelection();
2474
2542
  this.paintModeTabs();
2475
2543
  this.paintRail();
2544
+ if (changed) this.opts.onModeChange?.(mode);
2545
+ }
2546
+ /** Toggle the normalized sales-velocity outline overlay without changing seat colors. */
2547
+ setHeatOverlay(enabled) {
2548
+ this.heatEnabled = enabled;
2549
+ this.applyHeatOverlay();
2550
+ this.paintHeatButton();
2551
+ }
2552
+ /** Change the current-vs-previous sales window and refresh the private projection. */
2553
+ setTrendWindow(windowMinutes) {
2554
+ const normalized = Number.isFinite(windowMinutes) ? Math.floor(windowMinutes) : 15;
2555
+ this.trendWindowMinutes = Math.max(5, Math.min(60, normalized));
2556
+ this.paintTrendWindow();
2557
+ return this.refreshControlRoom();
2558
+ }
2559
+ async enterFullscreen() {
2560
+ if (!this.root?.requestFullscreen || this.isFullscreen()) return;
2561
+ await this.root.requestFullscreen();
2562
+ this.root.focus({ preventScroll: true });
2563
+ }
2564
+ async exitFullscreen() {
2565
+ if (typeof document === "undefined" || !this.isFullscreen()) return;
2566
+ await document.exitFullscreen();
2567
+ }
2568
+ isFullscreen() {
2569
+ return typeof document !== "undefined" && document.fullscreenElement === this.root;
2570
+ }
2571
+ toggleFullscreen() {
2572
+ const request2 = this.isFullscreen() ? this.exitFullscreen() : this.enterFullscreen();
2573
+ void request2.catch((err) => this.opts.onError?.(err));
2574
+ }
2575
+ /** Rotate the delegated credential without rebuilding DOM, canvas or socket. */
2576
+ setToken(token, expiresAt) {
2577
+ this.api.setToken(token);
2578
+ this.tokenExpiresAt = expiresAt ?? null;
2579
+ this.scheduleTokenRefresh();
2580
+ }
2581
+ scheduleTokenRefresh() {
2582
+ if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
2583
+ this.tokenRefreshTimer = null;
2584
+ const refresh = this.opts.onTokenRefresh;
2585
+ const expiresAt = this.tokenExpiresAt;
2586
+ if (this.closed || !refresh || !expiresAt || !Number.isFinite(expiresAt)) return;
2587
+ const remaining = expiresAt - Date.now();
2588
+ const lead = Math.min(12e4, Math.max(3e4, remaining * 0.2));
2589
+ const delay = Math.max(0, remaining - lead);
2590
+ this.tokenRefreshTimer = setTimeout(() => {
2591
+ this.tokenRefreshTimer = null;
2592
+ void this.rotateToken();
2593
+ }, delay);
2594
+ }
2595
+ async rotateToken() {
2596
+ if (this.closed || this.tokenRefreshInFlight || !this.opts.onTokenRefresh) return;
2597
+ this.tokenRefreshInFlight = true;
2598
+ try {
2599
+ const next = await this.opts.onTokenRefresh();
2600
+ if (!next?.token || !Number.isFinite(next.expiresAt)) throw new Error("invalid_token_refresh_result");
2601
+ this.setToken(next.token, next.expiresAt);
2602
+ } catch (err) {
2603
+ this.opts.onError?.(err);
2604
+ if (!this.closed) {
2605
+ this.tokenRefreshTimer = setTimeout(() => {
2606
+ this.tokenRefreshTimer = null;
2607
+ void this.rotateToken();
2608
+ }, 3e4);
2609
+ }
2610
+ } finally {
2611
+ this.tokenRefreshInFlight = false;
2612
+ }
2476
2613
  }
2477
2614
  /** Bulk block the given labels (or the current selection when omitted). */
2478
2615
  async block(labels, opts = {}) {
@@ -2557,7 +2694,13 @@ var SeatManager = class {
2557
2694
  return this.renderer?.getSelection() ?? [];
2558
2695
  }
2559
2696
  getReport() {
2560
- return this.api.report(this.key);
2697
+ return this.api.report(this.key).then((report) => {
2698
+ this.applyReportRevenue(report);
2699
+ return report;
2700
+ });
2701
+ }
2702
+ getControlRoomSnapshot(windowMinutes = this.trendWindowMinutes) {
2703
+ return this.setTrendWindow(windowMinutes);
2561
2704
  }
2562
2705
  getLog(opts = {}) {
2563
2706
  return this.api.log(this.key, opts);
@@ -2584,6 +2727,12 @@ var SeatManager = class {
2584
2727
  if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
2585
2728
  if (this.feedTimer) clearInterval(this.feedTimer);
2586
2729
  if (this.toastTimer) clearTimeout(this.toastTimer);
2730
+ if (this.revenueRefreshTimer) clearTimeout(this.revenueRefreshTimer);
2731
+ if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
2732
+ this.layoutObserver?.disconnect();
2733
+ this.layoutObserver = null;
2734
+ this.root?.removeEventListener("keydown", this.onKeyDown);
2735
+ if (typeof document !== "undefined") document.removeEventListener("fullscreenchange", this.onFullscreenChange);
2587
2736
  if (this.ws) {
2588
2737
  try {
2589
2738
  this.ws.close();
@@ -2595,26 +2744,45 @@ var SeatManager = class {
2595
2744
  this.renderer = null;
2596
2745
  if (this.root && this.root.parentNode === this.host) this.host.removeChild(this.root);
2597
2746
  }
2598
- // ---- renderer lifecycle (rebuilt per mode, like ManageEventPage) ----------
2747
+ // ---- renderer lifecycle ---------------------------------------------------
2599
2748
  buildRenderer() {
2600
2749
  if (!this.doc) return;
2601
- this.renderer?.destroy();
2602
2750
  const block = this.mode === "block";
2751
+ const inspect = this.mode === "inspect";
2603
2752
  this.renderer = new import_core3.SeatmapRenderer(this.mapHost, {
2604
2753
  manageMode: true,
2605
2754
  marqueeSelect: block,
2606
2755
  maxSelection: 1e6,
2607
- selectableStatuses: block ? ["free", "not_for_sale", "booked"] : [],
2756
+ selectableStatuses: block ? ["free", "not_for_sale"] : inspect ? ["free", "held", "booked", "not_for_sale"] : [],
2608
2757
  currency: this.currency,
2609
- onSelect: () => this.syncSelection(),
2758
+ onSelect: (seat) => this.handleSeatSelect(seat),
2610
2759
  onDeselect: () => this.syncSelection(),
2611
2760
  onMarquee: () => this.syncSelection(),
2612
2761
  onViewChange: () => this.updateZoomHint()
2613
2762
  });
2614
2763
  this.renderer.setChart(this.doc);
2615
2764
  this.repaintAll();
2765
+ this.applyHeatOverlay();
2766
+ this.updateZoomHint();
2767
+ }
2768
+ updateRendererInteraction() {
2769
+ const block = this.mode === "block";
2770
+ const inspect = this.mode === "inspect";
2771
+ this.renderer?.setManageInteraction({
2772
+ manageMode: true,
2773
+ marqueeSelect: block,
2774
+ maxSelection: 1e6,
2775
+ selectableStatuses: block ? ["free", "not_for_sale"] : inspect ? ["free", "held", "booked", "not_for_sale"] : []
2776
+ });
2616
2777
  this.updateZoomHint();
2617
2778
  }
2779
+ handleSeatSelect(seat) {
2780
+ if (this.mode === "inspect") {
2781
+ const others = this.getSelection().filter((selected) => selected.id !== seat.id).map((selected) => selected.id);
2782
+ if (others.length) this.renderer?.deselect(others);
2783
+ }
2784
+ this.syncSelection();
2785
+ }
2618
2786
  repaintAll() {
2619
2787
  const r = this.renderer;
2620
2788
  if (!r) return;
@@ -2642,7 +2810,7 @@ var SeatManager = class {
2642
2810
  ws.onopen = () => {
2643
2811
  this.attempt = 0;
2644
2812
  this.setLive(true);
2645
- void this.resnapshot();
2813
+ void this.resnapshot().then(() => this.scheduleRevenueRefresh(0));
2646
2814
  };
2647
2815
  ws.onmessage = (e) => this.onMessage(e);
2648
2816
  ws.onclose = () => {
@@ -2674,11 +2842,25 @@ var SeatManager = class {
2674
2842
  }
2675
2843
  if (!msg || typeof msg !== "object") return;
2676
2844
  const m = msg;
2845
+ if (m.type === "presence") {
2846
+ if (this.controlRoomSnapshot && typeof m.shoppingSessions === "number" && typeof m.activeHolds === "number") {
2847
+ this.controlRoomSnapshot = {
2848
+ ...this.controlRoomSnapshot,
2849
+ presence: { shoppingSessions: m.shoppingSessions, activeHolds: m.activeHolds }
2850
+ };
2851
+ this.lastSyncedAt = Date.now();
2852
+ this.recomputeTallies();
2853
+ this.paintMonitorInsights();
2854
+ this.opts.onControlRoom?.(this.controlRoomSnapshot);
2855
+ }
2856
+ return;
2857
+ }
2677
2858
  if (m.type === "hidden") return;
2678
2859
  if (m.seats && typeof m.seats === "object") {
2679
2860
  this.applySnapshot(m.seats);
2680
2861
  } else if (Array.isArray(m.changes)) {
2681
2862
  const ids = [];
2863
+ const groups = /* @__PURE__ */ new Map();
2682
2864
  for (const ch of m.changes) {
2683
2865
  const st = ["free", "held", "booked", "blocked"].includes(ch.status) ? ch.status : "free";
2684
2866
  const prev = this.status.get(ch.label) ?? "free";
@@ -2690,10 +2872,19 @@ var SeatManager = class {
2690
2872
  this.flash(id, st);
2691
2873
  ids.push(id);
2692
2874
  }
2693
- this.pushActivity(ch.label, prev, st);
2875
+ const verb = this.verbFor(prev, st);
2876
+ const groupKey = `${verb}:${st}`;
2877
+ const group = groups.get(groupKey) ?? { labels: [], verb, status: st };
2878
+ group.labels.push(ch.label);
2879
+ groups.set(groupKey, group);
2880
+ }
2881
+ for (const group of groups.values()) this.pushActivity(group.labels, group.verb, group.status);
2882
+ if (ids.length) {
2883
+ this.lastSyncedAt = Date.now();
2884
+ this.afterPaint();
2694
2885
  }
2695
- if (ids.length) this.afterPaint();
2696
2886
  this.recomputeTallies();
2887
+ if (ids.length) this.scheduleRevenueRefresh();
2697
2888
  }
2698
2889
  }
2699
2890
  async resnapshot() {
@@ -2709,6 +2900,7 @@ var SeatManager = class {
2709
2900
  next.set(label, ["free", "held", "booked", "blocked"].includes(st) ? st : "free");
2710
2901
  }
2711
2902
  this.status = next;
2903
+ this.lastSyncedAt = Date.now();
2712
2904
  this.repaintAll();
2713
2905
  this.afterPaint();
2714
2906
  this.recomputeTallies();
@@ -2732,6 +2924,47 @@ var SeatManager = class {
2732
2924
  else if (st === "booked") this.renderer?.flashSeat(id, "#22a06b");
2733
2925
  }
2734
2926
  // ---- tallies + feed -------------------------------------------------------
2927
+ applyReportRevenue(report) {
2928
+ this.authoritativeGrossRevenue = report.report.byCategory.reduce(
2929
+ (sum, row) => sum + (Number.isFinite(row.bookedRevenue) ? row.bookedRevenue : 0),
2930
+ 0
2931
+ );
2932
+ this.revenueStatus = "current";
2933
+ this.recomputeTallies();
2934
+ }
2935
+ async refreshControlRoom() {
2936
+ const request2 = ++this.revenueRequest;
2937
+ try {
2938
+ const snapshot = await this.api.controlRoom(this.key, this.trendWindowMinutes);
2939
+ if (request2 === this.revenueRequest) {
2940
+ this.controlRoomSnapshot = snapshot;
2941
+ this.lastSyncedAt = Date.now();
2942
+ this.authoritativeGrossRevenue = snapshot.revenue.gross;
2943
+ this.currency = snapshot.currency;
2944
+ this.revenueStatus = "current";
2945
+ this.recomputeTallies();
2946
+ this.applyHeatOverlay();
2947
+ this.paintMonitorInsights();
2948
+ this.opts.onControlRoom?.(snapshot);
2949
+ }
2950
+ return snapshot;
2951
+ } catch (err) {
2952
+ if (request2 === this.revenueRequest) {
2953
+ this.revenueStatus = "stale";
2954
+ this.recomputeTallies();
2955
+ }
2956
+ throw err;
2957
+ }
2958
+ }
2959
+ scheduleRevenueRefresh(delay = 140) {
2960
+ this.revenueStatus = "stale";
2961
+ this.recomputeTallies();
2962
+ if (this.revenueRefreshTimer) clearTimeout(this.revenueRefreshTimer);
2963
+ this.revenueRefreshTimer = setTimeout(() => {
2964
+ this.revenueRefreshTimer = null;
2965
+ void this.refreshControlRoom().catch((err) => this.opts.onError?.(err));
2966
+ }, delay);
2967
+ }
2735
2968
  recomputeTallies() {
2736
2969
  const t3 = {
2737
2970
  free: 0,
@@ -2741,24 +2974,24 @@ var SeatManager = class {
2741
2974
  total: this.allIds.length,
2742
2975
  capacityPct: 0,
2743
2976
  sellThroughPct: 0,
2744
- grossRevenue: 0,
2977
+ grossRevenue: this.authoritativeGrossRevenue,
2978
+ revenueStatus: this.revenueStatus,
2745
2979
  currency: this.currency
2746
2980
  };
2747
2981
  let nonFree = 0;
2748
- for (const [label, st] of this.status.entries()) {
2982
+ for (const st of this.status.values()) {
2749
2983
  t3[st] += 1;
2750
2984
  if (st !== "free") nonFree += 1;
2751
- if (st === "booked") {
2752
- const seat = this.labelToSeat.get(label);
2753
- if (seat) t3.grossRevenue += this.priceByCat.get(seat.categoryKey) ?? 0;
2754
- }
2755
2985
  }
2756
2986
  t3.free = Math.max(0, t3.total - nonFree);
2757
2987
  t3.capacityPct = t3.total ? Math.round(t3.booked / t3.total * 100) : 0;
2758
2988
  const sellable = t3.total - t3.blocked;
2759
2989
  t3.sellThroughPct = sellable > 0 ? Math.round(t3.booked / sellable * 100) : 0;
2760
2990
  this.paintKpis(t3);
2761
- if (this.mode === "view") this.paintLegend(t3);
2991
+ if (this.mode === "view") {
2992
+ this.paintLegend(t3);
2993
+ this.paintMonitorInsights();
2994
+ } else if (this.mode === "inspect") this.renderInspectRail(this.getSelection());
2762
2995
  this.opts.onTallies?.(t3);
2763
2996
  }
2764
2997
  verbFor(prev, next) {
@@ -2768,17 +3001,22 @@ var SeatManager = class {
2768
3001
  if (next === "free") return prev === "blocked" ? "unblocked" : prev === "booked" ? "cancelled" : "released";
2769
3002
  return next;
2770
3003
  }
2771
- pushActivity(label, prev, next) {
3004
+ pushActivity(labels, verb, status, at = Date.now()) {
3005
+ const label = labels[0];
3006
+ if (!label) return;
2772
3007
  const item = {
2773
- id: `${label}:${Date.now()}:${Math.random().toString(36).slice(2, 6)}`,
2774
- at: Date.now(),
3008
+ id: `${label}:${at}:${Math.random().toString(36).slice(2, 6)}`,
3009
+ at,
2775
3010
  label,
2776
- verb: this.verbFor(prev, next),
2777
- status: next
3011
+ labels: [...labels],
3012
+ count: labels.length,
3013
+ verb,
3014
+ status
2778
3015
  };
2779
3016
  this.feed.unshift(item);
2780
3017
  if (this.feed.length > FEED_CAP) this.feed.length = FEED_CAP;
2781
3018
  if (this.mode === "view") this.paintFeed();
3019
+ this.opts.onActivity?.(item);
2782
3020
  }
2783
3021
  seedFeed(entries) {
2784
3022
  const verbByAction = {
@@ -2800,14 +3038,17 @@ var SeatManager = class {
2800
3038
  for (const e of entries) {
2801
3039
  const label = e.labels[0];
2802
3040
  if (!label) continue;
2803
- const extra = e.labels.length > 1 ? ` +${e.labels.length - 1}` : "";
2804
- this.feed.push({
3041
+ const item = {
2805
3042
  id: `log:${e.id}`,
2806
3043
  at: e.at,
2807
- label: label + extra,
3044
+ label,
3045
+ labels: [...e.labels],
3046
+ count: e.labels.length,
2808
3047
  verb: verbByAction[e.action] ?? e.action,
2809
3048
  status: stByAction[e.action] ?? "free"
2810
- });
3049
+ };
3050
+ this.feed.push(item);
3051
+ this.opts.onActivity?.(item);
2811
3052
  }
2812
3053
  this.feed.sort((a, b) => b.at - a.at);
2813
3054
  if (this.feed.length > FEED_CAP) this.feed.length = FEED_CAP;
@@ -2815,7 +3056,10 @@ var SeatManager = class {
2815
3056
  }
2816
3057
  startFeedClock() {
2817
3058
  this.feedTimer = setInterval(() => {
2818
- if (this.mode === "view") this.paintFeed();
3059
+ if (this.mode === "view") {
3060
+ this.paintFeed();
3061
+ this.paintMonitorInsights();
3062
+ }
2819
3063
  }, 1e4);
2820
3064
  }
2821
3065
  // ---- selection ------------------------------------------------------------
@@ -2825,21 +3069,30 @@ var SeatManager = class {
2825
3069
  syncSelection() {
2826
3070
  const seats = this.getSelection();
2827
3071
  if (this.mode === "block") this.paintSelBar(seats);
3072
+ else if (this.mode === "inspect") this.renderInspectRail(seats);
2828
3073
  this.opts.onSelectionChange?.(seats);
2829
3074
  }
2830
3075
  // ---- DOM: chrome ----------------------------------------------------------
2831
3076
  buildChrome() {
2832
3077
  const root = document.createElement("div");
2833
3078
  root.className = "slm";
3079
+ root.tabIndex = 0;
3080
+ root.setAttribute("role", "region");
3081
+ root.setAttribute("aria-label", "SeatLayer live control room");
2834
3082
  const vars = themeVars(this.opts.theme);
2835
3083
  for (const [k, v] of Object.entries(vars)) root.style.setProperty(k, v);
2836
3084
  root.innerHTML = `
2837
3085
  <div class="slm-bar">
2838
3086
  <div class="slm-modes" data-ref="modes">
2839
- <button class="slm-mode" data-mode="view">View</button>
2840
- <button class="slm-mode" data-mode="block">Block</button>
3087
+ <button class="slm-mode" data-mode="view" title="Monitor (M)" aria-keyshortcuts="M">Monitor</button>
3088
+ <button class="slm-mode" data-mode="inspect" title="Inspect (I)" aria-keyshortcuts="I">Inspect</button>
3089
+ <button class="slm-mode" data-mode="block" title="Block (B)" aria-keyshortcuts="B">Block</button>
2841
3090
  </div>
2842
3091
  <span class="slm-live"><span class="slm-live-dot"></span><span data-ref="livetext">CONNECTING</span></span>
3092
+ <div class="slm-bar-actions">
3093
+ <button class="slm-barbtn" data-ref="heat" aria-pressed="false">Heat</button>
3094
+ <button class="slm-barbtn" data-ref="fullscreen" title="Full screen (F)" aria-keyshortcuts="F">Full screen</button>
3095
+ </div>
2843
3096
  <div class="slm-kpis" data-ref="kpis"></div>
2844
3097
  </div>
2845
3098
  <div class="slm-body">
@@ -2854,12 +3107,19 @@ var SeatManager = class {
2854
3107
  `;
2855
3108
  this.host.appendChild(root);
2856
3109
  this.root = root;
3110
+ this.updateContainerLayout();
3111
+ if (typeof ResizeObserver !== "undefined") {
3112
+ this.layoutObserver = new ResizeObserver(() => this.updateContainerLayout());
3113
+ this.layoutObserver.observe(root);
3114
+ }
2857
3115
  const ref = (n) => root.querySelector(`[data-ref="${n}"]`);
2858
3116
  this.mapHost = ref("maphost");
2859
3117
  this.els = {
2860
3118
  modes: ref("modes"),
2861
3119
  livetext: ref("livetext"),
2862
3120
  kpis: ref("kpis"),
3121
+ heat: ref("heat"),
3122
+ fullscreen: ref("fullscreen"),
2863
3123
  zoomhint: ref("zoomhint"),
2864
3124
  rail: ref("rail"),
2865
3125
  toast: ref("toast"),
@@ -2867,14 +3127,33 @@ var SeatManager = class {
2867
3127
  };
2868
3128
  this.els.modes.querySelectorAll("[data-mode]").forEach((b) => b.addEventListener("click", () => this.setMode(b.dataset.mode)));
2869
3129
  this.els.zfit.addEventListener("click", () => this.zoomToFit());
3130
+ this.els.heat.addEventListener("click", () => this.setHeatOverlay(!this.heatEnabled));
3131
+ this.els.fullscreen.addEventListener("click", () => this.toggleFullscreen());
3132
+ root.addEventListener("keydown", this.onKeyDown);
3133
+ document.addEventListener("fullscreenchange", this.onFullscreenChange);
2870
3134
  this.paintModeTabs();
3135
+ this.paintHeatButton();
3136
+ this.paintFullscreenButton();
3137
+ }
3138
+ updateContainerLayout() {
3139
+ const width = this.root?.getBoundingClientRect().width || this.host.clientWidth;
3140
+ this.root?.classList.toggle("compact", width > 0 && width < 800);
2871
3141
  }
2872
3142
  buildSectionOptions() {
2873
3143
  if (!this.doc) return;
2874
3144
  try {
2875
3145
  const secs = (0, import_core3.computeSections)(this.doc);
2876
- for (const s of secs.sections) this.sectionOptions.push({ id: s.id, label: s.label });
2877
- if (secs.ungrouped) this.sectionOptions.push({ id: import_core3.UNGROUPED_ID, label: secs.ungrouped.label });
3146
+ this.sectionOptions = [];
3147
+ this.sectionByObject = new Map(secs.objectToSection);
3148
+ this.sectionLabelById.clear();
3149
+ for (const s of secs.sections) {
3150
+ this.sectionOptions.push({ id: s.id, label: s.label });
3151
+ this.sectionLabelById.set(s.id, s.label);
3152
+ }
3153
+ if (secs.ungrouped) {
3154
+ this.sectionOptions.push({ id: import_core3.UNGROUPED_ID, label: secs.ungrouped.label });
3155
+ this.sectionLabelById.set(import_core3.UNGROUPED_ID, secs.ungrouped.label);
3156
+ }
2878
3157
  } catch {
2879
3158
  }
2880
3159
  }
@@ -2885,9 +3164,27 @@ var SeatManager = class {
2885
3164
  });
2886
3165
  this.root?.classList.toggle("block-mode", this.mode === "block");
2887
3166
  }
3167
+ paintHeatButton() {
3168
+ const button = this.els.heat;
3169
+ if (!button) return;
3170
+ button.classList.toggle("on", this.heatEnabled);
3171
+ button.setAttribute("aria-pressed", String(this.heatEnabled));
3172
+ button.textContent = this.heatEnabled ? "Heat on" : "Heat";
3173
+ }
3174
+ paintFullscreenButton() {
3175
+ if (!this.els.fullscreen) return;
3176
+ this.els.fullscreen.textContent = this.isFullscreen() ? "Exit full screen" : "Full screen";
3177
+ }
3178
+ paintTrendWindow() {
3179
+ this.els.rail?.querySelectorAll("[data-window]").forEach((button) => {
3180
+ const value = Number(button.dataset.window);
3181
+ button.classList.toggle("on", value === this.trendWindowMinutes);
3182
+ });
3183
+ }
2888
3184
  setLive(on) {
2889
3185
  this.root?.classList.toggle("live", on);
2890
3186
  if (this.els.livetext) this.els.livetext.textContent = on ? "LIVE" : "RECONNECTING";
3187
+ this.paintMonitorInsights();
2891
3188
  }
2892
3189
  updateZoomHint() {
2893
3190
  const hint = this.els.zoomhint;
@@ -2897,10 +3194,13 @@ var SeatManager = class {
2897
3194
  }
2898
3195
  paintKpis(t3) {
2899
3196
  if (!this.els.kpis) return;
2900
- const rev = fmtMoney(t3.grossRevenue, t3.currency);
3197
+ const rev = t3.revenueStatus === "current" ? fmtMoney(t3.grossRevenue, t3.currency) : "\u2014";
3198
+ const presence = this.controlRoomSnapshot?.presence;
2901
3199
  this.els.kpis.innerHTML = [
2902
3200
  { n: t3.booked.toLocaleString(), l: "Sold", dot: "#22a06b" },
2903
3201
  { n: t3.held.toLocaleString(), l: "Held", dot: "#f4b740" },
3202
+ { n: presence ? presence.shoppingSessions.toLocaleString() : "\u2014", l: "Shopping" },
3203
+ { n: presence ? presence.activeHolds.toLocaleString() : "\u2014", l: "Live holds" },
2904
3204
  { n: t3.free.toLocaleString(), l: "Free", dot: "#6e7bff" },
2905
3205
  { n: t3.blocked.toLocaleString(), l: "Blocked", dot: "#8b94ac" },
2906
3206
  { n: `${t3.capacityPct}%`, l: "Capacity" },
@@ -2910,22 +3210,120 @@ var SeatManager = class {
2910
3210
  // ---- DOM: rails -----------------------------------------------------------
2911
3211
  paintRail() {
2912
3212
  if (this.mode === "view") this.renderViewRail();
3213
+ else if (this.mode === "inspect") this.renderInspectRail(this.getSelection());
2913
3214
  else this.renderBlockRail();
2914
3215
  this.updateZoomHint();
2915
3216
  }
2916
3217
  renderViewRail() {
2917
3218
  this.els.rail.innerHTML = `
2918
- <p class="slm-eyebrow">Live board</p>
2919
- <p class="slm-hint">Read-only. Seats repaint and flash as buyers hold and book \u2014 watch your event breathe.</p>
3219
+ <p class="slm-eyebrow">Monitor</p>
3220
+ <p class="slm-hint">Read-only. Inventory, buyer presence and sales movement update on the same live board.</p>
3221
+ <div class="slm-health" data-ref="presence"></div>
2920
3222
  <div class="slm-legend" data-ref="legend"></div>
3223
+ <div class="slm-sectionhead">
3224
+ <div><p class="slm-eyebrow">Section performance</p><p class="slm-note">Exact booked revenue \xB7 net sales velocity</p></div>
3225
+ <div class="slm-windows" aria-label="Sales velocity window">
3226
+ ${[5, 15, 30, 60].map((window2) => `<button class="slm-window" data-window="${window2}">${window2}m</button>`).join("")}
3227
+ </div>
3228
+ </div>
3229
+ <div class="slm-sectionlist" data-ref="sections"></div>
2921
3230
  <p class="slm-eyebrow">Activity</p>
2922
3231
  <div class="slm-feed" data-ref="feed"></div>
2923
3232
  `;
3233
+ this.els.presence = this.els.rail.querySelector('[data-ref="presence"]');
2924
3234
  this.els.legend = this.els.rail.querySelector('[data-ref="legend"]');
3235
+ this.els.sections = this.els.rail.querySelector('[data-ref="sections"]');
2925
3236
  this.els.feed = this.els.rail.querySelector('[data-ref="feed"]');
3237
+ this.els.rail.querySelectorAll("[data-window]").forEach((button) => button.addEventListener("click", () => {
3238
+ const windowMinutes = Number(button.dataset.window);
3239
+ void this.setTrendWindow(windowMinutes).catch((err) => this.opts.onError?.(err));
3240
+ }));
2926
3241
  this.recomputeTallies();
3242
+ this.paintMonitorInsights();
3243
+ this.paintTrendWindow();
2927
3244
  this.paintFeed();
2928
3245
  }
3246
+ paintMonitorInsights() {
3247
+ if (this.mode !== "view") return;
3248
+ const snapshot = this.controlRoomSnapshot;
3249
+ if (this.els.presence) {
3250
+ const connected = this.root?.classList.contains("live");
3251
+ const sync = this.lastSyncedAt ? relTime(this.lastSyncedAt, Date.now()) : "waiting";
3252
+ this.els.presence.innerHTML = `
3253
+ <div class="slm-healthitem"><b>${snapshot ? snapshot.presence.shoppingSessions.toLocaleString() : "\u2014"}</b><span>Buyer sessions</span></div>
3254
+ <div class="slm-healthitem"><b>${snapshot ? snapshot.presence.activeHolds.toLocaleString() : "\u2014"}</b><span>Active holds</span></div>
3255
+ <div class="slm-healthitem"><b>${connected ? "Healthy" : "Reconnecting"}</b><span>Live connection</span></div>
3256
+ <div class="slm-healthitem"><b>${sync}</b><span>Last sync</span></div>`;
3257
+ }
3258
+ if (!this.els.sections) return;
3259
+ if (!snapshot) {
3260
+ this.els.sections.innerHTML = '<div class="slm-empty">Loading authoritative section metrics\u2026</div>';
3261
+ return;
3262
+ }
3263
+ const velocity = new Map(snapshot.velocity.bySection.map((row) => [row.sectionId, row]));
3264
+ const rows = [...snapshot.revenue.bySection].sort((a, b) => {
3265
+ const av = velocity.get(a.sectionId)?.netBooked ?? 0;
3266
+ const bv = velocity.get(b.sectionId)?.netBooked ?? 0;
3267
+ return bv - av || b.bookedRevenue - a.bookedRevenue;
3268
+ });
3269
+ this.els.sections.innerHTML = rows.length ? rows.map((row) => {
3270
+ const speed = velocity.get(row.sectionId);
3271
+ const net = speed?.netBooked ?? 0;
3272
+ const netLabel = `${net > 0 ? "+" : ""}${net}`;
3273
+ const trend = speed?.trend === "rising" || speed?.trend === "cooling" ? speed.trend : "steady";
3274
+ return `<div class="slm-sectionrow">
3275
+ <div class="slm-sectiontop"><span>${esc(row.sectionLabel)}</span><span>${fmtMoney(row.bookedRevenue, snapshot.currency)}</span></div>
3276
+ <div class="slm-sectionmeta"><span>${row.booked.toLocaleString()}/${row.total.toLocaleString()} sold \xB7 ${netLabel} in ${snapshot.velocity.windowMinutes}m</span><span class="slm-trend ${trend}">${trend}</span></div>
3277
+ </div>`;
3278
+ }).join("") : '<div class="slm-empty">No section metrics are available for this chart.</div>';
3279
+ this.paintTrendWindow();
3280
+ }
3281
+ applyHeatOverlay() {
3282
+ const snapshot = this.controlRoomSnapshot;
3283
+ if (!this.heatEnabled || !snapshot) {
3284
+ this.renderer?.setSectionHeat(null);
3285
+ return;
3286
+ }
3287
+ const capacity = new Map(snapshot.revenue.bySection.map((row) => [row.sectionId, Math.max(1, row.total)]));
3288
+ const rates = snapshot.velocity.bySection.map((row) => ({
3289
+ sectionId: row.sectionId,
3290
+ rate: Math.max(0, row.netBooked) / (capacity.get(row.sectionId) ?? 1) / snapshot.velocity.windowMinutes
3291
+ }));
3292
+ const max = Math.max(0, ...rates.map((row) => row.rate));
3293
+ const scores = {};
3294
+ for (const row of rates) scores[row.sectionId] = max > 0 ? Math.sqrt(row.rate / max) : 0;
3295
+ this.renderer?.setSectionHeat(scores);
3296
+ }
3297
+ renderInspectRail(seats) {
3298
+ const seat = seats[seats.length - 1];
3299
+ if (!seat) {
3300
+ this.els.rail.innerHTML = `
3301
+ <p class="slm-eyebrow">Inspect</p>
3302
+ <p class="slm-hint">Select any seat to see its live inventory context. Inspect is read-only and never changes availability.</p>
3303
+ <div class="slm-empty">Choose a seat on the map.</div>`;
3304
+ return;
3305
+ }
3306
+ const status = this.status.get(seat.label) ?? "free";
3307
+ const statusLabel = { free: "Free", held: "Held", booked: "Booked", blocked: "Blocked" };
3308
+ const sectionId = this.sectionByObject.get(seat.rowId) ?? import_core3.UNGROUPED_ID;
3309
+ const sectionLabel = this.sectionLabelById.get(sectionId) ?? "Other seats";
3310
+ const category = this.doc?.categories.find((item) => item.key === seat.categoryKey);
3311
+ const sectionMetric = this.controlRoomSnapshot?.revenue.bySection.find((row) => row.sectionId === sectionId);
3312
+ this.els.rail.innerHTML = `
3313
+ <p class="slm-eyebrow">Inspect</p>
3314
+ <p class="slm-hint">Live inventory context. Booked seats remain read-only; order and payment actions belong to the host commerce system.</p>
3315
+ <div class="slm-inspect-card">
3316
+ <div class="slm-inspect-label">${esc(seat.label)}</div>
3317
+ <div class="slm-inspect-grid">
3318
+ <div><span>Status</span><b>${statusLabel[status]}</b></div>
3319
+ <div><span>Section</span><b>${esc(sectionLabel)}</b></div>
3320
+ <div><span>Row</span><b>${esc(seat.rowId)}</b></div>
3321
+ <div><span>Category</span><b>${esc(category?.label ?? seat.categoryKey)}</b></div>
3322
+ <div><span>Section sold</span><b>${sectionMetric ? `${sectionMetric.booked}/${sectionMetric.total}` : "\u2014"}</b></div>
3323
+ <div><span>Section revenue</span><b>${sectionMetric && this.controlRoomSnapshot ? fmtMoney(sectionMetric.bookedRevenue, this.controlRoomSnapshot.currency) : "\u2014"}</b></div>
3324
+ </div>
3325
+ </div>`;
3326
+ }
2929
3327
  paintLegend(t3) {
2930
3328
  if (!this.els.legend) return;
2931
3329
  this.els.legend.innerHTML = LEGEND.map((l) => `<div class="slm-legrow"><span class="slm-legdot" style="background:${l.color}"></span>
@@ -2939,19 +3337,22 @@ var SeatManager = class {
2939
3337
  }
2940
3338
  const now = Date.now();
2941
3339
  const color = { free: "#6e7bff", held: "#f4b740", booked: "#22a06b", blocked: "#8b94ac" };
2942
- this.els.feed.innerHTML = this.feed.map((a) => `<div class="slm-feedrow"><span class="slm-feeddot" style="background:${color[a.status]}"></span>
2943
- <span class="slm-feedtext">Seat <b>${a.label}</b> ${a.verb}</span>
2944
- <span class="slm-feedtime">${relTime(a.at, now)}</span></div>`).join("");
3340
+ this.els.feed.innerHTML = this.feed.map((a) => {
3341
+ const extra = a.count > 1 ? ` +${a.count - 1}` : "";
3342
+ return `<div class="slm-feedrow"><span class="slm-feeddot" style="background:${color[a.status]}"></span>
3343
+ <span class="slm-feedtext">${a.count === 1 ? "Seat" : "Seats"} <b>${esc(a.label)}${extra}</b> ${esc(a.verb)}</span>
3344
+ <span class="slm-feedtime">${relTime(a.at, now)}</span></div>`;
3345
+ }).join("");
2945
3346
  }
2946
3347
  renderBlockRail() {
2947
3348
  const cats = this.doc?.categories ?? [];
2948
- const catChips = cats.map((c) => `<button class="slm-chip" data-cat="${c.key}"><span class="dot" style="background:${c.color ?? "#6e7bff"}"></span>${c.label ?? c.key}</button>`).join("");
3349
+ const catChips = cats.map((c) => `<button class="slm-chip" data-cat="${esc(c.key)}"><span class="dot" style="background:${esc(c.color ?? "#6e7bff")}"></span>${esc(c.label ?? c.key)}</button>`).join("");
2949
3350
  const sectionField = this.sectionOptions.length ? `<div class="slm-field"><label>Select a whole section</label>
2950
3351
  <select class="slm-select" data-ref="section"><option value="">Choose a section\u2026</option>
2951
- ${this.sectionOptions.map((s) => `<option value="${s.id}">${s.label}</option>`).join("")}</select></div>` : "";
3352
+ ${this.sectionOptions.map((s) => `<option value="${esc(s.id)}">${esc(s.label)}</option>`).join("")}</select></div>` : "";
2952
3353
  this.els.rail.innerHTML = `
2953
- <p class="slm-eyebrow">Block &amp; cancel</p>
2954
- <p class="slm-hint">Drag a box on the map to marquee-select, \u2318A for all, or pick a category/section \u2014 then block, unblock, or cancel bookings in one action.</p>
3354
+ <p class="slm-eyebrow">Block &amp; unblock</p>
3355
+ <p class="slm-hint">Drag a box on the map to marquee-select, \u2318A for all, or pick a category/section. Booked and held inventory is never actionable here.</p>
2955
3356
  <div class="slm-selbar"><span class="slm-selnum" data-ref="selnum">0</span><span class="slm-sellabel">selected</span></div>
2956
3357
  <div class="slm-row">
2957
3358
  <button class="slm-btn" data-ref="doblock" disabled>Block</button>
@@ -2960,7 +3361,6 @@ var SeatManager = class {
2960
3361
  <div class="slm-row">
2961
3362
  <button class="slm-btn ghost" data-ref="selall">Select all</button>
2962
3363
  <button class="slm-btn ghost" data-ref="clearsel">Clear</button>
2963
- <button class="slm-btn danger" data-ref="docancel" disabled>Cancel booking</button>
2964
3364
  </div>
2965
3365
  <p class="slm-eyebrow" style="margin-top:8px">By category</p>
2966
3366
  <div class="slm-chiprow">${catChips || '<span class="slm-empty">No categories.</span>'}</div>
@@ -2976,10 +3376,8 @@ var SeatManager = class {
2976
3376
  this.els.selnum = r("selnum");
2977
3377
  this.els.doblock = r("doblock");
2978
3378
  this.els.dounblock = r("dounblock");
2979
- this.els.docancel = r("docancel");
2980
3379
  r("doblock").addEventListener("click", () => void this.block());
2981
3380
  r("dounblock").addEventListener("click", () => void this.unblock());
2982
- r("docancel").addEventListener("click", () => this.promptCancel());
2983
3381
  r("selall").addEventListener("click", () => this.selectAll());
2984
3382
  r("clearsel").addEventListener("click", () => this.clearSelection());
2985
3383
  r("markall").addEventListener("click", () => void this.unblockAll());
@@ -3005,28 +3403,18 @@ var SeatManager = class {
3005
3403
  for (const [label, seat] of this.labelToSeat.entries()) if (seat.categoryKey === catKey) labels.push(label);
3006
3404
  this.selectByLabels(labels);
3007
3405
  }
3008
- promptCancel() {
3009
- const booked = this.getSelection().filter((s) => this.status.get(s.label) === "booked");
3010
- if (!booked.length) return;
3011
- if (typeof window === "undefined") return;
3012
- if (!window.confirm(`Cancel ${booked.length} booking${booked.length === 1 ? "" : "s"}? Seats return to sale (credit not refunded).`)) return;
3013
- const ref = window.prompt("Enter the original booking reference to cancel safely:")?.trim();
3014
- if (!ref) return;
3015
- void this.cancelBooking(booked.map((s) => s.label), ref);
3016
- }
3017
3406
  paintSelBar(seats) {
3018
3407
  if (!this.els.selnum) return;
3019
3408
  this.els.selnum.textContent = seats.length.toLocaleString();
3020
3409
  const hasFree = seats.some((s) => this.status.get(s.label) === "free");
3021
3410
  const hasBlocked = seats.some((s) => this.status.get(s.label) === "blocked");
3022
- const hasBooked = seats.some((s) => this.status.get(s.label) === "booked");
3023
3411
  this.els.doblock.disabled = !hasFree;
3024
3412
  this.els.dounblock.disabled = !hasBlocked;
3025
- this.els.docancel.disabled = !hasBooked;
3026
3413
  }
3027
3414
  // ---- toast / done / fail --------------------------------------------------
3028
3415
  done(action, labels, msg) {
3029
3416
  this.toastOk(msg);
3417
+ if (action !== "setHoldTtl") this.scheduleRevenueRefresh(0);
3030
3418
  this.opts.onActionComplete?.({ action, labels, count: labels.length });
3031
3419
  }
3032
3420
  toastOk(msg) {