@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 +0 -257
- package/dist/helpers/artifacts.js +1 -1
- package/dist/helpers/index.d.ts +1 -1
- package/dist/helpers/index.js +1 -1
- package/dist/helpers/parts.d.ts +97 -0
- package/dist/helpers/parts.js +329 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useScoutRealtimeParts.d.ts +5 -0
- package/dist/hooks/useScoutRealtimeParts.js +113 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/providers/ScoutRefreshProvider.d.ts +48 -45
- package/dist/supabase/middleware.d.ts +6 -1
- package/dist/supabase/middleware.js +26 -16
- package/dist/types/db.d.ts +3 -2
- package/dist/types/herd_module.js +20 -6
- package/dist/types/supabase.d.ts +51 -48
- package/package.json +2 -2
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
|
|
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());
|
package/dist/helpers/index.d.ts
CHANGED
|
@@ -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 "./
|
|
22
|
+
export * from "./parts";
|
|
23
23
|
export * from "./versions_software";
|
|
24
24
|
export * from "./artifacts";
|
|
25
25
|
export * from "./pins";
|
package/dist/helpers/index.js
CHANGED
|
@@ -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 "./
|
|
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
|
+
}
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/hooks/index.js
CHANGED
|
@@ -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/
|
|
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/
|
|
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
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
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.
|
|
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:
|
|
25
|
-
if
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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 =
|
|
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
|
package/dist/types/db.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
package/dist/types/supabase.d.ts
CHANGED
|
@@ -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
|
+
"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.
|
|
40
|
+
"@supabase/supabase-js": "^2.89.0",
|
|
41
41
|
"@supabase/ssr": "^0.7.0",
|
|
42
42
|
"@reduxjs/toolkit": "^2.0.0"
|
|
43
43
|
},
|