@adventurelabs/scout-core 1.0.101 → 1.0.103

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,5 @@
1
+ import { IWebResponseCompatible } from "../types/requests";
2
+ import { IOperator } from "../types/db";
3
+ export declare function server_get_operators_by_session_id(sessionId: number): Promise<IWebResponseCompatible<IOperator[]>>;
4
+ export declare function server_get_operators_by_user_id(userId: string): Promise<IWebResponseCompatible<IOperator[]>>;
5
+ export declare function server_get_operators_by_session_id_filtered(sessionId: number, action?: string, timestampAfter?: string): Promise<IWebResponseCompatible<IOperator[]>>;
@@ -0,0 +1,64 @@
1
+ "use server";
2
+ import { newServerClient } from "../supabase/server";
3
+ import { EnumWebResponse, IWebResponse, } from "../types/requests";
4
+ // Get operators by session id (server id)
5
+ export async function server_get_operators_by_session_id(sessionId) {
6
+ const supabase = await newServerClient();
7
+ const { data, error } = await supabase
8
+ .from("operators")
9
+ .select("*")
10
+ .eq("session_id", sessionId)
11
+ .order("created_at", { ascending: false });
12
+ if (error) {
13
+ console.warn("Error fetching operators by session id:", error.message);
14
+ return {
15
+ status: EnumWebResponse.ERROR,
16
+ msg: error.message,
17
+ data: [],
18
+ };
19
+ }
20
+ return IWebResponse.success(data || []).to_compatible();
21
+ }
22
+ // Get all operators for a specific user
23
+ export async function server_get_operators_by_user_id(userId) {
24
+ const supabase = await newServerClient();
25
+ const { data, error } = await supabase
26
+ .from("operators")
27
+ .select("*")
28
+ .eq("user_id", userId)
29
+ .order("created_at", { ascending: false });
30
+ if (error) {
31
+ console.warn("Error fetching operators by user id:", error.message);
32
+ return {
33
+ status: EnumWebResponse.ERROR,
34
+ msg: error.message,
35
+ data: [],
36
+ };
37
+ }
38
+ return IWebResponse.success(data || []).to_compatible();
39
+ }
40
+ // Get operators by session id with additional filters
41
+ export async function server_get_operators_by_session_id_filtered(sessionId, action, timestampAfter) {
42
+ const supabase = await newServerClient();
43
+ let query = supabase
44
+ .from("operators")
45
+ .select("*")
46
+ .eq("session_id", sessionId);
47
+ // Apply optional filters
48
+ if (action) {
49
+ query = query.eq("action", action);
50
+ }
51
+ if (timestampAfter) {
52
+ query = query.gte("timestamp", timestampAfter);
53
+ }
54
+ const { data, error } = await query.order("timestamp", { ascending: false });
55
+ if (error) {
56
+ console.warn("Error fetching filtered operators by session id:", error.message);
57
+ return {
58
+ status: EnumWebResponse.ERROR,
59
+ msg: error.message,
60
+ data: [],
61
+ };
62
+ }
63
+ return IWebResponse.success(data || []).to_compatible();
64
+ }
@@ -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 { getHoursAgoTimestamp } 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,125 +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
- 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
- }
36
+ operation = EnumRealtimeOperation.INSERT;
59
37
  break;
60
38
  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
- }
39
+ operation = EnumRealtimeOperation.UPDATE;
81
40
  break;
82
41
  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
- }
42
+ operation = EnumRealtimeOperation.DELETE;
103
43
  break;
44
+ default:
45
+ return;
104
46
  }
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]);
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
+ }, []);
140
58
  useEffect(() => {
141
59
  if (!scoutSupabase || gpsDeviceIds === "")
142
60
  return;
143
61
  // Clean up existing channels
144
62
  channels.current.forEach((channel) => scoutSupabase.removeChannel(channel));
145
63
  channels.current = [];
64
+ // Clear previous items when switching herds
65
+ clearNewItems();
146
66
  // Create connectivity channel
147
67
  const channel = scoutSupabase
148
68
  .channel(`${activeHerdId}-connectivity`, { config: { private: true } })
149
69
  .on("broadcast", { event: "*" }, handleConnectivityBroadcast)
150
70
  .subscribe();
151
71
  channels.current.push(channel);
152
- // Fetch initial data
153
- fetchInitialData();
154
72
  return () => {
155
73
  channels.current.forEach((ch) => scoutSupabase.removeChannel(ch));
156
74
  channels.current = [];
157
75
  };
158
- }, [scoutSupabase, gpsDeviceIds, activeHerdId, handleConnectivityBroadcast]);
76
+ }, [
77
+ scoutSupabase,
78
+ gpsDeviceIds,
79
+ activeHerdId,
80
+ handleConnectivityBroadcast,
81
+ clearNewItems,
82
+ ]);
83
+ return newConnectivityItems;
159
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) {
@@ -22,6 +22,7 @@ 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
24
  export type IHeartbeat = Database["public"]["Tables"]["heartbeats"]["Row"];
25
+ export type IOperator = Database["public"]["Tables"]["operators"]["Row"];
25
26
  export type IProvider = Database["public"]["Tables"]["providers"]["Row"];
26
27
  export type IEventWithTags = Database["public"]["CompositeTypes"]["event_with_tags"] & {
27
28
  earthranger_url: string | null;
@@ -32,6 +33,8 @@ export type IEventAndTagsPrettyLocation = Database["public"]["CompositeTypes"]["
32
33
  export type IZonesAndActionsPrettyLocation = Database["public"]["CompositeTypes"]["zones_and_actions_pretty_location"];
33
34
  export type ISessionWithCoordinates = Database["public"]["CompositeTypes"]["session_with_coordinates"];
34
35
  export type IConnectivityWithCoordinates = Database["public"]["CompositeTypes"]["connectivity_with_coordinates"];
36
+ export type IDeviceHeartbeatAnalysis = Database["public"]["CompositeTypes"]["device_heartbeat_analysis"];
37
+ export type IHerdUptimeSummary = Database["public"]["Functions"]["get_herd_uptime_summary"]["Returns"][0];
35
38
  export interface IZoneWithActions extends IZone {
36
39
  actions: IAction[];
37
40
  }
@@ -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.101",
3
+ "version": "1.0.103",
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",