@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.
- package/dist/helpers/operator.d.ts +5 -0
- package/dist/helpers/operator.js +64 -0
- package/dist/hooks/useScoutRealtimeConnectivity copy.d.ts +3 -0
- package/dist/hooks/useScoutRealtimeConnectivity copy.js +159 -0
- package/dist/hooks/useScoutRealtimeConnectivity.d.ts +3 -1
- package/dist/hooks/useScoutRealtimeConnectivity.js +30 -105
- package/dist/hooks/useScoutRealtimeDevices copy.d.ts +3 -0
- package/dist/hooks/useScoutRealtimeDevices copy.js +55 -0
- package/dist/hooks/useScoutRealtimeDevices.d.ts +3 -1
- package/dist/hooks/useScoutRealtimeDevices.js +32 -5
- package/dist/providers/ScoutRefreshProvider.d.ts +68 -0
- package/dist/providers/ScoutRefreshProvider.js +0 -5
- package/dist/types/db.d.ts +3 -0
- package/dist/types/herd_module.js +28 -45
- package/dist/types/realtime.d.ts +9 -0
- package/dist/types/realtime.js +6 -0
- package/dist/types/supabase.d.ts +68 -0
- package/package.json +1 -1
|
@@ -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,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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
39
|
-
const updatedConnectivity = { ...connectivity };
|
|
33
|
+
let operation;
|
|
40
34
|
switch (data.operation) {
|
|
41
35
|
case "INSERT":
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
}, [
|
|
76
|
+
}, [
|
|
77
|
+
scoutSupabase,
|
|
78
|
+
gpsDeviceIds,
|
|
79
|
+
activeHerdId,
|
|
80
|
+
handleConnectivityBroadcast,
|
|
81
|
+
clearNewItems,
|
|
82
|
+
]);
|
|
83
|
+
return newConnectivityItems;
|
|
159
84
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
+
operation = EnumRealtimeOperation.UPDATE;
|
|
30
|
+
if (data.record) {
|
|
21
31
|
dispatch(updateDevice(data.record));
|
|
32
|
+
}
|
|
22
33
|
break;
|
|
23
34
|
case "DELETE":
|
|
24
|
-
|
|
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) {
|
package/dist/types/db.d.ts
CHANGED
|
@@ -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
|
-
//
|
|
68
|
-
|
|
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
|
-
//
|
|
137
|
-
|
|
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
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
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.
|
|
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,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 = {}));
|
package/dist/types/supabase.d.ts
CHANGED
|
@@ -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;
|