@adventurelabs/scout-core 1.0.70 → 1.0.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/helpers/bounding_boxes.js +1 -0
- package/dist/helpers/tags.d.ts +5 -5
- package/dist/helpers/tags.js +83 -3
- package/dist/providers/ScoutRefreshProvider.d.ts +22 -1
- package/dist/supabase/server.d.ts +22 -1
- package/dist/types/db.d.ts +2 -1
- package/dist/types/supabase.d.ts +22 -1
- package/helpers/README.md +135 -0
- package/hooks/README.md +93 -0
- package/package.json +4 -4
package/dist/helpers/tags.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { IEventAndTagsPrettyLocation, ITag } from "../types/db";
|
|
2
2
|
import { IWebResponseCompatible } from "../types/requests";
|
|
3
3
|
export declare function server_create_tags(tags: ITag[]): Promise<IWebResponseCompatible<ITag[]>>;
|
|
4
4
|
export declare function server_delete_tags_by_ids(tag_ids: number[]): Promise<IWebResponseCompatible<boolean>>;
|
|
5
5
|
export declare function server_update_tags(tags: ITag[]): Promise<IWebResponseCompatible<ITag[]>>;
|
|
6
|
-
export declare function server_get_more_events_with_tags_by_herd(herd_id: number, offset: number, page_count?: number): Promise<IWebResponseCompatible<
|
|
7
|
-
export declare function server_get_events_and_tags_for_device(device_id: number, limit?: number): Promise<IWebResponseCompatible<
|
|
6
|
+
export declare function server_get_more_events_with_tags_by_herd(herd_id: number, offset: number, page_count?: number): Promise<IWebResponseCompatible<IEventAndTagsPrettyLocation[]>>;
|
|
7
|
+
export declare function server_get_events_and_tags_for_device(device_id: number, limit?: number): Promise<IWebResponseCompatible<IEventAndTagsPrettyLocation[]>>;
|
|
8
8
|
export declare function server_get_events_and_tags_for_devices_batch(device_ids: number[], limit?: number): Promise<IWebResponseCompatible<{
|
|
9
|
-
[device_id: number]:
|
|
9
|
+
[device_id: number]: IEventAndTagsPrettyLocation[];
|
|
10
10
|
}>>;
|
|
11
|
-
export declare function get_event_and_tags_by_event_id(event_id: number): Promise<IWebResponseCompatible<
|
|
11
|
+
export declare function get_event_and_tags_by_event_id(event_id: number): Promise<IWebResponseCompatible<IEventAndTagsPrettyLocation>>;
|
package/dist/helpers/tags.js
CHANGED
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
"use server";
|
|
2
2
|
// add tag to db
|
|
3
3
|
import { newServerClient } from "../supabase/server";
|
|
4
|
+
// Helper functions to extract coordinates from location field
|
|
5
|
+
function extractLatitude(location) {
|
|
6
|
+
try {
|
|
7
|
+
if (typeof location === "object" && location !== null) {
|
|
8
|
+
if ("coordinates" in location && Array.isArray(location.coordinates)) {
|
|
9
|
+
return location.coordinates[1]; // latitude is second coordinate
|
|
10
|
+
}
|
|
11
|
+
if ("y" in location) {
|
|
12
|
+
return location.y;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
if (typeof location === "string") {
|
|
16
|
+
const match = location.match(/Point\(([^)]+)\)/);
|
|
17
|
+
if (match) {
|
|
18
|
+
const coords = match[1].split(" ").map(Number);
|
|
19
|
+
if (coords.length === 2) {
|
|
20
|
+
return coords[1]; // latitude is second coordinate
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
console.warn("Error extracting latitude:", error);
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
function extractLongitude(location) {
|
|
31
|
+
try {
|
|
32
|
+
if (typeof location === "object" && location !== null) {
|
|
33
|
+
if ("coordinates" in location && Array.isArray(location.coordinates)) {
|
|
34
|
+
return location.coordinates[0]; // longitude is first coordinate
|
|
35
|
+
}
|
|
36
|
+
if ("x" in location) {
|
|
37
|
+
return location.x;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (typeof location === "string") {
|
|
41
|
+
const match = location.match(/Point\(([^)]+)\)/);
|
|
42
|
+
if (match) {
|
|
43
|
+
const coords = match[1].split(" ").map(Number);
|
|
44
|
+
if (coords.length === 2) {
|
|
45
|
+
return coords[0]; // longitude is first coordinate
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
console.warn("Error extracting longitude:", error);
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
4
55
|
import { EnumWebResponse, IWebResponse, } from "../types/requests";
|
|
5
56
|
import { addSignedUrlsToEvents, addSignedUrlToEvent } from "./storage";
|
|
6
57
|
export async function server_create_tags(tags) {
|
|
@@ -195,13 +246,13 @@ export async function server_get_events_and_tags_for_devices_batch(device_ids, l
|
|
|
195
246
|
}
|
|
196
247
|
export async function get_event_and_tags_by_event_id(event_id) {
|
|
197
248
|
const supabase = await newServerClient();
|
|
198
|
-
//
|
|
249
|
+
// Get event and tags
|
|
199
250
|
const { data, error } = await supabase
|
|
200
251
|
.from("events")
|
|
201
252
|
.select(`
|
|
202
253
|
*,
|
|
203
254
|
tags: tags (*)
|
|
204
|
-
|
|
255
|
+
`)
|
|
205
256
|
.eq("id", event_id);
|
|
206
257
|
if (error) {
|
|
207
258
|
console.warn("Error fetching event with tags by event id:", error.message);
|
|
@@ -218,6 +269,20 @@ export async function get_event_and_tags_by_event_id(event_id) {
|
|
|
218
269
|
data: null,
|
|
219
270
|
};
|
|
220
271
|
}
|
|
272
|
+
// Get herd_id from device
|
|
273
|
+
const { data: deviceData, error: deviceError } = await supabase
|
|
274
|
+
.from("devices")
|
|
275
|
+
.select("herd_id")
|
|
276
|
+
.eq("id", data[0].device_id)
|
|
277
|
+
.single();
|
|
278
|
+
if (deviceError) {
|
|
279
|
+
console.warn("Error fetching device herd_id:", deviceError.message);
|
|
280
|
+
return {
|
|
281
|
+
status: EnumWebResponse.ERROR,
|
|
282
|
+
msg: deviceError.message,
|
|
283
|
+
data: null,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
221
286
|
// Transform location to latitude/longitude with better error handling
|
|
222
287
|
let latitude = null;
|
|
223
288
|
let longitude = null;
|
|
@@ -266,7 +331,22 @@ export async function get_event_and_tags_by_event_id(event_id) {
|
|
|
266
331
|
device_id: data[0].device_id,
|
|
267
332
|
timestamp_observation: data[0].timestamp_observation,
|
|
268
333
|
is_public: data[0].is_public,
|
|
269
|
-
|
|
334
|
+
herd_id: deviceData.herd_id,
|
|
335
|
+
tags: (data[0].tags || []).map((tag) => ({
|
|
336
|
+
id: tag.id,
|
|
337
|
+
inserted_at: tag.inserted_at,
|
|
338
|
+
x: tag.x,
|
|
339
|
+
y: tag.y,
|
|
340
|
+
width: tag.width,
|
|
341
|
+
conf: tag.conf,
|
|
342
|
+
observation_type: tag.observation_type,
|
|
343
|
+
event_id: tag.event_id,
|
|
344
|
+
class_name: tag.class_name,
|
|
345
|
+
height: tag.height,
|
|
346
|
+
location: tag.location,
|
|
347
|
+
latitude: tag.location ? extractLatitude(tag.location) : null,
|
|
348
|
+
longitude: tag.location ? extractLongitude(tag.location) : null,
|
|
349
|
+
})),
|
|
270
350
|
earthranger_url: data[0].earthranger_url,
|
|
271
351
|
file_path: data[0].file_path,
|
|
272
352
|
};
|
|
@@ -52,18 +52,21 @@ export declare function useSupabase(): SupabaseClient<Database, "public", {
|
|
|
52
52
|
file_path: string;
|
|
53
53
|
id: number;
|
|
54
54
|
session_id: number | null;
|
|
55
|
+
timestamp_observation: string | null;
|
|
55
56
|
};
|
|
56
57
|
Insert: {
|
|
57
58
|
created_at?: string;
|
|
58
59
|
file_path: string;
|
|
59
60
|
id?: number;
|
|
60
61
|
session_id?: number | null;
|
|
62
|
+
timestamp_observation?: string | null;
|
|
61
63
|
};
|
|
62
64
|
Update: {
|
|
63
65
|
created_at?: string;
|
|
64
66
|
file_path?: string;
|
|
65
67
|
id?: number;
|
|
66
68
|
session_id?: number | null;
|
|
69
|
+
timestamp_observation?: string | null;
|
|
67
70
|
};
|
|
68
71
|
Relationships: [{
|
|
69
72
|
foreignKeyName: "artifacts_session_id_fkey";
|
|
@@ -465,6 +468,7 @@ export declare function useSupabase(): SupabaseClient<Database, "public", {
|
|
|
465
468
|
height: number;
|
|
466
469
|
id: number;
|
|
467
470
|
inserted_at: string;
|
|
471
|
+
location: unknown | null;
|
|
468
472
|
observation_type: Database["public"]["Enums"]["tag_observation_type"];
|
|
469
473
|
width: number;
|
|
470
474
|
x: number;
|
|
@@ -477,6 +481,7 @@ export declare function useSupabase(): SupabaseClient<Database, "public", {
|
|
|
477
481
|
height?: number;
|
|
478
482
|
id?: number;
|
|
479
483
|
inserted_at?: string;
|
|
484
|
+
location?: unknown | null;
|
|
480
485
|
observation_type: Database["public"]["Enums"]["tag_observation_type"];
|
|
481
486
|
width: number;
|
|
482
487
|
x: number;
|
|
@@ -489,6 +494,7 @@ export declare function useSupabase(): SupabaseClient<Database, "public", {
|
|
|
489
494
|
height?: number;
|
|
490
495
|
id?: number;
|
|
491
496
|
inserted_at?: string;
|
|
497
|
+
location?: unknown | null;
|
|
492
498
|
observation_type?: Database["public"]["Enums"]["tag_observation_type"];
|
|
493
499
|
width?: number;
|
|
494
500
|
x?: number;
|
|
@@ -890,7 +896,7 @@ export declare function useSupabase(): SupabaseClient<Database, "public", {
|
|
|
890
896
|
device_id: number | null;
|
|
891
897
|
timestamp_observation: string | null;
|
|
892
898
|
is_public: boolean | null;
|
|
893
|
-
tags: Database["public"]["
|
|
899
|
+
tags: Database["public"]["CompositeTypes"]["tags_pretty_location"][] | null;
|
|
894
900
|
herd_id: number | null;
|
|
895
901
|
};
|
|
896
902
|
event_plus_tags: {
|
|
@@ -941,6 +947,21 @@ export declare function useSupabase(): SupabaseClient<Database, "public", {
|
|
|
941
947
|
distance_total: number | null;
|
|
942
948
|
distance_max_from_start: number | null;
|
|
943
949
|
};
|
|
950
|
+
tags_pretty_location: {
|
|
951
|
+
id: number | null;
|
|
952
|
+
inserted_at: string | null;
|
|
953
|
+
x: number | null;
|
|
954
|
+
y: number | null;
|
|
955
|
+
width: number | null;
|
|
956
|
+
conf: number | null;
|
|
957
|
+
observation_type: Database["public"]["Enums"]["tag_observation_type"] | null;
|
|
958
|
+
event_id: number | null;
|
|
959
|
+
class_name: string | null;
|
|
960
|
+
height: number | null;
|
|
961
|
+
location: unknown | null;
|
|
962
|
+
latitude: number | null;
|
|
963
|
+
longitude: number | null;
|
|
964
|
+
};
|
|
944
965
|
zones_and_actions_pretty_location: {
|
|
945
966
|
id: number | null;
|
|
946
967
|
inserted_at: string | null;
|
|
@@ -43,18 +43,21 @@ export declare function newServerClient(): Promise<import("@supabase/supabase-js
|
|
|
43
43
|
file_path: string;
|
|
44
44
|
id: number;
|
|
45
45
|
session_id: number | null;
|
|
46
|
+
timestamp_observation: string | null;
|
|
46
47
|
};
|
|
47
48
|
Insert: {
|
|
48
49
|
created_at?: string;
|
|
49
50
|
file_path: string;
|
|
50
51
|
id?: number;
|
|
51
52
|
session_id?: number | null;
|
|
53
|
+
timestamp_observation?: string | null;
|
|
52
54
|
};
|
|
53
55
|
Update: {
|
|
54
56
|
created_at?: string;
|
|
55
57
|
file_path?: string;
|
|
56
58
|
id?: number;
|
|
57
59
|
session_id?: number | null;
|
|
60
|
+
timestamp_observation?: string | null;
|
|
58
61
|
};
|
|
59
62
|
Relationships: [{
|
|
60
63
|
foreignKeyName: "artifacts_session_id_fkey";
|
|
@@ -456,6 +459,7 @@ export declare function newServerClient(): Promise<import("@supabase/supabase-js
|
|
|
456
459
|
height: number;
|
|
457
460
|
id: number;
|
|
458
461
|
inserted_at: string;
|
|
462
|
+
location: unknown | null;
|
|
459
463
|
observation_type: Database["public"]["Enums"]["tag_observation_type"];
|
|
460
464
|
width: number;
|
|
461
465
|
x: number;
|
|
@@ -468,6 +472,7 @@ export declare function newServerClient(): Promise<import("@supabase/supabase-js
|
|
|
468
472
|
height?: number;
|
|
469
473
|
id?: number;
|
|
470
474
|
inserted_at?: string;
|
|
475
|
+
location?: unknown | null;
|
|
471
476
|
observation_type: Database["public"]["Enums"]["tag_observation_type"];
|
|
472
477
|
width: number;
|
|
473
478
|
x: number;
|
|
@@ -480,6 +485,7 @@ export declare function newServerClient(): Promise<import("@supabase/supabase-js
|
|
|
480
485
|
height?: number;
|
|
481
486
|
id?: number;
|
|
482
487
|
inserted_at?: string;
|
|
488
|
+
location?: unknown | null;
|
|
483
489
|
observation_type?: Database["public"]["Enums"]["tag_observation_type"];
|
|
484
490
|
width?: number;
|
|
485
491
|
x?: number;
|
|
@@ -881,7 +887,7 @@ export declare function newServerClient(): Promise<import("@supabase/supabase-js
|
|
|
881
887
|
device_id: number | null;
|
|
882
888
|
timestamp_observation: string | null;
|
|
883
889
|
is_public: boolean | null;
|
|
884
|
-
tags: Database["public"]["
|
|
890
|
+
tags: Database["public"]["CompositeTypes"]["tags_pretty_location"][] | null;
|
|
885
891
|
herd_id: number | null;
|
|
886
892
|
};
|
|
887
893
|
event_plus_tags: {
|
|
@@ -932,6 +938,21 @@ export declare function newServerClient(): Promise<import("@supabase/supabase-js
|
|
|
932
938
|
distance_total: number | null;
|
|
933
939
|
distance_max_from_start: number | null;
|
|
934
940
|
};
|
|
941
|
+
tags_pretty_location: {
|
|
942
|
+
id: number | null;
|
|
943
|
+
inserted_at: string | null;
|
|
944
|
+
x: number | null;
|
|
945
|
+
y: number | null;
|
|
946
|
+
width: number | null;
|
|
947
|
+
conf: number | null;
|
|
948
|
+
observation_type: Database["public"]["Enums"]["tag_observation_type"] | null;
|
|
949
|
+
event_id: number | null;
|
|
950
|
+
class_name: string | null;
|
|
951
|
+
height: number | null;
|
|
952
|
+
location: unknown | null;
|
|
953
|
+
latitude: number | null;
|
|
954
|
+
longitude: number | null;
|
|
955
|
+
};
|
|
935
956
|
zones_and_actions_pretty_location: {
|
|
936
957
|
id: number | null;
|
|
937
958
|
inserted_at: string | null;
|
package/dist/types/db.d.ts
CHANGED
|
@@ -7,10 +7,11 @@ export type TagObservationType = Database["public"]["Enums"]["tag_observation_ty
|
|
|
7
7
|
export type IUser = User;
|
|
8
8
|
export type IDevice = Database["public"]["CompositeTypes"]["device_pretty_location"] & {
|
|
9
9
|
api_keys_scout?: IApiKeyScout[];
|
|
10
|
-
recent_events?:
|
|
10
|
+
recent_events?: IEventAndTagsPrettyLocation[];
|
|
11
11
|
};
|
|
12
12
|
export type IEvent = Database["public"]["Tables"]["events"]["Row"];
|
|
13
13
|
export type ITag = Database["public"]["Tables"]["tags"]["Row"];
|
|
14
|
+
export type ITagPrettyLocation = Database["public"]["CompositeTypes"]["tags_pretty_location"];
|
|
14
15
|
export type IPlan = Database["public"]["Tables"]["plans"]["Row"];
|
|
15
16
|
export type ILayer = Database["public"]["Tables"]["layers"]["Row"];
|
|
16
17
|
export type IAction = Database["public"]["Tables"]["actions"]["Row"];
|
package/dist/types/supabase.d.ts
CHANGED
|
@@ -52,18 +52,21 @@ export type Database = {
|
|
|
52
52
|
file_path: string;
|
|
53
53
|
id: number;
|
|
54
54
|
session_id: number | null;
|
|
55
|
+
timestamp_observation: string | null;
|
|
55
56
|
};
|
|
56
57
|
Insert: {
|
|
57
58
|
created_at?: string;
|
|
58
59
|
file_path: string;
|
|
59
60
|
id?: number;
|
|
60
61
|
session_id?: number | null;
|
|
62
|
+
timestamp_observation?: string | null;
|
|
61
63
|
};
|
|
62
64
|
Update: {
|
|
63
65
|
created_at?: string;
|
|
64
66
|
file_path?: string;
|
|
65
67
|
id?: number;
|
|
66
68
|
session_id?: number | null;
|
|
69
|
+
timestamp_observation?: string | null;
|
|
67
70
|
};
|
|
68
71
|
Relationships: [
|
|
69
72
|
{
|
|
@@ -486,6 +489,7 @@ export type Database = {
|
|
|
486
489
|
height: number;
|
|
487
490
|
id: number;
|
|
488
491
|
inserted_at: string;
|
|
492
|
+
location: unknown | null;
|
|
489
493
|
observation_type: Database["public"]["Enums"]["tag_observation_type"];
|
|
490
494
|
width: number;
|
|
491
495
|
x: number;
|
|
@@ -498,6 +502,7 @@ export type Database = {
|
|
|
498
502
|
height?: number;
|
|
499
503
|
id?: number;
|
|
500
504
|
inserted_at?: string;
|
|
505
|
+
location?: unknown | null;
|
|
501
506
|
observation_type: Database["public"]["Enums"]["tag_observation_type"];
|
|
502
507
|
width: number;
|
|
503
508
|
x: number;
|
|
@@ -510,6 +515,7 @@ export type Database = {
|
|
|
510
515
|
height?: number;
|
|
511
516
|
id?: number;
|
|
512
517
|
inserted_at?: string;
|
|
518
|
+
location?: unknown | null;
|
|
513
519
|
observation_type?: Database["public"]["Enums"]["tag_observation_type"];
|
|
514
520
|
width?: number;
|
|
515
521
|
x?: number;
|
|
@@ -930,7 +936,7 @@ export type Database = {
|
|
|
930
936
|
device_id: number | null;
|
|
931
937
|
timestamp_observation: string | null;
|
|
932
938
|
is_public: boolean | null;
|
|
933
|
-
tags: Database["public"]["
|
|
939
|
+
tags: Database["public"]["CompositeTypes"]["tags_pretty_location"][] | null;
|
|
934
940
|
herd_id: number | null;
|
|
935
941
|
};
|
|
936
942
|
event_plus_tags: {
|
|
@@ -981,6 +987,21 @@ export type Database = {
|
|
|
981
987
|
distance_total: number | null;
|
|
982
988
|
distance_max_from_start: number | null;
|
|
983
989
|
};
|
|
990
|
+
tags_pretty_location: {
|
|
991
|
+
id: number | null;
|
|
992
|
+
inserted_at: string | null;
|
|
993
|
+
x: number | null;
|
|
994
|
+
y: number | null;
|
|
995
|
+
width: number | null;
|
|
996
|
+
conf: number | null;
|
|
997
|
+
observation_type: Database["public"]["Enums"]["tag_observation_type"] | null;
|
|
998
|
+
event_id: number | null;
|
|
999
|
+
class_name: string | null;
|
|
1000
|
+
height: number | null;
|
|
1001
|
+
location: unknown | null;
|
|
1002
|
+
latitude: number | null;
|
|
1003
|
+
longitude: number | null;
|
|
1004
|
+
};
|
|
984
1005
|
zones_and_actions_pretty_location: {
|
|
985
1006
|
id: number | null;
|
|
986
1007
|
inserted_at: string | null;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Scout Core Helpers
|
|
2
|
+
|
|
3
|
+
## Storage and Signed URLs
|
|
4
|
+
|
|
5
|
+
This module provides functionality for handling file storage and signed URL generation for media files.
|
|
6
|
+
|
|
7
|
+
### Key Features
|
|
8
|
+
|
|
9
|
+
- **Signed URL Generation**: Generate secure, time-limited URLs for accessing media files
|
|
10
|
+
- **Backward Compatibility**: Support for both new signed URLs and legacy public URLs
|
|
11
|
+
- **Seamless Integration**: Signed URLs are automatically set as `media_url` for easy use
|
|
12
|
+
- **Efficient Client Usage**: Accepts existing Supabase client to avoid creating multiple instances
|
|
13
|
+
|
|
14
|
+
### Files
|
|
15
|
+
|
|
16
|
+
- **`storage.ts`**: Server-side functions for generating signed URLs (requires "use server")
|
|
17
|
+
- **`eventUtils.ts`**: Client-side utility functions for working with event media URLs
|
|
18
|
+
|
|
19
|
+
### Storage Functions (Server-side)
|
|
20
|
+
|
|
21
|
+
#### `generateSignedUrl(filePath, expiresIn, supabaseClient?)`
|
|
22
|
+
|
|
23
|
+
Generates a signed URL for a file in Supabase storage.
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
// With existing client
|
|
27
|
+
const signedUrl = await generateSignedUrl(
|
|
28
|
+
"events/123/image.jpg",
|
|
29
|
+
3600,
|
|
30
|
+
supabaseClient
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Without existing client (creates new one)
|
|
34
|
+
const signedUrl = await generateSignedUrl("events/123/image.jpg", 3600);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
#### `addSignedUrlsToEvents(events, supabaseClient?)`
|
|
38
|
+
|
|
39
|
+
Adds signed URLs to an array of events, setting them as `media_url`.
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// With existing client
|
|
43
|
+
const eventsWithUrls = await addSignedUrlsToEvents(events, supabaseClient);
|
|
44
|
+
|
|
45
|
+
// Without existing client (creates new one)
|
|
46
|
+
const eventsWithUrls = await addSignedUrlsToEvents(events);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
#### `addSignedUrlToEvent(event, supabaseClient?)`
|
|
50
|
+
|
|
51
|
+
Adds a signed URL to a single event, setting it as `media_url`.
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
// With existing client
|
|
55
|
+
const eventWithUrl = await addSignedUrlToEvent(event, supabaseClient);
|
|
56
|
+
|
|
57
|
+
// Without existing client (creates new one)
|
|
58
|
+
const eventWithUrl = await addSignedUrlToEvent(event);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Event Utility Functions (Client-side)
|
|
62
|
+
|
|
63
|
+
#### `getEventMediaUrl(event)`
|
|
64
|
+
|
|
65
|
+
Gets the media URL for an event (now simply returns `event.media_url`).
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
const mediaUrl = getEventMediaUrl(event);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
#### `hasEventMedia(event)`
|
|
72
|
+
|
|
73
|
+
Checks if an event has any media URL available.
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
const hasMedia = hasEventMedia(event);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Usage in Components
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { getEventMediaUrl, hasEventMedia } from "@adventurelabs/scout-core";
|
|
83
|
+
|
|
84
|
+
function EventMedia({ event }) {
|
|
85
|
+
const mediaUrl = getEventMediaUrl(event);
|
|
86
|
+
|
|
87
|
+
if (!hasEventMedia(event)) {
|
|
88
|
+
return <div>No media available</div>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return <img src={mediaUrl} alt="Event media" />;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Database Schema
|
|
96
|
+
|
|
97
|
+
Events now have two URL-related fields:
|
|
98
|
+
|
|
99
|
+
- `file_path`: The storage path for generating signed URLs
|
|
100
|
+
- `media_url`: The URL for accessing media (signed URL when file_path exists, legacy public URL otherwise)
|
|
101
|
+
|
|
102
|
+
### How It Works
|
|
103
|
+
|
|
104
|
+
1. **Event Creation**: Files are uploaded and `file_path` is stored (no `media_url` initially)
|
|
105
|
+
2. **Event Fetching**: When events are retrieved, signed URLs are generated and set as `media_url`
|
|
106
|
+
3. **URL Access**: Components simply use `event.media_url` as before
|
|
107
|
+
4. **Fallback**: If signed URL generation fails, existing `media_url` is preserved
|
|
108
|
+
|
|
109
|
+
### Performance Optimization
|
|
110
|
+
|
|
111
|
+
The storage functions accept an optional `supabaseClient` parameter to reuse existing client instances:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// Efficient: Reuse existing client
|
|
115
|
+
const supabase = await newServerClient();
|
|
116
|
+
const events = await fetchEvents(supabase);
|
|
117
|
+
const eventsWithUrls = await addSignedUrlsToEvents(events, supabase);
|
|
118
|
+
|
|
119
|
+
// Less efficient: Creates new client for each operation
|
|
120
|
+
const events = await fetchEvents();
|
|
121
|
+
const eventsWithUrls = await addSignedUrlsToEvents(events); // Creates new client
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Migration Strategy
|
|
125
|
+
|
|
126
|
+
1. **Phase 1**: Store `file_path` for new events (no `media_url`)
|
|
127
|
+
2. **Phase 2**: Generate signed URLs on fetch and set as `media_url`
|
|
128
|
+
3. **Phase 3**: All events use signed URLs seamlessly
|
|
129
|
+
|
|
130
|
+
### Security Benefits
|
|
131
|
+
|
|
132
|
+
- **Time-limited access**: URLs expire after a configurable time
|
|
133
|
+
- **Secure access**: URLs are cryptographically signed
|
|
134
|
+
- **No public exposure**: Files are not publicly accessible without signed URLs
|
|
135
|
+
- **Seamless integration**: No changes needed in existing components
|
package/hooks/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Scout Hooks
|
|
2
|
+
|
|
3
|
+
This directory contains React hooks for the Scout application.
|
|
4
|
+
|
|
5
|
+
## useScoutRefresh
|
|
6
|
+
|
|
7
|
+
A hook for refreshing scout data with detailed timing measurements for performance monitoring and debugging.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- **Automatic refresh**: Automatically refreshes data on component mount
|
|
12
|
+
- **Manual refresh**: Provides a function to manually trigger refreshes
|
|
13
|
+
- **Detailed timing**: Measures the duration of each portion of the loading process
|
|
14
|
+
- **Concurrent protection**: Prevents multiple simultaneous refresh operations
|
|
15
|
+
- **Error handling**: Graceful error handling with state consistency
|
|
16
|
+
|
|
17
|
+
### Timing Measurements
|
|
18
|
+
|
|
19
|
+
The hook tracks the duration of several key operations:
|
|
20
|
+
|
|
21
|
+
1. **Herd Modules API Call** (`herd_modules_api_duration_ms`): Time taken to fetch herd modules from the server
|
|
22
|
+
2. **User API Call** (`user_api_duration_ms`): Time taken to fetch user data from the server
|
|
23
|
+
3. **Data Processing** (`data_processing_duration_ms`): Time taken to process and dispatch data to the store
|
|
24
|
+
4. **LocalStorage Operations** (`localStorage_duration_ms`): Time taken for localStorage read/write operations
|
|
25
|
+
5. **Total Duration** (`herd_modules_loaded_in_ms`): Overall time from start to completion
|
|
26
|
+
|
|
27
|
+
### Usage
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { useScoutRefresh } from "../hooks";
|
|
31
|
+
|
|
32
|
+
function MyComponent() {
|
|
33
|
+
const { handleRefresh, getTimingStats } = useScoutRefresh({
|
|
34
|
+
autoRefresh: true,
|
|
35
|
+
onRefreshComplete: () => {
|
|
36
|
+
console.log("Refresh completed!");
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const handleManualRefresh = async () => {
|
|
41
|
+
await handleRefresh();
|
|
42
|
+
|
|
43
|
+
// Get timing statistics
|
|
44
|
+
const stats = getTimingStats();
|
|
45
|
+
console.log("Performance breakdown:");
|
|
46
|
+
console.log(`- Herd modules API: ${stats.herdModulesApi}ms`);
|
|
47
|
+
console.log(`- User API: ${stats.userApi}ms`);
|
|
48
|
+
console.log(`- Data processing: ${stats.dataProcessing}ms`);
|
|
49
|
+
console.log(`- LocalStorage: ${stats.localStorage}ms`);
|
|
50
|
+
console.log(`- Total: ${stats.totalDuration}ms`);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div>
|
|
55
|
+
<button onClick={handleManualRefresh}>Refresh Data</button>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Redux State
|
|
62
|
+
|
|
63
|
+
The hook automatically updates the Redux store with timing information:
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
// Access timing data from the store
|
|
67
|
+
const timingData = useSelector((state) => ({
|
|
68
|
+
herdModulesApi: state.scout.herd_modules_api_duration_ms,
|
|
69
|
+
userApi: state.scout.user_api_duration_ms,
|
|
70
|
+
dataProcessing: state.scout.data_processing_duration_ms,
|
|
71
|
+
localStorage: state.scout.localStorage_duration_ms,
|
|
72
|
+
total: state.scout.herd_modules_loaded_in_ms,
|
|
73
|
+
}));
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Performance Monitoring
|
|
77
|
+
|
|
78
|
+
Use these timing measurements to:
|
|
79
|
+
|
|
80
|
+
- Identify performance bottlenecks in the loading process
|
|
81
|
+
- Monitor API response times
|
|
82
|
+
- Track data processing efficiency
|
|
83
|
+
- Debug localStorage performance issues
|
|
84
|
+
- Set performance budgets and alerts
|
|
85
|
+
|
|
86
|
+
### Error Handling
|
|
87
|
+
|
|
88
|
+
The hook includes comprehensive error handling:
|
|
89
|
+
|
|
90
|
+
- API response validation
|
|
91
|
+
- Graceful fallbacks for localStorage failures
|
|
92
|
+
- Consistent state updates even on errors
|
|
93
|
+
- Detailed error logging for debugging
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adventurelabs/scout-core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.72",
|
|
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",
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
},
|
|
34
34
|
"homepage": "https://github.com/Adventurous-Bytes/scout#readme",
|
|
35
35
|
"peerDependencies": {
|
|
36
|
+
"next": ">=15.0.0",
|
|
36
37
|
"react": ">=18.0.0",
|
|
37
38
|
"react-dom": ">=18.0.0",
|
|
38
|
-
"next": ">=15.0.0",
|
|
39
39
|
"react-redux": ">=9.0.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
"typescript": "^5.0.0"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@supabase/supabase-js": "^2.53.0",
|
|
48
|
-
"@supabase/ssr": "^0.6.1",
|
|
49
47
|
"@reduxjs/toolkit": "^2.0.0",
|
|
48
|
+
"@supabase/ssr": "^0.6.1",
|
|
49
|
+
"@supabase/supabase-js": "^2.53.0",
|
|
50
50
|
"zustand": "^4.0.0"
|
|
51
51
|
}
|
|
52
52
|
}
|