@adventurelabs/scout-core 1.0.38 → 1.0.40
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/api_keys/actions.d.ts +3 -0
- package/dist/api_keys/actions.js +28 -1
- package/dist/helpers/storage.d.ts +2 -6
- package/dist/helpers/storage.js +48 -16
- package/dist/helpers/tags.d.ts +3 -0
- package/dist/helpers/tags.js +78 -0
- package/dist/hooks/useScoutDbListener.d.ts +1 -48
- package/dist/hooks/useScoutDbListener.js +70 -227
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/providers/ScoutRefreshProvider.d.ts +43 -16
- package/dist/supabase/index.d.ts +0 -1
- package/dist/supabase/index.js +0 -1
- package/dist/supabase/server.d.ts +43 -16
- package/dist/types/db.d.ts +0 -1
- package/dist/types/herd_module.js +35 -31
- package/dist/types/supabase.d.ts +43 -16
- package/package.json +1 -1
- package/dist/supabase/client.d.ts +0 -1
- package/dist/supabase/client.js +0 -5
package/dist/api_keys/actions.js
CHANGED
|
@@ -12,9 +12,36 @@ export async function server_list_api_keys(device_id) {
|
|
|
12
12
|
return [];
|
|
13
13
|
const data_to_return = [];
|
|
14
14
|
for (let i = 0; i < data.length; i++) {
|
|
15
|
-
// convert data to IApiKeyScout
|
|
16
15
|
const converted_data = JSON.parse(data[i]);
|
|
17
16
|
data_to_return.push(converted_data);
|
|
18
17
|
}
|
|
19
18
|
return data_to_return;
|
|
20
19
|
}
|
|
20
|
+
export async function server_list_api_keys_batch(device_ids) {
|
|
21
|
+
console.log(`[API Keys Batch] Starting batch load for ${device_ids.length} devices`);
|
|
22
|
+
const supabase = await newServerClient();
|
|
23
|
+
const { data, error } = await supabase.rpc("load_api_keys_batch", {
|
|
24
|
+
device_ids: device_ids,
|
|
25
|
+
});
|
|
26
|
+
if (error) {
|
|
27
|
+
console.error("Error listing API keys in batch:", error.message);
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
if (!data)
|
|
31
|
+
return {};
|
|
32
|
+
console.log(`[API Keys Batch] Received ${data.length} API key records`);
|
|
33
|
+
const result = {};
|
|
34
|
+
// Group API keys by device_id
|
|
35
|
+
data.forEach((item) => {
|
|
36
|
+
const device_id = item.device_id;
|
|
37
|
+
if (!result[device_id]) {
|
|
38
|
+
result[device_id] = [];
|
|
39
|
+
}
|
|
40
|
+
result[device_id].push({
|
|
41
|
+
id: item.api_key_id.toString(),
|
|
42
|
+
key: item.api_key_key,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
console.log(`[API Keys Batch] Returning API keys for ${Object.keys(result).length} devices`);
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
@@ -8,12 +8,8 @@ import { Database } from "../types/supabase";
|
|
|
8
8
|
* @returns Promise<string | null> - The signed URL or null if error
|
|
9
9
|
*/
|
|
10
10
|
export declare function generateSignedUrl(filePath: string, expiresIn?: number, supabaseClient?: SupabaseClient<Database>): Promise<string | null>;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
* @param events - Array of events that may have file_path
|
|
14
|
-
* @param supabaseClient - Optional Supabase client (will create new one if not provided)
|
|
15
|
-
* @returns Promise<Array> - Events with signed URLs set as media_url
|
|
16
|
-
*/
|
|
11
|
+
export declare function generateSignedUrlsBatch(filePaths: string[], expiresIn?: number, supabaseClient?: SupabaseClient<Database>): Promise<Map<string, string | null>>;
|
|
12
|
+
export declare function addSignedUrlsToEventsBatch(events: any[], supabaseClient?: SupabaseClient<Database>): Promise<any[]>;
|
|
17
13
|
export declare function addSignedUrlsToEvents(events: any[], supabaseClient?: SupabaseClient<Database>): Promise<any[]>;
|
|
18
14
|
/**
|
|
19
15
|
* Generates a signed URL for a single event and sets it as media_url
|
package/dist/helpers/storage.js
CHANGED
|
@@ -25,26 +25,59 @@ export async function generateSignedUrl(filePath, expiresIn = 3600, supabaseClie
|
|
|
25
25
|
return null;
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
export async function generateSignedUrlsBatch(filePaths, expiresIn = 3600, supabaseClient) {
|
|
29
|
+
try {
|
|
30
|
+
const supabase = supabaseClient || (await newServerClient());
|
|
31
|
+
const urlMap = new Map();
|
|
32
|
+
const signedUrlPromises = filePaths.map(async (filePath) => {
|
|
33
|
+
try {
|
|
34
|
+
const { data, error } = await supabase.storage
|
|
35
|
+
.from(BUCKET_NAME_SCOUT)
|
|
36
|
+
.createSignedUrl(filePath, expiresIn);
|
|
37
|
+
if (error) {
|
|
38
|
+
console.error(`Error generating signed URL for ${filePath}:`, error.message);
|
|
39
|
+
return { filePath, signedUrl: null };
|
|
40
|
+
}
|
|
41
|
+
return { filePath, signedUrl: data.signedUrl };
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
console.error(`Error in generateSignedUrl for ${filePath}:`, error);
|
|
45
|
+
return { filePath, signedUrl: null };
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
const results = await Promise.all(signedUrlPromises);
|
|
49
|
+
results.forEach(({ filePath, signedUrl }) => {
|
|
50
|
+
urlMap.set(filePath, signedUrl);
|
|
51
|
+
});
|
|
52
|
+
return urlMap;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.error("Error in generateSignedUrlsBatch:", error);
|
|
56
|
+
return new Map();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export async function addSignedUrlsToEventsBatch(events, supabaseClient) {
|
|
60
|
+
const filePaths = events
|
|
61
|
+
.map((event) => event.file_path)
|
|
62
|
+
.filter((path) => path)
|
|
63
|
+
.filter((path, index, array) => array.indexOf(path) === index);
|
|
64
|
+
if (filePaths.length === 0) {
|
|
65
|
+
return events;
|
|
66
|
+
}
|
|
67
|
+
const urlMap = await generateSignedUrlsBatch(filePaths, 3600, supabaseClient);
|
|
68
|
+
return events.map((event) => {
|
|
69
|
+
if (event.file_path && urlMap.has(event.file_path)) {
|
|
70
|
+
const signedUrl = urlMap.get(event.file_path);
|
|
39
71
|
return {
|
|
40
72
|
...event,
|
|
41
|
-
media_url: signedUrl || event.media_url,
|
|
73
|
+
media_url: signedUrl || event.media_url,
|
|
42
74
|
};
|
|
43
75
|
}
|
|
44
|
-
// If no file_path, keep existing media_url
|
|
45
76
|
return event;
|
|
46
|
-
})
|
|
47
|
-
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
export async function addSignedUrlsToEvents(events, supabaseClient) {
|
|
80
|
+
return addSignedUrlsToEventsBatch(events, supabaseClient);
|
|
48
81
|
}
|
|
49
82
|
/**
|
|
50
83
|
* Generates a signed URL for a single event and sets it as media_url
|
|
@@ -61,6 +94,5 @@ export async function addSignedUrlToEvent(event, supabaseClient) {
|
|
|
61
94
|
media_url: signedUrl || event.media_url, // Fall back to existing media_url if signed URL fails
|
|
62
95
|
};
|
|
63
96
|
}
|
|
64
|
-
// If no file_path, keep existing media_url
|
|
65
97
|
return event;
|
|
66
98
|
}
|
package/dist/helpers/tags.d.ts
CHANGED
|
@@ -5,4 +5,7 @@ export declare function server_delete_tags_by_ids(tag_ids: number[]): Promise<IW
|
|
|
5
5
|
export declare function server_update_tags(tags: ITag[]): Promise<IWebResponseCompatible<ITag[]>>;
|
|
6
6
|
export declare function server_get_more_events_with_tags_by_herd(herd_id: number, offset: number, page_count?: number): Promise<IWebResponseCompatible<IEventWithTags[]>>;
|
|
7
7
|
export declare function server_get_events_and_tags_for_device(device_id: number, limit?: number): Promise<IWebResponseCompatible<IEventWithTags[]>>;
|
|
8
|
+
export declare function server_get_events_and_tags_for_devices_batch(device_ids: number[], limit?: number): Promise<IWebResponseCompatible<{
|
|
9
|
+
[device_id: number]: IEventWithTags[];
|
|
10
|
+
}>>;
|
|
8
11
|
export declare function get_event_and_tags_by_event_id(event_id: number): Promise<IWebResponseCompatible<IEventWithTags>>;
|
package/dist/helpers/tags.js
CHANGED
|
@@ -146,6 +146,84 @@ export async function server_get_events_and_tags_for_device(device_id, limit = 3
|
|
|
146
146
|
const eventsWithSignedUrls = await addSignedUrlsToEvents(data || [], supabase);
|
|
147
147
|
return IWebResponse.success(eventsWithSignedUrls).to_compatible();
|
|
148
148
|
}
|
|
149
|
+
export async function server_get_events_and_tags_for_devices_batch(device_ids, limit = 1) {
|
|
150
|
+
console.log(`[Events Batch] Starting batch load for ${device_ids.length} devices (limit: ${limit})`);
|
|
151
|
+
const supabase = await newServerClient();
|
|
152
|
+
// Use single RPC call for all devices
|
|
153
|
+
const { data, error } = await supabase.rpc("get_events_and_tags_for_devices_batch", {
|
|
154
|
+
device_ids: device_ids,
|
|
155
|
+
limit_per_device: limit,
|
|
156
|
+
});
|
|
157
|
+
if (error) {
|
|
158
|
+
console.error("Error fetching events for devices in batch:", error.message);
|
|
159
|
+
return {
|
|
160
|
+
status: EnumWebResponse.ERROR,
|
|
161
|
+
msg: error.message,
|
|
162
|
+
data: {},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (!data)
|
|
166
|
+
return IWebResponse.success({}).to_compatible();
|
|
167
|
+
console.log(`[Events Batch] Received ${data.length} event records`);
|
|
168
|
+
// Group events by device_id
|
|
169
|
+
const eventsByDevice = {};
|
|
170
|
+
data.forEach((row) => {
|
|
171
|
+
const device_id = row.device_id;
|
|
172
|
+
if (!eventsByDevice[device_id]) {
|
|
173
|
+
eventsByDevice[device_id] = [];
|
|
174
|
+
}
|
|
175
|
+
// Find existing event or create new one
|
|
176
|
+
let event = eventsByDevice[device_id].find((e) => e.id === row.event_id);
|
|
177
|
+
if (!event) {
|
|
178
|
+
event = {
|
|
179
|
+
id: row.event_id,
|
|
180
|
+
inserted_at: row.event_inserted_at,
|
|
181
|
+
message: row.event_message,
|
|
182
|
+
media_url: row.event_media_url,
|
|
183
|
+
file_path: row.event_file_path,
|
|
184
|
+
latitude: row.event_location
|
|
185
|
+
? JSON.parse(row.event_location).coordinates[1]
|
|
186
|
+
: null,
|
|
187
|
+
longitude: row.event_location
|
|
188
|
+
? JSON.parse(row.event_location).coordinates[0]
|
|
189
|
+
: null,
|
|
190
|
+
altitude: row.event_altitude,
|
|
191
|
+
heading: row.event_heading,
|
|
192
|
+
media_type: row.event_media_type,
|
|
193
|
+
device_id: device_id,
|
|
194
|
+
timestamp_observation: row.event_timestamp_observation,
|
|
195
|
+
is_public: row.event_is_public,
|
|
196
|
+
earthranger_url: row.event_earthranger_url,
|
|
197
|
+
tags: [],
|
|
198
|
+
};
|
|
199
|
+
eventsByDevice[device_id].push(event);
|
|
200
|
+
}
|
|
201
|
+
// Add tag if it exists
|
|
202
|
+
if (row.tag_id) {
|
|
203
|
+
event.tags.push({
|
|
204
|
+
id: row.tag_id,
|
|
205
|
+
x: row.tag_x || 0,
|
|
206
|
+
y: row.tag_y || 0,
|
|
207
|
+
width: row.tag_width || 0,
|
|
208
|
+
height: row.tag_height || 0,
|
|
209
|
+
class_name: row.tag_class_name || "",
|
|
210
|
+
conf: row.tag_conf || 0,
|
|
211
|
+
observation_type: row.tag_observation_type || "manual",
|
|
212
|
+
event_id: row.event_id,
|
|
213
|
+
inserted_at: row.tag_inserted_at || row.event_inserted_at,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
// Add signed URLs to all events
|
|
218
|
+
const result = {};
|
|
219
|
+
for (const device_id in eventsByDevice) {
|
|
220
|
+
const events = eventsByDevice[device_id];
|
|
221
|
+
const eventsWithSignedUrls = await addSignedUrlsToEvents(events, supabase);
|
|
222
|
+
result[parseInt(device_id)] = eventsWithSignedUrls;
|
|
223
|
+
}
|
|
224
|
+
console.log(`[Events Batch] Returning events for ${Object.keys(result).length} devices`);
|
|
225
|
+
return IWebResponse.success(result).to_compatible();
|
|
226
|
+
}
|
|
149
227
|
export async function get_event_and_tags_by_event_id(event_id) {
|
|
150
228
|
const supabase = await newServerClient();
|
|
151
229
|
// use actual sql query to get event and tags instead of rpc
|
|
@@ -7,59 +7,12 @@ declare enum ConnectionState {
|
|
|
7
7
|
ERROR = "error"
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* Hook for listening to real-time database changes
|
|
11
|
-
*
|
|
12
|
-
* Features:
|
|
13
|
-
* - Automatic reconnection with exponential backoff
|
|
14
|
-
* - Connection state tracking
|
|
15
|
-
* - Error handling and retry logic
|
|
16
|
-
* - Manual reconnection capability
|
|
17
|
-
*
|
|
18
|
-
* @param scoutSupabase - The Supabase client instance
|
|
19
|
-
* @returns Connection status and control functions
|
|
10
|
+
* Hook for listening to real-time database changes
|
|
20
11
|
*/
|
|
21
12
|
export declare function useScoutDbListener(scoutSupabase: SupabaseClient<Database>): {
|
|
22
13
|
connectionState: ConnectionState;
|
|
23
14
|
lastError: string | null;
|
|
24
|
-
retryCount: number;
|
|
25
|
-
reconnect: () => void;
|
|
26
15
|
isConnected: boolean;
|
|
27
16
|
isConnecting: boolean;
|
|
28
17
|
};
|
|
29
18
|
export {};
|
|
30
|
-
/**
|
|
31
|
-
* Return type for useScoutDbListener hook
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* ```tsx
|
|
35
|
-
* function MyComponent() {
|
|
36
|
-
* const {
|
|
37
|
-
* isConnected,
|
|
38
|
-
* isConnecting,
|
|
39
|
-
* lastError,
|
|
40
|
-
* retryCount,
|
|
41
|
-
* reconnect
|
|
42
|
-
* } = useConnectionStatus();
|
|
43
|
-
*
|
|
44
|
-
* if (isConnecting) {
|
|
45
|
-
* return <div>Connecting to database...</div>;
|
|
46
|
-
* }
|
|
47
|
-
*
|
|
48
|
-
* if (lastError) {
|
|
49
|
-
* return (
|
|
50
|
-
* <div>
|
|
51
|
-
* <p>Connection error: {lastError}</p>
|
|
52
|
-
* <p>Retry attempts: {retryCount}</p>
|
|
53
|
-
* <button onClick={reconnect}>Reconnect</button>
|
|
54
|
-
* </div>
|
|
55
|
-
* );
|
|
56
|
-
* }
|
|
57
|
-
*
|
|
58
|
-
* if (!isConnected) {
|
|
59
|
-
* return <div>Disconnected from database</div>;
|
|
60
|
-
* }
|
|
61
|
-
*
|
|
62
|
-
* return <div>Connected to database</div>;
|
|
63
|
-
* }
|
|
64
|
-
* ```
|
|
65
|
-
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useAppDispatch } from "../store/hooks";
|
|
3
|
-
import { useEffect, useRef, useState
|
|
4
|
-
import { addDevice, addPlan, addTag,
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { addDevice, addPlan, addTag, deleteDevice, deletePlan, deleteTag, updateDevice, updatePlan, updateTag, } from "../store/scout";
|
|
5
5
|
// Connection state enum
|
|
6
6
|
var ConnectionState;
|
|
7
7
|
(function (ConnectionState) {
|
|
@@ -10,52 +10,20 @@ var ConnectionState;
|
|
|
10
10
|
ConnectionState["CONNECTED"] = "connected";
|
|
11
11
|
ConnectionState["ERROR"] = "error";
|
|
12
12
|
})(ConnectionState || (ConnectionState = {}));
|
|
13
|
-
// Reconnection configuration
|
|
14
|
-
const RECONNECTION_CONFIG = {
|
|
15
|
-
MAX_RETRIES: 10,
|
|
16
|
-
INITIAL_DELAY: 1000, // 1 second
|
|
17
|
-
MAX_DELAY: 30000, // 30 seconds
|
|
18
|
-
BACKOFF_MULTIPLIER: 2,
|
|
19
|
-
JITTER_FACTOR: 0.1, // 10% jitter
|
|
20
|
-
};
|
|
21
13
|
/**
|
|
22
|
-
* Hook for listening to real-time database changes
|
|
23
|
-
*
|
|
24
|
-
* Features:
|
|
25
|
-
* - Automatic reconnection with exponential backoff
|
|
26
|
-
* - Connection state tracking
|
|
27
|
-
* - Error handling and retry logic
|
|
28
|
-
* - Manual reconnection capability
|
|
29
|
-
*
|
|
30
|
-
* @param scoutSupabase - The Supabase client instance
|
|
31
|
-
* @returns Connection status and control functions
|
|
14
|
+
* Hook for listening to real-time database changes
|
|
32
15
|
*/
|
|
33
16
|
export function useScoutDbListener(scoutSupabase) {
|
|
34
|
-
const supabase = useRef(null);
|
|
35
17
|
const channels = useRef([]);
|
|
36
18
|
const dispatch = useAppDispatch();
|
|
37
|
-
// Connection state management
|
|
38
19
|
const [connectionState, setConnectionState] = useState(ConnectionState.DISCONNECTED);
|
|
39
20
|
const [lastError, setLastError] = useState(null);
|
|
40
|
-
const [retryCount, setRetryCount] = useState(0);
|
|
41
|
-
// Reconnection management
|
|
42
|
-
const reconnectTimeoutRef = useRef(null);
|
|
43
|
-
const isInitializingRef = useRef(false);
|
|
44
|
-
const isDestroyedRef = useRef(false);
|
|
45
|
-
// Calculate exponential backoff delay with jitter
|
|
46
|
-
const calculateBackoffDelay = useCallback((attempt) => {
|
|
47
|
-
const baseDelay = Math.min(RECONNECTION_CONFIG.INITIAL_DELAY *
|
|
48
|
-
Math.pow(RECONNECTION_CONFIG.BACKOFF_MULTIPLIER, attempt), RECONNECTION_CONFIG.MAX_DELAY);
|
|
49
|
-
const jitter = baseDelay * RECONNECTION_CONFIG.JITTER_FACTOR * (Math.random() - 0.5);
|
|
50
|
-
return Math.max(100, baseDelay + jitter); // Minimum 100ms delay
|
|
51
|
-
}, []);
|
|
52
21
|
// Clean up all channels
|
|
53
|
-
const cleanupChannels =
|
|
54
|
-
console.log("[DB Listener] 🧹 Cleaning up channels");
|
|
22
|
+
const cleanupChannels = () => {
|
|
55
23
|
channels.current.forEach((channel) => {
|
|
56
|
-
if (channel &&
|
|
24
|
+
if (channel && scoutSupabase) {
|
|
57
25
|
try {
|
|
58
|
-
|
|
26
|
+
scoutSupabase.removeChannel(channel);
|
|
59
27
|
}
|
|
60
28
|
catch (error) {
|
|
61
29
|
console.warn("[DB Listener] Error removing channel:", error);
|
|
@@ -63,153 +31,91 @@ export function useScoutDbListener(scoutSupabase) {
|
|
|
63
31
|
}
|
|
64
32
|
});
|
|
65
33
|
channels.current = [];
|
|
66
|
-
}
|
|
67
|
-
//
|
|
68
|
-
const
|
|
69
|
-
if (reconnectTimeoutRef.current) {
|
|
70
|
-
clearTimeout(reconnectTimeoutRef.current);
|
|
71
|
-
reconnectTimeoutRef.current = null;
|
|
72
|
-
}
|
|
73
|
-
}, []);
|
|
74
|
-
// Test database connection
|
|
75
|
-
const testDbConnection = useCallback(async () => {
|
|
76
|
-
if (!supabase.current)
|
|
77
|
-
return false;
|
|
78
|
-
try {
|
|
79
|
-
const { data, error } = await supabase.current
|
|
80
|
-
.from("tags")
|
|
81
|
-
.select("count")
|
|
82
|
-
.limit(1);
|
|
83
|
-
if (error) {
|
|
84
|
-
console.warn("[DB Listener] DB connection test failed:", error);
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
console.log("[DB Listener] ✅ DB connection test successful");
|
|
88
|
-
return true;
|
|
89
|
-
}
|
|
90
|
-
catch (err) {
|
|
91
|
-
console.error("[DB Listener] DB connection test failed:", err);
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
}, []);
|
|
95
|
-
// Set up realtime authentication
|
|
96
|
-
const setupRealtimeAuth = useCallback(async () => {
|
|
97
|
-
if (!supabase.current)
|
|
98
|
-
return false;
|
|
99
|
-
try {
|
|
100
|
-
await supabase.current.realtime.setAuth();
|
|
101
|
-
console.log("[DB Listener] ✅ Realtime authentication set up successfully");
|
|
102
|
-
return true;
|
|
103
|
-
}
|
|
104
|
-
catch (err) {
|
|
105
|
-
console.warn("[DB Listener] ❌ Failed to set up realtime authentication:", err);
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
}, []);
|
|
109
|
-
// Generic event handler factory
|
|
110
|
-
const createEventHandler = useCallback((action, dataKey, entityName) => {
|
|
111
|
-
return (payload) => {
|
|
112
|
-
console.log(`[DB Listener] ${entityName} ${payload.event} received:`, payload);
|
|
113
|
-
const data = payload[dataKey];
|
|
114
|
-
if (!data) {
|
|
115
|
-
console.error(`[DB Listener] ${entityName} ${payload.event} - Invalid payload, missing ${dataKey} data`);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
action(data);
|
|
119
|
-
};
|
|
120
|
-
}, []);
|
|
121
|
-
// Create event handlers using the factory
|
|
122
|
-
const handlers = useCallback(() => ({
|
|
34
|
+
};
|
|
35
|
+
// Create event handlers
|
|
36
|
+
const handlers = {
|
|
123
37
|
tags: {
|
|
124
|
-
INSERT:
|
|
125
|
-
|
|
126
|
-
|
|
38
|
+
INSERT: (payload) => {
|
|
39
|
+
if (payload.new)
|
|
40
|
+
dispatch(addTag(payload.new));
|
|
41
|
+
},
|
|
42
|
+
UPDATE: (payload) => {
|
|
43
|
+
if (payload.new)
|
|
44
|
+
dispatch(updateTag(payload.new));
|
|
45
|
+
},
|
|
46
|
+
DELETE: (payload) => {
|
|
47
|
+
if (payload.old)
|
|
48
|
+
dispatch(deleteTag(payload.old));
|
|
49
|
+
},
|
|
127
50
|
},
|
|
128
51
|
devices: {
|
|
129
|
-
INSERT:
|
|
130
|
-
|
|
131
|
-
|
|
52
|
+
INSERT: (payload) => {
|
|
53
|
+
if (payload.new)
|
|
54
|
+
dispatch(addDevice(payload.new));
|
|
55
|
+
},
|
|
56
|
+
UPDATE: (payload) => {
|
|
57
|
+
if (payload.new)
|
|
58
|
+
dispatch(updateDevice(payload.new));
|
|
59
|
+
},
|
|
60
|
+
DELETE: (payload) => {
|
|
61
|
+
if (payload.old)
|
|
62
|
+
dispatch(deleteDevice(payload.old));
|
|
63
|
+
},
|
|
132
64
|
},
|
|
133
65
|
plans: {
|
|
134
|
-
INSERT:
|
|
135
|
-
|
|
136
|
-
|
|
66
|
+
INSERT: (payload) => {
|
|
67
|
+
if (payload.new)
|
|
68
|
+
dispatch(addPlan(payload.new));
|
|
69
|
+
},
|
|
70
|
+
UPDATE: (payload) => {
|
|
71
|
+
if (payload.new)
|
|
72
|
+
dispatch(updatePlan(payload.new));
|
|
73
|
+
},
|
|
74
|
+
DELETE: (payload) => {
|
|
75
|
+
if (payload.old)
|
|
76
|
+
dispatch(deletePlan(payload.old));
|
|
77
|
+
},
|
|
137
78
|
},
|
|
138
79
|
sessions: {
|
|
139
|
-
INSERT:
|
|
140
|
-
UPDATE:
|
|
141
|
-
DELETE:
|
|
80
|
+
INSERT: (payload) => console.log("[DB Listener] Session INSERT:", payload),
|
|
81
|
+
UPDATE: (payload) => console.log("[DB Listener] Session UPDATE:", payload),
|
|
82
|
+
DELETE: (payload) => console.log("[DB Listener] Session DELETE:", payload),
|
|
142
83
|
},
|
|
143
84
|
connectivity: {
|
|
144
|
-
INSERT: (payload) => console.log("[DB Listener] Connectivity INSERT
|
|
145
|
-
UPDATE: (payload) => console.log("[DB Listener] Connectivity UPDATE
|
|
146
|
-
DELETE: (payload) => console.log("[DB Listener] Connectivity DELETE
|
|
85
|
+
INSERT: (payload) => console.log("[DB Listener] Connectivity INSERT:", payload),
|
|
86
|
+
UPDATE: (payload) => console.log("[DB Listener] Connectivity UPDATE:", payload),
|
|
87
|
+
DELETE: (payload) => console.log("[DB Listener] Connectivity DELETE:", payload),
|
|
147
88
|
},
|
|
148
|
-
}
|
|
149
|
-
//
|
|
150
|
-
const
|
|
151
|
-
if (!
|
|
152
|
-
return null;
|
|
153
|
-
const channelName = `scout_broadcast_${tableName}_${Date.now()}`;
|
|
154
|
-
console.log(`[DB Listener] Creating broadcast channel for ${tableName}:`, channelName);
|
|
155
|
-
try {
|
|
156
|
-
const channel = supabase.current.channel(channelName, {
|
|
157
|
-
config: { private: true },
|
|
158
|
-
});
|
|
159
|
-
// Add system event handlers for connection monitoring
|
|
160
|
-
channel
|
|
161
|
-
.on("system", { event: "disconnect" }, () => {
|
|
162
|
-
console.log(`[DB Listener] 🔌 ${tableName} channel disconnected`);
|
|
163
|
-
setConnectionState(ConnectionState.DISCONNECTED);
|
|
164
|
-
setLastError("Channel disconnected");
|
|
165
|
-
})
|
|
166
|
-
.on("system", { event: "reconnect" }, () => {
|
|
167
|
-
console.log(`[DB Listener] 🔗 ${tableName} channel reconnected`);
|
|
168
|
-
})
|
|
169
|
-
.on("system", { event: "error" }, (error) => {
|
|
170
|
-
console.warn(`[DB Listener] ❌ ${tableName} channel error:`, error);
|
|
171
|
-
setLastError(`Channel error: ${error}`);
|
|
172
|
-
});
|
|
173
|
-
return channel;
|
|
174
|
-
}
|
|
175
|
-
catch (error) {
|
|
176
|
-
console.error(`[DB Listener] Failed to create ${tableName} channel:`, error);
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
}, []);
|
|
180
|
-
// Set up all channels
|
|
181
|
-
const setupChannels = useCallback(async () => {
|
|
182
|
-
if (!supabase.current)
|
|
89
|
+
};
|
|
90
|
+
// Set up channels
|
|
91
|
+
const setupChannels = async () => {
|
|
92
|
+
if (!scoutSupabase)
|
|
183
93
|
return false;
|
|
184
94
|
cleanupChannels();
|
|
185
|
-
const
|
|
186
|
-
const tables = Object.keys(tableHandlers);
|
|
95
|
+
const tables = Object.keys(handlers);
|
|
187
96
|
let successCount = 0;
|
|
188
97
|
const totalChannels = tables.length;
|
|
189
98
|
for (const tableName of tables) {
|
|
190
|
-
const channel = createChannel(tableName);
|
|
191
|
-
if (!channel)
|
|
192
|
-
continue;
|
|
193
99
|
try {
|
|
100
|
+
const channelName = `scout_broadcast_${tableName}_${Date.now()}`;
|
|
101
|
+
const channel = scoutSupabase.channel(channelName, {
|
|
102
|
+
config: { private: false },
|
|
103
|
+
});
|
|
194
104
|
// Set up event handlers
|
|
195
|
-
const tableHandler =
|
|
105
|
+
const tableHandler = handlers[tableName];
|
|
196
106
|
Object.entries(tableHandler).forEach(([event, handler]) => {
|
|
197
107
|
channel.on("broadcast", { event }, handler);
|
|
198
108
|
});
|
|
199
109
|
// Subscribe to the channel
|
|
200
110
|
channel.subscribe((status) => {
|
|
201
|
-
console.log(`[DB Listener] ${tableName} channel status:`, status);
|
|
202
111
|
if (status === "SUBSCRIBED") {
|
|
203
112
|
successCount++;
|
|
204
113
|
if (successCount === totalChannels) {
|
|
205
114
|
setConnectionState(ConnectionState.CONNECTED);
|
|
206
|
-
setRetryCount(0);
|
|
207
115
|
setLastError(null);
|
|
208
|
-
console.log("[DB Listener] ✅ All channels successfully subscribed");
|
|
209
116
|
}
|
|
210
117
|
}
|
|
211
118
|
else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") {
|
|
212
|
-
console.error(`[DB Listener] ${tableName} channel failed to subscribe:`, status);
|
|
213
119
|
setLastError(`Channel subscription failed: ${status}`);
|
|
214
120
|
}
|
|
215
121
|
});
|
|
@@ -220,107 +126,44 @@ export function useScoutDbListener(scoutSupabase) {
|
|
|
220
126
|
}
|
|
221
127
|
}
|
|
222
128
|
return successCount > 0;
|
|
223
|
-
}
|
|
224
|
-
// Schedule reconnection with exponential backoff
|
|
225
|
-
const scheduleReconnection = useCallback(() => {
|
|
226
|
-
if (isDestroyedRef.current ||
|
|
227
|
-
retryCount >= RECONNECTION_CONFIG.MAX_RETRIES) {
|
|
228
|
-
console.log("[DB Listener] Max reconnection attempts reached or hook destroyed");
|
|
229
|
-
setConnectionState(ConnectionState.ERROR);
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
const delay = calculateBackoffDelay(retryCount);
|
|
233
|
-
console.log(`[DB Listener] Scheduling reconnection attempt ${retryCount + 1} in ${delay}ms`);
|
|
234
|
-
reconnectTimeoutRef.current = setTimeout(() => {
|
|
235
|
-
if (!isDestroyedRef.current) {
|
|
236
|
-
initializeConnection();
|
|
237
|
-
}
|
|
238
|
-
}, delay);
|
|
239
|
-
}, [retryCount, calculateBackoffDelay]);
|
|
129
|
+
};
|
|
240
130
|
// Initialize connection
|
|
241
|
-
const initializeConnection =
|
|
242
|
-
if (
|
|
131
|
+
const initializeConnection = async () => {
|
|
132
|
+
if (!scoutSupabase)
|
|
243
133
|
return;
|
|
244
|
-
isInitializingRef.current = true;
|
|
245
134
|
setConnectionState(ConnectionState.CONNECTING);
|
|
246
135
|
try {
|
|
247
|
-
console.log("[DB Listener] 🔄 Initializing connection...");
|
|
248
136
|
// Test database connection
|
|
249
|
-
const
|
|
250
|
-
if (
|
|
137
|
+
const { error } = await scoutSupabase.from("tags").select("id").limit(1);
|
|
138
|
+
if (error) {
|
|
251
139
|
throw new Error("Database connection test failed");
|
|
252
140
|
}
|
|
253
|
-
// Set up realtime authentication
|
|
254
|
-
const authSuccess = await setupRealtimeAuth();
|
|
255
|
-
if (!authSuccess) {
|
|
256
|
-
throw new Error("Realtime authentication failed");
|
|
257
|
-
}
|
|
258
141
|
// Set up channels
|
|
259
|
-
const
|
|
260
|
-
if (!
|
|
142
|
+
const success = await setupChannels();
|
|
143
|
+
if (!success) {
|
|
261
144
|
throw new Error("Channel setup failed");
|
|
262
145
|
}
|
|
263
|
-
console.log("[DB Listener] ✅ Connection initialized successfully");
|
|
264
146
|
}
|
|
265
147
|
catch (error) {
|
|
266
|
-
console.error("[DB Listener] ❌ Connection initialization failed:", error);
|
|
267
148
|
setLastError(error instanceof Error ? error.message : "Unknown error");
|
|
268
149
|
setConnectionState(ConnectionState.ERROR);
|
|
269
|
-
setRetryCount((prev) => prev + 1);
|
|
270
|
-
// Schedule reconnection
|
|
271
|
-
scheduleReconnection();
|
|
272
150
|
}
|
|
273
|
-
|
|
274
|
-
isInitializingRef.current = false;
|
|
275
|
-
}
|
|
276
|
-
}, [
|
|
277
|
-
testDbConnection,
|
|
278
|
-
setupRealtimeAuth,
|
|
279
|
-
setupChannels,
|
|
280
|
-
scheduleReconnection,
|
|
281
|
-
]);
|
|
282
|
-
// Manual reconnection function
|
|
283
|
-
const reconnect = useCallback(() => {
|
|
284
|
-
if (isDestroyedRef.current)
|
|
285
|
-
return;
|
|
286
|
-
console.log("[DB Listener] 🔄 Manual reconnection requested");
|
|
287
|
-
cancelReconnection();
|
|
288
|
-
setRetryCount(0);
|
|
289
|
-
setLastError(null);
|
|
290
|
-
initializeConnection();
|
|
291
|
-
}, [cancelReconnection, initializeConnection]);
|
|
151
|
+
};
|
|
292
152
|
// Main effect
|
|
293
153
|
useEffect(() => {
|
|
294
|
-
console.log("=== SCOUT DB LISTENER INITIALIZATION ===");
|
|
295
154
|
if (!scoutSupabase) {
|
|
296
|
-
console.error("[DB Listener] No Supabase client available");
|
|
297
155
|
setConnectionState(ConnectionState.ERROR);
|
|
298
156
|
setLastError("No Supabase client available");
|
|
299
157
|
return;
|
|
300
158
|
}
|
|
301
|
-
supabase.current = scoutSupabase;
|
|
302
|
-
isDestroyedRef.current = false;
|
|
303
|
-
// Initialize connection
|
|
304
159
|
initializeConnection();
|
|
305
|
-
// Cleanup function
|
|
306
160
|
return () => {
|
|
307
|
-
console.log("[DB Listener] 🧹 Cleaning up hook");
|
|
308
|
-
isDestroyedRef.current = true;
|
|
309
|
-
cancelReconnection();
|
|
310
161
|
cleanupChannels();
|
|
311
162
|
};
|
|
312
|
-
}, [
|
|
313
|
-
scoutSupabase,
|
|
314
|
-
initializeConnection,
|
|
315
|
-
cancelReconnection,
|
|
316
|
-
cleanupChannels,
|
|
317
|
-
]);
|
|
318
|
-
// Return connection state and manual reconnect function
|
|
163
|
+
}, [scoutSupabase]);
|
|
319
164
|
return {
|
|
320
165
|
connectionState,
|
|
321
166
|
lastError,
|
|
322
|
-
retryCount,
|
|
323
|
-
reconnect,
|
|
324
167
|
isConnected: connectionState === ConnectionState.CONNECTED,
|
|
325
168
|
isConnecting: connectionState === ConnectionState.CONNECTING,
|
|
326
169
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -33,7 +33,6 @@ export * from "./hooks/useScoutRefresh";
|
|
|
33
33
|
export * from "./providers";
|
|
34
34
|
export * from "./store/scout";
|
|
35
35
|
export * from "./store/hooks";
|
|
36
|
-
export * from "./supabase/client";
|
|
37
36
|
export * from "./supabase/middleware";
|
|
38
37
|
export * from "./supabase/server";
|
|
39
38
|
export * from "./api_keys/actions";
|
package/dist/index.js
CHANGED
|
@@ -630,25 +630,11 @@ export declare function useSupabase(): SupabaseClient<Database, "public", {
|
|
|
630
630
|
};
|
|
631
631
|
Returns: Database["public"]["CompositeTypes"]["device_pretty_location"];
|
|
632
632
|
};
|
|
633
|
-
|
|
633
|
+
get_device_by_api_key: {
|
|
634
634
|
Args: {
|
|
635
635
|
device_api_key: string;
|
|
636
636
|
};
|
|
637
|
-
Returns:
|
|
638
|
-
altitude: number | null;
|
|
639
|
-
created_by: string;
|
|
640
|
-
description: string;
|
|
641
|
-
device_type: Database["public"]["Enums"]["device_type"];
|
|
642
|
-
domain_name: string | null;
|
|
643
|
-
heading: number | null;
|
|
644
|
-
herd_id: number;
|
|
645
|
-
id: number;
|
|
646
|
-
inserted_at: string;
|
|
647
|
-
location: unknown | null;
|
|
648
|
-
name: string;
|
|
649
|
-
video_publisher_token: string | null;
|
|
650
|
-
video_subscriber_token: string | null;
|
|
651
|
-
};
|
|
637
|
+
Returns: Database["public"]["CompositeTypes"]["device_pretty_location"];
|
|
652
638
|
};
|
|
653
639
|
get_device_id_from_key: {
|
|
654
640
|
Args: {
|
|
@@ -758,6 +744,47 @@ export declare function useSupabase(): SupabaseClient<Database, "public", {
|
|
|
758
744
|
};
|
|
759
745
|
Returns: string[];
|
|
760
746
|
};
|
|
747
|
+
load_api_keys_batch: {
|
|
748
|
+
Args: {
|
|
749
|
+
device_ids: number[];
|
|
750
|
+
};
|
|
751
|
+
Returns: {
|
|
752
|
+
device_id: number;
|
|
753
|
+
api_key_id: number;
|
|
754
|
+
api_key_key: string;
|
|
755
|
+
}[];
|
|
756
|
+
};
|
|
757
|
+
get_events_and_tags_for_devices_batch: {
|
|
758
|
+
Args: {
|
|
759
|
+
device_ids: number[];
|
|
760
|
+
limit_per_device: number;
|
|
761
|
+
};
|
|
762
|
+
Returns: {
|
|
763
|
+
device_id: number;
|
|
764
|
+
event_id: number;
|
|
765
|
+
event_message: string | null;
|
|
766
|
+
event_media_url: string | null;
|
|
767
|
+
event_file_path: string | null;
|
|
768
|
+
event_location: string | null;
|
|
769
|
+
event_altitude: number;
|
|
770
|
+
event_heading: number;
|
|
771
|
+
event_media_type: string;
|
|
772
|
+
event_timestamp_observation: string;
|
|
773
|
+
event_is_public: boolean;
|
|
774
|
+
event_earthranger_url: string | null;
|
|
775
|
+
event_session_id: number | null;
|
|
776
|
+
event_inserted_at: string;
|
|
777
|
+
tag_id: number | null;
|
|
778
|
+
tag_x: number | null;
|
|
779
|
+
tag_y: number | null;
|
|
780
|
+
tag_width: number | null;
|
|
781
|
+
tag_height: number | null;
|
|
782
|
+
tag_class_name: string | null;
|
|
783
|
+
tag_conf: number | null;
|
|
784
|
+
tag_observation_type: string | null;
|
|
785
|
+
tag_inserted_at: string | null;
|
|
786
|
+
}[];
|
|
787
|
+
};
|
|
761
788
|
};
|
|
762
789
|
Enums: {
|
|
763
790
|
app_permission: "herds.delete" | "events.delete";
|
package/dist/supabase/index.d.ts
CHANGED
package/dist/supabase/index.js
CHANGED
|
@@ -621,25 +621,11 @@ export declare function newServerClient(): Promise<import("@supabase/supabase-js
|
|
|
621
621
|
};
|
|
622
622
|
Returns: Database["public"]["CompositeTypes"]["device_pretty_location"];
|
|
623
623
|
};
|
|
624
|
-
|
|
624
|
+
get_device_by_api_key: {
|
|
625
625
|
Args: {
|
|
626
626
|
device_api_key: string;
|
|
627
627
|
};
|
|
628
|
-
Returns:
|
|
629
|
-
altitude: number | null;
|
|
630
|
-
created_by: string;
|
|
631
|
-
description: string;
|
|
632
|
-
device_type: Database["public"]["Enums"]["device_type"];
|
|
633
|
-
domain_name: string | null;
|
|
634
|
-
heading: number | null;
|
|
635
|
-
herd_id: number;
|
|
636
|
-
id: number;
|
|
637
|
-
inserted_at: string;
|
|
638
|
-
location: unknown | null;
|
|
639
|
-
name: string;
|
|
640
|
-
video_publisher_token: string | null;
|
|
641
|
-
video_subscriber_token: string | null;
|
|
642
|
-
};
|
|
628
|
+
Returns: Database["public"]["CompositeTypes"]["device_pretty_location"];
|
|
643
629
|
};
|
|
644
630
|
get_device_id_from_key: {
|
|
645
631
|
Args: {
|
|
@@ -749,6 +735,47 @@ export declare function newServerClient(): Promise<import("@supabase/supabase-js
|
|
|
749
735
|
};
|
|
750
736
|
Returns: string[];
|
|
751
737
|
};
|
|
738
|
+
load_api_keys_batch: {
|
|
739
|
+
Args: {
|
|
740
|
+
device_ids: number[];
|
|
741
|
+
};
|
|
742
|
+
Returns: {
|
|
743
|
+
device_id: number;
|
|
744
|
+
api_key_id: number;
|
|
745
|
+
api_key_key: string;
|
|
746
|
+
}[];
|
|
747
|
+
};
|
|
748
|
+
get_events_and_tags_for_devices_batch: {
|
|
749
|
+
Args: {
|
|
750
|
+
device_ids: number[];
|
|
751
|
+
limit_per_device: number;
|
|
752
|
+
};
|
|
753
|
+
Returns: {
|
|
754
|
+
device_id: number;
|
|
755
|
+
event_id: number;
|
|
756
|
+
event_message: string | null;
|
|
757
|
+
event_media_url: string | null;
|
|
758
|
+
event_file_path: string | null;
|
|
759
|
+
event_location: string | null;
|
|
760
|
+
event_altitude: number;
|
|
761
|
+
event_heading: number;
|
|
762
|
+
event_media_type: string;
|
|
763
|
+
event_timestamp_observation: string;
|
|
764
|
+
event_is_public: boolean;
|
|
765
|
+
event_earthranger_url: string | null;
|
|
766
|
+
event_session_id: number | null;
|
|
767
|
+
event_inserted_at: string;
|
|
768
|
+
tag_id: number | null;
|
|
769
|
+
tag_x: number | null;
|
|
770
|
+
tag_y: number | null;
|
|
771
|
+
tag_width: number | null;
|
|
772
|
+
tag_height: number | null;
|
|
773
|
+
tag_class_name: string | null;
|
|
774
|
+
tag_conf: number | null;
|
|
775
|
+
tag_observation_type: string | null;
|
|
776
|
+
tag_inserted_at: string | null;
|
|
777
|
+
}[];
|
|
778
|
+
};
|
|
752
779
|
};
|
|
753
780
|
Enums: {
|
|
754
781
|
app_permission: "herds.delete" | "events.delete";
|
package/dist/types/db.d.ts
CHANGED
|
@@ -3,11 +3,11 @@ import { get_devices_by_herd } from "../helpers/devices";
|
|
|
3
3
|
import { server_get_total_events_by_herd } from "../helpers/events";
|
|
4
4
|
import { EnumSessionsVisibility } from "./events";
|
|
5
5
|
import { server_get_plans_by_herd } from "../helpers/plans";
|
|
6
|
-
import {
|
|
6
|
+
import { server_get_events_and_tags_for_devices_batch, } from "../helpers/tags";
|
|
7
7
|
import { server_get_users_with_herd_access } from "../helpers/users";
|
|
8
8
|
import { EnumWebResponse } from "./requests";
|
|
9
9
|
import { server_get_more_zones_and_actions_for_herd } from "../helpers/zones";
|
|
10
|
-
import {
|
|
10
|
+
import { server_list_api_keys_batch } from "../api_keys/actions";
|
|
11
11
|
import { getSessionsByHerdId } from "../helpers/sessions";
|
|
12
12
|
export class HerdModule {
|
|
13
13
|
constructor(herd, devices, events, timestamp_last_refreshed, user_roles = null, events_page_index = 0, total_events = 0, total_events_with_filters = 0, labels = [], plans = [], zones = [], sessions = []) {
|
|
@@ -56,31 +56,37 @@ export class HerdModule {
|
|
|
56
56
|
return new HerdModule(herd, [], [], Date.now());
|
|
57
57
|
}
|
|
58
58
|
const new_devices = response_new_devices.data;
|
|
59
|
-
// get api keys
|
|
59
|
+
// get api keys and events for all devices in batch
|
|
60
|
+
let recent_events_batch = {};
|
|
60
61
|
if (new_devices.length > 0) {
|
|
61
62
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
const device_ids = new_devices.map((device) => device.id ?? 0);
|
|
64
|
+
console.log(`[HerdModule] Loading batch data for ${device_ids.length} devices:`, device_ids);
|
|
65
|
+
// Load API keys and events in parallel
|
|
66
|
+
const [api_keys_batch, events_response] = await Promise.all([
|
|
67
|
+
server_list_api_keys_batch(device_ids),
|
|
68
|
+
server_get_events_and_tags_for_devices_batch(device_ids, 1),
|
|
69
|
+
]);
|
|
70
|
+
// Assign API keys to devices
|
|
67
71
|
for (let i = 0; i < new_devices.length; i++) {
|
|
68
|
-
new_devices[i].
|
|
72
|
+
const device_id = new_devices[i].id ?? 0;
|
|
73
|
+
new_devices[i].api_keys_scout = api_keys_batch[device_id] || [];
|
|
74
|
+
}
|
|
75
|
+
console.log(`[HerdModule] API keys loaded for ${Object.keys(api_keys_batch).length} devices`);
|
|
76
|
+
// Process events response
|
|
77
|
+
if (events_response.status === EnumWebResponse.SUCCESS &&
|
|
78
|
+
events_response.data) {
|
|
79
|
+
recent_events_batch = events_response.data;
|
|
80
|
+
console.log(`[HerdModule] Events loaded for ${Object.keys(recent_events_batch).length} devices`);
|
|
69
81
|
}
|
|
70
82
|
}
|
|
71
83
|
catch (error) {
|
|
72
|
-
console.warn("Failed to load API keys for devices:", error);
|
|
73
|
-
// Continue without API keys
|
|
84
|
+
console.warn("Failed to load API keys or events for devices:", error);
|
|
85
|
+
// Continue without API keys and events
|
|
74
86
|
}
|
|
75
87
|
}
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
console.warn(`Failed to get events for device ${device.id}:`, error);
|
|
79
|
-
return { status: EnumWebResponse.ERROR, data: null };
|
|
80
|
-
}));
|
|
81
|
-
// Run all requests in parallel with individual error handling
|
|
82
|
-
const [recent_events, res_zones, res_user_roles, total_event_count, res_plans, res_sessions,] = await Promise.allSettled([
|
|
83
|
-
Promise.all(recent_events_promises),
|
|
88
|
+
// Run all remaining requests in parallel with individual error handling
|
|
89
|
+
const [res_zones, res_user_roles, total_event_count, res_plans, res_sessions,] = await Promise.allSettled([
|
|
84
90
|
server_get_more_zones_and_actions_for_herd(herd.id, 0, 10).catch((error) => {
|
|
85
91
|
console.warn("Failed to get zones and actions:", error);
|
|
86
92
|
return { status: EnumWebResponse.ERROR, data: null };
|
|
@@ -102,20 +108,18 @@ export class HerdModule {
|
|
|
102
108
|
return [];
|
|
103
109
|
}),
|
|
104
110
|
]);
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
new_devices[i].recent_events = x;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
catch (error) {
|
|
116
|
-
console.warn(`Failed to process events for device ${new_devices[i].id}:`, error);
|
|
111
|
+
// Assign recent events to devices from batch results
|
|
112
|
+
for (let i = 0; i < new_devices.length; i++) {
|
|
113
|
+
try {
|
|
114
|
+
const device_id = new_devices[i].id ?? 0;
|
|
115
|
+
const events = recent_events_batch[device_id];
|
|
116
|
+
if (events) {
|
|
117
|
+
new_devices[i].recent_events = events;
|
|
117
118
|
}
|
|
118
119
|
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
console.warn(`Failed to process events for device ${new_devices[i].id}:`, error);
|
|
122
|
+
}
|
|
119
123
|
}
|
|
120
124
|
// Extract data with safe fallbacks
|
|
121
125
|
const zones = res_zones.status === "fulfilled" && res_zones.value?.data
|
package/dist/types/supabase.d.ts
CHANGED
|
@@ -688,25 +688,11 @@ export type Database = {
|
|
|
688
688
|
};
|
|
689
689
|
Returns: Database["public"]["CompositeTypes"]["device_pretty_location"];
|
|
690
690
|
};
|
|
691
|
-
|
|
691
|
+
get_device_by_api_key: {
|
|
692
692
|
Args: {
|
|
693
693
|
device_api_key: string;
|
|
694
694
|
};
|
|
695
|
-
Returns:
|
|
696
|
-
altitude: number | null;
|
|
697
|
-
created_by: string;
|
|
698
|
-
description: string;
|
|
699
|
-
device_type: Database["public"]["Enums"]["device_type"];
|
|
700
|
-
domain_name: string | null;
|
|
701
|
-
heading: number | null;
|
|
702
|
-
herd_id: number;
|
|
703
|
-
id: number;
|
|
704
|
-
inserted_at: string;
|
|
705
|
-
location: unknown | null;
|
|
706
|
-
name: string;
|
|
707
|
-
video_publisher_token: string | null;
|
|
708
|
-
video_subscriber_token: string | null;
|
|
709
|
-
};
|
|
695
|
+
Returns: Database["public"]["CompositeTypes"]["device_pretty_location"];
|
|
710
696
|
};
|
|
711
697
|
get_device_id_from_key: {
|
|
712
698
|
Args: {
|
|
@@ -816,6 +802,47 @@ export type Database = {
|
|
|
816
802
|
};
|
|
817
803
|
Returns: string[];
|
|
818
804
|
};
|
|
805
|
+
load_api_keys_batch: {
|
|
806
|
+
Args: {
|
|
807
|
+
device_ids: number[];
|
|
808
|
+
};
|
|
809
|
+
Returns: {
|
|
810
|
+
device_id: number;
|
|
811
|
+
api_key_id: number;
|
|
812
|
+
api_key_key: string;
|
|
813
|
+
}[];
|
|
814
|
+
};
|
|
815
|
+
get_events_and_tags_for_devices_batch: {
|
|
816
|
+
Args: {
|
|
817
|
+
device_ids: number[];
|
|
818
|
+
limit_per_device: number;
|
|
819
|
+
};
|
|
820
|
+
Returns: {
|
|
821
|
+
device_id: number;
|
|
822
|
+
event_id: number;
|
|
823
|
+
event_message: string | null;
|
|
824
|
+
event_media_url: string | null;
|
|
825
|
+
event_file_path: string | null;
|
|
826
|
+
event_location: string | null;
|
|
827
|
+
event_altitude: number;
|
|
828
|
+
event_heading: number;
|
|
829
|
+
event_media_type: string;
|
|
830
|
+
event_timestamp_observation: string;
|
|
831
|
+
event_is_public: boolean;
|
|
832
|
+
event_earthranger_url: string | null;
|
|
833
|
+
event_session_id: number | null;
|
|
834
|
+
event_inserted_at: string;
|
|
835
|
+
tag_id: number | null;
|
|
836
|
+
tag_x: number | null;
|
|
837
|
+
tag_y: number | null;
|
|
838
|
+
tag_width: number | null;
|
|
839
|
+
tag_height: number | null;
|
|
840
|
+
tag_class_name: string | null;
|
|
841
|
+
tag_conf: number | null;
|
|
842
|
+
tag_observation_type: string | null;
|
|
843
|
+
tag_inserted_at: string | null;
|
|
844
|
+
}[];
|
|
845
|
+
};
|
|
819
846
|
};
|
|
820
847
|
Enums: {
|
|
821
848
|
app_permission: "herds.delete" | "events.delete";
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function newClient(): Promise<import("@supabase/supabase-js").SupabaseClient<any, "public", any>>;
|