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