@adventurelabs/scout-core 1.0.83 → 1.0.84

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.
@@ -47,8 +47,6 @@ export declare class ScoutCache {
47
47
  reason: string;
48
48
  }>;
49
49
  preloadCache(loadFunction: () => Promise<IHerdModule[]>, ttlMs?: number): Promise<void>;
50
- getProvidersForHerd(herdId: string): Promise<CacheResult<any[]>>;
51
- setProvidersForHerd(herdId: string, providers: any[], ttlMs?: number): Promise<void>;
52
50
  getDefaultTtl(): number;
53
51
  }
54
52
  export declare const scoutCache: ScoutCache;
@@ -2,7 +2,6 @@ const DB_NAME = "ScoutCache";
2
2
  const DB_VERSION = 1;
3
3
  const HERD_MODULES_STORE = "herd_modules";
4
4
  const CACHE_METADATA_STORE = "cache_metadata";
5
- const PROVIDERS_STORE = "providers";
6
5
  // Default TTL: 24 hours (1 day)
7
6
  const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
8
7
  export class ScoutCache {
@@ -47,15 +46,6 @@ export class ScoutCache {
47
46
  keyPath: "key",
48
47
  });
49
48
  }
50
- // Create providers store
51
- if (!db.objectStoreNames.contains(PROVIDERS_STORE)) {
52
- const providersStore = db.createObjectStore(PROVIDERS_STORE, {
53
- keyPath: "herdId",
54
- });
55
- providersStore.createIndex("timestamp", "timestamp", {
56
- unique: false,
57
- });
58
- }
59
49
  console.log("[ScoutCache] Database schema upgraded");
60
50
  };
61
51
  });
@@ -65,16 +55,15 @@ export class ScoutCache {
65
55
  await this.init();
66
56
  if (!this.db)
67
57
  throw new Error("Database not initialized");
68
- const transaction = this.db.transaction([HERD_MODULES_STORE, CACHE_METADATA_STORE, PROVIDERS_STORE], "readwrite");
58
+ const transaction = this.db.transaction([HERD_MODULES_STORE, CACHE_METADATA_STORE], "readwrite");
69
59
  return new Promise((resolve, reject) => {
70
60
  transaction.onerror = () => reject(transaction.error);
71
61
  transaction.oncomplete = () => resolve();
72
62
  const herdModulesStore = transaction.objectStore(HERD_MODULES_STORE);
73
63
  const metadataStore = transaction.objectStore(CACHE_METADATA_STORE);
74
- const providersStore = transaction.objectStore(PROVIDERS_STORE);
75
64
  const timestamp = Date.now();
76
65
  const version = "1.0.0";
77
- // Store each herd module and its providers
66
+ // Store each herd module
78
67
  herdModules.forEach((herdModule) => {
79
68
  const cacheEntry = {
80
69
  herdId: herdModule.herd.id.toString(),
@@ -82,15 +71,6 @@ export class ScoutCache {
82
71
  timestamp,
83
72
  };
84
73
  herdModulesStore.put(cacheEntry);
85
- // Store providers separately for easier querying
86
- if (herdModule.providers && herdModule.providers.length > 0) {
87
- const providersCacheEntry = {
88
- herdId: herdModule.herd.id.toString(),
89
- data: herdModule.providers,
90
- timestamp,
91
- };
92
- providersStore.put(providersCacheEntry);
93
- }
94
74
  });
95
75
  // Store cache metadata
96
76
  const metadata = {
@@ -102,16 +82,6 @@ export class ScoutCache {
102
82
  lastModified: timestamp,
103
83
  };
104
84
  metadataStore.put(metadata);
105
- // Store providers metadata
106
- const providersMetadata = {
107
- key: "providers",
108
- timestamp,
109
- ttl: ttlMs,
110
- version,
111
- etag,
112
- lastModified: timestamp,
113
- };
114
- metadataStore.put(providersMetadata);
115
85
  });
116
86
  }
117
87
  async getHerdModules() {
@@ -164,17 +134,14 @@ export class ScoutCache {
164
134
  await this.init();
165
135
  if (!this.db)
166
136
  throw new Error("Database not initialized");
167
- const transaction = this.db.transaction([HERD_MODULES_STORE, CACHE_METADATA_STORE, PROVIDERS_STORE], "readwrite");
137
+ const transaction = this.db.transaction([HERD_MODULES_STORE, CACHE_METADATA_STORE], "readwrite");
168
138
  return new Promise((resolve, reject) => {
169
139
  transaction.onerror = () => reject(transaction.error);
170
140
  transaction.oncomplete = () => resolve();
171
141
  const herdModulesStore = transaction.objectStore(HERD_MODULES_STORE);
172
142
  const metadataStore = transaction.objectStore(CACHE_METADATA_STORE);
173
- const providersStore = transaction.objectStore(PROVIDERS_STORE);
174
143
  herdModulesStore.clear();
175
- providersStore.clear();
176
144
  metadataStore.delete("herd_modules");
177
- metadataStore.delete("providers");
178
145
  });
179
146
  }
180
147
  async invalidateHerdModules() {
@@ -187,7 +154,6 @@ export class ScoutCache {
187
154
  transaction.oncomplete = () => resolve();
188
155
  const metadataStore = transaction.objectStore(CACHE_METADATA_STORE);
189
156
  metadataStore.delete("herd_modules");
190
- metadataStore.delete("providers");
191
157
  });
192
158
  }
193
159
  async getCacheStats() {
@@ -248,81 +214,6 @@ export class ScoutCache {
248
214
  console.warn("[ScoutCache] Background preload failed:", error);
249
215
  }
250
216
  }
251
- // Method to get providers for a specific herd from cache
252
- async getProvidersForHerd(herdId) {
253
- await this.init();
254
- if (!this.db)
255
- throw new Error("Database not initialized");
256
- const transaction = this.db.transaction([PROVIDERS_STORE, CACHE_METADATA_STORE], "readonly");
257
- return new Promise((resolve, reject) => {
258
- transaction.onerror = () => reject(transaction.error);
259
- const providersStore = transaction.objectStore(PROVIDERS_STORE);
260
- const metadataStore = transaction.objectStore(CACHE_METADATA_STORE);
261
- // Get metadata first
262
- const metadataRequest = metadataStore.get("providers");
263
- metadataRequest.onsuccess = () => {
264
- const metadata = metadataRequest.result;
265
- const now = Date.now();
266
- if (!metadata) {
267
- this.stats.misses++;
268
- resolve({ data: null, isStale: true, age: 0, metadata: null });
269
- return;
270
- }
271
- const age = now - metadata.timestamp;
272
- const isStale = age > metadata.ttl;
273
- // Get providers for specific herd
274
- const getRequest = providersStore.get(herdId);
275
- getRequest.onsuccess = () => {
276
- const cacheEntry = getRequest.result;
277
- const providers = cacheEntry?.data || [];
278
- // Update stats
279
- if (providers.length > 0) {
280
- this.stats.hits++;
281
- }
282
- else {
283
- this.stats.misses++;
284
- }
285
- resolve({
286
- data: providers,
287
- isStale,
288
- age,
289
- metadata,
290
- });
291
- };
292
- };
293
- });
294
- }
295
- // Method to set providers for a specific herd
296
- async setProvidersForHerd(herdId, providers, ttlMs = DEFAULT_TTL_MS) {
297
- await this.init();
298
- if (!this.db)
299
- throw new Error("Database not initialized");
300
- const transaction = this.db.transaction([PROVIDERS_STORE, CACHE_METADATA_STORE], "readwrite");
301
- return new Promise((resolve, reject) => {
302
- transaction.onerror = () => reject(transaction.error);
303
- transaction.oncomplete = () => resolve();
304
- const providersStore = transaction.objectStore(PROVIDERS_STORE);
305
- const metadataStore = transaction.objectStore(CACHE_METADATA_STORE);
306
- const timestamp = Date.now();
307
- const version = "1.0.0";
308
- // Store providers
309
- const providersCacheEntry = {
310
- herdId,
311
- data: providers,
312
- timestamp,
313
- };
314
- providersStore.put(providersCacheEntry);
315
- // Update providers metadata
316
- const metadata = {
317
- key: "providers",
318
- timestamp,
319
- ttl: ttlMs,
320
- version,
321
- lastModified: timestamp,
322
- };
323
- metadataStore.put(metadata);
324
- });
325
- }
326
217
  // Get the default TTL value
327
218
  getDefaultTtl() {
328
219
  return DEFAULT_TTL_MS;
@@ -0,0 +1,9 @@
1
+ import { IWebResponseCompatible } from "../types/requests";
2
+ import { IHeartbeat } from "../types/db";
3
+ export declare function server_get_last_heartbeat_by_device(device_id: number): Promise<IWebResponseCompatible<IHeartbeat | null>>;
4
+ export declare function server_get_heartbeats_by_device(device_id: number, limit?: number): Promise<IWebResponseCompatible<IHeartbeat[]>>;
5
+ export declare function server_check_device_online_status(device_id: number, offline_threshold_minutes?: number): Promise<IWebResponseCompatible<{
6
+ is_online: boolean;
7
+ last_heartbeat: IHeartbeat | null;
8
+ minutes_since_last_heartbeat: number | null;
9
+ }>>;
@@ -0,0 +1,90 @@
1
+ "use server";
2
+ import { newServerClient } from "../supabase/server";
3
+ import { EnumWebResponse, IWebResponse, } from "../types/requests";
4
+ // Function to get the last heartbeat for a device
5
+ export async function server_get_last_heartbeat_by_device(device_id) {
6
+ const supabase = await newServerClient();
7
+ const { data, error } = await supabase
8
+ .from("heartbeats")
9
+ .select("*")
10
+ .eq("device_id", device_id)
11
+ .order("timestamp", { ascending: false })
12
+ .limit(1)
13
+ .single();
14
+ if (error) {
15
+ // If no heartbeats found, return null data instead of error
16
+ if (error.code === "PGRST116") {
17
+ return {
18
+ status: EnumWebResponse.SUCCESS,
19
+ msg: null,
20
+ data: null,
21
+ };
22
+ }
23
+ return {
24
+ status: EnumWebResponse.ERROR,
25
+ msg: error.message,
26
+ data: null,
27
+ };
28
+ }
29
+ else {
30
+ return IWebResponse.success(data).to_compatible();
31
+ }
32
+ }
33
+ // Function to get all heartbeats for a device
34
+ export async function server_get_heartbeats_by_device(device_id, limit) {
35
+ const supabase = await newServerClient();
36
+ let query = supabase
37
+ .from("heartbeats")
38
+ .select("*")
39
+ .eq("device_id", device_id)
40
+ .order("timestamp", { ascending: false });
41
+ if (limit) {
42
+ query = query.limit(limit);
43
+ }
44
+ const { data, error } = await query;
45
+ if (error) {
46
+ return {
47
+ status: EnumWebResponse.ERROR,
48
+ msg: error.message,
49
+ data: [],
50
+ };
51
+ }
52
+ else {
53
+ return IWebResponse.success(data || []).to_compatible();
54
+ }
55
+ }
56
+ // Function to check if a device is online based on heartbeat recency
57
+ export async function server_check_device_online_status(device_id, offline_threshold_minutes = 5) {
58
+ const supabase = await newServerClient();
59
+ const { data, error } = await supabase
60
+ .from("heartbeats")
61
+ .select("*")
62
+ .eq("device_id", device_id)
63
+ .order("timestamp", { ascending: false })
64
+ .limit(1)
65
+ .single();
66
+ if (error) {
67
+ // If no heartbeats found
68
+ if (error.code === "PGRST116") {
69
+ return IWebResponse.success({
70
+ is_online: false,
71
+ last_heartbeat: null,
72
+ minutes_since_last_heartbeat: null,
73
+ }).to_compatible();
74
+ }
75
+ return {
76
+ status: EnumWebResponse.ERROR,
77
+ msg: error.message,
78
+ data: null,
79
+ };
80
+ }
81
+ const now = new Date();
82
+ const heartbeatTime = new Date(data.timestamp);
83
+ const minutesSinceLastHeartbeat = Math.floor((now.getTime() - heartbeatTime.getTime()) / (1000 * 60));
84
+ const isOnline = minutesSinceLastHeartbeat <= offline_threshold_minutes;
85
+ return IWebResponse.success({
86
+ is_online: isOnline,
87
+ last_heartbeat: data,
88
+ minutes_since_last_heartbeat: minutesSinceLastHeartbeat,
89
+ }).to_compatible();
90
+ }
@@ -6,6 +6,7 @@ export * from "./devices";
6
6
  export * from "./email";
7
7
  export * from "./events";
8
8
  export * from "./gps";
9
+ export * from "./heartbeats";
9
10
  export * from "./herds";
10
11
  export * from "./location";
11
12
  export * from "./plans";
@@ -6,6 +6,7 @@ export * from "./devices";
6
6
  export * from "./email";
7
7
  export * from "./events";
8
8
  export * from "./gps";
9
+ export * from "./heartbeats";
9
10
  export * from "./herds";
10
11
  export * from "./location";
11
12
  export * from "./plans";
@@ -289,6 +289,33 @@ export declare function useSupabase(): SupabaseClient<Database, "public", "publi
289
289
  referencedColumns: ["id"];
290
290
  }];
291
291
  };
292
+ heartbeats: {
293
+ Row: {
294
+ created_at: string;
295
+ device_id: number;
296
+ id: number;
297
+ timestamp: string;
298
+ };
299
+ Insert: {
300
+ created_at?: string;
301
+ device_id: number;
302
+ id?: number;
303
+ timestamp: string;
304
+ };
305
+ Update: {
306
+ created_at?: string;
307
+ device_id?: number;
308
+ id?: number;
309
+ timestamp?: string;
310
+ };
311
+ Relationships: [{
312
+ foreignKeyName: "heartbeats_device_id_fkey";
313
+ columns: ["device_id"];
314
+ isOneToOne: false;
315
+ referencedRelation: "devices";
316
+ referencedColumns: ["id"];
317
+ }];
318
+ };
292
319
  herds: {
293
320
  Row: {
294
321
  created_by: string;
@@ -21,7 +21,7 @@ export type IUserRolePerHerd = Database["public"]["Tables"]["users_roles_per_her
21
21
  export type IHerd = Database["public"]["Tables"]["herds"]["Row"];
22
22
  export type ISession = Database["public"]["Tables"]["sessions"]["Row"];
23
23
  export type IConnectivity = Database["public"]["Tables"]["connectivity"]["Row"];
24
- export type IProvider = Database["public"]["Tables"]["providers"]["Row"];
24
+ export type IHeartbeat = Database["public"]["Tables"]["heartbeats"]["Row"];
25
25
  export type IEventWithTags = Database["public"]["CompositeTypes"]["event_with_tags"] & {
26
26
  earthranger_url: string | null;
27
27
  file_path: string | null;
@@ -1,5 +1,5 @@
1
1
  import { SupabaseClient } from "@supabase/supabase-js";
2
- import { IDevice, IEventWithTags, IHerd, IPlan, ILayer, IProvider, IUserAndRole, IZoneWithActions, ISessionWithCoordinates } from "../types/db";
2
+ import { IDevice, IEventWithTags, IHerd, IPlan, ILayer, IUserAndRole, IZoneWithActions, ISessionWithCoordinates } from "../types/db";
3
3
  import { EnumWebResponse } from "./requests";
4
4
  export declare enum EnumHerdModulesLoadingState {
5
5
  NOT_LOADING = "NOT_LOADING",
@@ -21,8 +21,7 @@ export declare class HerdModule {
21
21
  labels: string[];
22
22
  plans: IPlan[];
23
23
  layers: ILayer[];
24
- providers: IProvider[];
25
- constructor(herd: IHerd, devices: IDevice[], events: IEventWithTags[], timestamp_last_refreshed: number, user_roles?: IUserAndRole[] | null, events_page_index?: number, total_events?: number, total_events_with_filters?: number, labels?: string[], plans?: IPlan[], zones?: IZoneWithActions[], sessions?: ISessionWithCoordinates[], layers?: ILayer[], providers?: IProvider[]);
24
+ constructor(herd: IHerd, devices: IDevice[], events: IEventWithTags[], timestamp_last_refreshed: number, user_roles?: IUserAndRole[] | null, events_page_index?: number, total_events?: number, total_events_with_filters?: number, labels?: string[], plans?: IPlan[], zones?: IZoneWithActions[], sessions?: ISessionWithCoordinates[], layers?: ILayer[]);
26
25
  to_serializable(): IHerdModule;
27
26
  static from_herd(herd: IHerd, client: SupabaseClient): Promise<HerdModule>;
28
27
  }
@@ -40,7 +39,6 @@ export interface IHerdModule {
40
39
  zones: IZoneWithActions[];
41
40
  sessions: ISessionWithCoordinates[];
42
41
  layers: ILayer[];
43
- providers: IProvider[];
44
42
  }
45
43
  export interface IHerdModulesResponse {
46
44
  data: IHerdModule[];
@@ -4,7 +4,6 @@ import { server_get_total_events_by_herd } from "../helpers/events";
4
4
  import { EnumSessionsVisibility } from "./events";
5
5
  import { server_get_plans_by_herd } from "../helpers/plans";
6
6
  import { server_get_layers_by_herd } from "../helpers/layers";
7
- import { server_get_providers_by_herd } from "../helpers/providers";
8
7
  import { server_get_events_and_tags_for_devices_batch } from "../helpers/tags";
9
8
  import { server_get_users_with_herd_access } from "../helpers/users";
10
9
  import { EnumWebResponse } from "./requests";
@@ -19,7 +18,7 @@ export var EnumHerdModulesLoadingState;
19
18
  EnumHerdModulesLoadingState["UNSUCCESSFULLY_LOADED"] = "UNSUCCESSFULLY_LOADED";
20
19
  })(EnumHerdModulesLoadingState || (EnumHerdModulesLoadingState = {}));
21
20
  export class HerdModule {
22
- constructor(herd, devices, events, timestamp_last_refreshed, user_roles = null, events_page_index = 0, total_events = 0, total_events_with_filters = 0, labels = [], plans = [], zones = [], sessions = [], layers = [], providers = []) {
21
+ constructor(herd, devices, events, timestamp_last_refreshed, user_roles = null, events_page_index = 0, total_events = 0, total_events_with_filters = 0, labels = [], plans = [], zones = [], sessions = [], layers = []) {
23
22
  this.user_roles = null;
24
23
  this.events_page_index = 0;
25
24
  this.total_events = 0;
@@ -27,7 +26,6 @@ export class HerdModule {
27
26
  this.labels = [];
28
27
  this.plans = [];
29
28
  this.layers = [];
30
- this.providers = [];
31
29
  this.herd = herd;
32
30
  this.devices = devices;
33
31
  this.events = events;
@@ -41,7 +39,6 @@ export class HerdModule {
41
39
  this.zones = zones;
42
40
  this.sessions = sessions;
43
41
  this.layers = layers;
44
- this.providers = providers;
45
42
  }
46
43
  to_serializable() {
47
44
  return {
@@ -58,7 +55,6 @@ export class HerdModule {
58
55
  zones: this.zones,
59
56
  sessions: this.sessions,
60
57
  layers: this.layers,
61
- providers: this.providers,
62
58
  };
63
59
  }
64
60
  static async from_herd(herd, client) {
@@ -99,7 +95,7 @@ export class HerdModule {
99
95
  }
100
96
  }
101
97
  // Run all remaining requests in parallel with individual error handling
102
- const [res_zones, res_user_roles, total_event_count, res_plans, res_sessions, res_layers, res_providers,] = await Promise.allSettled([
98
+ const [res_zones, res_user_roles, total_event_count, res_plans, res_sessions, res_layers,] = await Promise.allSettled([
103
99
  server_get_more_zones_and_actions_for_herd(herd.id, 0, 10).catch((error) => {
104
100
  console.warn(`[HerdModule] Failed to get zones and actions:`, error);
105
101
  return { status: EnumWebResponse.ERROR, data: null };
@@ -118,20 +114,12 @@ export class HerdModule {
118
114
  }),
119
115
  server_get_sessions_by_herd_id(herd.id).catch((error) => {
120
116
  console.warn(`[HerdModule] Failed to get sessions:`, error);
121
- return {
122
- status: EnumWebResponse.ERROR,
123
- data: [],
124
- msg: error.message,
125
- };
117
+ return { status: EnumWebResponse.ERROR, data: [], msg: error.message };
126
118
  }),
127
119
  server_get_layers_by_herd(herd.id).catch((error) => {
128
120
  console.warn(`[HerdModule] Failed to get layers:`, error);
129
121
  return { status: EnumWebResponse.ERROR, data: null };
130
122
  }),
131
- server_get_providers_by_herd(herd.id).catch((error) => {
132
- console.warn(`[HerdModule] Failed to get providers:`, error);
133
- return { status: EnumWebResponse.ERROR, data: null };
134
- }),
135
123
  ]);
136
124
  // Assign recent events to devices from batch results
137
125
  for (let i = 0; i < new_devices.length; i++) {
@@ -160,28 +148,23 @@ export class HerdModule {
160
148
  const plans = res_plans.status === "fulfilled" && res_plans.value?.data
161
149
  ? res_plans.value.data
162
150
  : [];
163
- const sessions = res_sessions.status === "fulfilled" && res_sessions.value?.data
164
- ? res_sessions.value.data
165
- : [];
151
+ const sessions = res_sessions.status === "fulfilled" && res_sessions.value?.data ? res_sessions.value.data : [];
166
152
  const layers = res_layers.status === "fulfilled" && res_layers.value?.data
167
153
  ? res_layers.value.data
168
154
  : [];
169
- const providers = res_providers.status === "fulfilled" && res_providers.value?.data
170
- ? res_providers.value.data
171
- : [];
172
155
  // TODO: store in DB and retrieve on load?
173
156
  const newLabels = LABELS;
174
157
  const endTime = Date.now();
175
158
  const loadTime = endTime - startTime;
176
159
  console.log(`[HerdModule] Loaded herd ${herd.slug} in ${loadTime}ms (${new_devices.length} devices)`);
177
- return new HerdModule(herd, new_devices, [], Date.now(), user_roles, 0, total_events, total_events, newLabels, plans, zones, sessions, layers, providers);
160
+ return new HerdModule(herd, new_devices, [], Date.now(), user_roles, 0, total_events, total_events, newLabels, plans, zones, sessions, layers);
178
161
  }
179
162
  catch (error) {
180
163
  const endTime = Date.now();
181
164
  const loadTime = endTime - startTime;
182
165
  console.error(`[HerdModule] Critical error in HerdModule.from_herd (${loadTime}ms):`, error);
183
166
  // Return a minimal but valid HerdModule instance to prevent complete failure
184
- return new HerdModule(herd, [], [], Date.now(), null, 0, 0, 0, [], [], [], [], [], []);
167
+ return new HerdModule(herd, [], [], Date.now(), null, 0, 0, 0, [], [], [], [], []);
185
168
  }
186
169
  }
187
170
  }
@@ -302,6 +302,35 @@ export type Database = {
302
302
  }
303
303
  ];
304
304
  };
305
+ heartbeats: {
306
+ Row: {
307
+ created_at: string;
308
+ device_id: number;
309
+ id: number;
310
+ timestamp: string;
311
+ };
312
+ Insert: {
313
+ created_at?: string;
314
+ device_id: number;
315
+ id?: number;
316
+ timestamp: string;
317
+ };
318
+ Update: {
319
+ created_at?: string;
320
+ device_id?: number;
321
+ id?: number;
322
+ timestamp?: string;
323
+ };
324
+ Relationships: [
325
+ {
326
+ foreignKeyName: "heartbeats_device_id_fkey";
327
+ columns: ["device_id"];
328
+ isOneToOne: false;
329
+ referencedRelation: "devices";
330
+ referencedColumns: ["id"];
331
+ }
332
+ ];
333
+ };
305
334
  herds: {
306
335
  Row: {
307
336
  created_by: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adventurelabs/scout-core",
3
- "version": "1.0.83",
3
+ "version": "1.0.84",
4
4
  "description": "Core utilities and helpers for Adventure Labs Scout applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",