@adventurelabs/scout-core 1.0.100 → 1.0.102

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.
@@ -10,7 +10,9 @@ export function convertDateToTimeString(date) {
10
10
  return `${date.toLocaleTimeString()}, ${date.toLocaleDateString()} UTC`;
11
11
  }
12
12
  // Format a Date object as a PostgreSQL-compatible timestamp string
13
- // Returns format: "YYYY-MM-DDTHH:MM:SS.SSSZ" (ISO 8601 with timezone)
13
+ // Returns ISO 8601 format: "YYYY-MM-DDTHH:MM:SS.SSSZ"
14
+ // PostgreSQL automatically converts this to its internal format: "YYYY-MM-DD HH:MM:SS.SSSSSS+00"
15
+ // This ensures compatibility with both PostgreSQL storage and RPC function expectations
14
16
  export function formatTimestampForDatabase(date) {
15
17
  return date.toISOString();
16
18
  }
@@ -0,0 +1,3 @@
1
+ import { SupabaseClient } from "@supabase/supabase-js";
2
+ import { Database } from "../types/supabase";
3
+ export declare function useScoutRealtimeConnectivity(scoutSupabase: SupabaseClient<Database>): void;
@@ -0,0 +1,159 @@
1
+ "use client";
2
+ import { useAppDispatch } from "../store/hooks";
3
+ import { useSelector } from "react-redux";
4
+ import { useEffect, useRef, useCallback, useMemo } from "react";
5
+ import { setActiveHerdGpsTrackersConnectivity } from "../store/scout";
6
+ import { server_get_connectivity_by_device_id } from "../helpers/connectivity";
7
+ import { EnumWebResponse } from "../types/requests";
8
+ import { getHoursAgoTimestamp } from "../helpers/time";
9
+ export function useScoutRealtimeConnectivity(scoutSupabase) {
10
+ const channels = useRef([]);
11
+ const dispatch = useAppDispatch();
12
+ const activeHerdId = useSelector((state) => state.scout.active_herd_id);
13
+ const connectivity = useSelector((state) => state.scout.active_herd_gps_trackers_connectivity);
14
+ const herdModules = useSelector((state) => state.scout.herd_modules);
15
+ // Create stable reference for GPS device IDs to prevent unnecessary refetching
16
+ const gpsDeviceIds = useMemo(() => {
17
+ if (!activeHerdId)
18
+ return "";
19
+ const activeHerdModule = herdModules.find((hm) => hm.herd.id.toString() === activeHerdId);
20
+ if (!activeHerdModule)
21
+ return "";
22
+ const gpsDevices = activeHerdModule.devices.filter((device) => device.device_type &&
23
+ ["gps_tracker", "gps_tracker_vehicle", "gps_tracker_person"].includes(device.device_type));
24
+ return gpsDevices
25
+ .map((d) => d.id)
26
+ .filter(Boolean)
27
+ .sort()
28
+ .join(",");
29
+ }, [activeHerdId, herdModules]);
30
+ // Handle connectivity broadcasts
31
+ const handleConnectivityBroadcast = useCallback((payload) => {
32
+ const { event, payload: data } = payload;
33
+ const connectivityData = data.record || data.old_record;
34
+ // Only process GPS tracker data (no session_id)
35
+ if (!connectivityData?.device_id || connectivityData.session_id) {
36
+ return;
37
+ }
38
+ const deviceId = connectivityData.device_id;
39
+ const updatedConnectivity = { ...connectivity };
40
+ switch (data.operation) {
41
+ case "INSERT":
42
+ console.log(`[CONNECTIVITY] INSERT for ${deviceId}, ${JSON.stringify(connectivityData)}`);
43
+ if (!updatedConnectivity[deviceId]) {
44
+ updatedConnectivity[deviceId] = {
45
+ most_recent: connectivityData,
46
+ history: [],
47
+ };
48
+ }
49
+ else {
50
+ const newHistory = [
51
+ updatedConnectivity[deviceId].most_recent,
52
+ ...updatedConnectivity[deviceId].history,
53
+ ].slice(0, 99);
54
+ updatedConnectivity[deviceId] = {
55
+ most_recent: connectivityData,
56
+ history: newHistory,
57
+ };
58
+ }
59
+ break;
60
+ case "UPDATE":
61
+ if (updatedConnectivity[deviceId]) {
62
+ if (updatedConnectivity[deviceId].most_recent.id ===
63
+ connectivityData.id) {
64
+ updatedConnectivity[deviceId] = {
65
+ ...updatedConnectivity[deviceId],
66
+ most_recent: connectivityData,
67
+ };
68
+ }
69
+ else {
70
+ const historyIndex = updatedConnectivity[deviceId].history.findIndex((c) => c.id === connectivityData.id);
71
+ if (historyIndex >= 0) {
72
+ const newHistory = [...updatedConnectivity[deviceId].history];
73
+ newHistory[historyIndex] = connectivityData;
74
+ updatedConnectivity[deviceId] = {
75
+ ...updatedConnectivity[deviceId],
76
+ history: newHistory,
77
+ };
78
+ }
79
+ }
80
+ }
81
+ break;
82
+ case "DELETE":
83
+ if (updatedConnectivity[deviceId]) {
84
+ if (updatedConnectivity[deviceId].most_recent.id ===
85
+ connectivityData.id) {
86
+ if (updatedConnectivity[deviceId].history.length === 0) {
87
+ delete updatedConnectivity[deviceId];
88
+ }
89
+ else {
90
+ updatedConnectivity[deviceId] = {
91
+ most_recent: updatedConnectivity[deviceId].history[0],
92
+ history: updatedConnectivity[deviceId].history.slice(1),
93
+ };
94
+ }
95
+ }
96
+ else {
97
+ updatedConnectivity[deviceId] = {
98
+ ...updatedConnectivity[deviceId],
99
+ history: updatedConnectivity[deviceId].history.filter((c) => c.id !== connectivityData.id),
100
+ };
101
+ }
102
+ }
103
+ break;
104
+ }
105
+ dispatch(setActiveHerdGpsTrackersConnectivity(updatedConnectivity));
106
+ }, [connectivity, dispatch]);
107
+ // Fetch initial connectivity data
108
+ const fetchInitialData = useCallback(async () => {
109
+ if (!gpsDeviceIds)
110
+ return;
111
+ const deviceIds = gpsDeviceIds.split(",").filter(Boolean).map(Number);
112
+ if (deviceIds.length === 0) {
113
+ return;
114
+ }
115
+ const timestampFilter = getHoursAgoTimestamp(1);
116
+ const connectivityData = {};
117
+ await Promise.all(deviceIds.map(async (deviceId) => {
118
+ try {
119
+ const response = await server_get_connectivity_by_device_id(deviceId, timestampFilter);
120
+ if (response.status === EnumWebResponse.SUCCESS && response.data) {
121
+ const trackerData = response.data.filter((conn) => !conn.session_id);
122
+ if (trackerData.length > 0) {
123
+ const sortedData = trackerData
124
+ .sort((a, b) => new Date(b.timestamp_start || 0).getTime() -
125
+ new Date(a.timestamp_start || 0).getTime())
126
+ .slice(0, 100);
127
+ connectivityData[deviceId] = {
128
+ most_recent: sortedData[0],
129
+ history: sortedData.slice(1), // Exclude the most recent item
130
+ };
131
+ }
132
+ }
133
+ }
134
+ catch (error) {
135
+ // Silent error handling
136
+ }
137
+ }));
138
+ dispatch(setActiveHerdGpsTrackersConnectivity(connectivityData));
139
+ }, [gpsDeviceIds, dispatch]);
140
+ useEffect(() => {
141
+ if (!scoutSupabase || gpsDeviceIds === "")
142
+ return;
143
+ // Clean up existing channels
144
+ channels.current.forEach((channel) => scoutSupabase.removeChannel(channel));
145
+ channels.current = [];
146
+ // Create connectivity channel
147
+ const channel = scoutSupabase
148
+ .channel(`${activeHerdId}-connectivity`, { config: { private: true } })
149
+ .on("broadcast", { event: "*" }, handleConnectivityBroadcast)
150
+ .subscribe();
151
+ channels.current.push(channel);
152
+ // Fetch initial data
153
+ fetchInitialData();
154
+ return () => {
155
+ channels.current.forEach((ch) => scoutSupabase.removeChannel(ch));
156
+ channels.current = [];
157
+ };
158
+ }, [scoutSupabase, gpsDeviceIds, activeHerdId, handleConnectivityBroadcast]);
159
+ }
@@ -1,3 +1,5 @@
1
1
  import { SupabaseClient } from "@supabase/supabase-js";
2
2
  import { Database } from "../types/supabase";
3
- export declare function useScoutRealtimeConnectivity(scoutSupabase: SupabaseClient<Database>): void;
3
+ import { IConnectivityWithCoordinates } from "../types/db";
4
+ import { RealtimeData } from "../types/realtime";
5
+ export declare function useScoutRealtimeConnectivity(scoutSupabase: SupabaseClient<Database>): RealtimeData<IConnectivityWithCoordinates>[];
@@ -1,16 +1,11 @@
1
1
  "use client";
2
- import { useAppDispatch } from "../store/hooks";
3
2
  import { useSelector } from "react-redux";
4
- import { useEffect, useRef, useCallback, useMemo } from "react";
5
- import { setActiveHerdGpsTrackersConnectivity } from "../store/scout";
6
- import { server_get_connectivity_by_device_id } from "../helpers/connectivity";
7
- import { EnumWebResponse } from "../types/requests";
8
- import { getDaysAgoTimestamp } from "../helpers/time";
3
+ import { useEffect, useRef, useCallback, useState, useMemo } from "react";
4
+ import { EnumRealtimeOperation } from "../types/realtime";
9
5
  export function useScoutRealtimeConnectivity(scoutSupabase) {
10
6
  const channels = useRef([]);
11
- const dispatch = useAppDispatch();
7
+ const [newConnectivityItems, setNewConnectivityItems] = useState([]);
12
8
  const activeHerdId = useSelector((state) => state.scout.active_herd_id);
13
- const connectivity = useSelector((state) => state.scout.active_herd_gps_trackers_connectivity);
14
9
  const herdModules = useSelector((state) => state.scout.herd_modules);
15
10
  // Create stable reference for GPS device IDs to prevent unnecessary refetching
16
11
  const gpsDeviceIds = useMemo(() => {
@@ -35,124 +30,55 @@ export function useScoutRealtimeConnectivity(scoutSupabase) {
35
30
  if (!connectivityData?.device_id || connectivityData.session_id) {
36
31
  return;
37
32
  }
38
- const deviceId = connectivityData.device_id;
39
- const updatedConnectivity = { ...connectivity };
33
+ let operation;
40
34
  switch (data.operation) {
41
35
  case "INSERT":
42
- if (!updatedConnectivity[deviceId]) {
43
- updatedConnectivity[deviceId] = {
44
- most_recent: connectivityData,
45
- history: [],
46
- };
47
- }
48
- else {
49
- const newHistory = [
50
- updatedConnectivity[deviceId].most_recent,
51
- ...updatedConnectivity[deviceId].history,
52
- ].slice(0, 99);
53
- updatedConnectivity[deviceId] = {
54
- most_recent: connectivityData,
55
- history: newHistory,
56
- };
57
- }
36
+ operation = EnumRealtimeOperation.INSERT;
58
37
  break;
59
38
  case "UPDATE":
60
- if (updatedConnectivity[deviceId]) {
61
- if (updatedConnectivity[deviceId].most_recent.id ===
62
- connectivityData.id) {
63
- updatedConnectivity[deviceId] = {
64
- ...updatedConnectivity[deviceId],
65
- most_recent: connectivityData,
66
- };
67
- }
68
- else {
69
- const historyIndex = updatedConnectivity[deviceId].history.findIndex((c) => c.id === connectivityData.id);
70
- if (historyIndex >= 0) {
71
- const newHistory = [...updatedConnectivity[deviceId].history];
72
- newHistory[historyIndex] = connectivityData;
73
- updatedConnectivity[deviceId] = {
74
- ...updatedConnectivity[deviceId],
75
- history: newHistory,
76
- };
77
- }
78
- }
79
- }
39
+ operation = EnumRealtimeOperation.UPDATE;
80
40
  break;
81
41
  case "DELETE":
82
- if (updatedConnectivity[deviceId]) {
83
- if (updatedConnectivity[deviceId].most_recent.id ===
84
- connectivityData.id) {
85
- if (updatedConnectivity[deviceId].history.length === 0) {
86
- delete updatedConnectivity[deviceId];
87
- }
88
- else {
89
- updatedConnectivity[deviceId] = {
90
- most_recent: updatedConnectivity[deviceId].history[0],
91
- history: updatedConnectivity[deviceId].history.slice(1),
92
- };
93
- }
94
- }
95
- else {
96
- updatedConnectivity[deviceId] = {
97
- ...updatedConnectivity[deviceId],
98
- history: updatedConnectivity[deviceId].history.filter((c) => c.id !== connectivityData.id),
99
- };
100
- }
101
- }
42
+ operation = EnumRealtimeOperation.DELETE;
102
43
  break;
44
+ default:
45
+ return;
103
46
  }
104
- dispatch(setActiveHerdGpsTrackersConnectivity(updatedConnectivity));
105
- }, [connectivity, dispatch]);
106
- // Fetch initial connectivity data
107
- const fetchInitialData = useCallback(async () => {
108
- if (!gpsDeviceIds)
109
- return;
110
- const deviceIds = gpsDeviceIds.split(",").filter(Boolean).map(Number);
111
- if (deviceIds.length === 0) {
112
- return;
113
- }
114
- const timestampFilter = getDaysAgoTimestamp(1);
115
- const connectivityData = {};
116
- await Promise.all(deviceIds.map(async (deviceId) => {
117
- try {
118
- const response = await server_get_connectivity_by_device_id(deviceId, timestampFilter);
119
- if (response.status === EnumWebResponse.SUCCESS && response.data) {
120
- const trackerData = response.data.filter((conn) => !conn.session_id);
121
- if (trackerData.length > 0) {
122
- const sortedData = trackerData
123
- .sort((a, b) => new Date(b.timestamp_start || 0).getTime() -
124
- new Date(a.timestamp_start || 0).getTime())
125
- .slice(0, 100);
126
- connectivityData[deviceId] = {
127
- most_recent: sortedData[0],
128
- history: sortedData.slice(1), // Exclude the most recent item
129
- };
130
- }
131
- }
132
- }
133
- catch (error) {
134
- // Silent error handling
135
- }
136
- }));
137
- dispatch(setActiveHerdGpsTrackersConnectivity(connectivityData));
138
- }, [gpsDeviceIds, dispatch]);
47
+ console.log(`[CONNECTIVITY] ${data.operation} received for device ${connectivityData.device_id}:`, JSON.stringify(connectivityData));
48
+ const realtimeData = {
49
+ data: connectivityData,
50
+ operation,
51
+ };
52
+ setNewConnectivityItems((prev) => [realtimeData, ...prev]);
53
+ }, []);
54
+ // Clear new items when gps device IDs change (herd change)
55
+ const clearNewItems = useCallback(() => {
56
+ setNewConnectivityItems([]);
57
+ }, []);
139
58
  useEffect(() => {
140
59
  if (!scoutSupabase || gpsDeviceIds === "")
141
60
  return;
142
61
  // Clean up existing channels
143
62
  channels.current.forEach((channel) => scoutSupabase.removeChannel(channel));
144
63
  channels.current = [];
64
+ // Clear previous items when switching herds
65
+ clearNewItems();
145
66
  // Create connectivity channel
146
67
  const channel = scoutSupabase
147
68
  .channel(`${activeHerdId}-connectivity`, { config: { private: true } })
148
69
  .on("broadcast", { event: "*" }, handleConnectivityBroadcast)
149
70
  .subscribe();
150
71
  channels.current.push(channel);
151
- // Fetch initial data
152
- fetchInitialData();
153
72
  return () => {
154
73
  channels.current.forEach((ch) => scoutSupabase.removeChannel(ch));
155
74
  channels.current = [];
156
75
  };
157
- }, [scoutSupabase, gpsDeviceIds, activeHerdId, handleConnectivityBroadcast]);
76
+ }, [
77
+ scoutSupabase,
78
+ gpsDeviceIds,
79
+ activeHerdId,
80
+ handleConnectivityBroadcast,
81
+ clearNewItems,
82
+ ]);
83
+ return newConnectivityItems;
158
84
  }
@@ -0,0 +1,3 @@
1
+ import { SupabaseClient } from "@supabase/supabase-js";
2
+ import { Database } from "../types/supabase";
3
+ export declare function useScoutRealtimeDevices(scoutSupabase: SupabaseClient<Database>): void;
@@ -0,0 +1,55 @@
1
+ "use client";
2
+ import { useAppDispatch } from "../store/hooks";
3
+ import { useSelector } from "react-redux";
4
+ import { useEffect, useRef, useCallback } from "react";
5
+ import { addDevice, deleteDevice, updateDevice } from "../store/scout";
6
+ export function useScoutRealtimeDevices(scoutSupabase) {
7
+ const channels = useRef([]);
8
+ const dispatch = useAppDispatch();
9
+ const activeHerdId = useSelector((state) => state.scout.active_herd_id);
10
+ // Device broadcast handler
11
+ const handleDeviceBroadcast = useCallback((payload) => {
12
+ console.log("[Devices] Broadcast received:", payload.payload.operation);
13
+ const data = payload.payload;
14
+ switch (data.operation) {
15
+ case "INSERT":
16
+ if (data.record)
17
+ dispatch(addDevice(data.record));
18
+ break;
19
+ case "UPDATE":
20
+ if (data.record)
21
+ dispatch(updateDevice(data.record));
22
+ break;
23
+ case "DELETE":
24
+ if (data.old_record)
25
+ dispatch(deleteDevice(data.old_record));
26
+ break;
27
+ }
28
+ }, [dispatch]);
29
+ const cleanupChannels = () => {
30
+ channels.current.forEach((channel) => scoutSupabase.removeChannel(channel));
31
+ channels.current = [];
32
+ };
33
+ const createDevicesChannel = (herdId) => {
34
+ return scoutSupabase
35
+ .channel(`${herdId}-devices`, { config: { private: true } })
36
+ .on("broadcast", { event: "*" }, handleDeviceBroadcast)
37
+ .subscribe((status) => {
38
+ if (status === "SUBSCRIBED") {
39
+ console.log(`[Devices] ✅ Connected to herd ${herdId}`);
40
+ }
41
+ else if (status === "CHANNEL_ERROR") {
42
+ console.warn(`[Devices] 🟡 Failed to connect to herd ${herdId}`);
43
+ }
44
+ });
45
+ };
46
+ useEffect(() => {
47
+ cleanupChannels();
48
+ // Create devices channel for active herd
49
+ if (activeHerdId) {
50
+ const channel = createDevicesChannel(activeHerdId);
51
+ channels.current.push(channel);
52
+ }
53
+ return cleanupChannels;
54
+ }, [activeHerdId]);
55
+ }
@@ -1,3 +1,5 @@
1
1
  import { SupabaseClient } from "@supabase/supabase-js";
2
2
  import { Database } from "../types/supabase";
3
- export declare function useScoutRealtimeDevices(scoutSupabase: SupabaseClient<Database>): void;
3
+ import { IDevicePrettyLocation } from "../types/db";
4
+ import { RealtimeData } from "../types/realtime";
5
+ export declare function useScoutRealtimeDevices(scoutSupabase: SupabaseClient<Database>): RealtimeData<IDevicePrettyLocation>[];
@@ -1,31 +1,55 @@
1
1
  "use client";
2
2
  import { useAppDispatch } from "../store/hooks";
3
3
  import { useSelector } from "react-redux";
4
- import { useEffect, useRef, useCallback } from "react";
4
+ import { useEffect, useRef, useCallback, useState } from "react";
5
5
  import { addDevice, deleteDevice, updateDevice } from "../store/scout";
6
+ import { EnumRealtimeOperation } from "../types/realtime";
6
7
  export function useScoutRealtimeDevices(scoutSupabase) {
7
8
  const channels = useRef([]);
8
9
  const dispatch = useAppDispatch();
10
+ const [newDeviceItems, setNewDeviceItems] = useState([]);
9
11
  const activeHerdId = useSelector((state) => state.scout.active_herd_id);
10
12
  // Device broadcast handler
11
13
  const handleDeviceBroadcast = useCallback((payload) => {
12
14
  console.log("[Devices] Broadcast received:", payload.payload.operation);
13
15
  const data = payload.payload;
16
+ const deviceData = data.record || data.old_record;
17
+ if (!deviceData)
18
+ return;
19
+ let operation;
14
20
  switch (data.operation) {
15
21
  case "INSERT":
16
- if (data.record)
22
+ operation = EnumRealtimeOperation.INSERT;
23
+ if (data.record) {
24
+ console.log("[Devices] New device received:", data.record);
17
25
  dispatch(addDevice(data.record));
26
+ }
18
27
  break;
19
28
  case "UPDATE":
20
- if (data.record)
29
+ operation = EnumRealtimeOperation.UPDATE;
30
+ if (data.record) {
21
31
  dispatch(updateDevice(data.record));
32
+ }
22
33
  break;
23
34
  case "DELETE":
24
- if (data.old_record)
35
+ operation = EnumRealtimeOperation.DELETE;
36
+ if (data.old_record) {
25
37
  dispatch(deleteDevice(data.old_record));
38
+ }
26
39
  break;
40
+ default:
41
+ return;
27
42
  }
43
+ const realtimeData = {
44
+ data: deviceData,
45
+ operation,
46
+ };
47
+ setNewDeviceItems((prev) => [realtimeData, ...prev]);
28
48
  }, [dispatch]);
49
+ // Clear new items when herd changes
50
+ const clearNewItems = useCallback(() => {
51
+ setNewDeviceItems([]);
52
+ }, []);
29
53
  const cleanupChannels = () => {
30
54
  channels.current.forEach((channel) => scoutSupabase.removeChannel(channel));
31
55
  channels.current = [];
@@ -45,11 +69,14 @@ export function useScoutRealtimeDevices(scoutSupabase) {
45
69
  };
46
70
  useEffect(() => {
47
71
  cleanupChannels();
72
+ // Clear previous items when switching herds
73
+ clearNewItems();
48
74
  // Create devices channel for active herd
49
75
  if (activeHerdId) {
50
76
  const channel = createDevicesChannel(activeHerdId);
51
77
  channels.current.push(channel);
52
78
  }
53
79
  return cleanupChannels;
54
- }, [activeHerdId]);
80
+ }, [activeHerdId, clearNewItems]);
81
+ return newDeviceItems;
55
82
  }
@@ -807,6 +807,45 @@ export declare function useSupabase(): SupabaseClient<Database, "public", "publi
807
807
  };
808
808
  };
809
809
  Functions: {
810
+ analyze_device_heartbeats: {
811
+ Args: {
812
+ p_device_id: number;
813
+ p_lookback_minutes?: number;
814
+ p_window_minutes?: number;
815
+ };
816
+ Returns: Database["public"]["CompositeTypes"]["device_heartbeat_analysis"];
817
+ SetofOptions: {
818
+ from: "*";
819
+ to: "device_heartbeat_analysis";
820
+ isOneToOne: true;
821
+ isSetofReturn: false;
822
+ };
823
+ };
824
+ analyze_herd_device_heartbeats: {
825
+ Args: {
826
+ p_device_types?: Database["public"]["Enums"]["device_type"][];
827
+ p_herd_id: number;
828
+ p_lookback_minutes?: number;
829
+ p_window_minutes?: number;
830
+ };
831
+ Returns: Database["public"]["CompositeTypes"]["device_heartbeat_analysis"][];
832
+ SetofOptions: {
833
+ from: "*";
834
+ to: "device_heartbeat_analysis";
835
+ isOneToOne: false;
836
+ isSetofReturn: true;
837
+ };
838
+ };
839
+ check_realtime_schema_status: {
840
+ Args: never;
841
+ Returns: {
842
+ check_type: string;
843
+ details: string;
844
+ schema_name: string;
845
+ status: string;
846
+ table_name: string;
847
+ }[];
848
+ };
810
849
  get_connectivity_with_coordinates: {
811
850
  Args: {
812
851
  session_id_caller: number;
@@ -942,6 +981,22 @@ export declare function useSupabase(): SupabaseClient<Database, "public", "publi
942
981
  isSetofReturn: true;
943
982
  };
944
983
  };
984
+ get_herd_uptime_summary: {
985
+ Args: {
986
+ p_device_types?: Database["public"]["Enums"]["device_type"][];
987
+ p_herd_id: number;
988
+ p_lookback_minutes?: number;
989
+ p_window_minutes?: number;
990
+ };
991
+ Returns: {
992
+ average_heartbeat_interval: number;
993
+ offline_devices: number;
994
+ online_devices: number;
995
+ overall_uptime_percentage: number;
996
+ total_devices: number;
997
+ total_heartbeats: number;
998
+ }[];
999
+ };
945
1000
  get_sessions_with_coordinates: {
946
1001
  Args: {
947
1002
  herd_id_caller: number;
@@ -1048,6 +1103,19 @@ export declare function useSupabase(): SupabaseClient<Database, "public", "publi
1048
1103
  h11_index: string | null;
1049
1104
  battery_percentage: number | null;
1050
1105
  };
1106
+ device_heartbeat_analysis: {
1107
+ device_id: number | null;
1108
+ is_online: boolean | null;
1109
+ last_heartbeat_time: string | null;
1110
+ minutes_since_last_heartbeat: number | null;
1111
+ heartbeat_history: boolean[] | null;
1112
+ uptime_percentage: number | null;
1113
+ heartbeat_intervals: number[] | null;
1114
+ average_heartbeat_interval: number | null;
1115
+ total_heartbeats: number | null;
1116
+ analysis_window_start: string | null;
1117
+ analysis_window_end: string | null;
1118
+ };
1051
1119
  device_pretty_location: {
1052
1120
  id: number | null;
1053
1121
  inserted_at: string | null;
@@ -3,8 +3,6 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useScoutRefresh } from "../hooks/useScoutRefresh";
4
4
  import { createContext, useContext, useMemo, useRef } from "react";
5
5
  import { createBrowserClient } from "@supabase/ssr";
6
- import { useScoutRealtimeConnectivity } from "../hooks/useScoutRealtimeConnectivity";
7
- import { useScoutRealtimeDevices } from "../hooks/useScoutRealtimeDevices";
8
6
  // Create context for the Supabase client
9
7
  const SupabaseContext = createContext(null);
10
8
  const ConnectionStatusContext = createContext(null);
@@ -33,9 +31,6 @@ export function ScoutRefreshProvider({ children }) {
33
31
  console.log("[ScoutRefreshProvider] Creating Supabase client");
34
32
  return createBrowserClient(urlRef.current, anonKeyRef.current);
35
33
  }, []); // Empty dependency array ensures this only runs once
36
- // Use the enhanced DB listener with connection status
37
- useScoutRealtimeConnectivity(supabaseClient);
38
- useScoutRealtimeDevices(supabaseClient);
39
34
  useScoutRefresh();
40
35
  // // Log connection status changes for debugging
41
36
  // if (connectionStatus.lastError) {
@@ -32,6 +32,8 @@ export type IEventAndTagsPrettyLocation = Database["public"]["CompositeTypes"]["
32
32
  export type IZonesAndActionsPrettyLocation = Database["public"]["CompositeTypes"]["zones_and_actions_pretty_location"];
33
33
  export type ISessionWithCoordinates = Database["public"]["CompositeTypes"]["session_with_coordinates"];
34
34
  export type IConnectivityWithCoordinates = Database["public"]["CompositeTypes"]["connectivity_with_coordinates"];
35
+ export type IDeviceHeartbeatAnalysis = Database["public"]["CompositeTypes"]["device_heartbeat_analysis"];
36
+ export type IHerdUptimeSummary = Database["public"]["Functions"]["get_herd_uptime_summary"]["Returns"][0];
35
37
  export interface IZoneWithActions extends IZone {
36
38
  actions: IAction[];
37
39
  }
@@ -5,7 +5,6 @@ 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
7
  import { server_get_providers_by_herd } from "../helpers/providers";
8
- 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";
11
10
  import { server_get_more_zones_and_actions_for_herd } from "../helpers/zones";
@@ -64,42 +63,8 @@ export class HerdModule {
64
63
  static async from_herd(herd, client) {
65
64
  const startTime = Date.now();
66
65
  try {
67
- // load devices
68
- let response_new_devices = await get_devices_by_herd(herd.id, client);
69
- if (response_new_devices.status == EnumWebResponse.ERROR ||
70
- !response_new_devices.data) {
71
- console.warn(`[HerdModule] No devices found for herd ${herd.id}`);
72
- return new HerdModule(herd, [], [], Date.now());
73
- }
74
- const new_devices = response_new_devices.data;
75
- // get api keys and events for all devices in batch
76
- let recent_events_batch = {};
77
- if (new_devices.length > 0) {
78
- try {
79
- const device_ids = new_devices.map((device) => device.id ?? 0);
80
- // Load API keys and events in parallel
81
- const [api_keys_batch, events_response] = await Promise.all([
82
- server_list_api_keys_batch(device_ids),
83
- server_get_events_and_tags_for_devices_batch(device_ids, 1),
84
- ]);
85
- // Assign API keys to devices
86
- for (let i = 0; i < new_devices.length; i++) {
87
- const device_id = new_devices[i].id ?? 0;
88
- new_devices[i].api_keys_scout = api_keys_batch[device_id] || [];
89
- }
90
- // Process events response
91
- if (events_response.status === EnumWebResponse.SUCCESS &&
92
- events_response.data) {
93
- recent_events_batch = events_response.data;
94
- }
95
- }
96
- catch (error) {
97
- console.error(`[HerdModule] Batch load error:`, error);
98
- // Continue without API keys and events
99
- }
100
- }
101
- // 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([
66
+ // Start loading herd-level data in parallel with devices
67
+ const herdLevelPromises = Promise.allSettled([
103
68
  server_get_more_zones_and_actions_for_herd(herd.id, 0, 10).catch((error) => {
104
69
  console.warn(`[HerdModule] Failed to get zones and actions:`, error);
105
70
  return { status: EnumWebResponse.ERROR, data: null };
@@ -133,20 +98,38 @@ export class HerdModule {
133
98
  return { status: EnumWebResponse.ERROR, data: null };
134
99
  }),
135
100
  ]);
136
- // Assign recent events to devices from batch results
137
- for (let i = 0; i < new_devices.length; i++) {
101
+ // Load devices
102
+ const devicesPromise = get_devices_by_herd(herd.id, client);
103
+ // Wait for both devices and herd-level data
104
+ const [deviceResponse, herdLevelResults] = await Promise.all([
105
+ devicesPromise,
106
+ herdLevelPromises,
107
+ ]);
108
+ // Check devices response
109
+ if (deviceResponse.status == EnumWebResponse.ERROR ||
110
+ !deviceResponse.data) {
111
+ console.warn(`[HerdModule] No devices found for herd ${herd.id}`);
112
+ return new HerdModule(herd, [], [], Date.now());
113
+ }
114
+ const new_devices = deviceResponse.data;
115
+ // Load API keys for devices if we have any
116
+ if (new_devices.length > 0) {
138
117
  try {
139
- const device_id = new_devices[i].id ?? 0;
140
- const events = recent_events_batch[device_id];
141
- if (events) {
142
- new_devices[i].recent_events = events;
118
+ const device_ids = new_devices.map((device) => device.id ?? 0);
119
+ const api_keys_batch = await server_list_api_keys_batch(device_ids);
120
+ // Assign API keys to devices
121
+ for (let i = 0; i < new_devices.length; i++) {
122
+ const device_id = new_devices[i].id ?? 0;
123
+ new_devices[i].api_keys_scout = api_keys_batch[device_id] || [];
143
124
  }
144
125
  }
145
126
  catch (error) {
146
- console.warn(`Failed to process events for device ${new_devices[i].id}:`, error);
127
+ console.error(`[HerdModule] Failed to load API keys:`, error);
128
+ // Continue without API keys
147
129
  }
148
130
  }
149
- // Extract data with safe fallbacks
131
+ // Extract herd-level data with safe fallbacks
132
+ const [res_zones, res_user_roles, total_event_count, res_plans, res_sessions, res_layers, res_providers,] = herdLevelResults;
150
133
  const zones = res_zones.status === "fulfilled" && res_zones.value?.data
151
134
  ? res_zones.value.data
152
135
  : [];
@@ -0,0 +1,9 @@
1
+ export declare enum EnumRealtimeOperation {
2
+ INSERT = 0,
3
+ UPDATE = 1,
4
+ DELETE = 2
5
+ }
6
+ export type RealtimeData<T> = {
7
+ data: T;
8
+ operation: EnumRealtimeOperation;
9
+ };
@@ -0,0 +1,6 @@
1
+ export var EnumRealtimeOperation;
2
+ (function (EnumRealtimeOperation) {
3
+ EnumRealtimeOperation[EnumRealtimeOperation["INSERT"] = 0] = "INSERT";
4
+ EnumRealtimeOperation[EnumRealtimeOperation["UPDATE"] = 1] = "UPDATE";
5
+ EnumRealtimeOperation[EnumRealtimeOperation["DELETE"] = 2] = "DELETE";
6
+ })(EnumRealtimeOperation || (EnumRealtimeOperation = {}));
@@ -855,6 +855,45 @@ export type Database = {
855
855
  };
856
856
  };
857
857
  Functions: {
858
+ analyze_device_heartbeats: {
859
+ Args: {
860
+ p_device_id: number;
861
+ p_lookback_minutes?: number;
862
+ p_window_minutes?: number;
863
+ };
864
+ Returns: Database["public"]["CompositeTypes"]["device_heartbeat_analysis"];
865
+ SetofOptions: {
866
+ from: "*";
867
+ to: "device_heartbeat_analysis";
868
+ isOneToOne: true;
869
+ isSetofReturn: false;
870
+ };
871
+ };
872
+ analyze_herd_device_heartbeats: {
873
+ Args: {
874
+ p_device_types?: Database["public"]["Enums"]["device_type"][];
875
+ p_herd_id: number;
876
+ p_lookback_minutes?: number;
877
+ p_window_minutes?: number;
878
+ };
879
+ Returns: Database["public"]["CompositeTypes"]["device_heartbeat_analysis"][];
880
+ SetofOptions: {
881
+ from: "*";
882
+ to: "device_heartbeat_analysis";
883
+ isOneToOne: false;
884
+ isSetofReturn: true;
885
+ };
886
+ };
887
+ check_realtime_schema_status: {
888
+ Args: never;
889
+ Returns: {
890
+ check_type: string;
891
+ details: string;
892
+ schema_name: string;
893
+ status: string;
894
+ table_name: string;
895
+ }[];
896
+ };
858
897
  get_connectivity_with_coordinates: {
859
898
  Args: {
860
899
  session_id_caller: number;
@@ -990,6 +1029,22 @@ export type Database = {
990
1029
  isSetofReturn: true;
991
1030
  };
992
1031
  };
1032
+ get_herd_uptime_summary: {
1033
+ Args: {
1034
+ p_device_types?: Database["public"]["Enums"]["device_type"][];
1035
+ p_herd_id: number;
1036
+ p_lookback_minutes?: number;
1037
+ p_window_minutes?: number;
1038
+ };
1039
+ Returns: {
1040
+ average_heartbeat_interval: number;
1041
+ offline_devices: number;
1042
+ online_devices: number;
1043
+ overall_uptime_percentage: number;
1044
+ total_devices: number;
1045
+ total_heartbeats: number;
1046
+ }[];
1047
+ };
993
1048
  get_sessions_with_coordinates: {
994
1049
  Args: {
995
1050
  herd_id_caller: number;
@@ -1096,6 +1151,19 @@ export type Database = {
1096
1151
  h11_index: string | null;
1097
1152
  battery_percentage: number | null;
1098
1153
  };
1154
+ device_heartbeat_analysis: {
1155
+ device_id: number | null;
1156
+ is_online: boolean | null;
1157
+ last_heartbeat_time: string | null;
1158
+ minutes_since_last_heartbeat: number | null;
1159
+ heartbeat_history: boolean[] | null;
1160
+ uptime_percentage: number | null;
1161
+ heartbeat_intervals: number[] | null;
1162
+ average_heartbeat_interval: number | null;
1163
+ total_heartbeats: number | null;
1164
+ analysis_window_start: string | null;
1165
+ analysis_window_end: string | null;
1166
+ };
1099
1167
  device_pretty_location: {
1100
1168
  id: number | null;
1101
1169
  inserted_at: string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adventurelabs/scout-core",
3
- "version": "1.0.100",
3
+ "version": "1.0.102",
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",