@devicechain/dashboards 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -845,12 +845,21 @@ function parseSlots(raw) {
845
845
  if (binding) slot.defaultBinding = binding;
846
846
  const scope = parseScope(spec.scope);
847
847
  if (scope) slot.scope = scope;
848
+ const candidates = parseCandidates(spec.candidates);
849
+ if (candidates) slot.candidates = candidates;
848
850
  slots[name] = slot;
849
851
  }
850
852
  if (Object.keys(slots).length === 0) return void 0;
851
853
  validateScopes(slots);
852
854
  return slots;
853
855
  }
856
+ function parseCandidates(raw) {
857
+ if (!isRecord(raw)) return void 0;
858
+ const token = (v) => typeof v === "string" && v ? v : void 0;
859
+ const areaType = token(raw.areaType);
860
+ const assetType = token(raw.assetType);
861
+ return areaType || assetType ? { areaType, assetType } : void 0;
862
+ }
854
863
  function parseScope(raw) {
855
864
  if (!isRecord(raw)) return void 0;
856
865
  const parent = typeof raw.parent === "string" ? raw.parent : "";
@@ -1371,10 +1380,78 @@ function resolveWidgetDatasource(datasource, bindings) {
1371
1380
  return { kind: "unbound", ...series };
1372
1381
  }
1373
1382
 
1374
- // src/context.ts
1383
+ // src/candidates.ts
1375
1384
  function ownGet(map, key) {
1376
1385
  return map && Object.prototype.hasOwnProperty.call(map, key) ? map[key] : void 0;
1377
1386
  }
1387
+ function sameBinding(a, b) {
1388
+ if (!a) return false;
1389
+ if (a.kind === "device" && b.kind === "device") return a.deviceToken === b.deviceToken;
1390
+ if (a.kind === "anchor" && b.kind === "anchor") return a.anchor.targetToken === b.anchor.targetToken;
1391
+ return false;
1392
+ }
1393
+ var listEntities = (lister, kind, typeToken) => lister(kind, typeToken);
1394
+ function slotTypeFilter(def, targetType) {
1395
+ return targetType === "customer" ? void 0 : def.candidates?.[targetType === "area" ? "areaType" : "assetType"];
1396
+ }
1397
+ async function resolveSlotCandidates(definition, slot, bindings, resolver, lister) {
1398
+ const slots = definition.slots ?? {};
1399
+ const def = ownGet(slots, slot);
1400
+ if (!def) return [];
1401
+ const current = ownGet(bindings, slot);
1402
+ const mark = (binding, label) => ({
1403
+ binding,
1404
+ label,
1405
+ selected: sameBinding(current, binding)
1406
+ });
1407
+ if (def.scope) {
1408
+ if (def.scope.strategy === "first") return [];
1409
+ const parentBinding = ownGet(bindings, def.scope.parent);
1410
+ if (!parentBinding || parentBinding.kind !== "anchor") return [];
1411
+ let members;
1412
+ try {
1413
+ members = [...await resolver.devicesForAnchor(parentBinding.anchor)].sort();
1414
+ } catch {
1415
+ return [];
1416
+ }
1417
+ return members.map((token) => mark({ kind: "device", deviceToken: token }, token));
1418
+ }
1419
+ if (def.type === "device") {
1420
+ let rows2;
1421
+ try {
1422
+ rows2 = await lister("device");
1423
+ } catch {
1424
+ return [];
1425
+ }
1426
+ return rows2.map((r) => mark({ kind: "device", deviceToken: r.token }, r.name || r.token));
1427
+ }
1428
+ const template = current?.kind === "anchor" ? current.anchor : def.defaultBinding?.kind === "anchor" ? def.defaultBinding.anchor : void 0;
1429
+ if (!template) return [];
1430
+ let rows;
1431
+ try {
1432
+ rows = await listEntities(lister, template.targetType, slotTypeFilter(def, template.targetType));
1433
+ } catch {
1434
+ return [];
1435
+ }
1436
+ return rows.map(
1437
+ (r) => mark(
1438
+ {
1439
+ kind: "anchor",
1440
+ anchor: {
1441
+ relationship: template.relationship,
1442
+ targetType: template.targetType,
1443
+ targetToken: r.token
1444
+ }
1445
+ },
1446
+ r.name || r.token
1447
+ )
1448
+ );
1449
+ }
1450
+
1451
+ // src/context.ts
1452
+ function ownGet2(map, key) {
1453
+ return map && Object.prototype.hasOwnProperty.call(map, key) ? map[key] : void 0;
1454
+ }
1378
1455
  function setBinding(map, key, value) {
1379
1456
  if (key === "__proto__") return;
1380
1457
  map[key] = value;
@@ -1390,7 +1467,7 @@ function bindingsWithoutScopedSlots(definition, map) {
1390
1467
  if (!slots) return map;
1391
1468
  const out = {};
1392
1469
  for (const name of Object.keys(map)) {
1393
- if (!ownGet(slots, name)?.scope) setBinding(out, name, map[name]);
1470
+ if (!ownGet2(slots, name)?.scope) setBinding(out, name, map[name]);
1394
1471
  }
1395
1472
  return out;
1396
1473
  }
@@ -1402,7 +1479,7 @@ function topoOrder(slots) {
1402
1479
  const done = /* @__PURE__ */ new Set();
1403
1480
  const visit = (name, stack) => {
1404
1481
  if (done.has(name) || stack.has(name)) return;
1405
- const slot = ownGet(slots, name);
1482
+ const slot = ownGet2(slots, name);
1406
1483
  if (!slot) return;
1407
1484
  stack.add(name);
1408
1485
  const parent = slot.scope?.parent;
@@ -1418,16 +1495,16 @@ async function resolveContextBindings(definition, base, overlay, resolver) {
1418
1495
  const slots = definition.slots ?? {};
1419
1496
  const out = {};
1420
1497
  for (const name of topoOrder(slots)) {
1421
- const slot = ownGet(slots, name);
1498
+ const slot = ownGet2(slots, name);
1422
1499
  const scope = slot?.scope;
1423
1500
  if (!scope) {
1424
- const sel = ownGet(overlay, name);
1425
- const b = sel && (!slot || bindingMatchesType(sel, slot.type)) ? sel : ownGet(base, name);
1501
+ const sel = ownGet2(overlay, name);
1502
+ const b = sel && (!slot || bindingMatchesType(sel, slot.type)) ? sel : ownGet2(base, name);
1426
1503
  if (b) setBinding(out, name, b);
1427
1504
  continue;
1428
1505
  }
1429
1506
  if (slot && slot.type !== "device") continue;
1430
- const parentBinding = ownGet(out, scope.parent);
1507
+ const parentBinding = ownGet2(out, scope.parent);
1431
1508
  if (!parentBinding || parentBinding.kind !== "anchor") continue;
1432
1509
  let members;
1433
1510
  try {
@@ -1438,7 +1515,7 @@ async function resolveContextBindings(definition, base, overlay, resolver) {
1438
1515
  if (scope.strategy === "first") {
1439
1516
  if (members.length > 0) setBinding(out, name, { kind: "device", deviceToken: members[0] });
1440
1517
  } else {
1441
- const pick = ownGet(overlay, name) ?? ownGet(base, name);
1518
+ const pick = ownGet2(overlay, name) ?? ownGet2(base, name);
1442
1519
  if (pick && pick.kind === "device" && members.includes(pick.deviceToken)) setBinding(out, name, pick);
1443
1520
  }
1444
1521
  }
@@ -1446,7 +1523,7 @@ async function resolveContextBindings(definition, base, overlay, resolver) {
1446
1523
  if (Object.prototype.hasOwnProperty.call(slots, name) || Object.prototype.hasOwnProperty.call(out, name)) {
1447
1524
  continue;
1448
1525
  }
1449
- const b = ownGet(overlay, name) ?? ownGet(base, name);
1526
+ const b = ownGet2(overlay, name) ?? ownGet2(base, name);
1450
1527
  if (b) setBinding(out, name, b);
1451
1528
  }
1452
1529
  return out;
@@ -1455,6 +1532,36 @@ function bindingMatchesType(binding, type) {
1455
1532
  return binding.kind === type;
1456
1533
  }
1457
1534
 
1535
+ // src/viewer-bindings.ts
1536
+ var ownGet3 = (map, key) => Object.prototype.hasOwnProperty.call(map, key) ? map[key] : void 0;
1537
+ function hasRootAnchorSlots(definition) {
1538
+ return Object.values(definition.slots ?? {}).some((s) => s.type === "anchor" && !s.scope);
1539
+ }
1540
+ async function resolveViewerBindings(definition, base, overlay, resolver, lister) {
1541
+ const slots = definition.slots ?? {};
1542
+ let corrected = { ...overlay };
1543
+ if (lister) {
1544
+ for (const name of Object.keys(slots)) {
1545
+ const slot = slots[name];
1546
+ if (slot.type !== "anchor" || slot.scope) continue;
1547
+ const picked = ownGet3(overlay, name);
1548
+ const binding = picked?.kind === "anchor" ? picked : ownGet3(base, name);
1549
+ if (binding?.kind !== "anchor") continue;
1550
+ const { targetType, targetToken } = binding.anchor;
1551
+ let rows;
1552
+ try {
1553
+ rows = await listEntities(lister, targetType, slotTypeFilter(slot, targetType));
1554
+ } catch {
1555
+ continue;
1556
+ }
1557
+ if (rows.length > 0 && !rows.some((r) => r.token === targetToken)) {
1558
+ corrected = { ...corrected, [name]: { kind: "anchor", anchor: { ...binding.anchor, targetToken: rows[0].token } } };
1559
+ }
1560
+ }
1561
+ }
1562
+ return resolveContextBindings(definition, base, corrected, resolver);
1563
+ }
1564
+
1458
1565
  // src/deep-link.ts
1459
1566
  import { isValidToken } from "@devicechain/client";
1460
1567
  var BIND_PARAM_PREFIX = "bind.";
@@ -1534,7 +1641,7 @@ function bindParamsOf(search) {
1534
1641
  function slotSelector(slot, measurements, location) {
1535
1642
  return location ? { kind: "slot", slot, measurements, location } : { kind: "slot", slot, measurements };
1536
1643
  }
1537
- function sameBinding(a, b) {
1644
+ function sameBinding2(a, b) {
1538
1645
  if (!a || !b) return a === b;
1539
1646
  if (a.kind === "device" && b.kind === "device") return a.deviceToken === b.deviceToken;
1540
1647
  if (a.kind === "anchor" && b.kind === "anchor") {
@@ -1558,7 +1665,7 @@ function bindingOfSelector(ds) {
1558
1665
  return void 0;
1559
1666
  }
1560
1667
  function findOrAddSlot(slots, binding) {
1561
- const existing = Object.keys(slots).find((k) => !slots[k].scope && sameBinding(slots[k].defaultBinding, binding));
1668
+ const existing = Object.keys(slots).find((k) => !slots[k].scope && sameBinding2(slots[k].defaultBinding, binding));
1562
1669
  if (existing) return existing;
1563
1670
  const name = nextSlotName(slots);
1564
1671
  slots[name] = { type: binding.kind, label: bindingLabel(binding), defaultBinding: binding };
@@ -1584,7 +1691,7 @@ function bindWidgetSlot(def, widgetId, binding, measurements, location) {
1584
1691
  const current = def.widgets.find((w) => w.id === widgetId)?.datasource;
1585
1692
  const currentSlot = current?.kind === "slot" ? current.slot : void 0;
1586
1693
  const currentDef = currentSlot ? slots[currentSlot] : void 0;
1587
- const slot = currentSlot && currentDef && (currentDef.scope || sameBinding(currentDef.defaultBinding, binding)) ? currentSlot : findOrAddSlot(slots, binding);
1694
+ const slot = currentSlot && currentDef && (currentDef.scope || sameBinding2(currentDef.defaultBinding, binding)) ? currentSlot : findOrAddSlot(slots, binding);
1588
1695
  const widgets = def.widgets.map(
1589
1696
  (w) => (
1590
1697
  // A placeholder for an unknown widget type binds nothing this build can see, so it is
@@ -1660,6 +1767,17 @@ function setSlotScope(def, slotName, scope) {
1660
1767
  slots[slotName] = { ...current, scope: { parent: scope.parent, strategy: scope.strategy } };
1661
1768
  return { ...def, slots };
1662
1769
  }
1770
+ function setSlotCandidates(def, slotName, candidates) {
1771
+ if (!def.slots || !Object.prototype.hasOwnProperty.call(def.slots, slotName)) return def;
1772
+ const current = def.slots[slotName];
1773
+ if (current.type !== "anchor" || current.scope) return def;
1774
+ const next = {};
1775
+ if (candidates?.areaType) next.areaType = candidates.areaType;
1776
+ if (candidates?.assetType) next.assetType = candidates.assetType;
1777
+ const { candidates: _drop, ...rest } = current;
1778
+ const slot = next.areaType || next.assetType ? { ...rest, candidates: next } : rest;
1779
+ return { ...def, slots: { ...def.slots, [slotName]: slot } };
1780
+ }
1663
1781
  function widgetBinding(def, widget) {
1664
1782
  const ds = widget.datasource;
1665
1783
  if (ds?.kind === "slot") return def.slots?.[ds.slot]?.defaultBinding;
@@ -1682,88 +1800,10 @@ function resolveConcrete(def, widget) {
1682
1800
  return binding.kind === "device" ? { kind: "device", deviceToken: binding.deviceToken, ...base } : { kind: "anchor", anchor: binding.anchor, ...base };
1683
1801
  }
1684
1802
 
1685
- // src/candidates.ts
1686
- function ownGet2(map, key) {
1687
- return map && Object.prototype.hasOwnProperty.call(map, key) ? map[key] : void 0;
1688
- }
1689
- function sameBinding2(a, b) {
1690
- if (!a) return false;
1691
- if (a.kind === "device" && b.kind === "device") return a.deviceToken === b.deviceToken;
1692
- if (a.kind === "anchor" && b.kind === "anchor") return a.anchor.targetToken === b.anchor.targetToken;
1693
- return false;
1694
- }
1695
- async function resolveSlotCandidates(definition, slot, bindings, resolver, lister) {
1696
- const slots = definition.slots ?? {};
1697
- const def = ownGet2(slots, slot);
1698
- if (!def) return [];
1699
- const current = ownGet2(bindings, slot);
1700
- const mark = (binding, label) => ({
1701
- binding,
1702
- label,
1703
- selected: sameBinding2(current, binding)
1704
- });
1705
- if (def.scope) {
1706
- if (def.scope.strategy === "first") return [];
1707
- const parentBinding = ownGet2(bindings, def.scope.parent);
1708
- if (!parentBinding || parentBinding.kind !== "anchor") return [];
1709
- let members;
1710
- try {
1711
- members = [...await resolver.devicesForAnchor(parentBinding.anchor)].sort();
1712
- } catch {
1713
- return [];
1714
- }
1715
- return members.map((token) => mark({ kind: "device", deviceToken: token }, token));
1716
- }
1717
- if (def.type === "device") {
1718
- let rows2;
1719
- try {
1720
- rows2 = await lister("device");
1721
- } catch {
1722
- return [];
1723
- }
1724
- return rows2.map((r) => mark({ kind: "device", deviceToken: r.token }, r.name || r.token));
1725
- }
1726
- const template = current?.kind === "anchor" ? current.anchor : def.defaultBinding?.kind === "anchor" ? def.defaultBinding.anchor : void 0;
1727
- if (!template) return [];
1728
- let rows;
1729
- try {
1730
- rows = await lister(template.targetType);
1731
- } catch {
1732
- return [];
1733
- }
1734
- return rows.map(
1735
- (r) => mark(
1736
- {
1737
- kind: "anchor",
1738
- anchor: {
1739
- relationship: template.relationship,
1740
- targetType: template.targetType,
1741
- targetToken: r.token
1742
- }
1743
- },
1744
- r.name || r.token
1745
- )
1746
- );
1747
- }
1748
-
1749
1803
  // src/entity-lister.ts
1750
1804
  import { gql } from "@devicechain/client";
1751
1805
 
1752
1806
  // src/queries.ts
1753
- var DEVICES_FOR_ANCHOR = `
1754
- query DevicesForAnchor($criteria: EntityRelationshipSearchCriteria!) {
1755
- entityRelationships(criteria: $criteria) {
1756
- results {
1757
- source {
1758
- token
1759
- }
1760
- }
1761
- pagination {
1762
- totalRecords
1763
- }
1764
- }
1765
- }
1766
- `;
1767
1807
  var CUSTOMER_DEVICES = `
1768
1808
  query DashboardCustomerDevices($criteria: CustomerDeviceSearchCriteria!) {
1769
1809
  customerDevices(criteria: $criteria) {
@@ -1780,6 +1820,7 @@ var CUSTOMER_DEVICES = `
1780
1820
  }
1781
1821
  }
1782
1822
  `;
1823
+ var ANCHOR_DEVICES = "query DashboardAnchorDevices($criteria: AnchorDeviceSearchCriteria!){anchorDevices(criteria:$criteria){results{token name deviceType{token}}pagination{totalRecords}}}";
1783
1824
  var DEVICES_BY_TOKEN = `
1784
1825
  query DashboardDevicesByToken($tokens: [String!]!) {
1785
1826
  devicesByToken(tokens: $tokens) {
@@ -1871,28 +1912,29 @@ var LIST_PAGE_SIZE = 500;
1871
1912
  function createEntityLister() {
1872
1913
  const cache = /* @__PURE__ */ new Map();
1873
1914
  const criteria = { pageNumber: 1, pageSize: LIST_PAGE_SIZE };
1874
- return (kind) => {
1875
- let pending = cache.get(kind);
1915
+ return (kind, typeToken) => {
1916
+ const key = `${kind}|${typeToken}`;
1917
+ let pending = cache.get(key);
1876
1918
  if (!pending) {
1877
- pending = fetchKind(kind, criteria).catch((err) => {
1878
- cache.delete(kind);
1919
+ pending = fetchKind(kind, criteria, typeToken).catch((err) => {
1920
+ cache.delete(key);
1879
1921
  throw err;
1880
1922
  });
1881
- cache.set(kind, pending);
1923
+ cache.set(key, pending);
1882
1924
  }
1883
1925
  return pending;
1884
1926
  };
1885
1927
  }
1886
- function fetchKind(kind, criteria) {
1928
+ function fetchKind(kind, criteria, typeToken) {
1887
1929
  switch (kind) {
1888
1930
  case "device":
1889
1931
  return gql("device-management", LIST_DEVICES, { criteria }).then((r) => r.devices.results);
1890
1932
  case "customer":
1891
1933
  return gql("device-management", LIST_CUSTOMERS, { criteria }).then((r) => r.customers.results);
1892
1934
  case "area":
1893
- return gql("device-management", LIST_AREAS, { criteria }).then((r) => r.areas.results);
1935
+ return gql("device-management", LIST_AREAS, { criteria: { ...criteria, areaTypeToken: typeToken } }).then((r) => r.areas.results);
1894
1936
  case "asset":
1895
- return gql("device-management", LIST_ASSETS, { criteria }).then((r) => r.assets.results);
1937
+ return gql("device-management", LIST_ASSETS, { criteria: { ...criteria, assetTypeToken: typeToken } }).then((r) => r.assets.results);
1896
1938
  default:
1897
1939
  return Promise.resolve([]);
1898
1940
  }
@@ -2090,6 +2132,7 @@ var ALARM_STREAM = `
2090
2132
  $state: String
2091
2133
  $severity: String
2092
2134
  $alarmKey: String
2135
+ $anchor: AlarmAnchor
2093
2136
  ) {
2094
2137
  alarmStream(
2095
2138
  originatorType: $originatorType
@@ -2097,6 +2140,7 @@ var ALARM_STREAM = `
2097
2140
  state: $state
2098
2141
  severity: $severity
2099
2142
  alarmKey: $alarmKey
2143
+ anchor: $anchor
2100
2144
  ) {
2101
2145
  alarmToken
2102
2146
  eventType
@@ -2243,6 +2287,7 @@ var COMMAND_POLL_MS = 4e3;
2243
2287
  var LOCATION_POLL_MS = 15e3;
2244
2288
  var PRESENCE_POLL_MS = 15e3;
2245
2289
  var PRESENCE_CHUNK = 1e3;
2290
+ var STAMPED = /* @__PURE__ */ new Set(["customer", "area", "asset"]);
2246
2291
  var DashboardHub = class {
2247
2292
  constructor(config) {
2248
2293
  // Per alarm-subscription reconcile triggers — invoked after an ack/clear so the
@@ -2469,9 +2514,9 @@ var DashboardHub = class {
2469
2514
  this.alarmDisposers.delete(dispose);
2470
2515
  };
2471
2516
  this.alarmDisposers.add(dispose);
2472
- const reconcile = (tokens, tenantWide) => {
2517
+ const reconcile = (scope) => {
2473
2518
  const gen = ++generation;
2474
- this.queryAlarms(subscription, tokens, tenantWide).then((snapshot) => {
2519
+ this.queryAlarms(subscription, scope).then((snapshot) => {
2475
2520
  if (!disposed && gen === generation) sink.next(snapshot);
2476
2521
  }).catch((err) => {
2477
2522
  if (!disposed && gen === generation) sink.error?.(err);
@@ -2479,32 +2524,32 @@ var DashboardHub = class {
2479
2524
  };
2480
2525
  this.resolveAlarmScope(subscription.datasource).then((scope) => {
2481
2526
  if (disposed) return;
2482
- if (!scope.tenantWide && scope.tokens.length === 0) {
2527
+ if (!scope.tenantWide && !scope.anchor && scope.tokens.length === 0) {
2483
2528
  sink.next({ alarms: [], total: 0 });
2484
2529
  return;
2485
2530
  }
2486
2531
  const trigger = () => {
2487
2532
  if (debounce) clearTimeout(debounce);
2488
- debounce = setTimeout(() => reconcile(scope.tokens, scope.tenantWide), ALARM_RECONCILE_DEBOUNCE_MS);
2533
+ debounce = setTimeout(() => reconcile(scope), ALARM_RECONCILE_DEBOUNCE_MS);
2489
2534
  };
2490
2535
  const handle = this.connection.register();
2491
2536
  const adapter = {
2492
2537
  next: () => trigger(),
2493
2538
  connected: (wasRetry) => {
2494
2539
  handle.connected();
2495
- if (wasRetry) reconcile(scope.tokens, scope.tenantWide);
2540
+ if (wasRetry) reconcile(scope);
2496
2541
  },
2497
2542
  closed: () => handle.closed()
2498
2543
  };
2499
- const closeSocket = subscribe(DEVICE_AREA, ALARM_STREAM, {}, adapter);
2544
+ const closeSocket = subscribe(DEVICE_AREA, ALARM_STREAM, { anchor: scope.anchor }, adapter);
2500
2545
  unsubscribe = () => {
2501
2546
  handle.release();
2502
2547
  closeSocket();
2503
2548
  };
2504
- poll = setInterval(() => reconcile(scope.tokens, scope.tenantWide), ALARM_POLL_MS);
2505
- reconciler = () => reconcile(scope.tokens, scope.tenantWide);
2549
+ poll = setInterval(() => reconcile(scope), ALARM_POLL_MS);
2550
+ reconciler = () => reconcile(scope);
2506
2551
  this.alarmReconcilers.add(reconciler);
2507
- reconcile(scope.tokens, scope.tenantWide);
2552
+ reconcile(scope);
2508
2553
  }).catch((err) => {
2509
2554
  if (!disposed) sink.error?.(err);
2510
2555
  });
@@ -2518,6 +2563,9 @@ var DashboardHub = class {
2518
2563
  // it resolves to zero devices like any other empty scope.
2519
2564
  async resolveAlarmScope(datasource) {
2520
2565
  if (!datasource) return { tenantWide: true, tokens: [] };
2566
+ if (datasource.kind === "anchor" && datasource.anchor.relationship === "assigned" && STAMPED.has(datasource.anchor.targetType)) {
2567
+ return { tenantWide: false, tokens: [], anchor: { type: datasource.anchor.targetType, token: datasource.anchor.targetToken } };
2568
+ }
2521
2569
  const groups = await this.resolveDevices(datasource);
2522
2570
  return { tenantWide: false, tokens: groups.map((g) => g.deviceToken) };
2523
2571
  }
@@ -2525,13 +2573,13 @@ var DashboardHub = class {
2525
2573
  // runs one query per originator device (the alarms query filters a single originator)
2526
2574
  // and merges — deduped by token, newest first, capped to pageSize; total is the sum of
2527
2575
  // per-originator match counts.
2528
- async queryAlarms(sub, tokens, tenantWide) {
2576
+ async queryAlarms(sub, { tenantWide, tokens, anchor }) {
2529
2577
  const filters = {
2530
2578
  state: sub.state ?? null,
2531
2579
  severity: sub.severity ?? null,
2532
2580
  acknowledged: sub.acknowledged ?? null
2533
2581
  };
2534
- const originators = tenantWide ? [{ originatorType: null, originator: null }] : tokens.map((token) => ({ originatorType: "device", originator: token }));
2582
+ const originators = tenantWide || anchor ? [{ originatorType: null, originator: null, anchor }] : tokens.map((token) => ({ originatorType: "device", originator: token }));
2535
2583
  if (sub.countBySeverity) {
2536
2584
  const pages2 = await Promise.all(
2537
2585
  originators.map(
@@ -2889,9 +2937,9 @@ var DashboardHub = class {
2889
2937
  }
2890
2938
  }
2891
2939
  // isDatasourceSupported reports whether the viewer is offered the binding at all: false only
2892
- // when resolving an anchor is refused as SCOPE_UNSUPPORTED (an area or asset anchor, for a
2893
- // customer user). The resolution is the same memoized one the widget's channel makes, so a
2894
- // supported anchor costs no second read. Every other answer, including a failure, is true.
2940
+ // when resolving an anchor is refused as SCOPE_UNSUPPORTED (an anchor the platform still
2941
+ // declines for a customer user; areas and assets are answered through anchorDevices). The resolution is the same
2942
+ // memoized one the widget's channel makes, so a supported anchor costs no second read. Every other answer, including a failure, is true.
2895
2943
  async isDatasourceSupported(datasource) {
2896
2944
  if (datasource?.kind !== "anchor") return true;
2897
2945
  try {
@@ -3576,7 +3624,7 @@ function parseBoardRecording(input) {
3576
3624
  "alarms",
3577
3625
  "locations"
3578
3626
  ],
3579
- ["excerpt", "declaredMetrics"]
3627
+ ["excerpt", "declaredMetrics", "anchors"]
3580
3628
  );
3581
3629
  if (root.kind !== RECORDING_KIND) {
3582
3630
  throw new RecordingFormatError("$.kind", `expected ${JSON.stringify(RECORDING_KIND)}`);
@@ -3628,6 +3676,33 @@ function parseBoardRecording(input) {
3628
3676
  declaredMetrics[kind] = names;
3629
3677
  }
3630
3678
  }
3679
+ let anchors;
3680
+ if (root.anchors !== void 0) {
3681
+ if (version === 1) throw new RecordingFormatError("$.anchors", "version 1 carries no anchors");
3682
+ const listed = column(root.anchors, "$.anchors", (x, p) => {
3683
+ const o = strict(x, p, ["relationship", "targetType", "targetToken", "devices"]);
3684
+ const members = column(o.devices, `${p}.devices`, str);
3685
+ if (members.length === 0) throw new RecordingFormatError(`${p}.devices`, "has no member; leave the anchor out instead");
3686
+ if (new Set(members).size !== members.length) throw new RecordingFormatError(`${p}.devices`, "duplicate member");
3687
+ for (const m2 of members) {
3688
+ if (!tokens.has(m2)) throw new RecordingFormatError(`${p}.devices`, `'${m2}' is not a device of the run`);
3689
+ }
3690
+ return {
3691
+ relationship: str(o.relationship, `${p}.relationship`),
3692
+ targetType: oneOf2(o.targetType, `${p}.targetType`, ["customer", "area", "asset"]),
3693
+ targetToken: str(o.targetToken, `${p}.targetToken`),
3694
+ devices: members
3695
+ };
3696
+ });
3697
+ if (listed.length === 0) throw new RecordingFormatError("$.anchors", "declares nothing; leave it out instead");
3698
+ const seen = /* @__PURE__ */ new Set();
3699
+ listed.forEach((a, i) => {
3700
+ const key = `${a.relationship}\0${a.targetType}\0${a.targetToken}`;
3701
+ if (seen.has(key)) throw new RecordingFormatError(`$.anchors[${i}]`, "the same anchor is declared twice");
3702
+ seen.add(key);
3703
+ });
3704
+ anchors = listed;
3705
+ }
3631
3706
  const ch = strict(root.channels, "$.channels", ["measurements", "alarms", "locations", "commands"]);
3632
3707
  if (version === 1 && ch.locations !== false) {
3633
3708
  throw new RecordingFormatError("$.channels.locations", "version 1 records no positions; must be false");
@@ -3832,6 +3907,7 @@ function parseBoardRecording(input) {
3832
3907
  };
3833
3908
  if (excerpt) rec.excerpt = excerpt;
3834
3909
  if (declaredMetrics) rec.declaredMetrics = declaredMetrics;
3910
+ if (anchors) rec.anchors = anchors;
3835
3911
  return rec;
3836
3912
  }
3837
3913
 
@@ -3999,7 +4075,8 @@ function retimeShiftMs(rec, cursorMs, nowMs = Date.now()) {
3999
4075
  function sameAnchor(a, b) {
4000
4076
  return a.relationship === b.relationship && a.targetType === b.targetType && a.targetToken === b.targetToken;
4001
4077
  }
4002
- function declaredSite(options) {
4078
+ function declaredSite(options, rec) {
4079
+ if (rec?.anchors) return null;
4003
4080
  const anchors = options.siteAnchors;
4004
4081
  if (anchors === void 0) return null;
4005
4082
  if (anchors.length !== 1) {
@@ -4012,7 +4089,17 @@ function declaredSite(options) {
4012
4089
  }
4013
4090
  return anchors[0];
4014
4091
  }
4092
+ function recordedMembers(rec, anchor) {
4093
+ if (!rec.anchors) return null;
4094
+ const hit = rec.anchors.find((a) => sameAnchor(a, anchor));
4095
+ if (!hit) {
4096
+ throw new Error(`anchor ${anchor.targetType} '${anchor.targetToken}' (${anchor.relationship}) is not in this recording`);
4097
+ }
4098
+ return hit.devices;
4099
+ }
4015
4100
  function anchorMembers(rec, site, anchor) {
4101
+ const own2 = recordedMembers(rec, anchor);
4102
+ if (own2) return own2;
4016
4103
  if (!site || !sameAnchor(site, anchor)) {
4017
4104
  throw new Error(
4018
4105
  `anchor ${anchor.targetType} '${anchor.targetToken}' (${anchor.relationship}) is not in this recording`
@@ -4022,16 +4109,17 @@ function anchorMembers(rec, site, anchor) {
4022
4109
  }
4023
4110
  function createRecordedResolver(rec, options = {}) {
4024
4111
  const devices = new Set(rec.devices.map((d) => d.token));
4025
- const site = declaredSite(options);
4112
+ const site = declaredSite(options, rec);
4026
4113
  return {
4027
4114
  devicesForAnchor: async (anchor) => anchorMembers(rec, site, anchor),
4028
4115
  deviceExists: async (token) => devices.has(token)
4029
4116
  };
4030
4117
  }
4031
4118
  function createRecordedLister(rec, options = {}) {
4032
- const site = declaredSite(options);
4119
+ const site = declaredSite(options, rec);
4033
4120
  return async (kind) => {
4034
4121
  if (kind === "device") return rec.devices.map((d) => ({ token: d.token, name: null }));
4122
+ if (rec.anchors) return rec.anchors.filter((a) => a.targetType === kind).map((a) => ({ token: a.targetToken, name: null }));
4035
4123
  return site && site.targetType === kind ? [{ token: site.targetToken, name: null }] : [];
4036
4124
  };
4037
4125
  }
@@ -4073,7 +4161,7 @@ var RecordedDataSource = class {
4073
4161
  this.subs = /* @__PURE__ */ new Set();
4074
4162
  this.stale = false;
4075
4163
  this.rec = rec;
4076
- this.site = declaredSite(options);
4164
+ this.site = declaredSite(options, rec);
4077
4165
  this.index = indexOf(rec);
4078
4166
  this.cursor = clock.timeMs;
4079
4167
  const shift = options.timeShiftMs ?? 0;
@@ -5169,7 +5257,7 @@ function pasteWidgets(def, clip, at) {
5169
5257
  if (known) return known;
5170
5258
  const source = own(clip.slots, from);
5171
5259
  const target = own(slots, from);
5172
- const keep = sameTenant && target !== void 0 && source !== void 0 && target.type === source.type && (sameBoard || sameBinding(target.defaultBinding, source.defaultBinding));
5260
+ const keep = sameTenant && target !== void 0 && source !== void 0 && target.type === source.type && (sameBoard || sameBinding2(target.defaultBinding, source.defaultBinding));
5173
5261
  let result;
5174
5262
  if (keep) result = { slot: from };
5175
5263
  else {
@@ -5231,7 +5319,7 @@ function duplicateWidget(def, id) {
5231
5319
  }
5232
5320
 
5233
5321
  // src/resolver.ts
5234
- import { gql as gql3 } from "@devicechain/client";
5322
+ import { gql as gql3, isScopeUnsupportedError as isScopeUnsupportedError2 } from "@devicechain/client";
5235
5323
  var ANCHOR_PAGE_SIZE = 500;
5236
5324
  var DECLARED_BATCH_SIZE = 100;
5237
5325
  var trueSize = (reported, page) => Math.max(reported ?? page, page);
@@ -5254,18 +5342,29 @@ function readAnchorPage(anchor) {
5254
5342
  };
5255
5343
  });
5256
5344
  }
5257
- return gql3("device-management", DEVICES_FOR_ANCHOR, {
5345
+ return readAreaOrAssetPage(anchor);
5346
+ }
5347
+ function readAreaOrAssetPage(anchor) {
5348
+ const read = (relationshipType) => gql3("device-management", ANCHOR_DEVICES, {
5258
5349
  criteria: {
5259
5350
  pageNumber: 1,
5260
5351
  pageSize: ANCHOR_PAGE_SIZE,
5261
- sourceType: "device",
5262
5352
  targetType: anchor.targetType,
5263
5353
  target: anchor.targetToken,
5264
- relationshipType: anchor.relationship
5354
+ relationshipType
5265
5355
  }
5356
+ });
5357
+ return read(anchor.relationship).catch((err) => {
5358
+ if (!isScopeUnsupportedError2(err)) throw err;
5359
+ return read();
5266
5360
  }).then((r) => {
5267
- const tokens = r.entityRelationships.results.map((rel) => rel.source.token);
5268
- return { tokens, total: trueSize(r.entityRelationships.pagination?.totalRecords, tokens.length) };
5361
+ const rows = r.anchorDevices.results;
5362
+ return {
5363
+ tokens: rows.map((d) => d.token),
5364
+ total: trueSize(r.anchorDevices.pagination?.totalRecords, rows.length),
5365
+ names: new Map(rows.map((d) => [d.token, d.name])),
5366
+ types: new Map(rows.map((d) => [d.token, d.deviceType?.token ?? null]))
5367
+ };
5269
5368
  });
5270
5369
  }
5271
5370
  function createDeviceResolver() {
@@ -5293,29 +5392,16 @@ function createDeviceResolver() {
5293
5392
  devicesForAnchor(anchor) {
5294
5393
  return anchorPage(anchor).then((page) => page.tokens);
5295
5394
  },
5296
- // The members of an anchor with their names and the anchor's true size. One page (shared with
5297
- // devicesForAnchor) plus, when that page carried no names, ONE batched name read, whatever the
5298
- // size of the fleet. A failed name read is not a failed roster: the tokens still stream, and
5299
- // the widget labels them by token.
5395
+ // The members of an anchor with their names and the anchor's true size: ONE page, shared with
5396
+ // devicesForAnchor, which carries every member's name and device type.
5300
5397
  anchorMembers(anchor) {
5301
5398
  const key = anchorKey(anchor);
5302
5399
  let pending = memberCache.get(key);
5303
5400
  if (!pending) {
5304
- pending = anchorPage(anchor).then(async ({ tokens, total, names: listed, types: listedTypes }) => {
5305
- const names = listed ?? /* @__PURE__ */ new Map();
5306
- const types = listedTypes ?? /* @__PURE__ */ new Map();
5307
- if (!listed && tokens.length > 0) {
5308
- try {
5309
- const r = await gql3("device-management", DEVICE_NAMES, { tokens });
5310
- for (const d of r.devicesByToken) {
5311
- names.set(d.token, d.name);
5312
- types.set(d.token, d.deviceType?.token ?? null);
5313
- }
5314
- } catch {
5315
- }
5316
- }
5317
- return { members: tokens.map((token) => ({ token, name: names.get(token) ?? null, deviceType: types.get(token) ?? null })), total };
5318
- }).catch((err) => {
5401
+ pending = anchorPage(anchor).then(({ tokens, total, names, types }) => ({
5402
+ members: tokens.map((token) => ({ token, name: names.get(token) ?? null, deviceType: types.get(token) ?? null })),
5403
+ total
5404
+ })).catch((err) => {
5319
5405
  memberCache.delete(key);
5320
5406
  throw err;
5321
5407
  });
@@ -6294,6 +6380,7 @@ export {
6294
6380
  gridPlacementToPx,
6295
6381
  gridRect,
6296
6382
  gutterPx,
6383
+ hasRootAnchorSlots,
6297
6384
  hasScopedSlots,
6298
6385
  historyWindowFor,
6299
6386
  initialTimeRange,
@@ -6353,10 +6440,11 @@ export {
6353
6440
  resolveFrame,
6354
6441
  resolvePlacement,
6355
6442
  resolveSlotCandidates,
6443
+ resolveViewerBindings,
6356
6444
  resolveWidgetDatasource,
6357
6445
  resumeRange,
6358
6446
  retimeShiftMs,
6359
- sameBinding,
6447
+ sameBinding2 as sameBinding,
6360
6448
  sameLabel,
6361
6449
  sealHistory,
6362
6450
  seedBucketCount,
@@ -6369,6 +6457,7 @@ export {
6369
6457
  setCanvasSizing,
6370
6458
  setRelativeAnchor,
6371
6459
  setRelativeOffset,
6460
+ setSlotCandidates,
6372
6461
  setSlotScope,
6373
6462
  setTitle,
6374
6463
  setWidgetPlacement,