@seatlayer/js 0.47.1 → 0.48.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/manager.cjs CHANGED
@@ -42,6 +42,99 @@ async function parse(res) {
42
42
  }
43
43
  return data;
44
44
  }
45
+ function record(value) {
46
+ return value && typeof value === "object" ? value : {};
47
+ }
48
+ function finite(primary, legacy, fallback = 0) {
49
+ if (typeof primary === "number" && Number.isFinite(primary)) return primary;
50
+ if (typeof legacy === "number" && Number.isFinite(legacy)) return legacy;
51
+ return fallback;
52
+ }
53
+ function nullableFinite(primary, legacy) {
54
+ if (primary === null) return null;
55
+ if (typeof primary === "number" && Number.isFinite(primary)) return primary;
56
+ if (primary !== void 0) return null;
57
+ if (typeof legacy === "number" && Number.isFinite(legacy)) return legacy;
58
+ return null;
59
+ }
60
+ function normalizeSection(value) {
61
+ const row = record(value);
62
+ const bookedValue = finite(row.bookedValue, row.bookedRevenue);
63
+ return { ...row, bookedValue, bookedRevenue: bookedValue };
64
+ }
65
+ function normalizeReportResult(value) {
66
+ const source = record(value);
67
+ const report = record(source.report);
68
+ const byCategory = Array.isArray(report.byCategory) ? report.byCategory.map((value2) => {
69
+ const row = record(value2);
70
+ const bookedValue = finite(row.bookedValue, row.bookedRevenue);
71
+ return { ...row, bookedValue, bookedRevenue: bookedValue };
72
+ }) : [];
73
+ const bySection = Array.isArray(report.bySection) ? report.bySection.map(normalizeSection) : void 0;
74
+ return {
75
+ ...source,
76
+ report: { ...report, byCategory, ...bySection ? { bySection } : {} }
77
+ };
78
+ }
79
+ function normalizeControlRoomSnapshot(value) {
80
+ const source = record(value);
81
+ const canonical = record(source.bookedValue);
82
+ const legacy = record(source.revenue);
83
+ const selected = Object.keys(canonical).length ? canonical : legacy;
84
+ const bySectionSource = Array.isArray(canonical.bySection) ? canonical.bySection : Array.isArray(legacy.bySection) ? legacy.bySection : [];
85
+ const bookedValue = {
86
+ ...selected,
87
+ gross: finite(canonical.gross, legacy.gross),
88
+ bySection: bySectionSource.map(normalizeSection)
89
+ };
90
+ const velocity = record(source.velocity);
91
+ const velocityRows = Array.isArray(velocity.bySection) ? velocity.bySection.map((value2) => {
92
+ const row = record(value2);
93
+ const rowValue = finite(row.bookedValue, row.grossRevenue);
94
+ return { ...row, bookedValue: rowValue, grossRevenue: rowValue };
95
+ }) : [];
96
+ return {
97
+ ...source,
98
+ bookedValue,
99
+ revenue: bookedValue,
100
+ velocity: { ...velocity, bySection: velocityRows }
101
+ };
102
+ }
103
+ function normalizeChannelReportResult(value) {
104
+ const source = record(value);
105
+ const report = record(source.report);
106
+ const includesBookedValue = typeof report.includesBookedValue === "boolean" ? report.includesBookedValue : report.includesRevenue === true;
107
+ const rows = Array.isArray(report.rows) ? report.rows.map((value2) => {
108
+ const row = record(value2);
109
+ const attribution = record(row.attribution);
110
+ const bookedValue = nullableFinite(attribution.bookedValue, attribution.revenue);
111
+ return {
112
+ ...row,
113
+ attribution: { ...attribution, bookedValue, revenue: bookedValue }
114
+ };
115
+ }) : [];
116
+ const totals = record(report.totals);
117
+ const totalBookedValue = nullableFinite(totals.bookedValue, totals.revenue);
118
+ return {
119
+ ...source,
120
+ report: {
121
+ ...report,
122
+ includesBookedValue,
123
+ includesRevenue: includesBookedValue,
124
+ rows,
125
+ totals: { ...totals, bookedValue: totalBookedValue, revenue: totalBookedValue }
126
+ }
127
+ };
128
+ }
129
+ function normalizeChannelReportLink(value) {
130
+ const link = record(value);
131
+ const includesBookedValue = typeof link.includesBookedValue === "boolean" ? link.includesBookedValue : link.includesRevenue === true;
132
+ return {
133
+ ...link,
134
+ includesBookedValue,
135
+ includesRevenue: includesBookedValue
136
+ };
137
+ }
45
138
  var ManageApiError, ManageApi;
46
139
  var init_manageApi = __esm({
47
140
  "src/manageApi.ts"() {
@@ -76,13 +169,28 @@ var init_manageApi = __esm({
76
169
  }
77
170
  return fetch(`${this.base}${path}`, { method, headers, body, credentials: "omit" }).then((r) => parse(r));
78
171
  }
79
- pub(path) {
80
- return fetch(`${this.base}${path}`, { credentials: "omit" }).then((r) => parse(r));
172
+ async authBlob(path) {
173
+ const res = await fetch(`${this.base}${path}`, {
174
+ method: "GET",
175
+ headers: { Authorization: `Bearer ${this.token}` },
176
+ credentials: "omit"
177
+ });
178
+ if (!res.ok) await parse(res);
179
+ return res.blob();
81
180
  }
82
181
  // ---- realtime read ----
83
- /** The chart geometry. Genuinely public it is the same map buyers see. */
182
+ /** Event-pinned organizer geometry. A manage token is never sent to `/pub`. */
84
183
  chart(key) {
85
- return this.pub(`/pub/events/${encodeURIComponent(key)}/chart`);
184
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/chart`);
185
+ }
186
+ /** Authenticated bytes for an Event-scoped organizer chart asset. */
187
+ asset(key, asset) {
188
+ if (!/^[a-zA-Z0-9._-]+$/.test(asset)) {
189
+ return Promise.reject(new ManageApiError(404, "not_found", "not_found"));
190
+ }
191
+ return this.authBlob(
192
+ `/v1/events/${encodeURIComponent(key)}/assets/${encodeURIComponent(asset)}`
193
+ );
86
194
  }
87
195
  /**
88
196
  * The ORGANIZER's seat map: physical state, token-authed.
@@ -142,6 +250,31 @@ var init_manageApi = __esm({
142
250
  setHoldTtl(key, holdTtlMs) {
143
251
  return this.auth(`/v1/events/${encodeURIComponent(key)}/hold-ttl`, { method: "POST", body: { holdTtlMs } });
144
252
  }
253
+ // ---- Platform inventory booking history (token) ----
254
+ /** Inventory lifecycle by stable integrator bookingRef. Absent on Managed. */
255
+ bookings(key, query = {}) {
256
+ const params = new URLSearchParams();
257
+ if (query.q) params.set("q", query.q);
258
+ if (query.state) params.set("state", query.state);
259
+ if (query.cursor) params.set("cursor", query.cursor);
260
+ if (query.limit != null) params.set("limit", String(query.limit));
261
+ const qs = params.toString();
262
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/bookings${qs ? `?${qs}` : ""}`);
263
+ }
264
+ /** Exact configured-value snapshot plus book/replay/cancellation audit. */
265
+ booking(key, bookingRef) {
266
+ return this.auth(
267
+ `/v1/events/${encodeURIComponent(key)}/bookings/${encodeURIComponent(bookingRef)}`
268
+ );
269
+ }
270
+ /** Alias matching the server SDK vocabulary. */
271
+ listBookings(key, query = {}) {
272
+ return this.bookings(key, query);
273
+ }
274
+ /** Alias matching the server SDK vocabulary. */
275
+ retrieveBooking(key, bookingRef) {
276
+ return this.booking(key, bookingRef);
277
+ }
145
278
  // ---- availability windows (token) ----
146
279
  /** The organizer's current per section/zone availability windows (needs
147
280
  * `event:view`). Ids absent from `rules` are open / on sale. */
@@ -323,10 +456,46 @@ var init_manageApi = __esm({
323
456
  }
324
457
  // ---- reports (token) ----
325
458
  report(key) {
326
- return this.auth(`/v1/events/${encodeURIComponent(key)}/report`);
459
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/report`).then(normalizeReportResult);
327
460
  }
328
461
  controlRoom(key, windowMinutes = 15) {
329
- return this.auth(`/v1/events/${encodeURIComponent(key)}/control-room?window=${windowMinutes}`);
462
+ return this.auth(
463
+ `/v1/events/${encodeURIComponent(key)}/control-room?window=${windowMinutes}`
464
+ ).then(normalizeControlRoomSnapshot);
465
+ }
466
+ /** Allocation beside immutable booking-time channel attribution. */
467
+ channelReport(key) {
468
+ return this.auth(
469
+ `/v1/events/${encodeURIComponent(key)}/channels/report`
470
+ ).then(normalizeChannelReportResult);
471
+ }
472
+ createChannelReportLink(key, channelId, input = {}) {
473
+ return this.auth(
474
+ `/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}/report-links`,
475
+ { method: "POST", body: input }
476
+ ).then((value) => {
477
+ const reveal = record(value);
478
+ return { ...reveal, link: normalizeChannelReportLink(reveal.link) };
479
+ });
480
+ }
481
+ channelReportLinks(key, channelId) {
482
+ return this.auth(
483
+ `/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}/report-links`
484
+ ).then((value) => {
485
+ const result = record(value);
486
+ return {
487
+ links: Array.isArray(result.links) ? result.links.map(normalizeChannelReportLink) : []
488
+ };
489
+ });
490
+ }
491
+ revokeChannelReportLink(key, channelId, linkId) {
492
+ return this.auth(
493
+ `/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}/report-links/${encodeURIComponent(linkId)}`,
494
+ { method: "DELETE" }
495
+ ).then((value) => {
496
+ const result = record(value);
497
+ return { ...result, link: normalizeChannelReportLink(result.link) };
498
+ });
330
499
  }
331
500
  log(key, opts = {}) {
332
501
  const params = new URLSearchParams();
@@ -3533,6 +3702,108 @@ module.exports = __toCommonJS(manager_exports);
3533
3702
  // src/SeatManager.ts
3534
3703
  var import_core = require("@seatlayer/core");
3535
3704
  init_manageApi();
3705
+
3706
+ // src/manageAssets.ts
3707
+ var SAFE_ASSET = /^[a-zA-Z0-9._-]+$/;
3708
+ function organizerEventAssetReference(value) {
3709
+ let url;
3710
+ try {
3711
+ url = new URL(value, "https://seatlayer.invalid");
3712
+ } catch {
3713
+ return null;
3714
+ }
3715
+ if (url.search || url.hash) return null;
3716
+ const match = /^\/v1\/events\/([^/]+)\/assets\/([^/]+)$/.exec(url.pathname);
3717
+ if (!match) return null;
3718
+ try {
3719
+ const eventKey = decodeURIComponent(match[1]);
3720
+ const asset = decodeURIComponent(match[2]);
3721
+ if (!eventKey || !SAFE_ASSET.test(asset)) return null;
3722
+ return { eventKey, asset };
3723
+ } catch {
3724
+ return null;
3725
+ }
3726
+ }
3727
+ function looksLikeOrganizerAsset(value) {
3728
+ try {
3729
+ return /^\/v1\/events\/[^/]+\/assets(?:\/|$)/.test(
3730
+ new URL(value, "https://seatlayer.invalid").pathname
3731
+ );
3732
+ } catch {
3733
+ return false;
3734
+ }
3735
+ }
3736
+ var OrganizerAssetObjectUrls = class {
3737
+ constructor(eventKey, load) {
3738
+ this.eventKey = eventKey;
3739
+ this.load = load;
3740
+ this.pending = /* @__PURE__ */ new Map();
3741
+ this.created = /* @__PURE__ */ new Set();
3742
+ this.disposed = false;
3743
+ }
3744
+ resolve(reference) {
3745
+ const parsed = organizerEventAssetReference(reference);
3746
+ if (!parsed) {
3747
+ return Promise.resolve(looksLikeOrganizerAsset(reference) ? null : reference);
3748
+ }
3749
+ if (parsed.eventKey !== this.eventKey || this.disposed) return Promise.resolve(null);
3750
+ const cacheKey = `${parsed.eventKey}/${parsed.asset}`;
3751
+ const existing = this.pending.get(cacheKey);
3752
+ if (existing) return existing;
3753
+ const task = this.load(parsed.eventKey, parsed.asset).then((blob) => {
3754
+ const objectUrl = URL.createObjectURL(blob);
3755
+ if (this.disposed) {
3756
+ URL.revokeObjectURL(objectUrl);
3757
+ return null;
3758
+ }
3759
+ this.created.add(objectUrl);
3760
+ return objectUrl;
3761
+ }).catch((error) => {
3762
+ this.pending.delete(cacheKey);
3763
+ throw error;
3764
+ });
3765
+ this.pending.set(cacheKey, task);
3766
+ return task;
3767
+ }
3768
+ /**
3769
+ * Resolve the image fields the synchronous map renderer loads immediately.
3770
+ * View-from-seat media stays lazy: SeatManager does not open that buyer
3771
+ * surface, and eagerly downloading every row panorama would be unbounded.
3772
+ */
3773
+ async prepareRendererChart(doc) {
3774
+ const prepareBackground = async (background) => {
3775
+ if (!background?.url) return;
3776
+ const resolved = await this.resolve(background.url);
3777
+ if (!resolved) throw new Error("organizer_event_asset_scope_mismatch");
3778
+ background.url = resolved;
3779
+ };
3780
+ const prepareObjects = async (objects) => {
3781
+ for (const object of objects) {
3782
+ if (object.type !== "decorImage") continue;
3783
+ const image = object;
3784
+ const resolved = await this.resolve(image.href);
3785
+ if (!resolved) throw new Error("organizer_event_asset_scope_mismatch");
3786
+ image.href = resolved;
3787
+ }
3788
+ };
3789
+ const prepareOwner = async (owner) => {
3790
+ await prepareBackground(owner.backgroundImage);
3791
+ await prepareObjects(owner.objects);
3792
+ };
3793
+ await prepareOwner(doc);
3794
+ for (const floor of doc.floors ?? []) await prepareOwner(floor);
3795
+ return doc;
3796
+ }
3797
+ dispose() {
3798
+ if (this.disposed) return;
3799
+ this.disposed = true;
3800
+ for (const url of this.created) URL.revokeObjectURL(url);
3801
+ this.created.clear();
3802
+ this.pending.clear();
3803
+ }
3804
+ };
3805
+
3806
+ // src/SeatManager.ts
3536
3807
  function availabilityModeOf(rule) {
3537
3808
  return rule ? rule.mode : "open";
3538
3809
  }
@@ -3811,8 +4082,8 @@ var MANAGER_CSS = (
3811
4082
  .slm.compact .slm-modes{min-width:0}.slm.compact .slm-mode{padding-inline:11px}
3812
4083
  .slm.compact .slm-live{justify-self:end}.slm.compact .slm-bar-actions{grid-column:1/-1;justify-self:stretch}
3813
4084
  .slm.compact .slm-barbtn{flex:1;padding:6px 9px}.slm.compact .slm-kpis{grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
3814
- .slm.compact .slm-kpi[data-kpi="buyers"],.slm.compact .slm-kpi[data-kpi="active-holds"],
3815
- .slm.compact .slm-kpi[data-kpi="sold-pct"],.slm.compact .slm-kpi[data-kpi="gross-sales"]{display:none}
4085
+ .slm.compact .slm-kpi[data-kpi="viewing-map"],.slm.compact .slm-kpi[data-kpi="active-holds"],
4086
+ .slm.compact .slm-kpi[data-kpi="booked-pct"],.slm.compact .slm-kpi[data-kpi="booked-value"]{display:none}
3816
4087
  /* Reduced motion, as a BLANKET over the cockpit subtree rather than a list of
3817
4088
  selectors. The list this replaces named four animations and two transitions,
3818
4089
  and had silently fallen behind the stylesheet: the zoom hint, the toast and
@@ -4022,6 +4293,10 @@ var SeatManager = class {
4022
4293
  this.currency = options.currency ?? "USD";
4023
4294
  this.tokenExpiresAt = options.tokenExpiresAt ?? null;
4024
4295
  this.api = new ManageApi(options.apiBase ?? DEFAULT_API_BASE, options.token);
4296
+ this.organizerAssetUrls = new OrganizerAssetObjectUrls(
4297
+ this.key,
4298
+ (key, asset) => this.withAuthRetry(() => this.api.asset(key, asset))
4299
+ );
4025
4300
  this.host = resolveContainer(options.container);
4026
4301
  }
4027
4302
  /** Build the DOM, load the chart, subscribe to realtime, mount the board. */
@@ -4029,10 +4304,10 @@ var SeatManager = class {
4029
4304
  injectStyle();
4030
4305
  this.buildChrome();
4031
4306
  try {
4032
- const res = await this.api.chart(this.key);
4033
- this.doc = res.doc;
4307
+ const res = await this.withAuthRetry(() => this.api.chart(this.key));
4308
+ this.doc = await this.organizerAssetUrls.prepareRendererChart(res.doc);
4034
4309
  this.currency = res.event.currency ?? this.opts.currency ?? this.currency;
4035
- this.buildUnitUniverse(res.doc);
4310
+ this.buildUnitUniverse(this.doc);
4036
4311
  this.buildRenderer();
4037
4312
  this.buildSectionOptions();
4038
4313
  const [, controlRoom] = await Promise.all([
@@ -4379,10 +4654,10 @@ var SeatManager = class {
4379
4654
  try {
4380
4655
  await this.api.unbook(this.key, targets, bookingRef);
4381
4656
  this.clearSelection();
4382
- this.done("cancelBooking", targets, `Cancelled ${targets.length} booking${targets.length === 1 ? "" : "s"}.`);
4657
+ this.done("cancelBooking", targets, `Released ${targets.length} booked unit${targets.length === 1 ? "" : "s"}.`);
4383
4658
  } catch (err) {
4384
4659
  this.setSeatsLocal(targets, "booked");
4385
- this.toastErr("Couldn't cancel that booking. Check the reference.");
4660
+ this.toastErr("Couldn't release that booked inventory. Check the booking reference.");
4386
4661
  this.opts.onError?.(err);
4387
4662
  }
4388
4663
  }
@@ -4435,9 +4710,9 @@ var SeatManager = class {
4435
4710
  async setHoldTtl(ms) {
4436
4711
  try {
4437
4712
  await this.api.setHoldTtl(this.key, ms);
4438
- this.done("setHoldTtl", [], ms ? `Checkout window set to ${Math.round(ms / 6e4)} min.` : "Checkout window reset.");
4713
+ this.done("setHoldTtl", [], ms ? `Hold window set to ${Math.round(ms / 6e4)} min.` : "Hold window reset.");
4439
4714
  } catch (err) {
4440
- this.toastErr("Couldn't update the checkout window.");
4715
+ this.toastErr("Couldn't update the hold window.");
4441
4716
  this.opts.onError?.(err);
4442
4717
  }
4443
4718
  }
@@ -4482,6 +4757,7 @@ var SeatManager = class {
4482
4757
  }
4483
4758
  this.renderer?.destroy();
4484
4759
  this.renderer = null;
4760
+ this.organizerAssetUrls.dispose();
4485
4761
  if (this.root && this.root.parentNode === this.host) this.host.removeChild(this.root);
4486
4762
  }
4487
4763
  // ---- renderer lifecycle ---------------------------------------------------
@@ -4602,24 +4878,28 @@ var SeatManager = class {
4602
4878
  * projects its deltas, so any change inside a private channel allocation is
4603
4879
  * structurally suppressed and the map silently drifts.
4604
4880
  *
4605
- * If the mint fails (an expired token, a worker that predates the route) we
4606
- * still connect unticketed rather than going dark the public-sale stream is
4607
- * worth having, and every `resnapshot()` re-establishes physical truth from
4608
- * the authenticated HTTP read.
4881
+ * If the mint fails, remain reconnecting. An unticketed socket is a buyer
4882
+ * projection, so applying it to organizer state would be worse than staying
4883
+ * visibly offline while the host refreshes authority or upgrades the API.
4609
4884
  */
4610
4885
  async connect() {
4611
4886
  if (this.closed) return;
4612
4887
  let protocols;
4613
4888
  try {
4614
- protocols = (await this.api.subscribeTicket(this.key)).protocols;
4615
- } catch {
4616
- protocols = void 0;
4889
+ protocols = (await this.withAuthRetry(() => this.api.subscribeTicket(this.key))).protocols;
4890
+ if (!protocols.length) throw new Error("manage_subscribe_ticket_missing");
4891
+ } catch (err) {
4892
+ this.setLive(false);
4893
+ this.opts.onError?.(err);
4894
+ this.scheduleReconnect();
4895
+ return;
4617
4896
  }
4618
4897
  if (this.closed) return;
4619
4898
  let ws;
4620
4899
  try {
4621
- ws = protocols ? new WebSocket(this.api.socketUrl(this.key), protocols) : new WebSocket(this.api.socketUrl(this.key));
4622
- } catch {
4900
+ ws = new WebSocket(this.api.socketUrl(this.key), protocols);
4901
+ } catch (err) {
4902
+ this.opts.onError?.(err);
4623
4903
  this.scheduleReconnect();
4624
4904
  return;
4625
4905
  }
@@ -4710,8 +4990,9 @@ var SeatManager = class {
4710
4990
  this.lastSyncedAt = Date.now();
4711
4991
  this.afterPaint();
4712
4992
  }
4713
- if (typeof m.revenue?.gross === "number" && Number.isFinite(m.revenue.gross)) {
4714
- this.applyLiveGross(m.revenue.gross);
4993
+ const liveBookedValue = typeof m.bookedValue?.gross === "number" ? m.bookedValue.gross : m.revenue?.gross;
4994
+ if (typeof liveBookedValue === "number" && Number.isFinite(liveBookedValue)) {
4995
+ this.applyLiveGross(liveBookedValue);
4715
4996
  }
4716
4997
  this.recomputeTallies();
4717
4998
  }
@@ -4728,9 +5009,12 @@ var SeatManager = class {
4728
5009
  this.authoritativeGrossRevenue = gross;
4729
5010
  this.revenueStatus = "current";
4730
5011
  if (this.controlRoomSnapshot) {
5012
+ const current = this.controlRoomSnapshot.bookedValue ?? this.controlRoomSnapshot.revenue;
5013
+ const bookedValue = { ...current, gross };
4731
5014
  this.controlRoomSnapshot = {
4732
5015
  ...this.controlRoomSnapshot,
4733
- revenue: { ...this.controlRoomSnapshot.revenue, gross }
5016
+ bookedValue,
5017
+ revenue: bookedValue
4734
5018
  };
4735
5019
  this.opts.onControlRoom?.(this.controlRoomSnapshot);
4736
5020
  }
@@ -4908,7 +5192,10 @@ var SeatManager = class {
4908
5192
  // ---- tallies + feed -------------------------------------------------------
4909
5193
  applyReportRevenue(report) {
4910
5194
  this.authoritativeGrossRevenue = report.report.byCategory.reduce(
4911
- (sum, row) => sum + (Number.isFinite(row.bookedRevenue) ? row.bookedRevenue : 0),
5195
+ (sum, row) => {
5196
+ const value = Number.isFinite(row.bookedValue) ? row.bookedValue : row.bookedRevenue;
5197
+ return sum + (Number.isFinite(value) ? value : 0);
5198
+ },
4912
5199
  0
4913
5200
  );
4914
5201
  this.revenueStatus = "current";
@@ -4927,7 +5214,13 @@ var SeatManager = class {
4927
5214
  const requestedAt = Date.now();
4928
5215
  try {
4929
5216
  const fetched = await this.api.controlRoom(this.key, this.trendWindowMinutes);
4930
- let snapshot = fetched;
5217
+ const incoming = fetched.bookedValue ?? fetched.revenue ?? { gross: 0, bySection: [] };
5218
+ const normalizedSections = (incoming.bySection ?? []).map((row) => {
5219
+ const value = Number.isFinite(row.bookedValue) ? row.bookedValue : row.bookedRevenue;
5220
+ return { ...row, bookedValue: value ?? 0, bookedRevenue: value ?? 0 };
5221
+ });
5222
+ const canonical = { ...incoming, bySection: normalizedSections };
5223
+ let snapshot = { ...fetched, bookedValue: canonical, revenue: canonical };
4931
5224
  if (request === this.revenueRequest) {
4932
5225
  if (this.livePresence && this.livePresence.at >= requestedAt) {
4933
5226
  snapshot = { ...snapshot, presence: this.livePresence.value };
@@ -4935,14 +5228,15 @@ var SeatManager = class {
4935
5228
  this.livePresence = null;
4936
5229
  }
4937
5230
  if (this.liveGross && this.liveGross.at >= requestedAt) {
4938
- snapshot = { ...snapshot, revenue: { ...snapshot.revenue, gross: this.liveGross.value } };
5231
+ const bookedValue = { ...snapshot.bookedValue, gross: this.liveGross.value };
5232
+ snapshot = { ...snapshot, bookedValue, revenue: bookedValue };
4939
5233
  } else {
4940
5234
  this.liveGross = null;
4941
5235
  }
4942
5236
  this.controlRoomSnapshot = snapshot;
4943
5237
  this.rebaseServerTotals(snapshot);
4944
5238
  this.lastSyncedAt = Date.now();
4945
- this.authoritativeGrossRevenue = snapshot.revenue.gross;
5239
+ this.authoritativeGrossRevenue = snapshot.bookedValue.gross;
4946
5240
  this.currency = snapshot.currency;
4947
5241
  this.revenueStatus = "current";
4948
5242
  this.recomputeTallies();
@@ -5000,7 +5294,9 @@ var SeatManager = class {
5000
5294
  total: Number.isFinite(seatTotal) ? seatTotal : this.unitTotal(),
5001
5295
  capacityPct: 0,
5002
5296
  sellThroughPct: 0,
5297
+ bookedValue: this.authoritativeGrossRevenue,
5003
5298
  grossRevenue: this.authoritativeGrossRevenue,
5299
+ bookedValueStatus: this.revenueStatus,
5004
5300
  revenueStatus: this.revenueStatus,
5005
5301
  currency: this.currency
5006
5302
  };
@@ -5155,8 +5451,8 @@ var SeatManager = class {
5155
5451
  <button class="slm-barbtn follow" data-ref="follow" aria-pressed="false"
5156
5452
  title="Stay on the current map view unless enabled">Follow live</button>
5157
5453
  <button class="slm-barbtn" data-ref="heat" aria-pressed="false"
5158
- aria-label="Sales momentum overlay off"
5159
- title="Highlight sections selling fastest in the selected time window">Sales momentum</button>
5454
+ aria-label="Booking momentum overlay off"
5455
+ title="Highlight sections booking fastest in the selected time window">Booking momentum</button>
5160
5456
  <button class="slm-barbtn" data-ref="fullscreen" title="Full screen (F)" aria-keyshortcuts="F">Full screen</button>
5161
5457
  </div>
5162
5458
  <div class="slm-kpis" data-ref="kpis"></div>
@@ -5259,16 +5555,16 @@ var SeatManager = class {
5259
5555
  if (!button) return;
5260
5556
  button.classList.toggle("on", this.followLive);
5261
5557
  button.setAttribute("aria-pressed", String(this.followLive));
5262
- button.setAttribute("title", this.followLive ? "Following new buyer holds and bookings. Turn off to keep the current view." : "Stay on the current map view. Enable to follow new buyer holds and bookings.");
5558
+ button.setAttribute("title", this.followLive ? "Following new holds and bookings. Turn off to keep the current view." : "Stay on the current map view. Enable to follow new holds and bookings.");
5263
5559
  }
5264
5560
  paintHeatButton() {
5265
5561
  const button = this.els.heat;
5266
5562
  if (!button) return;
5267
5563
  button.classList.toggle("on", this.heatEnabled);
5268
5564
  button.setAttribute("aria-pressed", String(this.heatEnabled));
5269
- button.setAttribute("aria-label", `Sales momentum overlay ${this.heatEnabled ? "on" : "off"}`);
5270
- button.setAttribute("title", `${this.heatEnabled ? "Hide" : "Highlight"} sections selling fastest in the selected time window`);
5271
- button.textContent = "Sales momentum";
5565
+ button.setAttribute("aria-label", `Booking momentum overlay ${this.heatEnabled ? "on" : "off"}`);
5566
+ button.setAttribute("title", `${this.heatEnabled ? "Hide" : "Highlight"} sections booking fastest in the selected time window`);
5567
+ button.textContent = "Booking momentum";
5272
5568
  this.paintMomentumHelp();
5273
5569
  }
5274
5570
  paintMomentumHelp() {
@@ -5312,23 +5608,23 @@ var SeatManager = class {
5312
5608
  formatKpiDelta(key, delta, currency) {
5313
5609
  const sign = delta > 0 ? "+" : "\u2212";
5314
5610
  const absolute = Math.abs(delta);
5315
- if (key === "gross-sales") return `${sign}${fmtMoney(absolute, currency)}`;
5316
- if (key === "sold-pct") return `${sign}${absolute.toLocaleString()}pt`;
5611
+ if (key === "booked-value") return `${sign}${fmtMoney(absolute, currency)}`;
5612
+ if (key === "booked-pct") return `${sign}${absolute.toLocaleString()}pt`;
5317
5613
  return `${sign}${absolute.toLocaleString()}`;
5318
5614
  }
5319
5615
  paintKpis(t) {
5320
5616
  if (!this.els.kpis) return;
5321
- const rev = t.revenueStatus === "current" ? fmtMoney(t.grossRevenue, t.currency) : "\u2014";
5617
+ const bookedValue = t.bookedValueStatus === "current" ? fmtMoney(t.bookedValue, t.currency) : "\u2014";
5322
5618
  const presence = this.presenceCounts();
5323
5619
  const items = [
5324
- { key: "sold-seats", raw: t.booked, n: t.booked.toLocaleString(), l: "Sold seats", dot: "#22a06b", title: "Seats booked" },
5325
- { key: "held-seats", raw: t.held, n: t.held.toLocaleString(), l: "Held seats", dot: "#f4b740", title: "Seats held in a checkout right now" },
5326
- { key: "free-seats", raw: t.free, n: t.free.toLocaleString(), l: "Free seats", dot: "#6e7bff", title: "Seats on sale and unsold" },
5327
- { key: "blocked", raw: t.blocked, n: t.blocked.toLocaleString(), l: "Blocked", dot: "#8b94ac", title: "Seats withheld from sale" },
5328
- { key: "buyers", raw: presence?.shoppingSessions ?? null, n: presence ? presence.shoppingSessions.toLocaleString() : "\u2014", l: "Buyers", title: "People on the map right now" },
5329
- { key: "carts", raw: presence?.activeHolds ?? null, n: presence ? presence.activeHolds.toLocaleString() : "\u2014", l: "Carts", title: "Checkouts holding seats right now \u2014 sessions, not seats" },
5330
- { key: "sold-pct", raw: t.capacityPct, n: `${t.capacityPct}%`, l: "Sold", title: "Sold seats as a share of the whole event" },
5331
- { key: "gross-sales", raw: t.revenueStatus === "current" ? t.grossRevenue : null, n: rev, l: "Gross sales", title: "Exact booked gross" }
5620
+ { key: "booked-inventory", raw: t.booked, n: t.booked.toLocaleString(), l: "Booked inventory", dot: "#22a06b", title: "Inventory units booked" },
5621
+ { key: "held-seats", raw: t.held, n: t.held.toLocaleString(), l: "Held inventory", dot: "#f4b740", title: "Inventory currently held" },
5622
+ { key: "free-seats", raw: t.free, n: t.free.toLocaleString(), l: "Available", dot: "#6e7bff", title: "Inventory available to book" },
5623
+ { key: "blocked", raw: t.blocked, n: t.blocked.toLocaleString(), l: "Blocked", dot: "#8b94ac", title: "Inventory withheld from booking" },
5624
+ { key: "viewing-map", raw: presence?.shoppingSessions ?? null, n: presence ? presence.shoppingSessions.toLocaleString() : "\u2014", l: "Viewing map", title: "Active map sessions right now" },
5625
+ { key: "active-holds", raw: presence?.activeHolds ?? null, n: presence ? presence.activeHolds.toLocaleString() : "\u2014", l: "Active holds", title: "Sessions currently holding inventory" },
5626
+ { key: "booked-pct", raw: t.capacityPct, n: `${t.capacityPct}%`, l: "Booked", title: "Booked inventory as a share of the whole event" },
5627
+ { key: "booked-value", raw: t.bookedValueStatus === "current" ? t.bookedValue : null, n: bookedValue, l: "Booked value", title: "Configured value attached to booked inventory" }
5332
5628
  ];
5333
5629
  let hasChanges = false;
5334
5630
  this.els.kpis.innerHTML = items.map((item) => {
@@ -5377,12 +5673,12 @@ var SeatManager = class {
5377
5673
  renderViewRail() {
5378
5674
  this.els.rail.innerHTML = `
5379
5675
  <p class="slm-eyebrow">Monitor</p>
5380
- <p class="slm-hint">Read-only. Inventory, buyer presence and sales movement update on the same live board.</p>
5676
+ <p class="slm-hint">Read-only. Inventory, map activity and booking movement update on the same live board.</p>
5381
5677
  <div class="slm-health" data-ref="presence"></div>
5382
5678
  <div class="slm-legend" data-ref="legend"></div>
5383
5679
  <div class="slm-sectionhead">
5384
- <div><p class="slm-eyebrow">Section performance</p><p class="slm-note">Exact booked revenue \xB7 net sales velocity</p></div>
5385
- <div class="slm-windows" aria-label="Sales velocity window">
5680
+ <div><p class="slm-eyebrow">Section inventory</p><p class="slm-note">Configured booked value \xB7 booking momentum</p></div>
5681
+ <div class="slm-windows" aria-label="Booking momentum window">
5386
5682
  ${[5, 15, 30, 60].map((window2) => `<button class="slm-window" data-window="${window2}">${window2}m</button>`).join("")}
5387
5683
  </div>
5388
5684
  </div>
@@ -5421,8 +5717,8 @@ var SeatManager = class {
5421
5717
  const sync = this.lastSyncedAt ? relTime(this.lastSyncedAt, Date.now()) : "waiting";
5422
5718
  const presence = this.presenceCounts();
5423
5719
  this.els.presence.innerHTML = `
5424
- <div class="slm-healthitem" title="People on the map right now"><b>${presence ? presence.shoppingSessions.toLocaleString() : "\u2014"}</b><span>Buyers</span></div>
5425
- <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>
5720
+ <div class="slm-healthitem" title="Active map sessions right now"><b>${presence ? presence.shoppingSessions.toLocaleString() : "\u2014"}</b><span>Viewing map</span></div>
5721
+ <div class="slm-healthitem" title="Sessions currently holding inventory"><b>${presence ? presence.activeHolds.toLocaleString() : "\u2014"}</b><span>Active holds</span></div>
5426
5722
  <div class="slm-healthitem"><b>${connected ? "Healthy" : "Reconnecting"}</b><span>Live connection</span></div>
5427
5723
  <div class="slm-healthitem"><b>${sync}</b><span>Last sync</span></div>`;
5428
5724
  }
@@ -5432,10 +5728,10 @@ var SeatManager = class {
5432
5728
  return;
5433
5729
  }
5434
5730
  const velocity = new Map(snapshot.velocity.bySection.map((row) => [row.sectionId, row]));
5435
- const rows = [...snapshot.revenue.bySection].sort((a, b) => {
5731
+ const rows = [...snapshot.bookedValue.bySection].sort((a, b) => {
5436
5732
  const av = velocity.get(a.sectionId)?.netBooked ?? 0;
5437
5733
  const bv = velocity.get(b.sectionId)?.netBooked ?? 0;
5438
- return bv - av || b.bookedRevenue - a.bookedRevenue;
5734
+ return bv - av || b.bookedValue - a.bookedValue;
5439
5735
  });
5440
5736
  this.els.sections.innerHTML = rows.length ? rows.map((row) => {
5441
5737
  const speed = velocity.get(row.sectionId);
@@ -5443,8 +5739,8 @@ var SeatManager = class {
5443
5739
  const netLabel = `${net > 0 ? "+" : ""}${net}`;
5444
5740
  const trend = speed?.trend === "rising" || speed?.trend === "cooling" ? speed.trend : "steady";
5445
5741
  return `<button type="button" class="slm-sectionrow" data-section-focus="${esc2(row.sectionId)}" title="Focus ${esc2(row.sectionLabel)} on the map">
5446
- <span class="slm-sectiontop"><span>${esc2(row.sectionLabel)}</span><span>${fmtMoney(row.bookedRevenue, snapshot.currency)}</span></span>
5447
- <span 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><span class="slm-sectionlocate">Locate</span></span>
5742
+ <span class="slm-sectiontop"><span>${esc2(row.sectionLabel)}</span><span>${fmtMoney(row.bookedValue, snapshot.currency)}</span></span>
5743
+ <span class="slm-sectionmeta"><span>${row.booked.toLocaleString()}/${row.total.toLocaleString()} booked \xB7 ${netLabel} in ${snapshot.velocity.windowMinutes}m</span><span class="slm-trend ${trend}">${trend}</span><span class="slm-sectionlocate">Locate</span></span>
5448
5744
  </button>`;
5449
5745
  }).join("") : '<div class="slm-empty">No section metrics are available for this chart.</div>';
5450
5746
  this.paintTrendWindow();
@@ -5456,7 +5752,7 @@ var SeatManager = class {
5456
5752
  this.renderer?.setSectionHeat(null);
5457
5753
  return;
5458
5754
  }
5459
- const capacity = new Map(snapshot.revenue.bySection.map((row) => [row.sectionId, Math.max(1, row.total)]));
5755
+ const capacity = new Map(snapshot.bookedValue.bySection.map((row) => [row.sectionId, Math.max(1, row.total)]));
5460
5756
  const rates = snapshot.velocity.bySection.map((row) => ({
5461
5757
  sectionId: row.sectionId,
5462
5758
  rate: Math.max(0, row.netBooked) / (capacity.get(row.sectionId) ?? 1) / snapshot.velocity.windowMinutes
@@ -5471,7 +5767,7 @@ var SeatManager = class {
5471
5767
  if (!seat) {
5472
5768
  this.els.rail.innerHTML = `
5473
5769
  <p class="slm-eyebrow">Inspect seats</p>
5474
- <p class="slm-hint">Select a seat to see its availability and sales context. Nothing changes in this view.</p>
5770
+ <p class="slm-hint">Select a seat to see its availability and booking context. Nothing changes in this view.</p>
5475
5771
  <div class="slm-empty">Select a seat on the map.</div>`;
5476
5772
  return;
5477
5773
  }
@@ -5480,7 +5776,7 @@ var SeatManager = class {
5480
5776
  const sectionId = this.sectionByObject.get(seat.rowId) ?? import_core.UNGROUPED_ID;
5481
5777
  const sectionLabel = this.sectionLabelById.get(sectionId) ?? "Other seats";
5482
5778
  const category = this.doc?.categories.find((item) => item.key === seat.categoryKey);
5483
- const sectionMetric = this.controlRoomSnapshot?.revenue.bySection.find((row) => row.sectionId === sectionId);
5779
+ const sectionMetric = this.controlRoomSnapshot?.bookedValue.bySection.find((row) => row.sectionId === sectionId);
5484
5780
  const object = this.doc?.objects.find((item) => item.id === seat.rowId);
5485
5781
  const location = object?.type === "row" ? { label: "Row", value: object.label } : object?.type === "table" ? { label: "Table", value: object.label } : seat.kind === "booth" ? { label: "Type", value: "Booth" } : null;
5486
5782
  const itemKind = seat.kind === "booth" ? "Booth" : "Seat";
@@ -5494,8 +5790,8 @@ var SeatManager = class {
5494
5790
  <div><span>Section</span><b>${esc2(sectionLabel)}</b></div>
5495
5791
  ${location ? `<div><span>${location.label}</span><b>${esc2(location.value)}</b></div>` : ""}
5496
5792
  <div><span>Category</span><b>${esc2(category?.label ?? seat.categoryKey)}</b></div>
5497
- <div><span>Sold in section</span><b>${sectionMetric ? `${sectionMetric.booked} of ${sectionMetric.total}` : "\u2014"}</b></div>
5498
- <div><span>Section revenue</span><b>${sectionMetric && this.controlRoomSnapshot ? fmtMoney(sectionMetric.bookedRevenue, this.controlRoomSnapshot.currency) : "\u2014"}</b></div>
5793
+ <div><span>Booked in section</span><b>${sectionMetric ? `${sectionMetric.booked} of ${sectionMetric.total}` : "\u2014"}</b></div>
5794
+ <div><span>Section booked value</span><b>${sectionMetric && this.controlRoomSnapshot ? fmtMoney(sectionMetric.bookedValue, this.controlRoomSnapshot.currency) : "\u2014"}</b></div>
5499
5795
  </div>
5500
5796
  </div>`;
5501
5797
  }