@adventurelabs/scout-core 1.3.6 → 1.4.2

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/README.md CHANGED
@@ -1,260 +1,3 @@
1
1
  # Scout Core
2
2
 
3
3
  Core utilities and helpers for Adventure Labs Scout applications.
4
-
5
- ## Features
6
-
7
- - **Herd Management**: Comprehensive herd and device management
8
- - **Event Tracking**: Wildlife event monitoring and tagging
9
- - **Real-time Updates**: Supabase-powered real-time data synchronization
10
- - **State Management**: Redux-based state management with loading states
11
-
12
- ## Herd Modules Loading State
13
-
14
- The core provides a global loading state for herd modules, which are essential for many consuming applications. This state tracks whether herd modules are currently loading, have loaded successfully, or failed to load.
15
-
16
- ### Loading State Enum
17
-
18
- ```typescript
19
- import { EnumHerdModulesLoadingState } from "@adventurelabs/scout-core";
20
-
21
- enum EnumHerdModulesLoadingState {
22
- NOT_LOADING = "NOT_LOADING",
23
- LOADING = "LOADING",
24
- SUCCESSFULLY_LOADED = "SUCCESSFULLY_LOADED",
25
- UNSUCCESSFULLY_LOADED = "UNSUCCESSFULLY_LOADED",
26
- }
27
-
28
- **Available Hooks:**
29
- - `useHerdModulesLoadingState()` - Get current loading state
30
- - `useIsHerdModulesLoading()` - Check if currently loading
31
- - `useIsHerdModulesLoaded()` - Check if successfully loaded
32
- - `useIsHerdModulesFailed()` - Check if loading failed
33
- - `useHerdModulesLoadedAt()` - Get how long the last loading took (in milliseconds)
34
- - `useHerdModulesLoadingDuration()` - Get loading duration in milliseconds
35
- - `useHerdModulesLoadingTimeAgo()` - Get formatted time ago since last loaded (e.g., "2.5s ago")
36
- ```
37
-
38
- ### Usage in Components
39
-
40
- ```typescript
41
- import {
42
- useHerdModulesLoadingState,
43
- useIsHerdModulesLoading,
44
- useIsHerdModulesLoaded,
45
- useIsHerdModulesFailed,
46
- useHerdModulesLoadedAt,
47
- useHerdModulesLoadingTimeAgo,
48
- useHerdModulesLoadingDuration,
49
- } from "@adventurelabs/scout-core";
50
-
51
- function MyComponent() {
52
- const loadingState = useHerdModulesLoadingState();
53
- const isLoading = useIsHerdModulesLoading();
54
- const isLoaded = useIsHerdModulesLoaded();
55
- const isFailed = useIsHerdModulesFailed();
56
-
57
- if (isLoading) {
58
- return <div>Loading herd modules...</div>;
59
- }
60
-
61
- if (isFailed) {
62
- return <div>Failed to load herd modules</div>;
63
- }
64
-
65
- if (isLoaded) {
66
- return <div>Herd modules loaded successfully!</div>;
67
- }
68
-
69
- return <div>Not loading</div>;
70
- }
71
-
72
- // Example with loading duration information
73
- function HerdModulesStatus() {
74
- const loadingState = useHerdModulesLoadingState();
75
- const loadingTimeMs = useHerdModulesLoadedAt();
76
- const timeAgo = useHerdModulesLoadingTimeAgo();
77
- const loadingDuration = useHerdModulesLoadingDuration();
78
-
79
- return (
80
- <div>
81
- <div>Status: {loadingState}</div>
82
- {loadingTimeMs && (
83
- <>
84
- <div>Last loading took: {loadingTimeMs}ms</div>
85
- <div>Loaded: {timeAgo}</div>
86
- <div>Loading duration: {loadingDuration}ms</div>
87
- </>
88
- )}
89
- </div>
90
- );
91
- }
92
- ```
93
-
94
- ### Manual Refresh
95
-
96
- ```typescript
97
- import { useScoutRefresh } from "@adventurelabs/scout-core";
98
-
99
- function RefreshButton() {
100
- const { handleRefresh } = useScoutRefresh({ autoRefresh: false });
101
-
102
- return <button onClick={handleRefresh}>Refresh Data</button>;
103
- }
104
- ```
105
-
106
- ## Installation
107
-
108
- ```bash
109
- npm install @adventurelabs/scout-core
110
- # or
111
- yarn add @adventurelabs/scout-core
112
- ```
113
-
114
- ## Setup
115
-
116
- Wrap your app with the ScoutRefreshProvider:
117
-
118
- ```typescript
119
- import { ScoutRefreshProvider } from "@adventurelabs/scout-core";
120
-
121
- function App() {
122
- return (
123
- <ScoutRefreshProvider>{/* Your app components */}</ScoutRefreshProvider>
124
- );
125
- }
126
- ```
127
-
128
- ## Recent Updates
129
-
130
- - **v1.0.58**: Added global herd modules loading state tracking with timestamps
131
- - Fixed repeat Supabase client creation logs
132
- - Enhanced loading state management for better UX
133
- - Added loading duration and time-ago tracking
134
- - Added comprehensive edge case handling and race condition prevention
135
-
136
- ## Usage
137
-
138
- ````typescript
139
- import "../../app/globals.css";
140
- import StoreProvider from "../../components/Store/StoreProvider";
141
- import { ScoutRefreshProvider } from "@adventurelabs/scout-core";
142
-
143
- export default function ScoutLayout({
144
- children,
145
- }: Readonly<{
146
- children: React.ReactNode;
147
- }>) {
148
- return (
149
- {/* Store provider for state management */}
150
- <StoreProvider>
151
- {/* Listen for updates and refresh data in background */}
152
- <ScoutRefreshProvider>
153
- <div className="">{children}</div>
154
- </ScoutRefreshProvider>
155
-
156
- </StoreProvider>
157
- );
158
- }
159
-
160
- ## Available Modules
161
-
162
- ### Types
163
-
164
- - Database types from Supabase
165
- - Herd, Device, Event, User interfaces
166
- - Request/Response types
167
- - Herd module loading state enums (`EnumHerdModulesLoadingState`)
168
-
169
- ### Helpers
170
-
171
- - Authentication utilities
172
- - Database operations
173
- - Email validation
174
- - GPS and location helpers
175
- - Device and event management
176
- - Tag and annotation utilities
177
-
178
- ### Hooks
179
-
180
- - `useScoutDbListener` - Real-time database listening for plans, devices, and tags with robust disconnect handling
181
- - `useScoutRefresh` - Data refresh utilities
182
- - `useConnectionStatus` - Connection status monitoring and manual reconnection controls
183
-
184
- #### Robust Connection Features
185
-
186
- The `useScoutDbListener` hook includes several features to handle network disconnections and connection issues:
187
-
188
- - **Automatic Reconnection**: Automatically attempts to reconnect when the connection is lost
189
- - **Exponential Backoff**: Uses exponential backoff with jitter to avoid overwhelming the server
190
- - **Connection State Tracking**: Provides real-time connection status (connected, connecting, disconnected, error)
191
- - **Error Handling**: Comprehensive error handling with detailed error messages
192
- - **Manual Reconnection**: Allows manual reconnection attempts via the `reconnect()` function
193
- - **Retry Limits**: Configurable maximum retry attempts to prevent infinite reconnection loops
194
- - **Graceful Cleanup**: Proper cleanup of resources when the component unmounts
195
-
196
- Example usage:
197
-
198
- ```tsx
199
- import { useConnectionStatus } from "@adventurelabs/scout-core";
200
-
201
- function ConnectionStatus() {
202
- const { isConnected, isConnecting, lastError, retryCount, reconnect } =
203
- useConnectionStatus();
204
-
205
- if (isConnecting) {
206
- return <div>Connecting to database...</div>;
207
- }
208
-
209
- if (lastError) {
210
- return (
211
- <div>
212
- <p>Connection error: {lastError}</p>
213
- <p>Retry attempts: {retryCount}</p>
214
- <button onClick={reconnect}>Reconnect</button>
215
- </div>
216
- );
217
- }
218
-
219
- return <div>Status: {isConnected ? "Connected" : "Disconnected"}</div>;
220
- }
221
- ````
222
-
223
- ### Store
224
-
225
- - Zustand-based state management for Scout applications
226
-
227
- ### Supabase
228
-
229
- - Client, server, and middleware utilities for Supabase integration
230
-
231
- ### API Keys
232
-
233
- - API key management utilities
234
-
235
- ## Development
236
-
237
- ```bash
238
- # Install dependencies
239
- yarn install
240
-
241
- # Build the package
242
- yarn build
243
-
244
- # Watch for changes
245
- yarn dev
246
-
247
- # Clean build artifacts
248
- yarn clean
249
- ```
250
-
251
- ## License
252
-
253
- GPL-3.0
254
-
255
- **New Hooks** (in `core/store/hooks.ts`):
256
-
257
- - `useHerdModulesLoadingState()` - Get current loading state
258
- - `useIsHerdModulesLoading()` - Check if currently loading
259
- - `useIsHerdModulesLoaded()` - Check if successfully loaded
260
- - `
@@ -1,6 +1,6 @@
1
1
  "use server";
2
2
  import { newServerClient } from "../supabase/server";
3
- import { IWebResponse, } from "../types/requests";
3
+ import { IWebResponse } from "../types/requests";
4
4
  import { generateSignedUrlsBatch } from "./storage";
5
5
  export async function server_get_artifacts_by_herd(herd_id, limit = 50, offset = 0, client) {
6
6
  const supabase = client || (await newServerClient());
@@ -19,7 +19,7 @@ export * from "./web";
19
19
  export * from "./zones";
20
20
  export * from "./cache";
21
21
  export * from "./operators";
22
- export * from "./components";
22
+ export * from "./parts";
23
23
  export * from "./versions_software";
24
24
  export * from "./artifacts";
25
25
  export * from "./pins";
@@ -19,7 +19,7 @@ export * from "./web";
19
19
  export * from "./zones";
20
20
  export * from "./cache";
21
21
  export * from "./operators";
22
- export * from "./components";
22
+ export * from "./parts";
23
23
  export * from "./versions_software";
24
24
  export * from "./artifacts";
25
25
  export * from "./pins";
@@ -0,0 +1,97 @@
1
+ import { Database } from "../types/supabase";
2
+ import { IPart, PartInsert } from "../types/db";
3
+ import { IWebResponseCompatible } from "../types/requests";
4
+ import { SupabaseClient } from "@supabase/supabase-js";
5
+ /**
6
+ * Retrieves all active parts for a specific device
7
+ * @param client - Supabase client instance
8
+ * @param device_id - ID of the device to get parts for
9
+ */
10
+ export declare function get_parts_by_device_id(client: SupabaseClient<Database>, device_id: number): Promise<IWebResponseCompatible<IPart[]>>;
11
+ /**
12
+ * Retrieves a single active part by its ID
13
+ * @param client - Supabase client instance
14
+ * @param part_id - ID of the part to retrieve
15
+ */
16
+ export declare function get_part_by_id(client: SupabaseClient<Database>, part_id: number): Promise<IWebResponseCompatible<IPart | null>>;
17
+ /**
18
+ * Retrieves all active parts with a specific serial number
19
+ * @param client - Supabase client instance
20
+ * @param serial_number - Serial number to search for
21
+ */
22
+ export declare function get_parts_by_serial_number(client: SupabaseClient<Database>, serial_number: string): Promise<IWebResponseCompatible<IPart[]>>;
23
+ /**
24
+ * Retrieves all active parts with a specific product number
25
+ * @param client - Supabase client instance
26
+ * @param product_number - Product number to search for
27
+ */
28
+ export declare function get_parts_by_product_number(client: SupabaseClient<Database>, product_number: string): Promise<IWebResponseCompatible<IPart[]>>;
29
+ /**
30
+ * Retrieves all active parts with a specific status
31
+ * @param client - Supabase client instance
32
+ * @param status - Component status to filter by
33
+ */
34
+ export declare function get_parts_by_status(client: SupabaseClient<Database>, status: Database["public"]["Enums"]["component_status"]): Promise<IWebResponseCompatible<IPart[]>>;
35
+ /**
36
+ * Creates a new part with validation
37
+ * @param client - Supabase client instance
38
+ * @param newPart - Part data to create
39
+ */
40
+ export declare function create_part(client: SupabaseClient<Database>, newPart: PartInsert): Promise<IWebResponseCompatible<IPart | null>>;
41
+ /**
42
+ * Updates an existing part
43
+ * @param client - Supabase client instance
44
+ * @param part_id - ID of the part to update
45
+ * @param updatedPart - Partial part data to update
46
+ */
47
+ export declare function update_part(client: SupabaseClient<Database>, part_id: number, updatedPart: Partial<PartInsert>): Promise<IWebResponseCompatible<IPart | null>>;
48
+ /**
49
+ * Soft deletes a part by setting deleted_at timestamp
50
+ * @param client - Supabase client instance
51
+ * @param part_id - ID of the part to delete
52
+ */
53
+ export declare function delete_part(client: SupabaseClient<Database>, part_id: number): Promise<IWebResponseCompatible<IPart | null>>;
54
+ /**
55
+ * Updates the status of a specific part
56
+ * @param client - Supabase client instance
57
+ * @param part_id - ID of the part to update
58
+ * @param status - New status to set
59
+ */
60
+ export declare function update_part_status(client: SupabaseClient<Database>, part_id: number, status: Database["public"]["Enums"]["component_status"]): Promise<IWebResponseCompatible<IPart | null>>;
61
+ /**
62
+ * Retrieves all active parts associated with a certificate
63
+ * @param client - Supabase client instance
64
+ * @param certificate_id - ID of the certificate to search for
65
+ */
66
+ export declare function get_parts_by_certificate_id(client: SupabaseClient<Database>, certificate_id: number): Promise<IWebResponseCompatible<IPart[]>>;
67
+ /**
68
+ * Retrieves all active parts for devices in a specific herd
69
+ * @param client - Supabase client instance
70
+ * @param herd_id - ID of the herd to get parts for
71
+ */
72
+ export declare function get_parts_by_herd_id(client: SupabaseClient<Database>, herd_id: number): Promise<IWebResponseCompatible<IPart[]>>;
73
+ /**
74
+ * Restores a soft-deleted part by clearing deleted_at timestamp
75
+ * @param client - Supabase client instance
76
+ * @param part_id - ID of the part to restore
77
+ */
78
+ export declare function restore_part(client: SupabaseClient<Database>, part_id: number): Promise<IWebResponseCompatible<IPart | null>>;
79
+ /**
80
+ * Permanently deletes a soft-deleted part from database
81
+ * @param client - Supabase client instance
82
+ * @param part_id - ID of the part to permanently delete
83
+ */
84
+ export declare function hard_delete_part(client: SupabaseClient<Database>, part_id: number): Promise<IWebResponseCompatible<IPart | null>>;
85
+ /**
86
+ * Retrieves all soft-deleted parts for a specific device
87
+ * @param client - Supabase client instance
88
+ * @param device_id - ID of the device to get deleted parts for
89
+ */
90
+ export declare function get_deleted_parts_by_device_id(client: SupabaseClient<Database>, device_id: number): Promise<IWebResponseCompatible<IPart[]>>;
91
+ /**
92
+ * Retrieves a part by its composite unique constraint (product + serial)
93
+ * @param client - Supabase client instance
94
+ * @param product_number - Product number to search for
95
+ * @param serial_number - Serial number to search for
96
+ */
97
+ export declare function get_parts_by_product_and_serial(client: SupabaseClient<Database>, product_number: string, serial_number: string): Promise<IWebResponseCompatible<IPart | null>>;
@@ -0,0 +1,329 @@
1
+ import { IWebResponse } from "../types/requests";
2
+ /**
3
+ * Retrieves all active parts for a specific device
4
+ * @param client - Supabase client instance
5
+ * @param device_id - ID of the device to get parts for
6
+ */
7
+ export async function get_parts_by_device_id(client, device_id) {
8
+ const { data, error } = await client
9
+ .from("parts")
10
+ .select("*")
11
+ .eq("device_id", device_id)
12
+ .is("deleted_at", null)
13
+ .order("created_at", { ascending: false });
14
+ if (error) {
15
+ return IWebResponse.error(error.message).to_compatible();
16
+ }
17
+ if (!data) {
18
+ return IWebResponse.error("No parts found for device").to_compatible();
19
+ }
20
+ return IWebResponse.success(data).to_compatible();
21
+ }
22
+ /**
23
+ * Retrieves a single active part by its ID
24
+ * @param client - Supabase client instance
25
+ * @param part_id - ID of the part to retrieve
26
+ */
27
+ export async function get_part_by_id(client, part_id) {
28
+ const { data, error } = await client
29
+ .from("parts")
30
+ .select("*")
31
+ .eq("id", part_id)
32
+ .is("deleted_at", null)
33
+ .single();
34
+ if (error) {
35
+ return IWebResponse.error(error.message).to_compatible();
36
+ }
37
+ if (!data) {
38
+ return IWebResponse.error("Part not found").to_compatible();
39
+ }
40
+ return IWebResponse.success(data).to_compatible();
41
+ }
42
+ /**
43
+ * Retrieves all active parts with a specific serial number
44
+ * @param client - Supabase client instance
45
+ * @param serial_number - Serial number to search for
46
+ */
47
+ export async function get_parts_by_serial_number(client, serial_number) {
48
+ const { data, error } = await client
49
+ .from("parts")
50
+ .select("*")
51
+ .eq("serial_number", serial_number)
52
+ .is("deleted_at", null)
53
+ .order("created_at", { ascending: false });
54
+ if (error) {
55
+ return IWebResponse.error(error.message).to_compatible();
56
+ }
57
+ if (!data) {
58
+ return IWebResponse.error(`No parts found with serial number: ${serial_number}`).to_compatible();
59
+ }
60
+ return IWebResponse.success(data).to_compatible();
61
+ }
62
+ /**
63
+ * Retrieves all active parts with a specific product number
64
+ * @param client - Supabase client instance
65
+ * @param product_number - Product number to search for
66
+ */
67
+ export async function get_parts_by_product_number(client, product_number) {
68
+ const { data, error } = await client
69
+ .from("parts")
70
+ .select("*")
71
+ .eq("product_number", product_number)
72
+ .is("deleted_at", null)
73
+ .order("created_at", { ascending: false });
74
+ if (error) {
75
+ return IWebResponse.error(error.message).to_compatible();
76
+ }
77
+ if (!data) {
78
+ return IWebResponse.error(`No parts found with product number: ${product_number}`).to_compatible();
79
+ }
80
+ return IWebResponse.success(data).to_compatible();
81
+ }
82
+ /**
83
+ * Retrieves all active parts with a specific status
84
+ * @param client - Supabase client instance
85
+ * @param status - Component status to filter by
86
+ */
87
+ export async function get_parts_by_status(client, status) {
88
+ const { data, error } = await client
89
+ .from("parts")
90
+ .select("*")
91
+ .eq("status", status)
92
+ .is("deleted_at", null)
93
+ .order("created_at", { ascending: false });
94
+ if (error) {
95
+ return IWebResponse.error(error.message).to_compatible();
96
+ }
97
+ if (!data) {
98
+ return IWebResponse.error(`No parts found with status: ${status}`).to_compatible();
99
+ }
100
+ return IWebResponse.success(data).to_compatible();
101
+ }
102
+ /**
103
+ * Creates a new part with validation
104
+ * @param client - Supabase client instance
105
+ * @param newPart - Part data to create
106
+ */
107
+ export async function create_part(client, newPart) {
108
+ // Validate required fields
109
+ if (!newPart.device_id) {
110
+ return IWebResponse.error("Device ID is required").to_compatible();
111
+ }
112
+ if (!newPart.serial_number) {
113
+ return IWebResponse.error("Serial number is required").to_compatible();
114
+ }
115
+ if (!newPart.product_number) {
116
+ return IWebResponse.error("Product number is required").to_compatible();
117
+ }
118
+ const { data, error } = await client
119
+ .from("parts")
120
+ .insert([newPart])
121
+ .select("*")
122
+ .single();
123
+ if (error) {
124
+ return IWebResponse.error(error.message).to_compatible();
125
+ }
126
+ if (!data) {
127
+ return IWebResponse.error("Failed to create part").to_compatible();
128
+ }
129
+ return IWebResponse.success(data).to_compatible();
130
+ }
131
+ /**
132
+ * Updates an existing part
133
+ * @param client - Supabase client instance
134
+ * @param part_id - ID of the part to update
135
+ * @param updatedPart - Partial part data to update
136
+ */
137
+ export async function update_part(client, part_id, updatedPart) {
138
+ // Remove fields that shouldn't be updated
139
+ const updateData = { ...updatedPart };
140
+ delete updateData.id;
141
+ delete updateData.created_at;
142
+ const { data, error } = await client
143
+ .from("parts")
144
+ .update(updateData)
145
+ .eq("id", part_id)
146
+ .select("*")
147
+ .single();
148
+ if (error) {
149
+ return IWebResponse.error(error.message).to_compatible();
150
+ }
151
+ if (!data) {
152
+ return IWebResponse.error("Part not found or update failed").to_compatible();
153
+ }
154
+ return IWebResponse.success(data).to_compatible();
155
+ }
156
+ /**
157
+ * Soft deletes a part by setting deleted_at timestamp
158
+ * @param client - Supabase client instance
159
+ * @param part_id - ID of the part to delete
160
+ */
161
+ export async function delete_part(client, part_id) {
162
+ // Soft delete by setting deleted_at timestamp
163
+ const { data, error } = await client
164
+ .from("parts")
165
+ .update({ deleted_at: new Date().toISOString() })
166
+ .eq("id", part_id)
167
+ .is("deleted_at", null)
168
+ .select("*")
169
+ .single();
170
+ if (error) {
171
+ return IWebResponse.error(error.message).to_compatible();
172
+ }
173
+ if (!data) {
174
+ return IWebResponse.error("Part not found or deletion failed").to_compatible();
175
+ }
176
+ return IWebResponse.success(data).to_compatible();
177
+ }
178
+ /**
179
+ * Updates the status of a specific part
180
+ * @param client - Supabase client instance
181
+ * @param part_id - ID of the part to update
182
+ * @param status - New status to set
183
+ */
184
+ export async function update_part_status(client, part_id, status) {
185
+ const { data, error } = await client
186
+ .from("parts")
187
+ .update({ status })
188
+ .eq("id", part_id)
189
+ .is("deleted_at", null)
190
+ .select("*")
191
+ .single();
192
+ if (error) {
193
+ return IWebResponse.error(error.message).to_compatible();
194
+ }
195
+ if (!data) {
196
+ return IWebResponse.error("Part not found or status update failed").to_compatible();
197
+ }
198
+ return IWebResponse.success(data).to_compatible();
199
+ }
200
+ /**
201
+ * Retrieves all active parts associated with a certificate
202
+ * @param client - Supabase client instance
203
+ * @param certificate_id - ID of the certificate to search for
204
+ */
205
+ export async function get_parts_by_certificate_id(client, certificate_id) {
206
+ const { data, error } = await client
207
+ .from("parts")
208
+ .select("*")
209
+ .eq("certificate_id", certificate_id)
210
+ .is("deleted_at", null)
211
+ .order("created_at", { ascending: false });
212
+ if (error) {
213
+ return IWebResponse.error(error.message).to_compatible();
214
+ }
215
+ if (!data) {
216
+ return IWebResponse.error(`No parts found with certificate ID: ${certificate_id}`).to_compatible();
217
+ }
218
+ return IWebResponse.success(data).to_compatible();
219
+ }
220
+ /**
221
+ * Retrieves all active parts for devices in a specific herd
222
+ * @param client - Supabase client instance
223
+ * @param herd_id - ID of the herd to get parts for
224
+ */
225
+ export async function get_parts_by_herd_id(client, herd_id) {
226
+ const { data, error } = await client
227
+ .from("parts")
228
+ .select(`
229
+ *,
230
+ devices!parts_device_id_fkey(herd_id)
231
+ `)
232
+ .eq("devices.herd_id", herd_id)
233
+ .is("deleted_at", null)
234
+ .order("created_at", { ascending: false });
235
+ if (error) {
236
+ return IWebResponse.error(error.message).to_compatible();
237
+ }
238
+ if (!data) {
239
+ return IWebResponse.error(`No parts found for herd: ${herd_id}`).to_compatible();
240
+ }
241
+ return IWebResponse.success(data).to_compatible();
242
+ }
243
+ /**
244
+ * Restores a soft-deleted part by clearing deleted_at timestamp
245
+ * @param client - Supabase client instance
246
+ * @param part_id - ID of the part to restore
247
+ */
248
+ export async function restore_part(client, part_id) {
249
+ // Restore soft deleted part by setting deleted_at to null
250
+ const { data, error } = await client
251
+ .from("parts")
252
+ .update({ deleted_at: null })
253
+ .eq("id", part_id)
254
+ .not("deleted_at", "is", null)
255
+ .select("*")
256
+ .single();
257
+ if (error) {
258
+ return IWebResponse.error(error.message).to_compatible();
259
+ }
260
+ if (!data) {
261
+ return IWebResponse.error("Part not found or restore failed").to_compatible();
262
+ }
263
+ return IWebResponse.success(data).to_compatible();
264
+ }
265
+ /**
266
+ * Permanently deletes a soft-deleted part from database
267
+ * @param client - Supabase client instance
268
+ * @param part_id - ID of the part to permanently delete
269
+ */
270
+ export async function hard_delete_part(client, part_id) {
271
+ // Permanently delete the part (only use for already soft-deleted parts)
272
+ const { data, error } = await client
273
+ .from("parts")
274
+ .delete()
275
+ .eq("id", part_id)
276
+ .not("deleted_at", "is", null)
277
+ .select("*")
278
+ .single();
279
+ if (error) {
280
+ return IWebResponse.error(error.message).to_compatible();
281
+ }
282
+ if (!data) {
283
+ return IWebResponse.error("Part not found or permanent deletion failed").to_compatible();
284
+ }
285
+ return IWebResponse.success(data).to_compatible();
286
+ }
287
+ /**
288
+ * Retrieves all soft-deleted parts for a specific device
289
+ * @param client - Supabase client instance
290
+ * @param device_id - ID of the device to get deleted parts for
291
+ */
292
+ export async function get_deleted_parts_by_device_id(client, device_id) {
293
+ const { data, error } = await client
294
+ .from("parts")
295
+ .select("*")
296
+ .eq("device_id", device_id)
297
+ .not("deleted_at", "is", null)
298
+ .order("deleted_at", { ascending: false });
299
+ if (error) {
300
+ return IWebResponse.error(error.message).to_compatible();
301
+ }
302
+ if (!data) {
303
+ return IWebResponse.error("No deleted parts found for device").to_compatible();
304
+ }
305
+ return IWebResponse.success(data).to_compatible();
306
+ }
307
+ /**
308
+ * Retrieves a part by its composite unique constraint (product + serial)
309
+ * @param client - Supabase client instance
310
+ * @param product_number - Product number to search for
311
+ * @param serial_number - Serial number to search for
312
+ */
313
+ export async function get_parts_by_product_and_serial(client, product_number, serial_number) {
314
+ // Get part by the composite unique constraint
315
+ const { data, error } = await client
316
+ .from("parts")
317
+ .select("*")
318
+ .eq("product_number", product_number)
319
+ .eq("serial_number", serial_number)
320
+ .is("deleted_at", null)
321
+ .single();
322
+ if (error) {
323
+ return IWebResponse.error(error.message).to_compatible();
324
+ }
325
+ if (!data) {
326
+ return IWebResponse.error(`No part found with product number: ${product_number} and serial number: ${serial_number}`).to_compatible();
327
+ }
328
+ return IWebResponse.success(data).to_compatible();
329
+ }
@@ -7,5 +7,6 @@ export { useScoutRealtimeTags } from "./useScoutRealtimeTags";
7
7
  export { useScoutRealtimeSessions } from "./useScoutRealtimeSessions";
8
8
  export { useScoutRealtimePlans } from "./useScoutRealtimePlans";
9
9
  export { useScoutRealtimePins } from "./useScoutRealtimePins";
10
+ export { useScoutRealtimeParts } from "./useScoutRealtimeParts";
10
11
  export { useInfiniteSessionsByHerd, useInfiniteSessionsByDevice, useInfiniteEventsByHerd, useInfiniteEventsByDevice, useInfiniteArtifactsByHerd, useInfiniteArtifactsByDevice, useIntersectionObserver, } from "./useInfiniteQuery";
11
12
  export { useLoadingPerformance, useSessionSummariesByHerd, useHasSessionSummaries, } from "../store/hooks";
@@ -7,6 +7,7 @@ export { useScoutRealtimeTags } from "./useScoutRealtimeTags";
7
7
  export { useScoutRealtimeSessions } from "./useScoutRealtimeSessions";
8
8
  export { useScoutRealtimePlans } from "./useScoutRealtimePlans";
9
9
  export { useScoutRealtimePins } from "./useScoutRealtimePins";
10
+ export { useScoutRealtimeParts } from "./useScoutRealtimeParts";
10
11
  // RTK Query infinite scroll hooks
11
12
  export { useInfiniteSessionsByHerd, useInfiniteSessionsByDevice, useInfiniteEventsByHerd, useInfiniteEventsByDevice, useInfiniteArtifactsByHerd, useInfiniteArtifactsByDevice, useIntersectionObserver, } from "./useInfiniteQuery";
12
13
  // Session summaries and performance hooks
@@ -0,0 +1,5 @@
1
+ import { SupabaseClient } from "@supabase/supabase-js";
2
+ import { Database } from "../types/supabase";
3
+ import { IPart } from "../types/db";
4
+ import { RealtimeData } from "../types/realtime";
5
+ export declare function useScoutRealtimeParts(scoutSupabase: SupabaseClient<Database>): [RealtimeData<IPart> | null, () => void];
@@ -0,0 +1,113 @@
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 { EnumRealtimeOperation } from "../types/realtime";
6
+ export function useScoutRealtimeParts(scoutSupabase) {
7
+ const channels = useRef([]);
8
+ const dispatch = useAppDispatch();
9
+ const [latestPartUpdate, setLatestPartUpdate] = useState(null);
10
+ const activeHerdId = useSelector((state) => state.scout.active_herd_id);
11
+ const herdModules = useSelector((state) => state.scout.herd_modules);
12
+ // Part broadcast handler
13
+ const handlePartBroadcast = useCallback((payload) => {
14
+ console.log("[Parts] Broadcast received:", payload.payload.operation);
15
+ const data = payload.payload;
16
+ const partData = data.record || data.old_record;
17
+ if (!partData)
18
+ return;
19
+ let operation;
20
+ // Find the target herd module and device
21
+ const herdModule = herdModules.find((hm) => hm.herd.id.toString() === activeHerdId);
22
+ if (!herdModule) {
23
+ console.warn("[Parts] No herd module found for active herd");
24
+ return;
25
+ }
26
+ const targetDevice = herdModule.devices.find((device) => device.id === partData.device_id);
27
+ if (!targetDevice) {
28
+ console.warn(`[Parts] No device found with ID: ${partData.device_id}`);
29
+ return;
30
+ }
31
+ // Ensure device has parts array
32
+ if (!targetDevice.parts) {
33
+ targetDevice.parts = [];
34
+ }
35
+ switch (data.operation) {
36
+ case "INSERT":
37
+ operation = EnumRealtimeOperation.INSERT;
38
+ if (data.record) {
39
+ console.log("[Parts] New part received:", data.record);
40
+ // Add part to device's parts array if not already present
41
+ const existingPartIndex = targetDevice.parts.findIndex((p) => p.id === data.record.id);
42
+ if (existingPartIndex === -1) {
43
+ targetDevice.parts.push(data.record);
44
+ }
45
+ }
46
+ break;
47
+ case "UPDATE":
48
+ operation = EnumRealtimeOperation.UPDATE;
49
+ if (data.record) {
50
+ console.log("[Parts] Part updated:", data.record);
51
+ // Update existing part in device's parts array
52
+ const partIndex = targetDevice.parts.findIndex((p) => p.id === data.record.id);
53
+ if (partIndex !== -1) {
54
+ targetDevice.parts[partIndex] = data.record;
55
+ }
56
+ else {
57
+ // Part not found, add it
58
+ targetDevice.parts.push(data.record);
59
+ }
60
+ }
61
+ break;
62
+ case "DELETE":
63
+ operation = EnumRealtimeOperation.DELETE;
64
+ if (data.old_record) {
65
+ console.log("[Parts] Part deleted:", data.old_record);
66
+ // Remove part from device's parts array
67
+ targetDevice.parts = targetDevice.parts.filter((p) => p.id !== data.old_record.id);
68
+ }
69
+ break;
70
+ default:
71
+ return;
72
+ }
73
+ const realtimeData = {
74
+ data: partData,
75
+ operation,
76
+ };
77
+ console.log(`[scout-core realtime] PART ${data.operation} received:`, JSON.stringify(realtimeData));
78
+ setLatestPartUpdate(realtimeData);
79
+ }, [dispatch, activeHerdId, herdModules]);
80
+ // Clear latest update
81
+ const clearLatestUpdate = useCallback(() => {
82
+ setLatestPartUpdate(null);
83
+ }, []);
84
+ const cleanupChannels = () => {
85
+ channels.current.forEach((channel) => scoutSupabase.removeChannel(channel));
86
+ channels.current = [];
87
+ };
88
+ const createPartsChannel = (herdId) => {
89
+ return scoutSupabase
90
+ .channel(`${herdId}-parts`, { config: { private: true } })
91
+ .on("broadcast", { event: "*" }, handlePartBroadcast)
92
+ .subscribe((status) => {
93
+ if (status === "SUBSCRIBED") {
94
+ console.log(`[Parts] ✅ Connected to herd ${herdId}`);
95
+ }
96
+ else if (status === "CHANNEL_ERROR") {
97
+ console.warn(`[Parts] 🟡 Failed to connect to herd ${herdId}`);
98
+ }
99
+ });
100
+ };
101
+ useEffect(() => {
102
+ cleanupChannels();
103
+ // Clear previous update when switching herds
104
+ clearLatestUpdate();
105
+ // Create parts channel for active herd
106
+ if (activeHerdId) {
107
+ const channel = createPartsChannel(activeHerdId);
108
+ channels.current.push(channel);
109
+ }
110
+ return cleanupChannels;
111
+ }, [activeHerdId, clearLatestUpdate]);
112
+ return [latestPartUpdate, clearLatestUpdate];
113
+ }
package/dist/index.d.ts CHANGED
@@ -38,13 +38,14 @@ export * from "./helpers/heartbeats";
38
38
  export * from "./helpers/providers";
39
39
  export * from "./helpers/operators";
40
40
  export * from "./helpers/versions_software";
41
- export * from "./helpers/components";
41
+ export * from "./helpers/parts";
42
42
  export * from "./hooks/useScoutRealtimeConnectivity";
43
43
  export * from "./hooks/useScoutRealtimeDevices";
44
44
  export * from "./hooks/useScoutRealtimeVersionsSoftware";
45
45
  export * from "./hooks/useScoutRealtimeEvents";
46
46
  export * from "./hooks/useScoutRealtimeTags";
47
47
  export * from "./hooks/useScoutRealtimeSessions";
48
+ export * from "./hooks/useScoutRealtimeParts";
48
49
  export * from "./hooks/useScoutRealtimePlans";
49
50
  export * from "./hooks/useScoutRealtimePins";
50
51
  export * from "./hooks/useScoutRefresh";
package/dist/index.js CHANGED
@@ -41,7 +41,7 @@ export * from "./helpers/heartbeats";
41
41
  export * from "./helpers/providers";
42
42
  export * from "./helpers/operators";
43
43
  export * from "./helpers/versions_software";
44
- export * from "./helpers/components";
44
+ export * from "./helpers/parts";
45
45
  // Hooks
46
46
  export * from "./hooks/useScoutRealtimeConnectivity";
47
47
  export * from "./hooks/useScoutRealtimeDevices";
@@ -49,6 +49,7 @@ export * from "./hooks/useScoutRealtimeVersionsSoftware";
49
49
  export * from "./hooks/useScoutRealtimeEvents";
50
50
  export * from "./hooks/useScoutRealtimeTags";
51
51
  export * from "./hooks/useScoutRealtimeSessions";
52
+ export * from "./hooks/useScoutRealtimeParts";
52
53
  export * from "./hooks/useScoutRealtimePlans";
53
54
  export * from "./hooks/useScoutRealtimePins";
54
55
  export * from "./hooks/useScoutRefresh";
@@ -160,51 +160,6 @@ export declare function useSupabase(): SupabaseClient<Database, "public", "publi
160
160
  referencedColumns: ["id"];
161
161
  }];
162
162
  };
163
- components: {
164
- Row: {
165
- certificate_id: number | null;
166
- created_at: string;
167
- device_id: number;
168
- id: number;
169
- product_number: string | null;
170
- serial_number: string;
171
- status: Database["public"]["Enums"]["component_status"];
172
- updated_at: string | null;
173
- };
174
- Insert: {
175
- certificate_id?: number | null;
176
- created_at?: string;
177
- device_id: number;
178
- id?: number;
179
- product_number?: string | null;
180
- serial_number: string;
181
- status?: Database["public"]["Enums"]["component_status"];
182
- updated_at?: string | null;
183
- };
184
- Update: {
185
- certificate_id?: number | null;
186
- created_at?: string;
187
- device_id?: number;
188
- id?: number;
189
- product_number?: string | null;
190
- serial_number?: string;
191
- status?: Database["public"]["Enums"]["component_status"];
192
- updated_at?: string | null;
193
- };
194
- Relationships: [{
195
- foreignKeyName: "components_certificate_id_fkey";
196
- columns: ["certificate_id"];
197
- isOneToOne: false;
198
- referencedRelation: "certificates";
199
- referencedColumns: ["id"];
200
- }, {
201
- foreignKeyName: "components_device_id_fkey";
202
- columns: ["device_id"];
203
- isOneToOne: false;
204
- referencedRelation: "devices";
205
- referencedColumns: ["id"];
206
- }];
207
- };
208
163
  connectivity: {
209
164
  Row: {
210
165
  altitude: number;
@@ -547,6 +502,54 @@ export declare function useSupabase(): SupabaseClient<Database, "public", "publi
547
502
  referencedColumns: ["id"];
548
503
  }];
549
504
  };
505
+ parts: {
506
+ Row: {
507
+ certificate_id: number | null;
508
+ created_at: string;
509
+ deleted_at: string | null;
510
+ device_id: number;
511
+ id: number;
512
+ product_number: string;
513
+ serial_number: string;
514
+ status: Database["public"]["Enums"]["component_status"];
515
+ updated_at: string | null;
516
+ };
517
+ Insert: {
518
+ certificate_id?: number | null;
519
+ created_at?: string;
520
+ deleted_at?: string | null;
521
+ device_id: number;
522
+ id?: number;
523
+ product_number: string;
524
+ serial_number: string;
525
+ status?: Database["public"]["Enums"]["component_status"];
526
+ updated_at?: string | null;
527
+ };
528
+ Update: {
529
+ certificate_id?: number | null;
530
+ created_at?: string;
531
+ deleted_at?: string | null;
532
+ device_id?: number;
533
+ id?: number;
534
+ product_number?: string;
535
+ serial_number?: string;
536
+ status?: Database["public"]["Enums"]["component_status"];
537
+ updated_at?: string | null;
538
+ };
539
+ Relationships: [{
540
+ foreignKeyName: "parts_certificate_id_fkey";
541
+ columns: ["certificate_id"];
542
+ isOneToOne: false;
543
+ referencedRelation: "certificates";
544
+ referencedColumns: ["id"];
545
+ }, {
546
+ foreignKeyName: "parts_device_id_fkey";
547
+ columns: ["device_id"];
548
+ isOneToOne: false;
549
+ referencedRelation: "devices";
550
+ referencedColumns: ["id"];
551
+ }];
552
+ };
550
553
  pins: {
551
554
  Row: {
552
555
  altitude_relative_to_ground: number;
@@ -1,2 +1,7 @@
1
1
  import { NextResponse, type NextRequest } from "next/server";
2
- export declare function updateSession(request: NextRequest): Promise<NextResponse<unknown>>;
2
+ export interface IOptionsMiddlewareAuth {
3
+ allowed_email_domains?: string[];
4
+ allowed_page_paths_without_auth: string[];
5
+ login_page_path: string;
6
+ }
7
+ export declare function updateSession(request: NextRequest, options: IOptionsMiddlewareAuth): Promise<NextResponse<unknown>>;
@@ -1,33 +1,43 @@
1
1
  import { createServerClient } from "@supabase/ssr";
2
2
  import { NextResponse } from "next/server";
3
- export async function updateSession(request) {
4
- let supabaseResponse = NextResponse.next({
5
- request,
6
- });
7
- const supabase = createServerClient(process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, {
3
+ export async function updateSession(request, options) {
4
+ // Validate environment variables
5
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
6
+ const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
7
+ if (!supabaseUrl || !supabaseAnonKey) {
8
+ throw new Error("Missing required Supabase environment variables");
9
+ }
10
+ let supabaseResponse = NextResponse.next({ request });
11
+ const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {
8
12
  cookies: {
9
13
  getAll() {
10
14
  return request.cookies.getAll();
11
15
  },
12
16
  setAll(cookiesToSet) {
13
- cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value));
14
- supabaseResponse = NextResponse.next({
15
- request,
16
- });
17
+ cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value));
18
+ supabaseResponse = NextResponse.next({ request });
17
19
  cookiesToSet.forEach(({ name, value, options }) => supabaseResponse.cookies.set(name, value, options));
18
20
  },
19
21
  },
20
22
  });
21
23
  // IMPORTANT: Avoid writing any logic between createServerClient and
22
- // supabase.auth.getUser(). A simple mistake could make it very hard to debug
24
+ // supabase.auth.getClaims(). A simple mistake could make it very hard to debug
23
25
  // issues with users being randomly logged out.
24
- const { data: { user }, } = await supabase.auth.getUser();
25
- if (!user &&
26
- !request.nextUrl.pathname.startsWith("/auth/login") &&
27
- !request.nextUrl.pathname.startsWith("/auth")) {
28
- // no user, potentially respond by redirecting the user to the login page
26
+ const { data: claims, error } = await supabase.auth.getClaims();
27
+ // Check if current path is allowed without authentication
28
+ const isPublicPath = options.allowed_page_paths_without_auth.some((page) => request.nextUrl.pathname.startsWith(page));
29
+ if (isPublicPath) {
30
+ return supabaseResponse;
31
+ }
32
+ // Check authentication requirements
33
+ const hasValidClaims = !error && claims?.claims?.sub;
34
+ const hasValidEmail = claims?.claims?.email &&
35
+ (!options.allowed_email_domains ||
36
+ options.allowed_email_domains.some((domain) => claims.claims.email.endsWith(`@${domain}`)));
37
+ if (!hasValidClaims || (claims?.claims?.email && !hasValidEmail)) {
38
+ // no valid claims - respond by redirecting the user to the login page
29
39
  const url = request.nextUrl.clone();
30
- url.pathname = "/auth/login";
40
+ url.pathname = options.login_page_path;
31
41
  return NextResponse.redirect(url);
32
42
  }
33
43
  // IMPORTANT: You *must* return the supabaseResponse object as it is. If you're
@@ -8,6 +8,7 @@ export type TagObservationType = Database["public"]["Enums"]["tag_observation_ty
8
8
  export type IUser = User;
9
9
  export type IDevice = Database["public"]["CompositeTypes"]["device_pretty_location"] & {
10
10
  api_keys_scout?: IApiKeyScout[];
11
+ parts?: IPart[];
11
12
  };
12
13
  export type IPin = Database["public"]["CompositeTypes"]["pins_pretty_location"];
13
14
  export type IEvent = Database["public"]["Tables"]["events"]["Row"];
@@ -24,7 +25,7 @@ export type IConnectivity = Database["public"]["Tables"]["connectivity"]["Row"];
24
25
  export type IHeartbeat = Database["public"]["Tables"]["heartbeats"]["Row"];
25
26
  export type IOperator = Database["public"]["Tables"]["operators"]["Row"];
26
27
  export type IProvider = Database["public"]["Tables"]["providers"]["Row"];
27
- export type IComponent = Database["public"]["Tables"]["components"]["Row"];
28
+ export type IPart = Database["public"]["Tables"]["parts"]["Row"];
28
29
  export type IVersionsSoftware = Database["public"]["Tables"]["versions_software"]["Row"];
29
30
  export type IArtifact = Database["public"]["Tables"]["artifacts"]["Row"];
30
31
  export type IArtifactWithMediaUrl = IArtifact & {
@@ -49,7 +50,7 @@ export interface ISessionSummary {
49
50
  };
50
51
  }
51
52
  export type ISessionUsageOverTime = Database["public"]["Functions"]["get_session_usage_over_time"]["Returns"];
52
- export type ComponentInsert = Database["public"]["Tables"]["components"]["Insert"];
53
+ export type PartInsert = Database["public"]["Tables"]["parts"]["Insert"];
53
54
  export type VersionsSoftwareInsert = Database["public"]["Tables"]["versions_software"]["Insert"];
54
55
  export type ArtifactInsert = Database["public"]["Tables"]["artifacts"]["Insert"];
55
56
  export type PinInsert = Database["public"]["Tables"]["pins"]["Insert"];
@@ -4,6 +4,7 @@ import { server_get_plans_by_herd } from "../helpers/plans";
4
4
  import { server_get_layers_by_herd } from "../helpers/layers";
5
5
  import { server_get_providers_by_herd } from "../helpers/providers";
6
6
  import { server_get_users_with_herd_access } from "../helpers/users";
7
+ import { get_parts_by_herd_id } from "../helpers/parts";
7
8
  import { EnumWebResponse } from "./requests";
8
9
  import { server_get_more_zones_and_actions_for_herd } from "../helpers/zones";
9
10
  import { server_list_api_keys_batch } from "../api_keys/actions";
@@ -86,13 +87,11 @@ export class HerdModule {
86
87
  return { status: EnumWebResponse.ERROR, data: null };
87
88
  }),
88
89
  ]);
89
- // Load devices
90
+ // Load devices and parts in parallel
90
91
  const devicesPromise = get_devices_by_herd(herd.id, client);
91
- // Wait for both devices and herd-level data
92
- const [deviceResponse, herdLevelResults] = await Promise.all([
93
- devicesPromise,
94
- herdLevelPromises,
95
- ]);
92
+ const partsPromise = get_parts_by_herd_id(client, herd.id);
93
+ // Wait for devices, parts, and herd-level data
94
+ const [deviceResponse, partsResponse, herdLevelResults] = await Promise.all([devicesPromise, partsPromise, herdLevelPromises]);
96
95
  // Check devices response
97
96
  if (deviceResponse.status == EnumWebResponse.ERROR ||
98
97
  !deviceResponse.data) {
@@ -100,6 +99,15 @@ export class HerdModule {
100
99
  return new HerdModule(herd, [], Date.now());
101
100
  }
102
101
  const new_devices = deviceResponse.data;
102
+ // Get parts data (optional - don't fail if parts can't be loaded)
103
+ let parts_data = [];
104
+ if (partsResponse.status !== EnumWebResponse.ERROR &&
105
+ partsResponse.data) {
106
+ parts_data = partsResponse.data;
107
+ }
108
+ else {
109
+ console.warn(`[HerdModule] Failed to load parts for herd ${herd.id}:`, partsResponse.status);
110
+ }
103
111
  // Load API keys for devices if we have any
104
112
  if (new_devices.length > 0) {
105
113
  try {
@@ -116,6 +124,12 @@ export class HerdModule {
116
124
  // Continue without API keys
117
125
  }
118
126
  }
127
+ // Associate parts with devices
128
+ if (parts_data.length > 0) {
129
+ for (const device of new_devices) {
130
+ device.parts = parts_data.filter((part) => part.device_id === device.id);
131
+ }
132
+ }
119
133
  // Extract herd-level data with safe fallbacks
120
134
  const [res_zones, res_user_roles, res_plans, res_layers, res_providers, session_summaries_result, session_usage_result,] = herdLevelResults;
121
135
  const zones = res_zones.status === "fulfilled" && res_zones.value?.data
@@ -166,54 +166,6 @@ export type Database = {
166
166
  }
167
167
  ];
168
168
  };
169
- components: {
170
- Row: {
171
- certificate_id: number | null;
172
- created_at: string;
173
- device_id: number;
174
- id: number;
175
- product_number: string | null;
176
- serial_number: string;
177
- status: Database["public"]["Enums"]["component_status"];
178
- updated_at: string | null;
179
- };
180
- Insert: {
181
- certificate_id?: number | null;
182
- created_at?: string;
183
- device_id: number;
184
- id?: number;
185
- product_number?: string | null;
186
- serial_number: string;
187
- status?: Database["public"]["Enums"]["component_status"];
188
- updated_at?: string | null;
189
- };
190
- Update: {
191
- certificate_id?: number | null;
192
- created_at?: string;
193
- device_id?: number;
194
- id?: number;
195
- product_number?: string | null;
196
- serial_number?: string;
197
- status?: Database["public"]["Enums"]["component_status"];
198
- updated_at?: string | null;
199
- };
200
- Relationships: [
201
- {
202
- foreignKeyName: "components_certificate_id_fkey";
203
- columns: ["certificate_id"];
204
- isOneToOne: false;
205
- referencedRelation: "certificates";
206
- referencedColumns: ["id"];
207
- },
208
- {
209
- foreignKeyName: "components_device_id_fkey";
210
- columns: ["device_id"];
211
- isOneToOne: false;
212
- referencedRelation: "devices";
213
- referencedColumns: ["id"];
214
- }
215
- ];
216
- };
217
169
  connectivity: {
218
170
  Row: {
219
171
  altitude: number;
@@ -574,6 +526,57 @@ export type Database = {
574
526
  }
575
527
  ];
576
528
  };
529
+ parts: {
530
+ Row: {
531
+ certificate_id: number | null;
532
+ created_at: string;
533
+ deleted_at: string | null;
534
+ device_id: number;
535
+ id: number;
536
+ product_number: string;
537
+ serial_number: string;
538
+ status: Database["public"]["Enums"]["component_status"];
539
+ updated_at: string | null;
540
+ };
541
+ Insert: {
542
+ certificate_id?: number | null;
543
+ created_at?: string;
544
+ deleted_at?: string | null;
545
+ device_id: number;
546
+ id?: number;
547
+ product_number: string;
548
+ serial_number: string;
549
+ status?: Database["public"]["Enums"]["component_status"];
550
+ updated_at?: string | null;
551
+ };
552
+ Update: {
553
+ certificate_id?: number | null;
554
+ created_at?: string;
555
+ deleted_at?: string | null;
556
+ device_id?: number;
557
+ id?: number;
558
+ product_number?: string;
559
+ serial_number?: string;
560
+ status?: Database["public"]["Enums"]["component_status"];
561
+ updated_at?: string | null;
562
+ };
563
+ Relationships: [
564
+ {
565
+ foreignKeyName: "parts_certificate_id_fkey";
566
+ columns: ["certificate_id"];
567
+ isOneToOne: false;
568
+ referencedRelation: "certificates";
569
+ referencedColumns: ["id"];
570
+ },
571
+ {
572
+ foreignKeyName: "parts_device_id_fkey";
573
+ columns: ["device_id"];
574
+ isOneToOne: false;
575
+ referencedRelation: "devices";
576
+ referencedColumns: ["id"];
577
+ }
578
+ ];
579
+ };
577
580
  pins: {
578
581
  Row: {
579
582
  altitude_relative_to_ground: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adventurelabs/scout-core",
3
- "version": "1.3.6",
3
+ "version": "1.4.2",
4
4
  "description": "Core utilities and helpers for Adventure Labs Scout applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -37,7 +37,7 @@
37
37
  "react": ">=18.0.0",
38
38
  "react-dom": ">=18.0.0",
39
39
  "react-redux": ">=9.0.0",
40
- "@supabase/supabase-js": "^2.57.4",
40
+ "@supabase/supabase-js": "^2.89.0",
41
41
  "@supabase/ssr": "^0.7.0",
42
42
  "@reduxjs/toolkit": "^2.0.0"
43
43
  },