@adventurelabs/scout-core 2.0.18 → 2.1.0
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/client/index.d.ts +4 -0
- package/dist/client/index.js +4 -0
- package/dist/helpers/cache.d.ts +4 -4
- package/dist/helpers/cache.js +52 -17
- package/dist/helpers/eventMedia.d.ts +6 -4
- package/dist/helpers/eventMedia.js +7 -6
- package/dist/helpers/gallery.d.ts +4 -0
- package/dist/helpers/gallery.js +5 -0
- package/dist/helpers/gallery.queries.d.ts +14 -0
- package/dist/helpers/gallery.queries.js +49 -0
- package/dist/helpers/galleryMedia.d.ts +9 -0
- package/dist/helpers/galleryMedia.js +41 -0
- package/dist/helpers/herd_modules.queries.js +14 -6
- package/dist/helpers/mediaRows.d.ts +12 -0
- package/dist/helpers/mediaRows.js +80 -0
- package/dist/helpers/mediaState.d.ts +33 -0
- package/dist/helpers/mediaState.js +86 -0
- package/dist/helpers/media_urls.d.ts +2 -1
- package/dist/helpers/media_urls.js +13 -2
- package/dist/helpers/parts.d.ts +7 -2
- package/dist/helpers/parts.js +16 -7
- package/dist/helpers/parts_server.d.ts +1 -1
- package/dist/helpers/product_catalog.d.ts +9 -0
- package/dist/helpers/product_catalog.js +36 -0
- package/dist/helpers/session_incidents_server.d.ts +5 -0
- package/dist/helpers/storage_internal.d.ts +4 -3
- package/dist/helpers/storage_internal.js +46 -14
- package/dist/helpers/tags.queries.js +2 -0
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/useInfiniteQuery.d.ts +3 -1
- package/dist/hooks/useInfiniteQuery.js +37 -43
- package/dist/hooks/useScoutRealtimeBroadcast.d.ts +4 -0
- package/dist/hooks/useScoutRealtimeBroadcast.js +76 -11
- package/dist/hooks/useScoutRefresh.js +18 -11
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.js +4 -0
- package/dist/store/api.d.ts +389 -9
- package/dist/store/api.js +163 -75
- package/dist/store/configureStore.d.ts +4 -0
- package/dist/store/hooks.d.ts +4 -1
- package/dist/store/hooks.js +16 -0
- package/dist/store/scout.d.ts +4 -2
- package/dist/store/scout.js +21 -2
- package/dist/types/db.d.ts +19 -5
- package/dist/types/herd_module.d.ts +12 -1
- package/dist/types/supabase.d.ts +77 -0
- package/package.json +4 -2
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { isNonEmptyStorageFilePath } from "./storagePath";
|
|
2
|
+
/** Extensions classified by the client; image/video lists mirror the queue predicates. */
|
|
3
|
+
export const VIDEO_FILE_EXTENSIONS = [
|
|
4
|
+
"mp4",
|
|
5
|
+
"m4v",
|
|
6
|
+
"webm",
|
|
7
|
+
"mov",
|
|
8
|
+
"mkv",
|
|
9
|
+
"avi",
|
|
10
|
+
];
|
|
11
|
+
export const IMAGE_FILE_EXTENSIONS = [
|
|
12
|
+
"jpg",
|
|
13
|
+
"jpeg",
|
|
14
|
+
"png",
|
|
15
|
+
"gif",
|
|
16
|
+
"webp",
|
|
17
|
+
"bmp",
|
|
18
|
+
];
|
|
19
|
+
export const AUDIO_FILE_EXTENSIONS = [
|
|
20
|
+
"wav",
|
|
21
|
+
"mp3",
|
|
22
|
+
"m4a",
|
|
23
|
+
"aac",
|
|
24
|
+
"flac",
|
|
25
|
+
"ogg",
|
|
26
|
+
];
|
|
27
|
+
const KIND_BY_EXTENSION = new Map([
|
|
28
|
+
...VIDEO_FILE_EXTENSIONS.map((ext) => [ext, "video"]),
|
|
29
|
+
...IMAGE_FILE_EXTENSIONS.map((ext) => [ext, "image"]),
|
|
30
|
+
...AUDIO_FILE_EXTENSIONS.map((ext) => [ext, "audio"]),
|
|
31
|
+
]);
|
|
32
|
+
/** Lowercased extension of a storage path or URL, or null when there is none. */
|
|
33
|
+
export function mediaFileExtension(filePath) {
|
|
34
|
+
if (!isNonEmptyStorageFilePath(filePath))
|
|
35
|
+
return null;
|
|
36
|
+
const withoutQuery = filePath.split(/[?#]/)[0];
|
|
37
|
+
const name = withoutQuery.slice(withoutQuery.lastIndexOf("/") + 1);
|
|
38
|
+
const dot = name.lastIndexOf(".");
|
|
39
|
+
if (dot <= 0 || dot === name.length - 1)
|
|
40
|
+
return null;
|
|
41
|
+
return name.slice(dot + 1).toLowerCase();
|
|
42
|
+
}
|
|
43
|
+
/** One classifier for events and artifacts; a recognized extension wins over the stored type. */
|
|
44
|
+
export function classifyMedia(row) {
|
|
45
|
+
const extension = mediaFileExtension(row.file_path) ?? mediaFileExtension(row.media_url);
|
|
46
|
+
const byExtension = extension ? KIND_BY_EXTENSION.get(extension) : undefined;
|
|
47
|
+
if (byExtension)
|
|
48
|
+
return byExtension;
|
|
49
|
+
if (row.media_type)
|
|
50
|
+
return row.media_type;
|
|
51
|
+
if (row.modality === "video")
|
|
52
|
+
return "video";
|
|
53
|
+
return "unknown";
|
|
54
|
+
}
|
|
55
|
+
export const isVideoMedia = (row) => classifyMedia(row) === "video";
|
|
56
|
+
/** Readiness of image/video derivatives or the original for media without derivatives. */
|
|
57
|
+
export function mediaStatus(row) {
|
|
58
|
+
const kind = classifyMedia(row);
|
|
59
|
+
if (kind !== "image" && kind !== "video")
|
|
60
|
+
return originalMediaStatus(row);
|
|
61
|
+
const derivatives = kind === "video"
|
|
62
|
+
? [
|
|
63
|
+
{ path: row.proxy_file_path, url: row.proxy_url },
|
|
64
|
+
{ path: row.thumbnail_file_path, url: row.thumbnail_url },
|
|
65
|
+
]
|
|
66
|
+
: [{ path: row.thumbnail_file_path, url: row.thumbnail_url }];
|
|
67
|
+
const present = derivatives.filter(({ path }) => isNonEmptyStorageFilePath(path));
|
|
68
|
+
if (present.some(({ url }) => url))
|
|
69
|
+
return "ready";
|
|
70
|
+
// An unsigned derivative is recoverable by re-signing, unlike work that never ran.
|
|
71
|
+
if (present.length > 0)
|
|
72
|
+
return "url_unavailable";
|
|
73
|
+
if (row.processing_blocked_at)
|
|
74
|
+
return "blocked";
|
|
75
|
+
return "processing";
|
|
76
|
+
}
|
|
77
|
+
/** Readiness of the original media itself, ignoring derivatives. */
|
|
78
|
+
export function originalMediaStatus(row) {
|
|
79
|
+
if (row.media_url)
|
|
80
|
+
return "ready";
|
|
81
|
+
if (isNonEmptyStorageFilePath(row.file_path))
|
|
82
|
+
return "url_unavailable";
|
|
83
|
+
if (row.processing_blocked_at)
|
|
84
|
+
return "blocked";
|
|
85
|
+
return "processing";
|
|
86
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
-
import type { IArtifact, IArtifactWithMediaUrl, ISignedMediaUrls } from "../types/db";
|
|
2
|
+
import type { IArtifact, IArtifactWithMediaUrl, IGalleryItem, IGalleryItemRow, ISignedMediaUrls } from "../types/db";
|
|
3
3
|
import type { Database } from "../types/supabase";
|
|
4
4
|
import { type IEventMediaPaths } from "./eventMedia";
|
|
5
5
|
export declare function createSignedUrlMap(client: SupabaseClient<Database>, paths: Iterable<string | null | undefined>): Promise<Map<string, string | null>>;
|
|
6
6
|
export declare function addEventMediaUrls<T extends IEventMediaPaths>(client: SupabaseClient<Database>, events: T[]): Promise<Array<T & ISignedMediaUrls>>;
|
|
7
7
|
export declare function addArtifactMediaUrls(client: SupabaseClient<Database>, artifacts: IArtifact[]): Promise<IArtifactWithMediaUrl[]>;
|
|
8
|
+
export declare function addGalleryMediaUrls(client: SupabaseClient<Database>, items: IGalleryItemRow[]): Promise<IGalleryItem[]>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { collectArtifactStoragePaths, withArtifactMediaUrls, } from "./artifactMedia";
|
|
2
|
-
import {
|
|
2
|
+
import { withGalleryMediaUrls } from "./galleryMedia";
|
|
3
|
+
import { collectMediaStoragePaths, withEventMediaUrls, } from "./eventMedia";
|
|
3
4
|
import { generateSignedUrlsBatchWithClient } from "./storage_internal";
|
|
4
5
|
import { isNonEmptyStorageFilePath } from "./storagePath";
|
|
5
6
|
export async function createSignedUrlMap(client, paths) {
|
|
@@ -8,10 +9,20 @@ export async function createSignedUrlMap(client, paths) {
|
|
|
8
9
|
return new Map(uniquePaths.map((path, index) => [path, urls[index]]));
|
|
9
10
|
}
|
|
10
11
|
export async function addEventMediaUrls(client, events) {
|
|
11
|
-
const urlMap = await createSignedUrlMap(client,
|
|
12
|
+
const urlMap = await createSignedUrlMap(client, collectMediaStoragePaths(events));
|
|
12
13
|
return events.map((event) => withEventMediaUrls(event, urlMap));
|
|
13
14
|
}
|
|
14
15
|
export async function addArtifactMediaUrls(client, artifacts) {
|
|
15
16
|
const urlMap = await createSignedUrlMap(client, collectArtifactStoragePaths(artifacts));
|
|
16
17
|
return artifacts.map((artifact) => withArtifactMediaUrls(artifact, urlMap));
|
|
17
18
|
}
|
|
19
|
+
export async function addGalleryMediaUrls(client, items) {
|
|
20
|
+
let urlMap = new Map();
|
|
21
|
+
try {
|
|
22
|
+
urlMap = await createSignedUrlMap(client, collectMediaStoragePaths(items));
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
console.warn("Failed to generate signed URLs for gallery:", error);
|
|
26
|
+
}
|
|
27
|
+
return items.map((item) => withGalleryMediaUrls(item, urlMap));
|
|
28
|
+
}
|
package/dist/helpers/parts.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { Database } from "../types/supabase";
|
|
2
|
-
import { EntityLifecycle, IPart,
|
|
2
|
+
import { EntityLifecycle, IPart, IProductCatalog, PartInsert } from "../types/db";
|
|
3
3
|
import { IWebResponseCompatible } from "../types/requests";
|
|
4
4
|
import { SupabaseClient } from "@supabase/supabase-js";
|
|
5
5
|
import { LifecycleUpdateOptions } from "./lifecycle";
|
|
6
|
+
/** Parts grouped by device, plus one shared copy of each referenced product. */
|
|
7
|
+
export interface IPartsByDeviceWithCatalog {
|
|
8
|
+
parts_by_device: Record<number, IPart[]>;
|
|
9
|
+
product_catalog: IProductCatalog;
|
|
10
|
+
}
|
|
6
11
|
export declare function get_parts_by_device_id(client: SupabaseClient<Database>, device_id: number): Promise<IWebResponseCompatible<IPart[]>>;
|
|
7
12
|
export declare function get_part_by_id(client: SupabaseClient<Database>, part_id: number): Promise<IWebResponseCompatible<IPart | null>>;
|
|
8
13
|
export declare function get_parts_by_serial_number(client: SupabaseClient<Database>, serial_number: string): Promise<IWebResponseCompatible<IPart[]>>;
|
|
@@ -16,7 +21,7 @@ export declare function delete_part(client: SupabaseClient<Database>, part_id: n
|
|
|
16
21
|
export declare function update_part_status(client: SupabaseClient<Database>, part_id: number, lifecycle: EntityLifecycle, options?: LifecycleUpdateOptions): Promise<IWebResponseCompatible<IPart | null>>;
|
|
17
22
|
export declare function get_parts_by_certificate_id(client: SupabaseClient<Database>, certificate_id: number): Promise<IWebResponseCompatible<IPart[]>>;
|
|
18
23
|
export declare function get_parts_by_herd_id(client: SupabaseClient<Database>, herd_id: number): Promise<IWebResponseCompatible<IPart[]>>;
|
|
19
|
-
export declare function get_parts_by_herd_ids(client: SupabaseClient<Database>, herd_ids: number[]): Promise<IWebResponseCompatible<
|
|
24
|
+
export declare function get_parts_by_herd_ids(client: SupabaseClient<Database>, herd_ids: number[]): Promise<IWebResponseCompatible<IPartsByDeviceWithCatalog>>;
|
|
20
25
|
export declare function restore_part(client: SupabaseClient<Database>, part_id: number, options?: LifecycleUpdateOptions): Promise<IWebResponseCompatible<IPart | null>>;
|
|
21
26
|
export declare function hard_delete_part(client: SupabaseClient<Database>, part_id: number): Promise<IWebResponseCompatible<IPart | null>>;
|
|
22
27
|
export declare function get_deleted_parts_by_device_id(client: SupabaseClient<Database>, device_id: number): Promise<IWebResponseCompatible<IPart[]>>;
|
package/dist/helpers/parts.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { IWebResponse } from "../types/requests";
|
|
2
2
|
import { update_part_lifecycle } from "./lifecycle";
|
|
3
|
+
import { build_product_catalog, EMPTY_PRODUCT_CATALOG, } from "./product_catalog";
|
|
3
4
|
export async function get_parts_by_device_id(client, device_id) {
|
|
4
5
|
const { data, error } = await client
|
|
5
6
|
.from("parts")
|
|
@@ -173,7 +174,10 @@ export async function get_parts_by_herd_id(client, herd_id) {
|
|
|
173
174
|
export async function get_parts_by_herd_ids(client, herd_ids) {
|
|
174
175
|
var _a;
|
|
175
176
|
if (herd_ids.length === 0) {
|
|
176
|
-
return IWebResponse.success({
|
|
177
|
+
return IWebResponse.success({
|
|
178
|
+
parts_by_device: {},
|
|
179
|
+
product_catalog: EMPTY_PRODUCT_CATALOG,
|
|
180
|
+
}).to_compatible();
|
|
177
181
|
}
|
|
178
182
|
const { data, error } = await client
|
|
179
183
|
.from("parts")
|
|
@@ -191,18 +195,23 @@ export async function get_parts_by_herd_ids(client, herd_ids) {
|
|
|
191
195
|
if (error) {
|
|
192
196
|
return IWebResponse.error(error.message).to_compatible();
|
|
193
197
|
}
|
|
194
|
-
const
|
|
198
|
+
const parts_by_device = {};
|
|
199
|
+
const product_entries = [];
|
|
195
200
|
for (const row of data ?? []) {
|
|
196
201
|
if (row.device_id == null)
|
|
197
202
|
continue;
|
|
198
203
|
const { product_numbers, devices: _devices, ...partRow } = row;
|
|
199
|
-
const part =
|
|
200
|
-
|
|
204
|
+
const part = partRow;
|
|
205
|
+
product_entries.push({
|
|
206
|
+
product_number: part.product_number,
|
|
201
207
|
product: product_numbers?.products ?? null,
|
|
202
|
-
};
|
|
203
|
-
(
|
|
208
|
+
});
|
|
209
|
+
(parts_by_device[_a = row.device_id] ?? (parts_by_device[_a] = [])).push(part);
|
|
204
210
|
}
|
|
205
|
-
return IWebResponse.success(
|
|
211
|
+
return IWebResponse.success({
|
|
212
|
+
parts_by_device,
|
|
213
|
+
product_catalog: build_product_catalog(product_entries),
|
|
214
|
+
}).to_compatible();
|
|
206
215
|
}
|
|
207
216
|
export async function restore_part(client, part_id, options) {
|
|
208
217
|
const { data, error } = await client
|
|
@@ -193,7 +193,7 @@ export declare function server_get_parts_by_herd_id(herd_id: number): Promise<im
|
|
|
193
193
|
serial_number: string;
|
|
194
194
|
updated_at: string | null;
|
|
195
195
|
}[]>>;
|
|
196
|
-
export declare function server_get_parts_by_herd_ids(herd_ids: number[]): Promise<import("../types").IWebResponseCompatible<
|
|
196
|
+
export declare function server_get_parts_by_herd_ids(herd_ids: number[]): Promise<import("../types").IWebResponseCompatible<import("./parts").IPartsByDeviceWithCatalog>>;
|
|
197
197
|
export declare function server_restore_part(part_id: number, options?: LifecycleUpdateOptions): Promise<import("../types").IWebResponseCompatible<{
|
|
198
198
|
certificate_id: number | null;
|
|
199
199
|
created_at: string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { IDevice, IPart, IProduct, IProductCatalog } from "../types/db";
|
|
2
|
+
export declare const EMPTY_PRODUCT_CATALOG: IProductCatalog;
|
|
3
|
+
export declare function build_product_catalog(entries: Iterable<{
|
|
4
|
+
product_number: string;
|
|
5
|
+
product: IProduct | null | undefined;
|
|
6
|
+
}>): IProductCatalog;
|
|
7
|
+
export declare function get_product_for_part(catalog: IProductCatalog | null | undefined, part: Pick<IPart, "product_number"> | null | undefined): IProduct | null;
|
|
8
|
+
/** Distinct products across a device's parts, in part order. */
|
|
9
|
+
export declare function get_products_for_device(catalog: IProductCatalog | null | undefined, device: Pick<IDevice, "parts"> | null | undefined): IProduct[];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const EMPTY_PRODUCT_CATALOG = {
|
|
2
|
+
products: {},
|
|
3
|
+
product_id_by_number: {},
|
|
4
|
+
};
|
|
5
|
+
export function build_product_catalog(entries) {
|
|
6
|
+
var _a, _b;
|
|
7
|
+
const catalog = {
|
|
8
|
+
products: {},
|
|
9
|
+
product_id_by_number: {},
|
|
10
|
+
};
|
|
11
|
+
for (const { product_number, product } of entries) {
|
|
12
|
+
if (!product)
|
|
13
|
+
continue;
|
|
14
|
+
catalog.product_id_by_number[product_number] = product.id;
|
|
15
|
+
(_a = catalog.products)[_b = product.id] ?? (_a[_b] = product);
|
|
16
|
+
}
|
|
17
|
+
return catalog;
|
|
18
|
+
}
|
|
19
|
+
export function get_product_for_part(catalog, part) {
|
|
20
|
+
if (!catalog || !part?.product_number)
|
|
21
|
+
return null;
|
|
22
|
+
const product_id = catalog.product_id_by_number[part.product_number];
|
|
23
|
+
if (product_id == null)
|
|
24
|
+
return null;
|
|
25
|
+
return catalog.products[product_id] ?? null;
|
|
26
|
+
}
|
|
27
|
+
/** Distinct products across a device's parts, in part order. */
|
|
28
|
+
export function get_products_for_device(catalog, device) {
|
|
29
|
+
const products = new Map();
|
|
30
|
+
for (const part of device?.parts ?? []) {
|
|
31
|
+
const product = get_product_for_part(catalog, part);
|
|
32
|
+
if (product)
|
|
33
|
+
products.set(product.id, product);
|
|
34
|
+
}
|
|
35
|
+
return Array.from(products.values());
|
|
36
|
+
}
|
|
@@ -17,6 +17,7 @@ export declare function server_get_session_incidents_by_session_id(session_id: n
|
|
|
17
17
|
operator: string | null;
|
|
18
18
|
photo: string | null;
|
|
19
19
|
session_id: number;
|
|
20
|
+
timestamp_observation: string | null;
|
|
20
21
|
updated_at: string;
|
|
21
22
|
updated_by: string | null;
|
|
22
23
|
}[]>>;
|
|
@@ -38,6 +39,7 @@ export declare function server_get_session_incident_by_id(incident_id: number):
|
|
|
38
39
|
operator: string | null;
|
|
39
40
|
photo: string | null;
|
|
40
41
|
session_id: number;
|
|
42
|
+
timestamp_observation: string | null;
|
|
41
43
|
updated_at: string;
|
|
42
44
|
updated_by: string | null;
|
|
43
45
|
} | null>>;
|
|
@@ -59,6 +61,7 @@ export declare function server_create_session_incident(row: SessionIncidentInser
|
|
|
59
61
|
operator: string | null;
|
|
60
62
|
photo: string | null;
|
|
61
63
|
session_id: number;
|
|
64
|
+
timestamp_observation: string | null;
|
|
62
65
|
updated_at: string;
|
|
63
66
|
updated_by: string | null;
|
|
64
67
|
} | null>>;
|
|
@@ -80,6 +83,7 @@ export declare function server_update_session_incident(incident_id: number, patc
|
|
|
80
83
|
operator: string | null;
|
|
81
84
|
photo: string | null;
|
|
82
85
|
session_id: number;
|
|
86
|
+
timestamp_observation: string | null;
|
|
83
87
|
updated_at: string;
|
|
84
88
|
updated_by: string | null;
|
|
85
89
|
} | null>>;
|
|
@@ -101,6 +105,7 @@ export declare function server_delete_session_incident(incident_id: number): Pro
|
|
|
101
105
|
operator: string | null;
|
|
102
106
|
photo: string | null;
|
|
103
107
|
session_id: number;
|
|
108
|
+
timestamp_observation: string | null;
|
|
104
109
|
updated_at: string;
|
|
105
110
|
updated_by: string | null;
|
|
106
111
|
} | null>>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
-
import { Database } from "../types/supabase";
|
|
3
|
-
|
|
1
|
+
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
import type { Database } from "../types/supabase";
|
|
3
|
+
/** Signs every path with one request per bucket rather than one per path. */
|
|
4
|
+
export declare function generateSignedUrlsBatchWithClient(supabase: SupabaseClient<Database>, filePaths: readonly string[], expiresIn?: number): Promise<(string | null)[]>;
|
|
4
5
|
export declare function generateSignedUrlWithClient(supabase: SupabaseClient<Database>, filePath: string, expiresIn?: number): Promise<string | null>;
|
|
@@ -1,27 +1,59 @@
|
|
|
1
1
|
import { SIGNED_URL_EXPIRATION_SECONDS } from "../constants/db";
|
|
2
2
|
import { parseStorageFilePath } from "./storagePath";
|
|
3
|
+
const normalizeExpiration = (expiresIn) => Number.isSafeInteger(expiresIn) && expiresIn > 0
|
|
4
|
+
? expiresIn
|
|
5
|
+
: SIGNED_URL_EXPIRATION_SECONDS;
|
|
6
|
+
/** Signs every path with one request per bucket rather than one per path. */
|
|
3
7
|
export async function generateSignedUrlsBatchWithClient(supabase, filePaths, expiresIn = SIGNED_URL_EXPIRATION_SECONDS) {
|
|
4
|
-
|
|
8
|
+
const signedUrls = filePaths.map(() => null);
|
|
9
|
+
const expirationSeconds = normalizeExpiration(expiresIn);
|
|
10
|
+
const requestsByBucket = new Map();
|
|
11
|
+
let unparsedPathCount = 0;
|
|
12
|
+
filePaths.forEach((filePath, index) => {
|
|
13
|
+
const parsed = parseStorageFilePath(filePath);
|
|
14
|
+
if (!parsed) {
|
|
15
|
+
unparsedPathCount += 1;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const requests = requestsByBucket.get(parsed.bucket) ?? new Map();
|
|
19
|
+
const indices = requests.get(parsed.objectPath);
|
|
20
|
+
if (indices) {
|
|
21
|
+
indices.push(index);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
requests.set(parsed.objectPath, [index]);
|
|
25
|
+
}
|
|
26
|
+
requestsByBucket.set(parsed.bucket, requests);
|
|
27
|
+
});
|
|
28
|
+
// Legacy device-id-prefixed paths never parse, so warn once per page instead of per path.
|
|
29
|
+
if (unparsedPathCount > 0) {
|
|
30
|
+
console.warn(`Skipped ${unparsedPathCount} unparseable storage path(s)`);
|
|
31
|
+
}
|
|
32
|
+
await Promise.all(Array.from(requestsByBucket, async ([bucket, requestsByPath]) => {
|
|
33
|
+
const objectPaths = Array.from(requestsByPath.keys());
|
|
5
34
|
try {
|
|
6
|
-
const parsed = parseStorageFilePath(filePath);
|
|
7
|
-
if (!parsed) {
|
|
8
|
-
console.error("Invalid file path:", filePath);
|
|
9
|
-
return null;
|
|
10
|
-
}
|
|
11
35
|
const { data, error } = await supabase.storage
|
|
12
|
-
.from(
|
|
13
|
-
.
|
|
14
|
-
if (error) {
|
|
15
|
-
console.warn(`Error generating signed
|
|
16
|
-
return
|
|
36
|
+
.from(bucket)
|
|
37
|
+
.createSignedUrls(objectPaths, expirationSeconds);
|
|
38
|
+
if (error || !data) {
|
|
39
|
+
console.warn(`Error generating signed URLs for bucket ${bucket}:`, error?.message);
|
|
40
|
+
return;
|
|
17
41
|
}
|
|
18
|
-
|
|
42
|
+
// Signing fails per path, so an unsigned path stays null without losing the others.
|
|
43
|
+
data.forEach((entry, position) => {
|
|
44
|
+
const objectPath = entry.path != null && requestsByPath.has(entry.path)
|
|
45
|
+
? entry.path
|
|
46
|
+
: objectPaths[position];
|
|
47
|
+
requestsByPath.get(objectPath)?.forEach((index) => {
|
|
48
|
+
signedUrls[index] = entry.signedUrl || null;
|
|
49
|
+
});
|
|
50
|
+
});
|
|
19
51
|
}
|
|
20
52
|
catch (error) {
|
|
21
|
-
console.warn(`Exception generating signed
|
|
22
|
-
return null;
|
|
53
|
+
console.warn(`Exception generating signed URLs for bucket ${bucket}:`, error);
|
|
23
54
|
}
|
|
24
55
|
}));
|
|
56
|
+
return signedUrls;
|
|
25
57
|
}
|
|
26
58
|
export async function generateSignedUrlWithClient(supabase, filePath, expiresIn = SIGNED_URL_EXPIRATION_SECONDS) {
|
|
27
59
|
const [signedUrl] = await generateSignedUrlsBatchWithClient(supabase, [filePath], expiresIn);
|
|
@@ -344,6 +344,8 @@ export async function get_event_and_tags_by_event_id_query(client, event_id) {
|
|
|
344
344
|
thumbnail_generated_at: data[0].thumbnail_generated_at ?? null,
|
|
345
345
|
proxy_file_path: data[0].proxy_file_path ?? null,
|
|
346
346
|
proxy_generated_at: data[0].proxy_generated_at ?? null,
|
|
347
|
+
processing_blocked_at: data[0].processing_blocked_at ?? null,
|
|
348
|
+
processing_blocked_reason: data[0].processing_blocked_reason ?? null,
|
|
347
349
|
};
|
|
348
350
|
const [eventWithUrl] = await addEventMediaUrls(client, [transformedData]);
|
|
349
351
|
return IWebResponse.success(eventWithUrl).to_compatible();
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -18,5 +18,5 @@ export { useScoutRealtimeMaintenanceRequests } from "./useScoutRealtimeMaintenan
|
|
|
18
18
|
export { useScoutRealtimeContacts } from "./useScoutRealtimeContacts";
|
|
19
19
|
export { useScoutRealtimeSessionIncidents } from "./useScoutRealtimeSessionIncidents";
|
|
20
20
|
export { useScoutRealtimeOperatingContexts, type ComplianceRealtimeData, type ComplianceRealtimeRow, type OperatingContextRealtimeData, type OperatingContextRealtimeRow, } from "./useScoutRealtimeOperatingContexts";
|
|
21
|
-
export { useInfiniteSessionsByHerd, useInfiniteSessionsByDevice, useInfiniteEventsByHerd, useInfiniteEventsByDevice, useInfiniteArtifactsByHerd, useInfiniteArtifactsByDevice, useInfiniteFeedByHerd, useInfiniteFeedByDevice, useInfiniteAnalysisJobs, useInfiniteAnalysisTasks, useIntersectionObserver, type InfiniteScrollData, type InfiniteRefetchOptions, type UseInfiniteScrollOptions, type UseAnalysisJobsInfiniteOptions, type UseAnalysisTasksInfiniteOptions, } from "./useInfiniteQuery";
|
|
21
|
+
export { useInfiniteSessionsByHerd, useInfiniteSessionsByDevice, useInfiniteEventsByHerd, useInfiniteEventsByDevice, useInfiniteArtifactsByHerd, useInfiniteArtifactsByDevice, useInfiniteFeedByHerd, useInfiniteFeedByDevice, useInfiniteGalleryByHerd, useInfiniteGalleryByDevice, useInfiniteAnalysisJobs, useInfiniteAnalysisTasks, useIntersectionObserver, type InfiniteScrollData, type InfiniteRefetchOptions, type UseInfiniteScrollOptions, type UseAnalysisJobsInfiniteOptions, type UseAnalysisTasksInfiniteOptions, } from "./useInfiniteQuery";
|
|
22
22
|
export { useLoadingPerformance, useSessionSummariesByHerd, useHasSessionSummaries, } from "../store/hooks";
|
package/dist/hooks/index.js
CHANGED
|
@@ -19,6 +19,6 @@ export { useScoutRealtimeContacts } from "./useScoutRealtimeContacts";
|
|
|
19
19
|
export { useScoutRealtimeSessionIncidents } from "./useScoutRealtimeSessionIncidents";
|
|
20
20
|
export { useScoutRealtimeOperatingContexts, } from "./useScoutRealtimeOperatingContexts";
|
|
21
21
|
// RTK Query infinite scroll hooks
|
|
22
|
-
export { useInfiniteSessionsByHerd, useInfiniteSessionsByDevice, useInfiniteEventsByHerd, useInfiniteEventsByDevice, useInfiniteArtifactsByHerd, useInfiniteArtifactsByDevice, useInfiniteFeedByHerd, useInfiniteFeedByDevice, useInfiniteAnalysisJobs, useInfiniteAnalysisTasks, useIntersectionObserver, } from "./useInfiniteQuery";
|
|
22
|
+
export { useInfiniteSessionsByHerd, useInfiniteSessionsByDevice, useInfiniteEventsByHerd, useInfiniteEventsByDevice, useInfiniteArtifactsByHerd, useInfiniteArtifactsByDevice, useInfiniteFeedByHerd, useInfiniteFeedByDevice, useInfiniteGalleryByHerd, useInfiniteGalleryByDevice, useInfiniteAnalysisJobs, useInfiniteAnalysisTasks, useIntersectionObserver, } from "./useInfiniteQuery";
|
|
23
23
|
// Session summaries and performance hooks
|
|
24
24
|
export { useLoadingPerformance, useSessionSummariesByHerd, useHasSessionSummaries, } from "../store/hooks";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
-
import { IArtifactWithMediaUrl, ISessionWithCoordinates, IEventAndTagsPrettyLocation, IFeedItem, IAnalysisJob, IAnalysisTask, AnalysisWorkStatus } from "../types/db";
|
|
2
|
+
import { IArtifactWithMediaUrl, ISessionWithCoordinates, IEventAndTagsPrettyLocation, IFeedItem, IGalleryItem, IAnalysisJob, IAnalysisTask, AnalysisWorkStatus } from "../types/db";
|
|
3
3
|
export interface UseInfiniteScrollOptions {
|
|
4
4
|
limit?: number;
|
|
5
5
|
enabled?: boolean;
|
|
@@ -37,6 +37,8 @@ export declare const useInfiniteArtifactsByHerd: (herdId: number, options: UseIn
|
|
|
37
37
|
export declare const useInfiniteArtifactsByDevice: (deviceId: number, options: UseInfiniteScrollOptions) => InfiniteScrollData<IArtifactWithMediaUrl>;
|
|
38
38
|
export declare const useInfiniteFeedByHerd: (herdId: number, options: UseInfiniteScrollOptions) => InfiniteScrollData<IFeedItem>;
|
|
39
39
|
export declare const useInfiniteFeedByDevice: (deviceId: number, options: UseInfiniteScrollOptions) => InfiniteScrollData<IFeedItem>;
|
|
40
|
+
export declare const useInfiniteGalleryByHerd: (herdId: number, options: UseInfiniteScrollOptions) => InfiniteScrollData<IGalleryItem>;
|
|
41
|
+
export declare const useInfiniteGalleryByDevice: (deviceId: number, options: UseInfiniteScrollOptions) => InfiniteScrollData<IGalleryItem>;
|
|
40
42
|
export interface UseAnalysisJobsInfiniteOptions {
|
|
41
43
|
limit?: number;
|
|
42
44
|
enabled?: boolean;
|
|
@@ -1,34 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useState, useCallback, useMemo, useEffect, useRef } from "react";
|
|
3
|
-
import { useGetSessionsInfiniteByHerdQuery, useGetSessionsInfiniteByDeviceQuery, useGetEventsInfiniteByHerdQuery, useGetEventsInfiniteByDeviceQuery, useGetArtifactsInfiniteByHerdQuery, useGetArtifactsInfiniteByDeviceQuery, useGetFeedInfiniteByHerdQuery, useGetFeedInfiniteByDeviceQuery, useGetAnalysisJobsInfiniteQuery, useGetAnalysisTasksInfiniteQuery, } from "../store/api";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
function rowsEqual(left, right) {
|
|
7
|
-
if (left === right)
|
|
8
|
-
return true;
|
|
9
|
-
if (typeof left !== "object" ||
|
|
10
|
-
typeof right !== "object" ||
|
|
11
|
-
left === null ||
|
|
12
|
-
right === null) {
|
|
13
|
-
return false;
|
|
14
|
-
}
|
|
15
|
-
if (Array.isArray(left) || Array.isArray(right)) {
|
|
16
|
-
return (Array.isArray(left) &&
|
|
17
|
-
Array.isArray(right) &&
|
|
18
|
-
left.length === right.length &&
|
|
19
|
-
left.every((item, index) => rowsEqual(item, right[index])));
|
|
20
|
-
}
|
|
21
|
-
const keys = Object.keys(left).filter((key) => !REMINTED_URL_KEYS.has(key));
|
|
22
|
-
const rightKeys = Object.keys(right).filter((key) => !REMINTED_URL_KEYS.has(key));
|
|
23
|
-
return (keys.length === rightKeys.length &&
|
|
24
|
-
keys.every((key) => Object.prototype.hasOwnProperty.call(right, key) &&
|
|
25
|
-
rowsEqual(Reflect.get(left, key), Reflect.get(right, key))));
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Reuse the previous row object when a refetch returned the same content, so consumers
|
|
29
|
-
* can memoize on row identity. Rows that only differ by a re-minted signed URL keep the
|
|
30
|
-
* earlier object, and therefore the earlier URL, until the row itself changes.
|
|
31
|
-
*/
|
|
3
|
+
import { useGetSessionsInfiniteByHerdQuery, useGetSessionsInfiniteByDeviceQuery, useGetEventsInfiniteByHerdQuery, useGetEventsInfiniteByDeviceQuery, useGetArtifactsInfiniteByHerdQuery, useGetArtifactsInfiniteByDeviceQuery, useGetFeedInfiniteByHerdQuery, useGetFeedInfiniteByDeviceQuery, useGetGalleryInfiniteByHerdQuery, useGetGalleryInfiniteByDeviceQuery, useGetAnalysisJobsInfiniteQuery, useGetAnalysisTasksInfiniteQuery, } from "../store/api";
|
|
4
|
+
import { normalizePageLimit, rowsEqualIgnoringSignedUrls, withRefreshedSignedUrls, } from "../helpers/mediaRows";
|
|
5
|
+
/** Reuses the previous row object, with fresh signed URLs, so consumers can memoize on identity. */
|
|
32
6
|
function preserveRowReferences(previous, next, rowKey) {
|
|
33
7
|
const previousByKey = new Map(previous
|
|
34
8
|
.map((row) => [rowKey(row), row])
|
|
@@ -36,7 +10,9 @@ function preserveRowReferences(previous, next, rowKey) {
|
|
|
36
10
|
return next.map((row) => {
|
|
37
11
|
const key = rowKey(row);
|
|
38
12
|
const previousRow = key == null ? undefined : previousByKey.get(key);
|
|
39
|
-
return previousRow &&
|
|
13
|
+
return previousRow && rowsEqualIgnoringSignedUrls(previousRow, row)
|
|
14
|
+
? withRefreshedSignedUrls(previousRow, row)
|
|
15
|
+
: row;
|
|
40
16
|
});
|
|
41
17
|
}
|
|
42
18
|
/** Cursors are flat records of scalars, so a shallow comparison identifies a page. */
|
|
@@ -51,6 +27,7 @@ function sameCursor(left, right) {
|
|
|
51
27
|
}
|
|
52
28
|
const rowId = (row) => row.id;
|
|
53
29
|
const feedRowKey = (row) => `${row.sort_ts ?? ""}_${row.sort_id ?? ""}_${row.feed_type ?? ""}`;
|
|
30
|
+
const galleryRowKey = (row) => `${row.feed_type ?? ""}_${row.id ?? ""}`;
|
|
54
31
|
const emptyPaging = (key) => ({
|
|
55
32
|
key,
|
|
56
33
|
cursor: null,
|
|
@@ -234,7 +211,7 @@ function useFiltersKey(options) {
|
|
|
234
211
|
]);
|
|
235
212
|
}
|
|
236
213
|
const rangeArgs = (options) => ({
|
|
237
|
-
limit: options.limit
|
|
214
|
+
limit: normalizePageLimit(options.limit),
|
|
238
215
|
supabase: options.supabase,
|
|
239
216
|
rangeStart: options.rangeStart ?? null,
|
|
240
217
|
rangeEnd: options.rangeEnd ?? null,
|
|
@@ -259,16 +236,12 @@ const artifactsPage = (response) => ({
|
|
|
259
236
|
nextCursor: response.nextCursor,
|
|
260
237
|
hasMore: response.hasMore,
|
|
261
238
|
});
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
// non-empty page with a next cursor means there is more to load.
|
|
269
|
-
hasMore: rows.length > 0 && response.nextCursor !== null,
|
|
270
|
-
};
|
|
271
|
-
};
|
|
239
|
+
/** The RPCs over-fetch by one to answer `hasMore`, so an exactly-full page is terminal. */
|
|
240
|
+
const itemsPage = (response) => ({
|
|
241
|
+
rows: Array.isArray(response.items) ? response.items : [],
|
|
242
|
+
nextCursor: response.nextCursor,
|
|
243
|
+
hasMore: response.hasMore,
|
|
244
|
+
});
|
|
272
245
|
const analysisJobsPage = (response) => ({
|
|
273
246
|
rows: response.jobs,
|
|
274
247
|
nextCursor: response.nextCursor,
|
|
@@ -350,7 +323,7 @@ export const useInfiniteFeedByHerd = (herdId, options) => {
|
|
|
350
323
|
const paging = usePaging(`herd:${herdId}|${filtersKey}`);
|
|
351
324
|
const query = useGetFeedInfiniteByHerdQuery({ herdId, cursor: paging.cursor, ...rangeArgs(options) }, { skip: !options.enabled || !herdId });
|
|
352
325
|
return useInfinitePages(paging, query, {
|
|
353
|
-
toPage:
|
|
326
|
+
toPage: itemsPage,
|
|
354
327
|
rowKey: feedRowKey,
|
|
355
328
|
});
|
|
356
329
|
};
|
|
@@ -359,10 +332,31 @@ export const useInfiniteFeedByDevice = (deviceId, options) => {
|
|
|
359
332
|
const paging = usePaging(`device:${deviceId}|${filtersKey}`);
|
|
360
333
|
const query = useGetFeedInfiniteByDeviceQuery({ deviceId, cursor: paging.cursor, ...rangeArgs(options) }, { skip: !options.enabled || !deviceId });
|
|
361
334
|
return useInfinitePages(paging, query, {
|
|
362
|
-
toPage:
|
|
335
|
+
toPage: itemsPage,
|
|
363
336
|
rowKey: feedRowKey,
|
|
364
337
|
});
|
|
365
338
|
};
|
|
339
|
+
// =====================================================
|
|
340
|
+
// GALLERY (lean merged events + artifacts)
|
|
341
|
+
// =====================================================
|
|
342
|
+
export const useInfiniteGalleryByHerd = (herdId, options) => {
|
|
343
|
+
const filtersKey = useFiltersKey(options);
|
|
344
|
+
const paging = usePaging(`herd:${herdId}|${filtersKey}`);
|
|
345
|
+
const query = useGetGalleryInfiniteByHerdQuery({ herdId, cursor: paging.cursor, ...rangeArgs(options) }, { skip: !options.enabled || !herdId });
|
|
346
|
+
return useInfinitePages(paging, query, {
|
|
347
|
+
toPage: itemsPage,
|
|
348
|
+
rowKey: galleryRowKey,
|
|
349
|
+
});
|
|
350
|
+
};
|
|
351
|
+
export const useInfiniteGalleryByDevice = (deviceId, options) => {
|
|
352
|
+
const filtersKey = useFiltersKey(options);
|
|
353
|
+
const paging = usePaging(`device:${deviceId}|${filtersKey}`);
|
|
354
|
+
const query = useGetGalleryInfiniteByDeviceQuery({ deviceId, cursor: paging.cursor, ...rangeArgs(options) }, { skip: !options.enabled || !deviceId });
|
|
355
|
+
return useInfinitePages(paging, query, {
|
|
356
|
+
toPage: itemsPage,
|
|
357
|
+
rowKey: galleryRowKey,
|
|
358
|
+
});
|
|
359
|
+
};
|
|
366
360
|
export const useInfiniteAnalysisJobs = (options) => {
|
|
367
361
|
const status = options.status ?? null;
|
|
368
362
|
const paging = usePaging(`status:${status}`);
|
|
@@ -8,5 +8,9 @@ export type ScoutRealtimeBroadcastOptions = {
|
|
|
8
8
|
backoffInitialMs?: number;
|
|
9
9
|
backoffMaxMs?: number;
|
|
10
10
|
backoffMaxAttempts?: number;
|
|
11
|
+
/** Called once a topic is back after a drop, since broadcasts sent meanwhile are gone. */
|
|
12
|
+
onMissedUpdates?: () => void;
|
|
13
|
+
/** Rejoin topics no longer joined when the tab regains focus or the browser reconnects. */
|
|
14
|
+
recoverOnFocus?: boolean;
|
|
11
15
|
};
|
|
12
16
|
export declare function useScoutRealtimeBroadcast<T>(scoutSupabase: SupabaseClient<Database>, topics: string[], options?: ScoutRealtimeBroadcastOptions, rowFilter?: (row: T) => boolean): [RealtimeData<T> | null, () => void];
|