@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.cjs +216 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +89 -2
- package/dist/index.d.ts +89 -2
- package/dist/index.js +218 -52
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -9570,13 +9570,39 @@ var SeatManager = class {
|
|
|
9570
9570
|
this.labelToId = /* @__PURE__ */ new Map();
|
|
9571
9571
|
this.labelToSeat = /* @__PURE__ */ new Map();
|
|
9572
9572
|
this.allIds = [];
|
|
9573
|
+
/**
|
|
9574
|
+
* GA inventory units — real sellable labels the server counts, with NO seat
|
|
9575
|
+
* geometry and therefore no renderer binding. They live here rather than in
|
|
9576
|
+
* `labelToId`/`allIds` so every paint path keeps addressing paintable nodes
|
|
9577
|
+
* only, while the tally denominator finally covers the same universe the
|
|
9578
|
+
* numerator does. Without them a GA sale hit `booked` but not `total`:
|
|
9579
|
+
* Free under-reported by GA capacity and SOLD% could exceed 100%.
|
|
9580
|
+
*/
|
|
9581
|
+
this.gaUnitLabelSet = /* @__PURE__ */ new Set();
|
|
9573
9582
|
this.status = /* @__PURE__ */ new Map();
|
|
9583
|
+
/** Live non-free counters, moved by each delta rather than re-walked. */
|
|
9584
|
+
this.counts = { held: 0, booked: 0, blocked: 0 };
|
|
9585
|
+
/** Bumped whenever the seat model is replaced wholesale (a full snapshot). */
|
|
9586
|
+
this.modelVersion = 0;
|
|
9574
9587
|
this.currency = "USD";
|
|
9575
9588
|
this.authoritativeGrossRevenue = 0;
|
|
9576
9589
|
this.revenueStatus = "loading";
|
|
9577
9590
|
this.revenueRequest = 0;
|
|
9578
|
-
this.revenueRefreshTimer = null;
|
|
9579
9591
|
this.controlRoomSnapshot = null;
|
|
9592
|
+
/**
|
|
9593
|
+
* The server's own totals, pinned to the client model they were read against.
|
|
9594
|
+
* Display = server baseline + (client now − client then), so the authoritative
|
|
9595
|
+
* numbers land exactly on arrival and deltas still move them between reads.
|
|
9596
|
+
* A wholesale model replacement invalidates the pairing (`model`), and the
|
|
9597
|
+
* client tallies — themselves a fresh authenticated read — take over.
|
|
9598
|
+
*/
|
|
9599
|
+
this.serverBaseline = null;
|
|
9600
|
+
/** Latest presence frame, held whether or not a snapshot has landed yet. */
|
|
9601
|
+
this.livePresence = null;
|
|
9602
|
+
/** Latest cumulative booked gross pushed on a delta frame. */
|
|
9603
|
+
this.liveGross = null;
|
|
9604
|
+
/** Coalesces a burst of deltas into one KPI/rail repaint. */
|
|
9605
|
+
this.paintHandle = null;
|
|
9580
9606
|
this.trendWindowMinutes = 15;
|
|
9581
9607
|
this.heatEnabled = false;
|
|
9582
9608
|
this.lastKpiValues = /* @__PURE__ */ new Map();
|
|
@@ -9674,12 +9700,7 @@ var SeatManager = class {
|
|
|
9674
9700
|
const res = await this.api.chart(this.key);
|
|
9675
9701
|
this.doc = res.doc;
|
|
9676
9702
|
this.currency = res.event.currency ?? this.opts.currency ?? this.currency;
|
|
9677
|
-
|
|
9678
|
-
for (const s of seats) {
|
|
9679
|
-
this.labelToId.set(s.label, s.id);
|
|
9680
|
-
this.labelToSeat.set(s.label, s);
|
|
9681
|
-
this.allIds.push(s.id);
|
|
9682
|
-
}
|
|
9703
|
+
this.buildUnitUniverse(res.doc);
|
|
9683
9704
|
this.buildRenderer();
|
|
9684
9705
|
this.buildSectionOptions();
|
|
9685
9706
|
const [, controlRoom] = await Promise.all([
|
|
@@ -10040,7 +10061,10 @@ var SeatManager = class {
|
|
|
10040
10061
|
if (this.followLiveTimer) clearTimeout(this.followLiveTimer);
|
|
10041
10062
|
if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
|
|
10042
10063
|
if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
|
|
10043
|
-
if (this.
|
|
10064
|
+
if (this.paintHandle !== null && typeof cancelAnimationFrame === "function") {
|
|
10065
|
+
cancelAnimationFrame(this.paintHandle);
|
|
10066
|
+
}
|
|
10067
|
+
this.paintHandle = null;
|
|
10044
10068
|
this.channels?.destroy();
|
|
10045
10069
|
this.channels = null;
|
|
10046
10070
|
if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
|
|
@@ -10123,6 +10147,37 @@ var SeatManager = class {
|
|
|
10123
10147
|
}
|
|
10124
10148
|
this.syncSelection();
|
|
10125
10149
|
}
|
|
10150
|
+
/**
|
|
10151
|
+
* Build the client's inventory universe from the chart.
|
|
10152
|
+
*
|
|
10153
|
+
* `expandChart` yields SEATS — it has no output for a GA area, whose capacity
|
|
10154
|
+
* is sold as N synthetic unit labels. The server's seat map keys, its deltas
|
|
10155
|
+
* and its `totals` all speak those labels, so a client that only knows seats
|
|
10156
|
+
* counts GA sales in the numerator (every key of the snapshot is written into
|
|
10157
|
+
* `status`) while leaving them out of the denominator. Registering the GA
|
|
10158
|
+
* units here — labels only, never a render binding — is what makes the two
|
|
10159
|
+
* agree.
|
|
10160
|
+
*/
|
|
10161
|
+
buildUnitUniverse(doc) {
|
|
10162
|
+
for (const seat of (0, import_core3.expandChart)(doc)) {
|
|
10163
|
+
this.labelToId.set(seat.label, seat.id);
|
|
10164
|
+
this.labelToSeat.set(seat.label, seat);
|
|
10165
|
+
this.allIds.push(seat.id);
|
|
10166
|
+
}
|
|
10167
|
+
for (const area of (0, import_core3.gaAreasOf)(doc)) {
|
|
10168
|
+
for (const label of (0, import_core3.gaUnitLabels)(area)) {
|
|
10169
|
+
if (!this.labelToId.has(label)) this.gaUnitLabelSet.add(label);
|
|
10170
|
+
}
|
|
10171
|
+
}
|
|
10172
|
+
}
|
|
10173
|
+
/** Every sellable unit the client knows: seats + GA capacity. */
|
|
10174
|
+
unitTotal() {
|
|
10175
|
+
return this.allIds.length + this.gaUnitLabelSet.size;
|
|
10176
|
+
}
|
|
10177
|
+
/** Every label the client models, whether or not it can be painted. */
|
|
10178
|
+
knownLabels() {
|
|
10179
|
+
return [...this.labelToId.keys(), ...this.gaUnitLabelSet];
|
|
10180
|
+
}
|
|
10126
10181
|
repaintAll() {
|
|
10127
10182
|
const r = this.renderer;
|
|
10128
10183
|
if (!r) return;
|
|
@@ -10172,7 +10227,7 @@ var SeatManager = class {
|
|
|
10172
10227
|
ws.onopen = () => {
|
|
10173
10228
|
this.attempt = 0;
|
|
10174
10229
|
this.setLive(true);
|
|
10175
|
-
void this.resnapshot().then(() => this.
|
|
10230
|
+
void this.resnapshot().then(() => this.refreshControlRoom()).catch((err) => this.opts.onError?.(err));
|
|
10176
10231
|
void this.refreshAvailability();
|
|
10177
10232
|
};
|
|
10178
10233
|
ws.onmessage = (e) => this.onMessage(e);
|
|
@@ -10210,15 +10265,18 @@ var SeatManager = class {
|
|
|
10210
10265
|
this.updateEffectiveAvailability(m.hidden, m.closed);
|
|
10211
10266
|
}
|
|
10212
10267
|
if (m.type === "presence") {
|
|
10213
|
-
if (
|
|
10214
|
-
this.
|
|
10215
|
-
|
|
10216
|
-
|
|
10268
|
+
if (typeof m.shoppingSessions === "number" && typeof m.activeHolds === "number") {
|
|
10269
|
+
this.livePresence = {
|
|
10270
|
+
at: Date.now(),
|
|
10271
|
+
value: { shoppingSessions: m.shoppingSessions, activeHolds: m.activeHolds }
|
|
10217
10272
|
};
|
|
10273
|
+
if (this.controlRoomSnapshot) {
|
|
10274
|
+
this.controlRoomSnapshot = { ...this.controlRoomSnapshot, presence: this.livePresence.value };
|
|
10275
|
+
this.opts.onControlRoom?.(this.controlRoomSnapshot);
|
|
10276
|
+
}
|
|
10218
10277
|
this.lastSyncedAt = Date.now();
|
|
10219
10278
|
this.recomputeTallies();
|
|
10220
10279
|
this.paintMonitorInsights();
|
|
10221
|
-
this.opts.onControlRoom?.(this.controlRoomSnapshot);
|
|
10222
10280
|
}
|
|
10223
10281
|
return;
|
|
10224
10282
|
}
|
|
@@ -10232,7 +10290,7 @@ var SeatManager = class {
|
|
|
10232
10290
|
const st = ["free", "held", "booked", "blocked"].includes(ch.status) ? ch.status : "free";
|
|
10233
10291
|
const prev = this.status.get(ch.label) ?? "free";
|
|
10234
10292
|
if (prev === st) continue;
|
|
10235
|
-
this.
|
|
10293
|
+
this.setStatusLabel(ch.label, st, prev);
|
|
10236
10294
|
const id = this.labelToId.get(ch.label);
|
|
10237
10295
|
if (id) {
|
|
10238
10296
|
this.renderer?.setStatus([id], toRenderStatus(st));
|
|
@@ -10252,10 +10310,38 @@ var SeatManager = class {
|
|
|
10252
10310
|
this.lastSyncedAt = Date.now();
|
|
10253
10311
|
this.afterPaint();
|
|
10254
10312
|
}
|
|
10313
|
+
if (typeof m.revenue?.gross === "number" && Number.isFinite(m.revenue.gross)) {
|
|
10314
|
+
this.applyLiveGross(m.revenue.gross);
|
|
10315
|
+
}
|
|
10255
10316
|
this.recomputeTallies();
|
|
10256
|
-
if (ids.length) this.scheduleRevenueRefresh();
|
|
10257
10317
|
}
|
|
10258
10318
|
}
|
|
10319
|
+
/**
|
|
10320
|
+
* Adopt the cumulative booked gross a delta frame carried.
|
|
10321
|
+
*
|
|
10322
|
+
* Stashed with its arrival time so an in-flight control-room read can decide
|
|
10323
|
+
* whether it is holding the newer number: a frame that landed after the
|
|
10324
|
+
* request started is newer than the response, one that landed before is not.
|
|
10325
|
+
*/
|
|
10326
|
+
applyLiveGross(gross) {
|
|
10327
|
+
this.liveGross = { at: Date.now(), value: gross };
|
|
10328
|
+
this.authoritativeGrossRevenue = gross;
|
|
10329
|
+
this.revenueStatus = "current";
|
|
10330
|
+
if (this.controlRoomSnapshot) {
|
|
10331
|
+
this.controlRoomSnapshot = {
|
|
10332
|
+
...this.controlRoomSnapshot,
|
|
10333
|
+
revenue: { ...this.controlRoomSnapshot.revenue, gross }
|
|
10334
|
+
};
|
|
10335
|
+
this.opts.onControlRoom?.(this.controlRoomSnapshot);
|
|
10336
|
+
}
|
|
10337
|
+
}
|
|
10338
|
+
/** The single writer for a label's status, so the counters never drift. */
|
|
10339
|
+
setStatusLabel(label, next, prev = this.status.get(label) ?? "free") {
|
|
10340
|
+
this.status.set(label, next);
|
|
10341
|
+
if (prev === next) return;
|
|
10342
|
+
if (prev !== "free") this.counts[prev] -= 1;
|
|
10343
|
+
if (next !== "free") this.counts[next] += 1;
|
|
10344
|
+
}
|
|
10259
10345
|
async resnapshot() {
|
|
10260
10346
|
try {
|
|
10261
10347
|
const objs = await this.api.objects(this.key);
|
|
@@ -10278,23 +10364,31 @@ var SeatManager = class {
|
|
|
10278
10364
|
const next = /* @__PURE__ */ new Map();
|
|
10279
10365
|
if (fallback !== void 0) {
|
|
10280
10366
|
const base = known(fallback);
|
|
10281
|
-
for (const label of this.
|
|
10367
|
+
for (const label of this.knownLabels()) next.set(label, base);
|
|
10282
10368
|
}
|
|
10283
10369
|
for (const [label, st] of Object.entries(seats)) {
|
|
10284
10370
|
next.set(label, known(st));
|
|
10285
10371
|
}
|
|
10286
10372
|
this.status = next;
|
|
10373
|
+
this.modelVersion += 1;
|
|
10374
|
+
this.recountAll();
|
|
10287
10375
|
this.lastSyncedAt = Date.now();
|
|
10288
10376
|
this.repaintAll();
|
|
10289
10377
|
this.afterPaint();
|
|
10290
10378
|
this.recomputeTallies();
|
|
10291
10379
|
}
|
|
10380
|
+
/** The one O(n) walk left: a wholesale model replacement re-bases the counters. */
|
|
10381
|
+
recountAll() {
|
|
10382
|
+
const counts = { held: 0, booked: 0, blocked: 0 };
|
|
10383
|
+
for (const st of this.status.values()) if (st !== "free") counts[st] += 1;
|
|
10384
|
+
this.counts = counts;
|
|
10385
|
+
}
|
|
10292
10386
|
/** Optimistic local write shared by organizer actions. Paint and tally once,
|
|
10293
10387
|
* even when an arena-sized operation changes hundreds of seats. */
|
|
10294
10388
|
setSeatsLocal(labels, st) {
|
|
10295
10389
|
const ids = [];
|
|
10296
10390
|
for (const label of labels) {
|
|
10297
|
-
this.
|
|
10391
|
+
this.setStatusLabel(label, st);
|
|
10298
10392
|
const id = this.labelToId.get(label);
|
|
10299
10393
|
if (id) ids.push(id);
|
|
10300
10394
|
}
|
|
@@ -10420,12 +10514,33 @@ var SeatManager = class {
|
|
|
10420
10514
|
this.revenueStatus = "current";
|
|
10421
10515
|
this.recomputeTallies();
|
|
10422
10516
|
}
|
|
10517
|
+
/**
|
|
10518
|
+
* Read the server's own control-room projection.
|
|
10519
|
+
*
|
|
10520
|
+
* Called on mount, on every socket (re)connect and after an organizer action —
|
|
10521
|
+
* never on a timer and never per delta frame. Presence and gross that arrived
|
|
10522
|
+
* on the socket AFTER this request started are newer than the response, so
|
|
10523
|
+
* they survive it; anything older defers to the read.
|
|
10524
|
+
*/
|
|
10423
10525
|
async refreshControlRoom() {
|
|
10424
10526
|
const request = ++this.revenueRequest;
|
|
10527
|
+
const requestedAt = Date.now();
|
|
10425
10528
|
try {
|
|
10426
|
-
const
|
|
10529
|
+
const fetched = await this.api.controlRoom(this.key, this.trendWindowMinutes);
|
|
10530
|
+
let snapshot = fetched;
|
|
10427
10531
|
if (request === this.revenueRequest) {
|
|
10532
|
+
if (this.livePresence && this.livePresence.at >= requestedAt) {
|
|
10533
|
+
snapshot = { ...snapshot, presence: this.livePresence.value };
|
|
10534
|
+
} else {
|
|
10535
|
+
this.livePresence = null;
|
|
10536
|
+
}
|
|
10537
|
+
if (this.liveGross && this.liveGross.at >= requestedAt) {
|
|
10538
|
+
snapshot = { ...snapshot, revenue: { ...snapshot.revenue, gross: this.liveGross.value } };
|
|
10539
|
+
} else {
|
|
10540
|
+
this.liveGross = null;
|
|
10541
|
+
}
|
|
10428
10542
|
this.controlRoomSnapshot = snapshot;
|
|
10543
|
+
this.rebaseServerTotals(snapshot);
|
|
10429
10544
|
this.lastSyncedAt = Date.now();
|
|
10430
10545
|
this.authoritativeGrossRevenue = snapshot.revenue.gross;
|
|
10431
10546
|
this.currency = snapshot.currency;
|
|
@@ -10444,37 +10559,80 @@ var SeatManager = class {
|
|
|
10444
10559
|
throw err;
|
|
10445
10560
|
}
|
|
10446
10561
|
}
|
|
10447
|
-
|
|
10448
|
-
|
|
10449
|
-
|
|
10450
|
-
if (
|
|
10451
|
-
|
|
10452
|
-
|
|
10453
|
-
|
|
10454
|
-
|
|
10562
|
+
/** Pin the server's totals to the client model they were read against. */
|
|
10563
|
+
rebaseServerTotals(snapshot) {
|
|
10564
|
+
const totals = snapshot.totals;
|
|
10565
|
+
if (!totals || ["free", "held", "booked", "blocked"].some(
|
|
10566
|
+
(key) => !Number.isFinite(totals[key])
|
|
10567
|
+
)) {
|
|
10568
|
+
this.serverBaseline = null;
|
|
10569
|
+
return;
|
|
10570
|
+
}
|
|
10571
|
+
this.serverBaseline = {
|
|
10572
|
+
model: this.modelVersion,
|
|
10573
|
+
server: { free: totals.free, held: totals.held, booked: totals.booked, blocked: totals.blocked },
|
|
10574
|
+
client: this.clientTallies()
|
|
10575
|
+
};
|
|
10455
10576
|
}
|
|
10456
|
-
|
|
10577
|
+
/** What the client's own model says — GA units included since `render()`. */
|
|
10578
|
+
clientTallies() {
|
|
10579
|
+
const { held, booked, blocked } = this.counts;
|
|
10580
|
+
return { held, booked, blocked, free: Math.max(0, this.unitTotal() - held - booked - blocked) };
|
|
10581
|
+
}
|
|
10582
|
+
/**
|
|
10583
|
+
* The numbers the KPI bar and rail render.
|
|
10584
|
+
*
|
|
10585
|
+
* The server is the authority: its totals land exactly as read, and the
|
|
10586
|
+
* delta-driven client model carries them forward until the next read. Before
|
|
10587
|
+
* the first snapshot — and after a wholesale model replacement invalidates the
|
|
10588
|
+
* pairing — the client model stands alone.
|
|
10589
|
+
*/
|
|
10590
|
+
buildTallies() {
|
|
10591
|
+
const client = this.clientTallies();
|
|
10592
|
+
const baseline = this.serverBaseline?.model === this.modelVersion ? this.serverBaseline : null;
|
|
10593
|
+
const of = (key) => baseline ? Math.max(0, baseline.server[key] + (client[key] - baseline.client[key])) : client[key];
|
|
10594
|
+
const seatTotal = this.controlRoomSnapshot?.event?.seatTotal;
|
|
10457
10595
|
const t3 = {
|
|
10458
|
-
free:
|
|
10459
|
-
held:
|
|
10460
|
-
booked:
|
|
10461
|
-
blocked:
|
|
10462
|
-
total: this.
|
|
10596
|
+
free: of("free"),
|
|
10597
|
+
held: of("held"),
|
|
10598
|
+
booked: of("booked"),
|
|
10599
|
+
blocked: of("blocked"),
|
|
10600
|
+
total: Number.isFinite(seatTotal) ? seatTotal : this.unitTotal(),
|
|
10463
10601
|
capacityPct: 0,
|
|
10464
10602
|
sellThroughPct: 0,
|
|
10465
10603
|
grossRevenue: this.authoritativeGrossRevenue,
|
|
10466
10604
|
revenueStatus: this.revenueStatus,
|
|
10467
10605
|
currency: this.currency
|
|
10468
10606
|
};
|
|
10469
|
-
let nonFree = 0;
|
|
10470
|
-
for (const st of this.status.values()) {
|
|
10471
|
-
t3[st] += 1;
|
|
10472
|
-
if (st !== "free") nonFree += 1;
|
|
10473
|
-
}
|
|
10474
|
-
t3.free = Math.max(0, t3.total - nonFree);
|
|
10475
10607
|
t3.capacityPct = t3.total ? Math.round(t3.booked / t3.total * 100) : 0;
|
|
10476
10608
|
const sellable = t3.total - t3.blocked;
|
|
10477
10609
|
t3.sellThroughPct = sellable > 0 ? Math.round(t3.booked / sellable * 100) : 0;
|
|
10610
|
+
return t3;
|
|
10611
|
+
}
|
|
10612
|
+
/**
|
|
10613
|
+
* Queue one KPI/rail repaint for this burst of changes.
|
|
10614
|
+
*
|
|
10615
|
+
* A delta frame can carry hundreds of seats and `paintKpis` rebuilds eight
|
|
10616
|
+
* nodes from scratch, so painting per change is what made an arena-sized
|
|
10617
|
+
* frame expensive. Coalescing on a frame keeps the burst to a single rebuild;
|
|
10618
|
+
* without `requestAnimationFrame` (SSR, an older test env) it paints inline
|
|
10619
|
+
* rather than dropping the update.
|
|
10620
|
+
*/
|
|
10621
|
+
recomputeTallies() {
|
|
10622
|
+
if (this.closed) return;
|
|
10623
|
+
if (typeof requestAnimationFrame !== "function") {
|
|
10624
|
+
this.flushTallies();
|
|
10625
|
+
return;
|
|
10626
|
+
}
|
|
10627
|
+
if (this.paintHandle !== null) return;
|
|
10628
|
+
this.paintHandle = requestAnimationFrame(() => {
|
|
10629
|
+
this.paintHandle = null;
|
|
10630
|
+
this.flushTallies();
|
|
10631
|
+
});
|
|
10632
|
+
}
|
|
10633
|
+
flushTallies() {
|
|
10634
|
+
if (this.closed) return;
|
|
10635
|
+
const t3 = this.buildTallies();
|
|
10478
10636
|
this.paintKpis(t3);
|
|
10479
10637
|
if (this.mode === "view") {
|
|
10480
10638
|
this.paintLegend(t3);
|
|
@@ -10761,16 +10919,16 @@ var SeatManager = class {
|
|
|
10761
10919
|
paintKpis(t3) {
|
|
10762
10920
|
if (!this.els.kpis) return;
|
|
10763
10921
|
const rev = t3.revenueStatus === "current" ? fmtMoney(t3.grossRevenue, t3.currency) : "\u2014";
|
|
10764
|
-
const presence = this.
|
|
10922
|
+
const presence = this.presenceCounts();
|
|
10765
10923
|
const items = [
|
|
10766
|
-
{ key: "sold-seats", raw: t3.booked, n: t3.booked.toLocaleString(), l: "Sold seats", dot: "#22a06b" },
|
|
10767
|
-
{ key: "held-seats", raw: t3.held, n: t3.held.toLocaleString(), l: "Held seats", dot: "#f4b740" },
|
|
10768
|
-
{ key: "
|
|
10769
|
-
{ key: "
|
|
10770
|
-
{ key: "
|
|
10771
|
-
{ key: "
|
|
10772
|
-
{ key: "sold-pct", raw: t3.capacityPct, n: `${t3.capacityPct}%`, l: "Sold" },
|
|
10773
|
-
{ key: "gross-sales", raw: t3.revenueStatus === "current" ? t3.grossRevenue : null, n: rev, l: "Gross sales" }
|
|
10924
|
+
{ key: "sold-seats", raw: t3.booked, n: t3.booked.toLocaleString(), l: "Sold seats", dot: "#22a06b", title: "Seats booked" },
|
|
10925
|
+
{ key: "held-seats", raw: t3.held, n: t3.held.toLocaleString(), l: "Held seats", dot: "#f4b740", title: "Seats held in a checkout right now" },
|
|
10926
|
+
{ key: "free-seats", raw: t3.free, n: t3.free.toLocaleString(), l: "Free seats", dot: "#6e7bff", title: "Seats on sale and unsold" },
|
|
10927
|
+
{ key: "blocked", raw: t3.blocked, n: t3.blocked.toLocaleString(), l: "Blocked", dot: "#8b94ac", title: "Seats withheld from sale" },
|
|
10928
|
+
{ key: "buyers", raw: presence?.shoppingSessions ?? null, n: presence ? presence.shoppingSessions.toLocaleString() : "\u2014", l: "Buyers", title: "People on the map right now" },
|
|
10929
|
+
{ key: "carts", raw: presence?.activeHolds ?? null, n: presence ? presence.activeHolds.toLocaleString() : "\u2014", l: "Carts", title: "Checkouts holding seats right now \u2014 sessions, not seats" },
|
|
10930
|
+
{ key: "sold-pct", raw: t3.capacityPct, n: `${t3.capacityPct}%`, l: "Sold", title: "Sold seats as a share of the whole event" },
|
|
10931
|
+
{ key: "gross-sales", raw: t3.revenueStatus === "current" ? t3.grossRevenue : null, n: rev, l: "Gross sales", title: "Exact booked gross" }
|
|
10774
10932
|
];
|
|
10775
10933
|
let hasChanges = false;
|
|
10776
10934
|
this.els.kpis.innerHTML = items.map((item) => {
|
|
@@ -10786,7 +10944,7 @@ var SeatManager = class {
|
|
|
10786
10944
|
}
|
|
10787
10945
|
if (item.raw != null) this.lastKpiValues.set(item.key, item.raw);
|
|
10788
10946
|
const activeDelta = this.activeKpiDeltas.get(item.key);
|
|
10789
|
-
return `<div class="slm-kpi${activeDelta ? " changed" : ""}" data-kpi="${item.key}">
|
|
10947
|
+
return `<div class="slm-kpi${activeDelta ? " changed" : ""}" data-kpi="${item.key}" title="${esc2(item.title)}">
|
|
10790
10948
|
<b>${item.dot ? `<span class="dot" style="background:${item.dot}"></span>` : ""}${item.n}</b><span>${item.l}</span>
|
|
10791
10949
|
${activeDelta ? `<span class="slm-kpidelta${activeDelta.down ? " down" : ""}">${activeDelta.text}</span>` : ""}
|
|
10792
10950
|
</div>`;
|
|
@@ -10844,15 +11002,21 @@ var SeatManager = class {
|
|
|
10844
11002
|
this.paintMomentumHelp();
|
|
10845
11003
|
this.paintFeed();
|
|
10846
11004
|
}
|
|
11005
|
+
/** Live presence wins over the snapshot's copy — it is the fresher channel,
|
|
11006
|
+
* and it exists from the first frame rather than the first fetch. */
|
|
11007
|
+
presenceCounts() {
|
|
11008
|
+
return this.livePresence?.value ?? this.controlRoomSnapshot?.presence ?? null;
|
|
11009
|
+
}
|
|
10847
11010
|
paintMonitorInsights() {
|
|
10848
11011
|
if (this.mode !== "view") return;
|
|
10849
11012
|
const snapshot = this.controlRoomSnapshot;
|
|
10850
11013
|
if (this.els.presence) {
|
|
10851
11014
|
const connected = this.root?.classList.contains("live");
|
|
10852
11015
|
const sync = this.lastSyncedAt ? relTime(this.lastSyncedAt, Date.now()) : "waiting";
|
|
11016
|
+
const presence = this.presenceCounts();
|
|
10853
11017
|
this.els.presence.innerHTML = `
|
|
10854
|
-
<div class="slm-healthitem"><b>${
|
|
10855
|
-
<div class="slm-healthitem"><b>${
|
|
11018
|
+
<div class="slm-healthitem" title="People on the map right now"><b>${presence ? presence.shoppingSessions.toLocaleString() : "\u2014"}</b><span>Buyers</span></div>
|
|
11019
|
+
<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>
|
|
10856
11020
|
<div class="slm-healthitem"><b>${connected ? "Healthy" : "Reconnecting"}</b><span>Live connection</span></div>
|
|
10857
11021
|
<div class="slm-healthitem"><b>${sync}</b><span>Last sync</span></div>`;
|
|
10858
11022
|
}
|
|
@@ -11463,7 +11627,7 @@ var SeatManager = class {
|
|
|
11463
11627
|
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;
|
|
11464
11628
|
if (activity) this.paintSpatialActivity(activity);
|
|
11465
11629
|
}
|
|
11466
|
-
if (action !== "setHoldTtl") this.
|
|
11630
|
+
if (action !== "setHoldTtl") void this.refreshControlRoom().catch((err) => this.opts.onError?.(err));
|
|
11467
11631
|
this.opts.onActionComplete?.({ action, labels, count: labels.length });
|
|
11468
11632
|
}
|
|
11469
11633
|
toastOk(msg) {
|