@adventurelabs/scout-core 1.0.88 → 1.0.89

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,3 @@
1
+ import { IConnectivityWithCoordinates, IWebResponseCompatible } from "@/types";
2
+ export declare function server_get_connectivity_by_session_id(sessionId: number): Promise<IWebResponseCompatible<IConnectivityWithCoordinates[]>>;
3
+ export declare function server_get_connectivity_by_device_id(deviceId: number, timestamp: string): Promise<IWebResponseCompatible<IConnectivityWithCoordinates[]>>;
@@ -0,0 +1,45 @@
1
+ import { newServerClient } from "@/supabase";
2
+ import { EnumWebResponse, IWebResponse, } from "@/types";
3
+ // Get connectivity by session id using RPC function with coordinates
4
+ export async function server_get_connectivity_by_session_id(sessionId) {
5
+ const supabase = await newServerClient();
6
+ const { data, error } = await supabase.rpc("get_connectivity_with_coordinates", { session_id_caller: sessionId });
7
+ if (error) {
8
+ console.warn("Error fetching connectivity by session id:", error.message);
9
+ return {
10
+ status: EnumWebResponse.ERROR,
11
+ msg: error.message,
12
+ data: [],
13
+ };
14
+ }
15
+ // Sort by timestamp_start in ascending order
16
+ const sortedConnectivity = (data || []).sort((a, b) => {
17
+ if (!a.timestamp_start || !b.timestamp_start)
18
+ return 0;
19
+ return (new Date(a.timestamp_start).getTime() -
20
+ new Date(b.timestamp_start).getTime());
21
+ });
22
+ return IWebResponse.success(sortedConnectivity).to_compatible();
23
+ }
24
+ // Get all connectivity items after a specific timestamp, filtered by device ID
25
+ // Timestamp should be formatted as YYYY-MM-DDTHH:mm:ss.SSSZ
26
+ export async function server_get_connectivity_by_device_id(deviceId, timestamp) {
27
+ const supabase = await newServerClient();
28
+ const { data, error } = await supabase.rpc("get_connectivity_with_coordinates_by_device_and_timestamp", { device_id_caller: deviceId, timestamp_filter: timestamp });
29
+ if (error) {
30
+ console.warn("Error fetching connectivity by session id:", error.message);
31
+ return {
32
+ status: EnumWebResponse.ERROR,
33
+ msg: error.message,
34
+ data: [],
35
+ };
36
+ }
37
+ // Sort by timestamp_start in ascending order
38
+ const sortedConnectivity = (data || []).sort((a, b) => {
39
+ if (!a.timestamp_start || !b.timestamp_start)
40
+ return 0;
41
+ return (new Date(a.timestamp_start).getTime() -
42
+ new Date(b.timestamp_start).getTime());
43
+ });
44
+ return IWebResponse.success(sortedConnectivity).to_compatible();
45
+ }
@@ -11,7 +11,6 @@ export type ConnectivityUpdateInput = Partial<ConnectivityInput> & {
11
11
  };
12
12
  export type ConnectivityUpsertInput = ConnectivityInput | ConnectivityUpdateInput;
13
13
  export declare function server_get_sessions_by_herd_id(herdId: number): Promise<IWebResponseCompatible<ISessionWithCoordinates[]>>;
14
- export declare function server_get_connectivity_by_session_id(sessionId: number): Promise<IWebResponseCompatible<IConnectivityWithCoordinates[]>>;
15
14
  export declare function server_get_events_by_session_id(sessionId: number): Promise<IWebResponseCompatible<IEvent[]>>;
16
15
  export declare function server_get_events_and_tags_by_session_id(sessionId: number, limit?: number, offset?: number): Promise<IWebResponseCompatible<IEventAndTagsPrettyLocation[]>>;
17
16
  export declare function server_get_total_events_for_session(sessionId: number): Promise<IWebResponseCompatible<number>>;
@@ -1,6 +1,7 @@
1
1
  "use server";
2
2
  import { newServerClient } from "../supabase/server";
3
3
  import { EnumWebResponse, IWebResponse, } from "../types/requests";
4
+ import { server_get_connectivity_by_session_id } from "./connectivity";
4
5
  // Get sessions by herd id using RPC function with coordinates
5
6
  export async function server_get_sessions_by_herd_id(herdId) {
6
7
  const supabase = await newServerClient();
@@ -24,27 +25,6 @@ export async function server_get_sessions_by_herd_id(herdId) {
24
25
  });
25
26
  return IWebResponse.success(sortedSessions).to_compatible();
26
27
  }
27
- // Get connectivity by session id using RPC function with coordinates
28
- export async function server_get_connectivity_by_session_id(sessionId) {
29
- const supabase = await newServerClient();
30
- const { data, error } = await supabase.rpc("get_connectivity_with_coordinates", { session_id_caller: sessionId });
31
- if (error) {
32
- console.warn("Error fetching connectivity by session id:", error.message);
33
- return {
34
- status: EnumWebResponse.ERROR,
35
- msg: error.message,
36
- data: [],
37
- };
38
- }
39
- // Sort by timestamp_start in ascending order
40
- const sortedConnectivity = (data || []).sort((a, b) => {
41
- if (!a.timestamp_start || !b.timestamp_start)
42
- return 0;
43
- return (new Date(a.timestamp_start).getTime() -
44
- new Date(b.timestamp_start).getTime());
45
- });
46
- return IWebResponse.success(sortedConnectivity).to_compatible();
47
- }
48
28
  // Get events by session id
49
29
  export async function server_get_events_by_session_id(sessionId) {
50
30
  const supabase = await newServerClient();
@@ -1,3 +1,7 @@
1
1
  export declare function convertSecondsSinceEpochToDate(secondsSinceEpoch: number): Date;
2
2
  export declare function convertIsoStringToDate(isoString: string): Date;
3
3
  export declare function convertDateToTimeString(date: Date): string;
4
+ export declare function formatTimestampForDatabase(date: Date): string;
5
+ export declare function getDaysAgoTimestamp(daysAgo: number): string;
6
+ export declare function getHoursAgoTimestamp(hoursAgo: number): string;
7
+ export declare function getMinutesAgoTimestamp(minutesAgo: number): string;
@@ -9,3 +9,26 @@ export function convertIsoStringToDate(isoString) {
9
9
  export function convertDateToTimeString(date) {
10
10
  return `${date.toLocaleTimeString()}, ${date.toLocaleDateString()} UTC`;
11
11
  }
12
+ // Format a Date object as a PostgreSQL-compatible timestamp string
13
+ // Returns format: "YYYY-MM-DDTHH:MM:SS.SSSZ" (ISO 8601 with timezone)
14
+ export function formatTimestampForDatabase(date) {
15
+ return date.toISOString();
16
+ }
17
+ // Get a timestamp for N days ago, formatted for database queries
18
+ export function getDaysAgoTimestamp(daysAgo) {
19
+ const date = new Date();
20
+ date.setDate(date.getDate() - daysAgo);
21
+ return formatTimestampForDatabase(date);
22
+ }
23
+ // Get a timestamp for N hours ago, formatted for database queries
24
+ export function getHoursAgoTimestamp(hoursAgo) {
25
+ const date = new Date();
26
+ date.setHours(date.getHours() - hoursAgo);
27
+ return formatTimestampForDatabase(date);
28
+ }
29
+ // Get a timestamp for N minutes ago, formatted for database queries
30
+ export function getMinutesAgoTimestamp(minutesAgo) {
31
+ const date = new Date();
32
+ date.setMinutes(date.getMinutes() - minutesAgo);
33
+ return formatTimestampForDatabase(date);
34
+ }
@@ -1,2 +1,3 @@
1
- export { useScoutDbListener } from "./useScoutDbListener";
2
1
  export { useScoutRefresh, type UseScoutRefreshOptions, } from "./useScoutRefresh";
2
+ export { useScoutRealtimeConnectivity } from "./useScoutRealtimeConnectivity";
3
+ export { useScoutRealtimeDevices } from "./useScoutRealtimeDevices";
@@ -1,2 +1,3 @@
1
- export { useScoutDbListener } from "./useScoutDbListener";
2
1
  export { useScoutRefresh, } from "./useScoutRefresh";
2
+ export { useScoutRealtimeConnectivity } from "./useScoutRealtimeConnectivity";
3
+ export { useScoutRealtimeDevices } from "./useScoutRealtimeDevices";
@@ -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,157 @@
1
+ "use client";
2
+ import { useAppDispatch } from "../store/hooks";
3
+ import { useSelector } from "react-redux";
4
+ import { useEffect, useRef, useCallback, useState } 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";
9
+ export function useScoutRealtimeConnectivity(scoutSupabase) {
10
+ const channels = useRef([]);
11
+ const dispatch = useAppDispatch();
12
+ const [isLoading, setIsLoading] = useState(false);
13
+ const activeHerdId = useSelector((state) => state.scout.active_herd_id);
14
+ const activeHerdGpsTrackersConnectivity = useSelector((state) => state.scout.active_herd_gps_trackers_connectivity);
15
+ const herdModules = useSelector((state) => state.scout.herd_modules);
16
+ // Connectivity broadcast handler
17
+ const handleConnectivityBroadcast = useCallback((payload) => {
18
+ console.log("[Connectivity] Broadcast received:", payload.payload.event);
19
+ const event = payload.payload.event;
20
+ const data = payload.payload;
21
+ const connectivityData = data.new || data.old;
22
+ // Only process tracker connectivity data (no session_id)
23
+ if (!connectivityData || connectivityData.session_id) {
24
+ return;
25
+ }
26
+ const deviceId = connectivityData.device_id;
27
+ if (!deviceId)
28
+ return;
29
+ const currentConnectivity = {
30
+ ...activeHerdGpsTrackersConnectivity,
31
+ };
32
+ console.log("[Connectivity] Current connectivity:", currentConnectivity);
33
+ switch (event) {
34
+ case "INSERT":
35
+ if (!currentConnectivity[deviceId]) {
36
+ currentConnectivity[deviceId] = [];
37
+ }
38
+ currentConnectivity[deviceId].push(connectivityData);
39
+ // Keep only recent 100 entries
40
+ if (currentConnectivity[deviceId].length > 100) {
41
+ currentConnectivity[deviceId] = currentConnectivity[deviceId]
42
+ .sort((a, b) => new Date(b.timestamp_start || 0).getTime() -
43
+ new Date(a.timestamp_start || 0).getTime())
44
+ .slice(0, 100);
45
+ }
46
+ break;
47
+ case "UPDATE":
48
+ if (currentConnectivity[deviceId]) {
49
+ const index = currentConnectivity[deviceId].findIndex((c) => c.id === connectivityData.id);
50
+ if (index >= 0) {
51
+ currentConnectivity[deviceId][index] = connectivityData;
52
+ }
53
+ }
54
+ break;
55
+ case "DELETE":
56
+ if (currentConnectivity[deviceId]) {
57
+ currentConnectivity[deviceId] = currentConnectivity[deviceId].filter((c) => c.id !== connectivityData.id);
58
+ if (currentConnectivity[deviceId].length === 0) {
59
+ delete currentConnectivity[deviceId];
60
+ }
61
+ }
62
+ break;
63
+ }
64
+ dispatch(setActiveHerdGpsTrackersConnectivity(currentConnectivity));
65
+ }, [activeHerdGpsTrackersConnectivity, dispatch]);
66
+ const cleanupChannels = () => {
67
+ channels.current.forEach((channel) => scoutSupabase.removeChannel(channel));
68
+ channels.current = [];
69
+ };
70
+ const createConnectivityChannel = (herdId) => {
71
+ return scoutSupabase
72
+ .channel(`${herdId}-connectivity`, { config: { private: true } })
73
+ .on("broadcast", { event: "*" }, handleConnectivityBroadcast)
74
+ .subscribe((status) => {
75
+ if (status === "SUBSCRIBED") {
76
+ console.log(`[Connectivity] ✅ Connected to herd ${herdId}`);
77
+ }
78
+ else if (status === "CHANNEL_ERROR") {
79
+ console.error(`[Connectivity] ❌ Failed to connect to herd ${herdId}`);
80
+ }
81
+ });
82
+ };
83
+ // Fetch initial connectivity data for GPS trackers
84
+ const fetchInitialConnectivityData = useCallback(async () => {
85
+ if (!activeHerdId || isLoading)
86
+ return;
87
+ // Find the active herd module
88
+ const activeHerdModule = herdModules.find((hm) => hm.herd.id.toString() === activeHerdId);
89
+ if (!activeHerdModule)
90
+ return;
91
+ // Get GPS tracker devices from the herd
92
+ const gpsTrackerDevices = activeHerdModule.devices.filter((device) => device.device_type === "gps_tracker" ||
93
+ device.device_type === "gps_tracker_vehicle" ||
94
+ device.device_type === "gps_tracker_person");
95
+ if (gpsTrackerDevices.length === 0) {
96
+ console.log("[Connectivity] No GPS trackers found in herd");
97
+ return;
98
+ }
99
+ setIsLoading(true);
100
+ console.log(`[Connectivity] Fetching last day connectivity for ${gpsTrackerDevices.length} GPS trackers`);
101
+ // Calculate timestamp for last 24 hours
102
+ const timestampFilter = getDaysAgoTimestamp(1);
103
+ const connectivityData = {};
104
+ try {
105
+ // Fetch connectivity for each GPS tracker
106
+ await Promise.all(gpsTrackerDevices.map(async (device) => {
107
+ try {
108
+ if (!device.id)
109
+ return;
110
+ const response = await server_get_connectivity_by_device_id(device.id, timestampFilter);
111
+ if (response.status === EnumWebResponse.SUCCESS && response.data) {
112
+ // Filter out any data with session_id (only tracker data)
113
+ const trackerConnectivity = response.data.filter((conn) => !conn.session_id);
114
+ if (trackerConnectivity.length > 0 && device.id) {
115
+ // Keep only most recent 100 entries per device
116
+ connectivityData[device.id] = trackerConnectivity
117
+ .sort((a, b) => new Date(b.timestamp_start || 0).getTime() -
118
+ new Date(a.timestamp_start || 0).getTime())
119
+ .slice(0, 100);
120
+ console.log(`[Connectivity] Loaded ${connectivityData[device.id]?.length} records for device ${device.id}`);
121
+ }
122
+ }
123
+ }
124
+ catch (error) {
125
+ console.warn(`[Connectivity] Failed to fetch data for device ${device.id}:`, error);
126
+ }
127
+ }));
128
+ // Update the store with initial connectivity data
129
+ dispatch(setActiveHerdGpsTrackersConnectivity(connectivityData));
130
+ console.log(`[Connectivity] Initial data loaded for ${Object.keys(connectivityData).length} devices`);
131
+ }
132
+ catch (error) {
133
+ console.error("[Connectivity] Error fetching initial data:", error);
134
+ }
135
+ finally {
136
+ setIsLoading(false);
137
+ }
138
+ }, [activeHerdId, herdModules, isLoading, dispatch]);
139
+ useEffect(() => {
140
+ if (!scoutSupabase)
141
+ return;
142
+ cleanupChannels();
143
+ // Create connectivity channel for active herd
144
+ if (activeHerdId) {
145
+ const channel = createConnectivityChannel(activeHerdId);
146
+ channels.current.push(channel);
147
+ // Fetch initial connectivity data
148
+ fetchInitialConnectivityData();
149
+ }
150
+ return cleanupChannels;
151
+ }, [
152
+ scoutSupabase,
153
+ activeHerdId,
154
+ handleConnectivityBroadcast,
155
+ fetchInitialConnectivityData,
156
+ ]);
157
+ }
@@ -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,58 @@
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.event);
13
+ const event = payload.payload.event;
14
+ const data = payload.payload;
15
+ switch (event) {
16
+ case "INSERT":
17
+ if (data.new)
18
+ dispatch(addDevice(data.new));
19
+ break;
20
+ case "UPDATE":
21
+ if (data.new)
22
+ dispatch(updateDevice(data.new));
23
+ break;
24
+ case "DELETE":
25
+ if (data.old)
26
+ dispatch(deleteDevice(data.old));
27
+ break;
28
+ }
29
+ }, [dispatch]);
30
+ const cleanupChannels = () => {
31
+ channels.current.forEach((channel) => scoutSupabase.removeChannel(channel));
32
+ channels.current = [];
33
+ };
34
+ const createDevicesChannel = (herdId) => {
35
+ return scoutSupabase
36
+ .channel(`${herdId}-devices`, { config: { private: true } })
37
+ .on("broadcast", { event: "*" }, handleDeviceBroadcast)
38
+ .subscribe((status) => {
39
+ if (status === "SUBSCRIBED") {
40
+ console.log(`[Devices] ✅ Connected to herd ${herdId}`);
41
+ }
42
+ else if (status === "CHANNEL_ERROR") {
43
+ console.error(`[Devices] ❌ Failed to connect to herd ${herdId}`);
44
+ }
45
+ });
46
+ };
47
+ useEffect(() => {
48
+ if (!scoutSupabase)
49
+ return;
50
+ cleanupChannels();
51
+ // Create devices channel for active herd
52
+ if (activeHerdId) {
53
+ const channel = createDevicesChannel(activeHerdId);
54
+ channels.current.push(channel);
55
+ }
56
+ return cleanupChannels;
57
+ }, [scoutSupabase, activeHerdId, handleDeviceBroadcast]);
58
+ }
package/dist/index.d.ts CHANGED
@@ -9,9 +9,11 @@ export * from "./types/gps";
9
9
  export * from "./types/supabase";
10
10
  export * from "./types/bounding_boxes";
11
11
  export * from "./types/events";
12
+ export * from "./types/connectivity";
12
13
  export * from "./helpers/auth";
13
14
  export * from "./helpers/bounding_boxes";
14
15
  export * from "./helpers/chat";
16
+ export * from "./helpers/connectivity";
15
17
  export * from "./helpers/db";
16
18
  export * from "./helpers/devices";
17
19
  export * from "./helpers/email";
@@ -33,7 +35,8 @@ export * from "./helpers/eventUtils";
33
35
  export * from "./helpers/cache";
34
36
  export * from "./helpers/heartbeats";
35
37
  export * from "./helpers/providers";
36
- export * from "./hooks/useScoutDbListener";
38
+ export * from "./hooks/useScoutRealtimeConnectivity";
39
+ export * from "./hooks/useScoutRealtimeDevices";
37
40
  export * from "./hooks/useScoutRefresh";
38
41
  export * from "./providers";
39
42
  export * from "./store/scout";
package/dist/index.js CHANGED
@@ -11,10 +11,12 @@ export * from "./types/gps";
11
11
  export * from "./types/supabase";
12
12
  export * from "./types/bounding_boxes";
13
13
  export * from "./types/events";
14
+ export * from "./types/connectivity";
14
15
  // Helpers
15
16
  export * from "./helpers/auth";
16
17
  export * from "./helpers/bounding_boxes";
17
18
  export * from "./helpers/chat";
19
+ export * from "./helpers/connectivity";
18
20
  export * from "./helpers/db";
19
21
  export * from "./helpers/devices";
20
22
  export * from "./helpers/email";
@@ -37,7 +39,8 @@ export * from "./helpers/cache";
37
39
  export * from "./helpers/heartbeats";
38
40
  export * from "./helpers/providers";
39
41
  // Hooks
40
- export * from "./hooks/useScoutDbListener";
42
+ export * from "./hooks/useScoutRealtimeConnectivity";
43
+ export * from "./hooks/useScoutRealtimeDevices";
41
44
  export * from "./hooks/useScoutRefresh";
42
45
  // Providers
43
46
  export * from "./providers";