@adventurelabs/scout-core 1.4.83 → 1.4.85

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.
@@ -0,0 +1,49 @@
1
+ "use client";
2
+ import { useEffect, useState } from "react";
3
+ import { formatDeviceDisplayName } from "../helpers/ui";
4
+ const labelByDeviceId = new Map();
5
+ function rememberDevice(device) {
6
+ if (device.id == null)
7
+ return;
8
+ labelByDeviceId.set(device.id, formatDeviceDisplayName(device));
9
+ }
10
+ export function primeDeviceDisplayNameCache(devices) {
11
+ for (const device of devices)
12
+ rememberDevice(device);
13
+ }
14
+ export function useDeviceDisplayName(deviceId, options = {}) {
15
+ const [, setTick] = useState(0);
16
+ const bump = () => setTick(t => t + 1);
17
+ useEffect(() => {
18
+ if (!options.herdModules?.length)
19
+ return;
20
+ for (const hm of options.herdModules) {
21
+ for (const device of hm.devices ?? [])
22
+ rememberDevice(device);
23
+ }
24
+ bump();
25
+ }, [options.herdModules]);
26
+ useEffect(() => {
27
+ if (deviceId == null || labelByDeviceId.has(deviceId) || !options.supabase) {
28
+ return;
29
+ }
30
+ let cancelled = false;
31
+ options.supabase
32
+ .from("devices")
33
+ .select("id, name, domain_name, description")
34
+ .eq("id", deviceId)
35
+ .maybeSingle()
36
+ .then(({ data }) => {
37
+ if (cancelled || !data)
38
+ return;
39
+ rememberDevice(data);
40
+ bump();
41
+ });
42
+ return () => {
43
+ cancelled = true;
44
+ };
45
+ }, [deviceId, options.supabase]);
46
+ if (deviceId == null)
47
+ return null;
48
+ return labelByDeviceId.get(deviceId) ?? `Device ${deviceId}`;
49
+ }
@@ -136,6 +136,7 @@ export function useScoutRefresh(options = {}) {
136
136
  dispatch(setStatus(EnumScoutStateStatus.LOADING));
137
137
  dispatch(setHerdModulesLoadingState(EnumHerdModulesLoadingState.LOADING));
138
138
  let cachedHerdModules = null;
139
+ let cachedContentHash;
139
140
  let cacheLoadDuration = 0;
140
141
  // Step 1: Load from cache first if enabled
141
142
  if (cacheFirst) {
@@ -147,6 +148,7 @@ export function useScoutRefresh(options = {}) {
147
148
  dispatch(setCacheLoadDuration(cacheLoadDuration));
148
149
  if (cacheResult.data && cacheResult.data.length > 0) {
149
150
  cachedHerdModules = cacheResult.data;
151
+ cachedContentHash = cacheResult.metadata?.etag;
150
152
  console.log(`[useScoutRefresh] Loaded ${cachedHerdModules.length} herd modules from cache in ${cacheLoadDuration}ms (age: ${Math.round(cacheResult.age / 1000)}s, stale: ${cacheResult.isStale})`);
151
153
  // Set data source to CACHE initially
152
154
  dispatch(setDataSource(EnumDataSource.CACHE));
@@ -158,7 +160,10 @@ export function useScoutRefresh(options = {}) {
158
160
  }));
159
161
  // Update the store with cached data
160
162
  console.log(`[useScoutRefresh] Updating store with cached herd modules`);
161
- dispatchInTransition(dispatch, setHerdModules(cachedHerdModules));
163
+ dispatchInTransition(dispatch, setHerdModules({
164
+ modules: cachedHerdModules,
165
+ contentHash: cachedContentHash,
166
+ }));
162
167
  dispatch(setHerdModulesLoadingState(EnumHerdModulesLoadingState.SUCCESSFULLY_LOADED));
163
168
  // If cache is fresh, we still background fetch but don't wait (only when online)
164
169
  if (!cacheResult.isStale) {
@@ -193,21 +198,25 @@ export function useScoutRefresh(options = {}) {
193
198
  EnumWebResponse.SUCCESS &&
194
199
  Array.isArray(backgroundHerdModulesResult.data) &&
195
200
  backgroundUserResult) {
201
+ const backgroundContentHash = backgroundHerdModulesResult.content_hash;
196
202
  // Update cache with fresh data
197
203
  try {
198
- await scoutCache.setHerdModules(backgroundHerdModulesResult.data, cacheTtlMs);
204
+ await scoutCache.setHerdModules(backgroundHerdModulesResult.data, cacheTtlMs, backgroundContentHash);
199
205
  }
200
206
  catch (cacheError) {
201
207
  console.warn("[useScoutRefresh] Background cache save failed:", cacheError);
202
208
  await handleIndexedDbError(cacheError, "background cache save", async () => {
203
209
  if (backgroundHerdModulesResult.data) {
204
- await scoutCache.setHerdModules(backgroundHerdModulesResult.data, cacheTtlMs);
210
+ await scoutCache.setHerdModules(backgroundHerdModulesResult.data, cacheTtlMs, backgroundContentHash);
205
211
  }
206
212
  });
207
213
  }
208
214
  // Update store with fresh data from background request
209
215
  console.log(`[useScoutRefresh] Updating store with background herd modules`);
210
- dispatchInTransition(dispatch, setHerdModules(backgroundHerdModulesResult.data));
216
+ dispatchInTransition(dispatch, setHerdModules({
217
+ modules: backgroundHerdModulesResult.data,
218
+ contentHash: backgroundContentHash,
219
+ }));
211
220
  // Update data source to DATABASE
212
221
  dispatch(setDataSource(EnumDataSource.DATABASE));
213
222
  dispatch(setDataSourceInfo({
@@ -248,7 +257,10 @@ export function useScoutRefresh(options = {}) {
248
257
  source: EnumDataSource.CACHE,
249
258
  timestamp: Date.now(),
250
259
  }));
251
- dispatchInTransition(dispatch, setHerdModules(cachedHerdModules));
260
+ dispatchInTransition(dispatch, setHerdModules({
261
+ modules: cachedHerdModules,
262
+ contentHash: cachedContentHash,
263
+ }));
252
264
  dispatch(setHerdModulesLoadingState(EnumHerdModulesLoadingState.SUCCESSFULLY_LOADED));
253
265
  }
254
266
  else {
@@ -310,6 +322,7 @@ export function useScoutRefresh(options = {}) {
310
322
  console.log(`[useScoutRefresh] Data validation took: ${validationDuration}ms`);
311
323
  // Use the validated data
312
324
  const compatible_new_herd_modules = herdModulesResponse.data;
325
+ const freshContentHash = herdModulesResponse.content_hash;
313
326
  // Set data source to DATABASE
314
327
  dispatch(setDataSource(EnumDataSource.DATABASE));
315
328
  dispatch(setDataSourceInfo({
@@ -319,7 +332,7 @@ export function useScoutRefresh(options = {}) {
319
332
  // Step 3: Update cache with fresh data
320
333
  const cacheSaveStartTime = Date.now();
321
334
  try {
322
- await scoutCache.setHerdModules(compatible_new_herd_modules, cacheTtlMs);
335
+ await scoutCache.setHerdModules(compatible_new_herd_modules, cacheTtlMs, freshContentHash);
323
336
  const cacheSaveDuration = Date.now() - cacheSaveStartTime;
324
337
  timingRefs.current.cacheSaveDuration = cacheSaveDuration;
325
338
  console.log(`[useScoutRefresh] Cache updated in ${cacheSaveDuration}ms with TTL: ${Math.round(cacheTtlMs / 1000)}s`);
@@ -327,14 +340,17 @@ export function useScoutRefresh(options = {}) {
327
340
  catch (cacheError) {
328
341
  console.warn("[useScoutRefresh] Cache save failed:", cacheError);
329
342
  await handleIndexedDbError(cacheError, "cache save", async () => {
330
- await scoutCache.setHerdModules(compatible_new_herd_modules, cacheTtlMs);
343
+ await scoutCache.setHerdModules(compatible_new_herd_modules, cacheTtlMs, freshContentHash);
331
344
  });
332
345
  }
333
346
  // Step 4: Update store with fresh data (reducer skips no-op updates)
334
347
  const dataProcessingStartTime = Date.now();
335
348
  // Update store with new data
336
349
  console.log(`[useScoutRefresh] Updating store with fresh herd modules`);
337
- dispatchInTransition(dispatch, setHerdModules(compatible_new_herd_modules));
350
+ dispatchInTransition(dispatch, setHerdModules({
351
+ modules: compatible_new_herd_modules,
352
+ contentHash: freshContentHash,
353
+ }));
338
354
  dispatch(setHerdModulesLoadingState(EnumHerdModulesLoadingState.SUCCESSFULLY_LOADED));
339
355
  const dataProcessingDuration = Date.now() - dataProcessingStartTime;
340
356
  timingRefs.current.dataProcessingDuration = dataProcessingDuration;
package/dist/index.d.ts CHANGED
@@ -20,7 +20,8 @@ export * from "./helpers/bounding_boxes";
20
20
  export * from "./helpers/chat";
21
21
  export * from "./helpers/certificates";
22
22
  export * from "./helpers/connectivity";
23
- export * from "./helpers/credentials";
23
+ export * from "./helpers/user_credentials";
24
+ export * from "./helpers/herd_credentials";
24
25
  export * from "./helpers/db";
25
26
  export * from "./helpers/devices";
26
27
  export * from "./helpers/email";
@@ -82,6 +83,6 @@ export * from "./store/api";
82
83
  export * from "./supabase/middleware";
83
84
  export * from "./supabase/server";
84
85
  export * from "./api_keys/actions";
85
- export type { HerdModule, IHerdModule } from "./types/herd_module";
86
- export type { IDevice, IEvent, IUser, IHerd, IHerdPrettyLocation, IEventWithTags, IZoneWithActions, ICredential, CredentialInsert, CredentialUpdate, IPointsForUser, PointsForUserInsert, PointsForUserUpdate, ICertificate, CertificateInsert, CertificateUpdate, IUserAndRole, IUserProfileRow, IHerdInvitation, IHerdInvitationWithHerdSlug, IHerdAllowedDomain, IUserRolePerHerd, IAbility, IHerdRole, IHerdRoleWithAbilities, HerdRoleSystemName, HerdInvitationStatus, IApiKeyScout, ILayer, IHeartbeat, IProvider, IConnectivity, ISession, ISessionWithCoordinates, IConnectivityWithCoordinates, IObservation, ObservationInsert, ObservationUpdate, IAnalysisJob, IAnalysisTask, AnalysisWorkStatus, } from "./types/db";
86
+ export type { IHerdModule } from "./types/herd_module";
87
+ export type { IDevice, IEvent, IUser, IHerd, IHerdPrettyLocation, IEventWithTags, IZoneWithActions, IPointsForUser, PointsForUserInsert, PointsForUserUpdate, ICertificate, CertificateInsert, CertificateUpdate, IUserAndRole, IUserProfileRow, IHerdInvitation, IHerdInvitationWithHerdSlug, IHerdAllowedDomain, IUserRolePerHerd, IAbility, IHerdRole, IHerdRoleWithAbilities, HerdRoleSystemName, HerdInvitationStatus, IApiKeyScout, ILayer, IHeartbeat, IProvider, IConnectivity, ISession, ISessionWithCoordinates, IConnectivityWithCoordinates, IObservation, ObservationInsert, ObservationUpdate, IAnalysisJob, IAnalysisTask, AnalysisWorkStatus, } from "./types/db";
87
88
  export { EnumSessionsVisibility } from "./types/events";
package/dist/index.js CHANGED
@@ -23,7 +23,8 @@ export * from "./helpers/bounding_boxes";
23
23
  export * from "./helpers/chat";
24
24
  export * from "./helpers/certificates";
25
25
  export * from "./helpers/connectivity";
26
- export * from "./helpers/credentials";
26
+ export * from "./helpers/user_credentials";
27
+ export * from "./helpers/herd_credentials";
27
28
  export * from "./helpers/db";
28
29
  export * from "./helpers/devices";
29
30
  export * from "./helpers/email";
@@ -17,6 +17,7 @@ export interface LoadingPerformance {
17
17
  }
18
18
  export interface ScoutState {
19
19
  herd_modules: IHerdModule[];
20
+ herd_modules_content_hash: string | null;
20
21
  status: EnumScoutStateStatus;
21
22
  herd_modules_loading_state: EnumHerdModulesLoadingState;
22
23
  loading_performance: LoadingPerformance;
@@ -9,6 +9,7 @@ export var EnumScoutStateStatus;
9
9
  })(EnumScoutStateStatus || (EnumScoutStateStatus = {}));
10
10
  const initialState = {
11
11
  herd_modules: [],
12
+ herd_modules_content_hash: null,
12
13
  status: EnumScoutStateStatus.LOADING,
13
14
  herd_modules_loading_state: EnumHerdModulesLoadingState.NOT_LOADING,
14
15
  loading_performance: {
@@ -33,10 +34,25 @@ export const scoutSlice = createSlice({
33
34
  initialState,
34
35
  reducers: {
35
36
  setHerdModules: (state, action) => {
36
- if (herdModulesSemanticallyEqual(current(state.herd_modules), action.payload)) {
37
+ // Payload is the modules array (legacy) or { modules, contentHash }.
38
+ const payload = action.payload;
39
+ const modules = Array.isArray(payload)
40
+ ? payload
41
+ : payload.modules;
42
+ const contentHash = Array.isArray(payload)
43
+ ? null
44
+ : payload.contentHash ?? null;
45
+ // Prefer the cheap hash check; fall back to deep compare when absent.
46
+ if (contentHash != null) {
47
+ if (state.herd_modules_content_hash === contentHash) {
48
+ return;
49
+ }
50
+ }
51
+ else if (herdModulesSemanticallyEqual(current(state.herd_modules), modules)) {
37
52
  return;
38
53
  }
39
- state.herd_modules = action.payload;
54
+ state.herd_modules = modules;
55
+ state.herd_modules_content_hash = contentHash;
40
56
  state.lastRefreshed = Date.now();
41
57
  },
42
58
  setStatus: (state, action) => {
@@ -58,7 +58,8 @@ export type IOperator = Database["public"]["Tables"]["operators"]["Row"];
58
58
  export type IObservation = Database["public"]["Tables"]["observations"]["Row"];
59
59
  export type IProvider = Database["public"]["Tables"]["providers"]["Row"];
60
60
  export type IPart = Database["public"]["Tables"]["parts"]["Row"];
61
- export type ICredential = Database["public"]["Tables"]["credentials"]["Row"];
61
+ export type IUserCredential = Database["public"]["Tables"]["user_credentials"]["Row"];
62
+ export type IHerdCredential = Database["public"]["Tables"]["herd_credentials"]["Row"];
62
63
  export type IPointsForUser = Database["public"]["Tables"]["points_for_user"]["Row"];
63
64
  export type ICertificate = Database["public"]["Tables"]["certificates"]["Row"];
64
65
  export type IVersionsSoftware = Database["public"]["Tables"]["versions_software"]["Row"];
@@ -100,8 +101,13 @@ export interface ISessionSummary {
100
101
  }
101
102
  export type ISessionUsageOverTime = Database["public"]["Functions"]["get_session_usage_over_time"]["Returns"];
102
103
  export type PartInsert = Database["public"]["Tables"]["parts"]["Insert"];
103
- export type CredentialInsert = Database["public"]["Tables"]["credentials"]["Insert"];
104
- export type CredentialUpdate = Database["public"]["Tables"]["credentials"]["Update"];
104
+ export type UserCredentialInsert = Database["public"]["Tables"]["user_credentials"]["Insert"];
105
+ export type UserCredentialUpdate = Database["public"]["Tables"]["user_credentials"]["Update"];
106
+ export type HerdCredentialInsert = Database["public"]["Tables"]["herd_credentials"]["Insert"];
107
+ export type HerdCredentialUpdate = Database["public"]["Tables"]["herd_credentials"]["Update"];
108
+ export interface CredentialQueryArgs {
109
+ type?: string;
110
+ }
105
111
  export type PointsForUserInsert = Database["public"]["Tables"]["points_for_user"]["Insert"];
106
112
  export type PointsForUserUpdate = Database["public"]["Tables"]["points_for_user"]["Update"];
107
113
  export type CertificateInsert = Database["public"]["Tables"]["certificates"]["Insert"];
@@ -1,4 +1,3 @@
1
- import { SupabaseClient } from "@supabase/supabase-js";
2
1
  import { IDevice, IHerdPrettyLocation, IPlan, ILayer, IProvider, IUserAndRole, IZoneWithActions, ISessionSummary, ISessionUsageOverTime } from "../types/db";
3
2
  import { EnumWebResponse } from "./requests";
4
3
  export declare enum EnumHerdModulesLoadingState {
@@ -7,22 +6,6 @@ export declare enum EnumHerdModulesLoadingState {
7
6
  SUCCESSFULLY_LOADED = "SUCCESSFULLY_LOADED",
8
7
  UNSUCCESSFULLY_LOADED = "UNSUCCESSFULLY_LOADED"
9
8
  }
10
- export declare class HerdModule {
11
- herd: IHerdPrettyLocation;
12
- devices: IDevice[];
13
- zones: IZoneWithActions[];
14
- timestamp_last_refreshed: number;
15
- user_roles: IUserAndRole[] | null;
16
- labels: string[];
17
- plans: IPlan[];
18
- layers: ILayer[];
19
- providers: IProvider[];
20
- session_summaries: ISessionSummary | null;
21
- session_usage: ISessionUsageOverTime | null;
22
- constructor(herd: IHerdPrettyLocation, devices: IDevice[], timestamp_last_refreshed: number, user_roles?: IUserAndRole[] | null, labels?: string[], plans?: IPlan[], zones?: IZoneWithActions[], layers?: ILayer[], providers?: IProvider[], session_summaries?: ISessionSummary | null, session_usage?: ISessionUsageOverTime | null);
23
- to_serializable(): IHerdModule;
24
- static from_herd(herd: IHerdPrettyLocation, client: SupabaseClient): Promise<HerdModule>;
25
- }
26
9
  export interface IHerdModule {
27
10
  herd: IHerdPrettyLocation;
28
11
  devices: IDevice[];
@@ -40,6 +23,7 @@ export interface IHerdModulesResponse {
40
23
  data: IHerdModule[];
41
24
  time_finished: number;
42
25
  server_processing_time_ms: number;
26
+ content_hash: string;
43
27
  }
44
28
  export interface IHerdModulesResponseWithStatus {
45
29
  status: EnumWebResponse;
@@ -48,4 +32,5 @@ export interface IHerdModulesResponseWithStatus {
48
32
  time_finished: number;
49
33
  time_sent: number;
50
34
  server_processing_time_ms: number;
35
+ content_hash: string;
51
36
  }
@@ -1,15 +1,3 @@
1
- import { LABELS } from "../constants/annotator";
2
- import { get_devices_by_herd } from "../helpers/devices";
3
- import { server_get_plans_by_herd } from "../helpers/plans";
4
- import { server_get_layers_by_herd } from "../helpers/layers";
5
- import { server_get_providers_by_herd } from "../helpers/providers";
6
- import { server_get_users_with_herd_access } from "../helpers/users";
7
- import { get_parts_by_herd_id } from "../helpers/parts";
8
- import { EnumWebResponse } from "./requests";
9
- import { server_get_more_zones_and_actions_for_herd } from "../helpers/zones";
10
- import { server_list_api_keys_batch } from "../api_keys/actions";
11
- import { server_get_session_summaries_by_herd } from "../helpers/session_summaries";
12
- import { server_get_session_usage_over_time_by_herd } from "../helpers/sessions";
13
1
  export var EnumHerdModulesLoadingState;
14
2
  (function (EnumHerdModulesLoadingState) {
15
3
  EnumHerdModulesLoadingState["NOT_LOADING"] = "NOT_LOADING";
@@ -17,157 +5,3 @@ export var EnumHerdModulesLoadingState;
17
5
  EnumHerdModulesLoadingState["SUCCESSFULLY_LOADED"] = "SUCCESSFULLY_LOADED";
18
6
  EnumHerdModulesLoadingState["UNSUCCESSFULLY_LOADED"] = "UNSUCCESSFULLY_LOADED";
19
7
  })(EnumHerdModulesLoadingState || (EnumHerdModulesLoadingState = {}));
20
- export class HerdModule {
21
- constructor(herd, devices, timestamp_last_refreshed, user_roles = null, labels = [], plans = [], zones = [], layers = [], providers = [], session_summaries = null, session_usage = null) {
22
- this.user_roles = null;
23
- this.labels = [];
24
- this.plans = [];
25
- this.layers = [];
26
- this.providers = [];
27
- this.session_summaries = null;
28
- this.session_usage = null;
29
- this.herd = herd;
30
- this.devices = devices;
31
- this.timestamp_last_refreshed = timestamp_last_refreshed;
32
- this.user_roles = user_roles;
33
- this.labels = labels;
34
- this.plans = plans;
35
- this.zones = zones;
36
- this.layers = layers;
37
- this.providers = providers;
38
- this.session_summaries = session_summaries;
39
- this.session_usage = session_usage;
40
- }
41
- to_serializable() {
42
- return {
43
- herd: this.herd,
44
- devices: this.devices,
45
- timestamp_last_refreshed: this.timestamp_last_refreshed,
46
- user_roles: this.user_roles,
47
- labels: this.labels,
48
- plans: this.plans,
49
- zones: this.zones,
50
- layers: this.layers,
51
- providers: this.providers,
52
- session_summaries: this.session_summaries,
53
- session_usage: this.session_usage,
54
- };
55
- }
56
- static async from_herd(herd, client) {
57
- const startTime = Date.now();
58
- try {
59
- // Start loading herd-level data in parallel with devices
60
- const herdLevelPromises = Promise.allSettled([
61
- server_get_more_zones_and_actions_for_herd(herd.id, 0, 10).catch((error) => {
62
- console.warn(`[HerdModule] Failed to get zones and actions:`, error);
63
- return { status: EnumWebResponse.ERROR, data: null };
64
- }),
65
- server_get_users_with_herd_access(herd.id, client).catch((error) => {
66
- console.warn(`[HerdModule] Failed to get user roles:`, error);
67
- return { status: EnumWebResponse.ERROR, data: null };
68
- }),
69
- server_get_plans_by_herd(herd.id).catch((error) => {
70
- console.warn(`[HerdModule] Failed to get plans:`, error);
71
- return { status: EnumWebResponse.ERROR, data: null };
72
- }),
73
- server_get_layers_by_herd(herd.id).catch((error) => {
74
- console.warn(`[HerdModule] Failed to get layers:`, error);
75
- return { status: EnumWebResponse.ERROR, data: null };
76
- }),
77
- server_get_providers_by_herd(herd.id).catch((error) => {
78
- console.warn(`[HerdModule] Failed to get providers:`, error);
79
- return { status: EnumWebResponse.ERROR, data: null };
80
- }),
81
- server_get_session_summaries_by_herd(herd.id, client).catch((error) => {
82
- console.warn(`[HerdModule] Failed to get session summaries:`, error);
83
- return { status: EnumWebResponse.ERROR, data: null };
84
- }),
85
- server_get_session_usage_over_time_by_herd(herd.id, client).catch((error) => {
86
- console.warn(`[HerdModule] Failed to get session usage:`, error);
87
- return { status: EnumWebResponse.ERROR, data: null };
88
- }),
89
- ]);
90
- // Load devices and parts in parallel
91
- const devicesPromise = get_devices_by_herd(herd.id, client);
92
- const partsPromise = get_parts_by_herd_id(client, herd.id);
93
- // Wait for devices, parts, and herd-level data
94
- const [deviceResponse, partsResponse, herdLevelResults] = await Promise.all([devicesPromise, partsPromise, herdLevelPromises]);
95
- // Check devices response
96
- if (deviceResponse.status == EnumWebResponse.ERROR ||
97
- !deviceResponse.data) {
98
- console.warn(`[HerdModule] No devices found for herd ${herd.id}`);
99
- return new HerdModule(herd, [], Date.now());
100
- }
101
- const new_devices = deviceResponse.data;
102
- // Get parts data (optional - don't fail if parts can't be loaded)
103
- let parts_data = [];
104
- if (partsResponse.status !== EnumWebResponse.ERROR &&
105
- partsResponse.data) {
106
- parts_data = partsResponse.data;
107
- }
108
- else {
109
- console.warn(`[HerdModule] Failed to load parts for herd ${herd.id}:`, partsResponse.status);
110
- }
111
- // Load API keys for devices if we have any
112
- if (new_devices.length > 0) {
113
- try {
114
- const device_ids = new_devices.map((device) => device.id ?? 0);
115
- const api_keys_batch = await server_list_api_keys_batch(device_ids);
116
- // Assign API keys to devices
117
- for (let i = 0; i < new_devices.length; i++) {
118
- const device_id = new_devices[i].id ?? 0;
119
- new_devices[i].api_keys_scout = api_keys_batch[device_id] || [];
120
- }
121
- }
122
- catch (error) {
123
- console.error(`[HerdModule] Failed to load API keys:`, error);
124
- // Continue without API keys
125
- }
126
- }
127
- // Associate parts with devices
128
- if (parts_data.length > 0) {
129
- for (const device of new_devices) {
130
- device.parts = parts_data.filter((part) => part.device_id === device.id);
131
- }
132
- }
133
- // Extract herd-level data with safe fallbacks
134
- const [res_zones, res_user_roles, res_plans, res_layers, res_providers, session_summaries_result, session_usage_result,] = herdLevelResults;
135
- const zones = res_zones.status === "fulfilled" && res_zones.value?.data
136
- ? res_zones.value.data
137
- : [];
138
- const user_roles = res_user_roles.status === "fulfilled" && res_user_roles.value?.data
139
- ? res_user_roles.value.data
140
- : null;
141
- const plans = res_plans.status === "fulfilled" && res_plans.value?.data
142
- ? res_plans.value.data
143
- : [];
144
- const layers = res_layers.status === "fulfilled" && res_layers.value?.data
145
- ? res_layers.value.data
146
- : [];
147
- const providers = res_providers.status === "fulfilled" && res_providers.value?.data
148
- ? res_providers.value.data
149
- : [];
150
- const session_summaries = session_summaries_result.status === "fulfilled" &&
151
- session_summaries_result.value?.data
152
- ? session_summaries_result.value.data
153
- : null;
154
- const session_usage = session_usage_result.status === "fulfilled" &&
155
- session_usage_result.value?.data
156
- ? session_usage_result.value.data
157
- : null;
158
- // TODO: store in DB and retrieve on load?
159
- const newLabels = LABELS;
160
- const endTime = Date.now();
161
- const loadTime = endTime - startTime;
162
- console.log(`[HerdModule] Loaded herd ${herd.slug} in ${loadTime}ms (${new_devices.length} devices)`);
163
- return new HerdModule(herd, new_devices, Date.now(), user_roles, newLabels, plans, zones, layers, providers, session_summaries, session_usage);
164
- }
165
- catch (error) {
166
- const endTime = Date.now();
167
- const loadTime = endTime - startTime;
168
- console.error(`[HerdModule] Critical error in HerdModule.from_herd (${loadTime}ms):`, error);
169
- // Return a minimal but valid HerdModule instance to prevent complete failure
170
- return new HerdModule(herd, [], Date.now(), null, [], [], [], [], [], null, null);
171
- }
172
- }
173
- }