@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.
Files changed (48) hide show
  1. package/dist/client/index.d.ts +4 -0
  2. package/dist/client/index.js +4 -0
  3. package/dist/helpers/cache.d.ts +4 -4
  4. package/dist/helpers/cache.js +52 -17
  5. package/dist/helpers/eventMedia.d.ts +6 -4
  6. package/dist/helpers/eventMedia.js +7 -6
  7. package/dist/helpers/gallery.d.ts +4 -0
  8. package/dist/helpers/gallery.js +5 -0
  9. package/dist/helpers/gallery.queries.d.ts +14 -0
  10. package/dist/helpers/gallery.queries.js +49 -0
  11. package/dist/helpers/galleryMedia.d.ts +9 -0
  12. package/dist/helpers/galleryMedia.js +41 -0
  13. package/dist/helpers/herd_modules.queries.js +14 -6
  14. package/dist/helpers/mediaRows.d.ts +12 -0
  15. package/dist/helpers/mediaRows.js +80 -0
  16. package/dist/helpers/mediaState.d.ts +33 -0
  17. package/dist/helpers/mediaState.js +86 -0
  18. package/dist/helpers/media_urls.d.ts +2 -1
  19. package/dist/helpers/media_urls.js +13 -2
  20. package/dist/helpers/parts.d.ts +7 -2
  21. package/dist/helpers/parts.js +16 -7
  22. package/dist/helpers/parts_server.d.ts +1 -1
  23. package/dist/helpers/product_catalog.d.ts +9 -0
  24. package/dist/helpers/product_catalog.js +36 -0
  25. package/dist/helpers/session_incidents_server.d.ts +5 -0
  26. package/dist/helpers/storage_internal.d.ts +4 -3
  27. package/dist/helpers/storage_internal.js +46 -14
  28. package/dist/helpers/tags.queries.js +2 -0
  29. package/dist/hooks/index.d.ts +1 -1
  30. package/dist/hooks/index.js +1 -1
  31. package/dist/hooks/useInfiniteQuery.d.ts +3 -1
  32. package/dist/hooks/useInfiniteQuery.js +37 -43
  33. package/dist/hooks/useScoutRealtimeBroadcast.d.ts +4 -0
  34. package/dist/hooks/useScoutRealtimeBroadcast.js +76 -11
  35. package/dist/hooks/useScoutRefresh.js +18 -11
  36. package/dist/server/index.d.ts +5 -0
  37. package/dist/server/index.js +4 -0
  38. package/dist/store/api.d.ts +389 -9
  39. package/dist/store/api.js +163 -75
  40. package/dist/store/configureStore.d.ts +4 -0
  41. package/dist/store/hooks.d.ts +4 -1
  42. package/dist/store/hooks.js +16 -0
  43. package/dist/store/scout.d.ts +4 -2
  44. package/dist/store/scout.js +21 -2
  45. package/dist/types/db.d.ts +19 -5
  46. package/dist/types/herd_module.d.ts +12 -1
  47. package/dist/types/supabase.d.ts +77 -0
  48. package/package.json +4 -2
package/dist/store/api.js CHANGED
@@ -1,8 +1,9 @@
1
1
  "use client";
2
2
  import { createApi, fakeBaseQuery } from "@reduxjs/toolkit/query/react";
3
- import { addArtifactMediaUrls, addEventMediaUrls, createSignedUrlMap, } from "../helpers/media_urls";
3
+ import { addArtifactMediaUrls, addEventMediaUrls, addGalleryMediaUrls, createSignedUrlMap, } from "../helpers/media_urls";
4
4
  import { collectArtifactStoragePaths, withArtifactMediaUrls, } from "../helpers/artifactMedia";
5
5
  import { collectEventStoragePaths, withEventMediaUrls, } from "../helpers/eventMedia";
6
+ import { normalizePageLimit, splitOverFetchedPage } from "../helpers/mediaRows";
6
7
  async function signedUrlMapForFeedRows(supabase, rows) {
7
8
  return createSignedUrlMap(supabase, rows.flatMap((row) => [
8
9
  ...collectEventStoragePaths([row.event_data]),
@@ -11,11 +12,65 @@ async function signedUrlMapForFeedRows(supabase, rows) {
11
12
  : []),
12
13
  ]));
13
14
  }
15
+ /** Cursor for the row a page ends on, shared by the feed and gallery projections. */
16
+ const cursorForRow = (row) => row
17
+ ? {
18
+ timestamp: row.sort_ts ?? "",
19
+ id: row.sort_id ?? 0,
20
+ feed_type: row.feed_type ?? "",
21
+ }
22
+ : null;
23
+ /** Signs a gallery page, over-fetched by one on the same contract as the feed. */
24
+ async function toGalleryResponse(supabase, rows, limit) {
25
+ const { rows: resultRows, hasMore } = splitOverFetchedPage(rows, limit);
26
+ const items = await addGalleryMediaUrls(supabase, resultRows);
27
+ // `last` is absent when limit is 0, which still over-fetches one row and so reports hasMore.
28
+ const last = resultRows[resultRows.length - 1];
29
+ return {
30
+ items,
31
+ nextCursor: hasMore && last
32
+ ? cursorForRow({
33
+ sort_ts: last.sort_ts,
34
+ sort_id: last.id,
35
+ feed_type: last.feed_type,
36
+ })
37
+ : null,
38
+ hasMore,
39
+ };
40
+ }
41
+ /** Signs a feed page; the RPCs return `limit + 1` rows, so an exactly-full page is terminal. */
42
+ async function toFeedResponse(supabase, rows, limit) {
43
+ const { rows: resultRows, hasMore } = splitOverFetchedPage(rows, limit);
44
+ let urlMap = new Map();
45
+ try {
46
+ urlMap = await signedUrlMapForFeedRows(supabase, resultRows);
47
+ }
48
+ catch (urlError) {
49
+ console.warn("Failed to generate signed URLs for feed:", urlError);
50
+ }
51
+ const items = resultRows.map((row) => ({
52
+ ...row,
53
+ event_data: row.event_data
54
+ ? withEventMediaUrls(row.event_data, urlMap)
55
+ : null,
56
+ artifact_data: row.artifact_data
57
+ ? withArtifactMediaUrls(row.artifact_data, urlMap)
58
+ : null,
59
+ }));
60
+ return {
61
+ items,
62
+ nextCursor: hasMore
63
+ ? cursorForRow(resultRows[resultRows.length - 1])
64
+ : null,
65
+ hasMore,
66
+ };
67
+ }
14
68
  // Custom serialize function to exclude supabase client
15
69
  const serializeQueryArgs = ({ queryArgs, endpointDefinition, endpointName, }) => {
16
70
  const { supabase, ...serializableArgs } = queryArgs;
17
71
  return JSON.stringify({ endpointName, args: serializableArgs });
18
72
  };
73
+ const validEntityId = (id) => Number.isSafeInteger(id) && id > 0;
19
74
  // Create the API slice
20
75
  export const scoutApi = createApi({
21
76
  reducerPath: "scoutApi",
@@ -400,15 +455,19 @@ export const scoutApi = createApi({
400
455
  getFeedInfiniteByHerd: builder.query({
401
456
  serializeQueryArgs,
402
457
  async queryFn({ herdId, limit = 20, cursor, rangeStart, rangeEnd, supabase, }) {
458
+ const pageLimit = normalizePageLimit(limit);
403
459
  try {
404
- if (!herdId) {
460
+ if (!validEntityId(herdId)) {
405
461
  return {
406
- error: { status: "CUSTOM_ERROR", error: "Herd ID is required" },
462
+ error: {
463
+ status: "CUSTOM_ERROR",
464
+ error: "Herd ID must be a positive safe integer",
465
+ },
407
466
  };
408
467
  }
409
468
  const { data, error } = await supabase.rpc("get_feed_infinite_by_herd", {
410
469
  herd_id_caller: herdId,
411
- limit_caller: limit + 1,
470
+ limit_caller: pageLimit + 1,
412
471
  cursor_timestamp: cursor?.timestamp ?? null,
413
472
  cursor_id: cursor?.id ?? null,
414
473
  cursor_feed_type: cursor?.feed_type ?? null,
@@ -420,60 +479,35 @@ export const scoutApi = createApi({
420
479
  error: { status: "SUPABASE_ERROR", error: error.message },
421
480
  };
422
481
  }
423
- const rows = data || [];
424
- // Full page (rows.length >= limit) means there might be more; only signal hasMore when we have a nextCursor
425
- const hasMore = rows.length >= limit;
426
- const resultRows = hasMore ? rows.slice(0, limit) : rows;
427
- let urlMap = new Map();
428
- try {
429
- urlMap = await signedUrlMapForFeedRows(supabase, resultRows);
430
- }
431
- catch (urlError) {
432
- console.warn("Failed to generate signed URLs for feed:", urlError);
433
- }
434
- const items = resultRows.map((row) => ({
435
- ...row,
436
- event_data: row.event_data
437
- ? withEventMediaUrls(row.event_data, urlMap)
438
- : null,
439
- artifact_data: row.artifact_data
440
- ? withArtifactMediaUrls(row.artifact_data, urlMap)
441
- : null,
442
- }));
443
- const last = resultRows[resultRows.length - 1];
444
- const nextCursor = resultRows.length > 0
445
- ? {
446
- timestamp: last.sort_ts ?? "",
447
- id: last.sort_id ?? 0,
448
- feed_type: last.feed_type ?? "",
449
- }
450
- : null;
451
482
  return {
452
- data: {
453
- items,
454
- nextCursor,
455
- hasMore: nextCursor != null && hasMore,
456
- },
483
+ data: await toFeedResponse(supabase, data || [], pageLimit),
457
484
  };
458
485
  }
459
486
  catch (err) {
460
487
  return { error: { status: "FETCH_ERROR", error: String(err) } };
461
488
  }
462
489
  },
463
- providesTags: () => [{ type: "Event", id: "LIST" }, { type: "Artifact", id: "LIST" }],
490
+ providesTags: () => [
491
+ { type: "Event", id: "LIST" },
492
+ { type: "Artifact", id: "LIST" },
493
+ ],
464
494
  }),
465
495
  getFeedInfiniteByDevice: builder.query({
466
496
  serializeQueryArgs,
467
497
  async queryFn({ deviceId, limit = 20, cursor, rangeStart, rangeEnd, supabase, }) {
498
+ const pageLimit = normalizePageLimit(limit);
468
499
  try {
469
- if (!deviceId) {
500
+ if (!validEntityId(deviceId)) {
470
501
  return {
471
- error: { status: "CUSTOM_ERROR", error: "Device ID is required" },
502
+ error: {
503
+ status: "CUSTOM_ERROR",
504
+ error: "Device ID must be a positive safe integer",
505
+ },
472
506
  };
473
507
  }
474
508
  const { data, error } = await supabase.rpc("get_feed_infinite_by_device", {
475
509
  device_id_caller: deviceId,
476
- limit_caller: limit + 1,
510
+ limit_caller: pageLimit + 1,
477
511
  cursor_timestamp: cursor?.timestamp ?? null,
478
512
  cursor_id: cursor?.id ?? null,
479
513
  cursor_feed_type: cursor?.feed_type ?? null,
@@ -485,47 +519,101 @@ export const scoutApi = createApi({
485
519
  error: { status: "SUPABASE_ERROR", error: error.message },
486
520
  };
487
521
  }
488
- const rows = data || [];
489
- // Full page (rows.length >= limit) means there might be more; only signal hasMore when we have a nextCursor
490
- const hasMore = rows.length >= limit;
491
- const resultRows = hasMore ? rows.slice(0, limit) : rows;
492
- let urlMap = new Map();
493
- try {
494
- urlMap = await signedUrlMapForFeedRows(supabase, resultRows);
522
+ return {
523
+ data: await toFeedResponse(supabase, data || [], pageLimit),
524
+ };
525
+ }
526
+ catch (err) {
527
+ return { error: { status: "FETCH_ERROR", error: String(err) } };
528
+ }
529
+ },
530
+ providesTags: () => [
531
+ { type: "Event", id: "LIST" },
532
+ { type: "Artifact", id: "LIST" },
533
+ ],
534
+ }),
535
+ // =====================================================
536
+ // GALLERY INFINITE QUERIES (lean merged events + artifacts)
537
+ // =====================================================
538
+ getGalleryInfiniteByHerd: builder.query({
539
+ serializeQueryArgs,
540
+ async queryFn({ herdId, limit = 20, cursor, rangeStart, rangeEnd, supabase, }) {
541
+ const pageLimit = normalizePageLimit(limit);
542
+ try {
543
+ if (!validEntityId(herdId)) {
544
+ return {
545
+ error: {
546
+ status: "CUSTOM_ERROR",
547
+ error: "Herd ID must be a positive safe integer",
548
+ },
549
+ };
550
+ }
551
+ const { data, error } = await supabase.rpc("get_gallery_infinite_by_herd", {
552
+ herd_id_caller: herdId,
553
+ limit_caller: pageLimit + 1,
554
+ cursor_timestamp: cursor?.timestamp ?? null,
555
+ cursor_id: cursor?.id ?? null,
556
+ cursor_feed_type: cursor?.feed_type ?? null,
557
+ range_start: rangeStart ?? null,
558
+ range_end: rangeEnd ?? null,
559
+ });
560
+ if (error) {
561
+ return {
562
+ error: { status: "SUPABASE_ERROR", error: error.message },
563
+ };
495
564
  }
496
- catch (urlError) {
497
- console.warn("Failed to generate signed URLs for feed:", urlError);
498
- }
499
- const items = resultRows.map((row) => ({
500
- ...row,
501
- event_data: row.event_data
502
- ? withEventMediaUrls(row.event_data, urlMap)
503
- : null,
504
- artifact_data: row.artifact_data
505
- ? withArtifactMediaUrls(row.artifact_data, urlMap)
506
- : null,
507
- }));
508
- const last = resultRows[resultRows.length - 1];
509
- const nextCursor = resultRows.length > 0
510
- ? {
511
- timestamp: last.sort_ts ?? "",
512
- id: last.sort_id ?? 0,
513
- feed_type: last.feed_type ?? "",
514
- }
515
- : null;
516
565
  return {
517
- data: {
518
- items,
519
- nextCursor,
520
- hasMore: nextCursor != null && hasMore,
521
- },
566
+ data: await toGalleryResponse(supabase, data || [], pageLimit),
567
+ };
568
+ }
569
+ catch (err) {
570
+ return { error: { status: "FETCH_ERROR", error: String(err) } };
571
+ }
572
+ },
573
+ providesTags: () => [
574
+ { type: "Event", id: "LIST" },
575
+ { type: "Artifact", id: "LIST" },
576
+ ],
577
+ }),
578
+ getGalleryInfiniteByDevice: builder.query({
579
+ serializeQueryArgs,
580
+ async queryFn({ deviceId, limit = 20, cursor, rangeStart, rangeEnd, supabase, }) {
581
+ const pageLimit = normalizePageLimit(limit);
582
+ try {
583
+ if (!validEntityId(deviceId)) {
584
+ return {
585
+ error: {
586
+ status: "CUSTOM_ERROR",
587
+ error: "Device ID must be a positive safe integer",
588
+ },
589
+ };
590
+ }
591
+ const { data, error } = await supabase.rpc("get_gallery_infinite_by_device", {
592
+ device_id_caller: deviceId,
593
+ limit_caller: pageLimit + 1,
594
+ cursor_timestamp: cursor?.timestamp ?? null,
595
+ cursor_id: cursor?.id ?? null,
596
+ cursor_feed_type: cursor?.feed_type ?? null,
597
+ range_start: rangeStart ?? null,
598
+ range_end: rangeEnd ?? null,
599
+ });
600
+ if (error) {
601
+ return {
602
+ error: { status: "SUPABASE_ERROR", error: error.message },
603
+ };
604
+ }
605
+ return {
606
+ data: await toGalleryResponse(supabase, data || [], pageLimit),
522
607
  };
523
608
  }
524
609
  catch (err) {
525
610
  return { error: { status: "FETCH_ERROR", error: String(err) } };
526
611
  }
527
612
  },
528
- providesTags: () => [{ type: "Event", id: "LIST" }, { type: "Artifact", id: "LIST" }],
613
+ providesTags: () => [
614
+ { type: "Event", id: "LIST" },
615
+ { type: "Artifact", id: "LIST" },
616
+ ],
529
617
  }),
530
618
  // =====================================================
531
619
  // ANALYSIS JOBS / TASKS INFINITE (platform superadmin RLS)
@@ -634,4 +722,4 @@ export const scoutApi = createApi({
634
722
  }),
635
723
  });
636
724
  // Export hooks for usage in functional components
637
- export const { useGetSessionsInfiniteByHerdQuery, useGetSessionsInfiniteByDeviceQuery, useGetEventsInfiniteByHerdQuery, useGetEventsInfiniteByDeviceQuery, useGetArtifactsInfiniteByHerdQuery, useGetArtifactsInfiniteByDeviceQuery, useGetFeedInfiniteByHerdQuery, useGetFeedInfiniteByDeviceQuery, useGetAnalysisJobsInfiniteQuery, useGetAnalysisTasksInfiniteQuery, } = scoutApi;
725
+ export const { useGetSessionsInfiniteByHerdQuery, useGetSessionsInfiniteByDeviceQuery, useGetEventsInfiniteByHerdQuery, useGetEventsInfiniteByDeviceQuery, useGetArtifactsInfiniteByHerdQuery, useGetArtifactsInfiniteByDeviceQuery, useGetFeedInfiniteByHerdQuery, useGetFeedInfiniteByDeviceQuery, useGetGalleryInfiniteByHerdQuery, useGetGalleryInfiniteByDeviceQuery, useGetAnalysisJobsInfiniteQuery, useGetAnalysisTasksInfiniteQuery, } = scoutApi;
@@ -21,6 +21,8 @@ export declare const configureScoutStore: () => import("@reduxjs/toolkit").Enhan
21
21
  }, import("@reduxjs/toolkit/query").BaseQueryFn<any, unknown, unknown, {}, {}>, "Session" | "Event" | "Artifact" | "AnalysisJob" | "AnalysisTask", import("./api").ArtifactsInfiniteResponse, "scoutApi", unknown>;
22
22
  getFeedInfiniteByHerd: import("@reduxjs/toolkit/query").QueryDefinition<import("./api").FeedInfiniteByHerdQueryArgs, import("@reduxjs/toolkit/query").BaseQueryFn<any, unknown, unknown, {}, {}>, "Session" | "Event" | "Artifact" | "AnalysisJob" | "AnalysisTask", import("./api").FeedInfiniteResponse, "scoutApi", unknown>;
23
23
  getFeedInfiniteByDevice: import("@reduxjs/toolkit/query").QueryDefinition<import("./api").FeedInfiniteByDeviceQueryArgs, import("@reduxjs/toolkit/query").BaseQueryFn<any, unknown, unknown, {}, {}>, "Session" | "Event" | "Artifact" | "AnalysisJob" | "AnalysisTask", import("./api").FeedInfiniteResponse, "scoutApi", unknown>;
24
+ getGalleryInfiniteByHerd: import("@reduxjs/toolkit/query").QueryDefinition<import("./api").GalleryInfiniteByHerdQueryArgs, import("@reduxjs/toolkit/query").BaseQueryFn<any, unknown, unknown, {}, {}>, "Session" | "Event" | "Artifact" | "AnalysisJob" | "AnalysisTask", import("./api").GalleryInfiniteResponse, "scoutApi", unknown>;
25
+ getGalleryInfiniteByDevice: import("@reduxjs/toolkit/query").QueryDefinition<import("./api").GalleryInfiniteByDeviceQueryArgs, import("@reduxjs/toolkit/query").BaseQueryFn<any, unknown, unknown, {}, {}>, "Session" | "Event" | "Artifact" | "AnalysisJob" | "AnalysisTask", import("./api").GalleryInfiniteResponse, "scoutApi", unknown>;
24
26
  getAnalysisJobsInfinite: import("@reduxjs/toolkit/query").QueryDefinition<import("./api").AnalysisJobsInfiniteQueryArgs, import("@reduxjs/toolkit/query").BaseQueryFn<any, unknown, unknown, {}, {}>, "Session" | "Event" | "Artifact" | "AnalysisJob" | "AnalysisTask", import("./api").AnalysisJobsInfiniteResponse, "scoutApi", unknown>;
25
27
  getAnalysisTasksInfinite: import("@reduxjs/toolkit/query").QueryDefinition<import("./api").AnalysisTasksInfiniteQueryArgs, import("@reduxjs/toolkit/query").BaseQueryFn<any, unknown, unknown, {}, {}>, "Session" | "Event" | "Artifact" | "AnalysisJob" | "AnalysisTask", import("./api").AnalysisTasksInfiniteResponse, "scoutApi", unknown>;
26
28
  }, "Session" | "Event" | "Artifact" | "AnalysisJob" | "AnalysisTask", "scoutApi">;
@@ -48,6 +50,8 @@ export declare const configureScoutStore: () => import("@reduxjs/toolkit").Enhan
48
50
  }, import("@reduxjs/toolkit/query").BaseQueryFn<any, unknown, unknown, {}, {}>, "Session" | "Event" | "Artifact" | "AnalysisJob" | "AnalysisTask", import("./api").ArtifactsInfiniteResponse, "scoutApi", unknown>;
49
51
  getFeedInfiniteByHerd: import("@reduxjs/toolkit/query").QueryDefinition<import("./api").FeedInfiniteByHerdQueryArgs, import("@reduxjs/toolkit/query").BaseQueryFn<any, unknown, unknown, {}, {}>, "Session" | "Event" | "Artifact" | "AnalysisJob" | "AnalysisTask", import("./api").FeedInfiniteResponse, "scoutApi", unknown>;
50
52
  getFeedInfiniteByDevice: import("@reduxjs/toolkit/query").QueryDefinition<import("./api").FeedInfiniteByDeviceQueryArgs, import("@reduxjs/toolkit/query").BaseQueryFn<any, unknown, unknown, {}, {}>, "Session" | "Event" | "Artifact" | "AnalysisJob" | "AnalysisTask", import("./api").FeedInfiniteResponse, "scoutApi", unknown>;
53
+ getGalleryInfiniteByHerd: import("@reduxjs/toolkit/query").QueryDefinition<import("./api").GalleryInfiniteByHerdQueryArgs, import("@reduxjs/toolkit/query").BaseQueryFn<any, unknown, unknown, {}, {}>, "Session" | "Event" | "Artifact" | "AnalysisJob" | "AnalysisTask", import("./api").GalleryInfiniteResponse, "scoutApi", unknown>;
54
+ getGalleryInfiniteByDevice: import("@reduxjs/toolkit/query").QueryDefinition<import("./api").GalleryInfiniteByDeviceQueryArgs, import("@reduxjs/toolkit/query").BaseQueryFn<any, unknown, unknown, {}, {}>, "Session" | "Event" | "Artifact" | "AnalysisJob" | "AnalysisTask", import("./api").GalleryInfiniteResponse, "scoutApi", unknown>;
51
55
  getAnalysisJobsInfinite: import("@reduxjs/toolkit/query").QueryDefinition<import("./api").AnalysisJobsInfiniteQueryArgs, import("@reduxjs/toolkit/query").BaseQueryFn<any, unknown, unknown, {}, {}>, "Session" | "Event" | "Artifact" | "AnalysisJob" | "AnalysisTask", import("./api").AnalysisJobsInfiniteResponse, "scoutApi", unknown>;
52
56
  getAnalysisTasksInfinite: import("@reduxjs/toolkit/query").QueryDefinition<import("./api").AnalysisTasksInfiniteQueryArgs, import("@reduxjs/toolkit/query").BaseQueryFn<any, unknown, unknown, {}, {}>, "Session" | "Event" | "Artifact" | "AnalysisJob" | "AnalysisTask", import("./api").AnalysisTasksInfiniteResponse, "scoutApi", unknown>;
53
57
  }, "Session" | "Event" | "Artifact" | "AnalysisJob" | "AnalysisTask", "scoutApi">;
@@ -1,5 +1,5 @@
1
1
  import { LoadingPerformance } from "./scout";
2
- import { ISessionSummary } from "../types/db";
2
+ import { IDevice, IPart, IProduct, IProductCatalog, ISessionSummary } from "../types/db";
3
3
  export declare const useAppDispatch: import("react-redux").UseDispatch<import("redux").Dispatch<import("redux").UnknownAction>>;
4
4
  export declare const useHerdModulesLoadingState: () => any;
5
5
  export declare const useIsHerdModulesLoading: () => boolean;
@@ -10,4 +10,7 @@ export declare const useHerdModulesLoadingDuration: () => any;
10
10
  export declare const useLoadingPerformance: () => LoadingPerformance;
11
11
  export declare const useSessionSummariesByHerd: (herdId: number) => ISessionSummary | null;
12
12
  export declare const useHasSessionSummaries: (herdId: number) => boolean;
13
+ export declare const useProductCatalog: () => IProductCatalog;
14
+ export declare const useProductForPart: (part: Pick<IPart, "product_number"> | null | undefined) => IProduct | null;
15
+ export declare const useProductsForDevice: (device: Pick<IDevice, "parts"> | null | undefined) => IProduct[];
13
16
  export declare const useHerdModulesLoadingTimeAgo: () => string | null;
@@ -1,6 +1,8 @@
1
1
  "use client";
2
+ import { useMemo } from "react";
2
3
  import { useDispatch, useSelector } from "react-redux";
3
4
  import { EnumHerdModulesLoadingState } from "../types/herd_module";
5
+ import { EMPTY_PRODUCT_CATALOG, get_product_for_part, get_products_for_device, } from "../helpers/product_catalog";
4
6
  // Simple wrapper for useDispatch to maintain compatibility
5
7
  export const useAppDispatch = useDispatch;
6
8
  // Selector hook for herd modules loading state
@@ -50,6 +52,20 @@ export const useHasSessionSummaries = (herdId) => {
50
52
  return !!herdModule?.session_summaries;
51
53
  });
52
54
  };
55
+ // Selector hook for the shared product catalog referenced by device parts
56
+ export const useProductCatalog = () => {
57
+ return useSelector((state) => state.scout.product_catalog ?? EMPTY_PRODUCT_CATALOG);
58
+ };
59
+ // Selector hook for the product a part references
60
+ export const useProductForPart = (part) => {
61
+ const catalog = useProductCatalog();
62
+ return useMemo(() => get_product_for_part(catalog, part), [catalog, part]);
63
+ };
64
+ // Selector hook for the distinct products across a device's parts
65
+ export const useProductsForDevice = (device) => {
66
+ const catalog = useProductCatalog();
67
+ return useMemo(() => get_products_for_device(catalog, device), [catalog, device]);
68
+ };
53
69
  // Selector hook for getting formatted loading time (e.g., "2.5s ago")
54
70
  export const useHerdModulesLoadingTimeAgo = () => {
55
71
  return useSelector((state) => {
@@ -1,4 +1,4 @@
1
- import { IUser } from "../types/db";
1
+ import { IProductCatalog, IUser } from "../types/db";
2
2
  import { IHerdModule, EnumHerdModulesLoadingState } from "../types/herd_module";
3
3
  import { EnumDataSource, IDataSourceInfo } from "../types/data_source";
4
4
  import { MapDeviceIdToConnectivity } from "../types/connectivity";
@@ -16,6 +16,8 @@ export interface LoadingPerformance {
16
16
  }
17
17
  export interface ScoutState {
18
18
  herd_modules: IHerdModule[];
19
+ /** One shared copy of every product referenced by device parts. */
20
+ product_catalog: IProductCatalog;
19
21
  herd_modules_content_hash: string | null;
20
22
  status: EnumScoutStateStatus;
21
23
  herd_modules_loading_state: EnumHerdModulesLoadingState;
@@ -31,5 +33,5 @@ export interface ScoutState {
31
33
  export interface RootState {
32
34
  scout: ScoutState;
33
35
  }
34
- export declare const resetScoutState: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"scout/resetScoutState">, setHerdModules: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setHerdModules">, setStatus: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setStatus">, setHerdModulesLoadingState: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setHerdModulesLoadingState">, setLoadingPerformance: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setLoadingPerformance">, setHerdModulesLoadedInMs: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setHerdModulesLoadedInMs">, setHerdModulesApiServerProcessingDuration: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setHerdModulesApiServerProcessingDuration">, setHerdModulesApiTotalRequestDuration: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setHerdModulesApiTotalRequestDuration">, setUserApiDuration: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setUserApiDuration">, setDataProcessingDuration: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setDataProcessingDuration">, setCacheLoadDuration: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setCacheLoadDuration">, setActiveHerdId: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setActiveHerdId">, setActiveDeviceId: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setActiveDeviceId">, setDataSource: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setDataSource">, setDataSourceInfo: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setDataSourceInfo">, updateSessionSummariesForHerdModule: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/updateSessionSummariesForHerdModule">, appendPlansToHerdModule: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/appendPlansToHerdModule">, setUser: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setUser">, addNewDeviceToHerdModule: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/addNewDeviceToHerdModule">, updateDeviceForHerdModule: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/updateDeviceForHerdModule">, addDevice: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/addDevice">, deleteDevice: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/deleteDevice">, updateDevice: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/updateDevice">, addPlan: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/addPlan">, deletePlan: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/deletePlan">, updatePlan: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/updatePlan">, setActiveHerdGpsTrackersConnectivity: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setActiveHerdGpsTrackersConnectivity">;
36
+ export declare const resetScoutState: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"scout/resetScoutState">, setHerdModules: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setHerdModules">, mergeProductCatalog: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/mergeProductCatalog">, setStatus: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setStatus">, setHerdModulesLoadingState: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setHerdModulesLoadingState">, setLoadingPerformance: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setLoadingPerformance">, setHerdModulesLoadedInMs: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setHerdModulesLoadedInMs">, setHerdModulesApiServerProcessingDuration: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setHerdModulesApiServerProcessingDuration">, setHerdModulesApiTotalRequestDuration: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setHerdModulesApiTotalRequestDuration">, setUserApiDuration: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setUserApiDuration">, setDataProcessingDuration: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setDataProcessingDuration">, setCacheLoadDuration: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setCacheLoadDuration">, setActiveHerdId: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setActiveHerdId">, setActiveDeviceId: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setActiveDeviceId">, setDataSource: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setDataSource">, setDataSourceInfo: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setDataSourceInfo">, updateSessionSummariesForHerdModule: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/updateSessionSummariesForHerdModule">, appendPlansToHerdModule: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/appendPlansToHerdModule">, setUser: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setUser">, addNewDeviceToHerdModule: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/addNewDeviceToHerdModule">, updateDeviceForHerdModule: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/updateDeviceForHerdModule">, addDevice: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/addDevice">, deleteDevice: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/deleteDevice">, updateDevice: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/updateDevice">, addPlan: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/addPlan">, deletePlan: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/deletePlan">, updatePlan: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/updatePlan">, setActiveHerdGpsTrackersConnectivity: import("@reduxjs/toolkit").ActionCreatorWithPayload<any, "scout/setActiveHerdGpsTrackersConnectivity">;
35
37
  export declare const scoutReducer: import("redux").Reducer<ScoutState>;
@@ -1,7 +1,9 @@
1
1
  import { createSlice, current } from "@reduxjs/toolkit";
2
+ import { dequal } from "dequal";
2
3
  import { EnumHerdModulesLoadingState } from "../types/herd_module";
3
4
  import { EnumDataSource } from "../types/data_source";
4
5
  import { herdModulesSemanticallyEqual } from "../helpers/herd_modules_equal";
6
+ import { EMPTY_PRODUCT_CATALOG } from "../helpers/product_catalog";
5
7
  export var EnumScoutStateStatus;
6
8
  (function (EnumScoutStateStatus) {
7
9
  EnumScoutStateStatus["LOADING"] = "LOADING";
@@ -9,6 +11,7 @@ export var EnumScoutStateStatus;
9
11
  })(EnumScoutStateStatus || (EnumScoutStateStatus = {}));
10
12
  const initialState = {
11
13
  herd_modules: [],
14
+ product_catalog: EMPTY_PRODUCT_CATALOG,
12
15
  herd_modules_content_hash: null,
13
16
  status: EnumScoutStateStatus.LOADING,
14
17
  herd_modules_loading_state: EnumHerdModulesLoadingState.NOT_LOADING,
@@ -41,6 +44,10 @@ const scoutSlice = createSlice({
41
44
  const contentHash = Array.isArray(payload)
42
45
  ? null
43
46
  : payload.contentHash ?? null;
47
+ const productCatalog = Array.isArray(payload)
48
+ ? current(state.product_catalog)
49
+ : payload.productCatalog ??
50
+ current(state.product_catalog);
44
51
  if (state.active_herd_id &&
45
52
  !modules.some((module) => module.herd.id?.toString() === state.active_herd_id)) {
46
53
  state.active_herd_id = null;
@@ -52,13 +59,25 @@ const scoutSlice = createSlice({
52
59
  return;
53
60
  }
54
61
  }
55
- else if (herdModulesSemanticallyEqual(current(state.herd_modules), modules)) {
62
+ else if (herdModulesSemanticallyEqual(current(state.herd_modules), modules) &&
63
+ dequal(current(state.product_catalog), productCatalog)) {
56
64
  return;
57
65
  }
58
66
  state.herd_modules = modules;
67
+ state.product_catalog = productCatalog;
59
68
  state.herd_modules_content_hash = contentHash;
60
69
  state.lastRefreshed = Date.now();
61
70
  },
71
+ /**
72
+ * Merges products (built with `build_product_catalog`) into the shared
73
+ * catalog, for parts that arrive after the initial load.
74
+ */
75
+ mergeProductCatalog: (state, action) => {
76
+ const incoming = action.payload;
77
+ Object.assign(state.product_catalog.products, incoming.products);
78
+ Object.assign(state.product_catalog.product_id_by_number, incoming.product_id_by_number);
79
+ state.herd_modules_content_hash = null;
80
+ },
62
81
  setStatus: (state, action) => {
63
82
  state.status = action.payload;
64
83
  },
@@ -211,5 +230,5 @@ const scoutSlice = createSlice({
211
230
  },
212
231
  },
213
232
  });
214
- export const { resetScoutState, setHerdModules, setStatus, setHerdModulesLoadingState, setLoadingPerformance, setHerdModulesLoadedInMs, setHerdModulesApiServerProcessingDuration, setHerdModulesApiTotalRequestDuration, setUserApiDuration, setDataProcessingDuration, setCacheLoadDuration, setActiveHerdId, setActiveDeviceId, setDataSource, setDataSourceInfo, updateSessionSummariesForHerdModule, appendPlansToHerdModule, setUser, addNewDeviceToHerdModule, updateDeviceForHerdModule, addDevice, deleteDevice, updateDevice, addPlan, deletePlan, updatePlan, setActiveHerdGpsTrackersConnectivity, } = scoutSlice.actions;
233
+ export const { resetScoutState, setHerdModules, mergeProductCatalog, setStatus, setHerdModulesLoadingState, setLoadingPerformance, setHerdModulesLoadedInMs, setHerdModulesApiServerProcessingDuration, setHerdModulesApiTotalRequestDuration, setUserApiDuration, setDataProcessingDuration, setCacheLoadDuration, setActiveHerdId, setActiveDeviceId, setDataSource, setDataSourceInfo, updateSessionSummariesForHerdModule, appendPlansToHerdModule, setUser, addNewDeviceToHerdModule, updateDeviceForHerdModule, addDevice, deleteDevice, updateDevice, addPlan, deletePlan, updatePlan, setActiveHerdGpsTrackersConnectivity, } = scoutSlice.actions;
215
234
  export const scoutReducer = scoutSlice.reducer;
@@ -14,7 +14,7 @@ export type IUserProfileRow = Database["public"]["Tables"]["users"]["Row"];
14
14
  export type IDeviceRow = Database["public"]["Tables"]["devices"]["Row"];
15
15
  export type IDevice = Database["public"]["CompositeTypes"]["device_pretty_location"] & {
16
16
  api_keys_scout?: IApiKeyScout[];
17
- parts?: IPartWithProduct[];
17
+ parts?: IPart[];
18
18
  };
19
19
  export type IPin = Database["public"]["CompositeTypes"]["pins_pretty_location"];
20
20
  export type IEvent = Database["public"]["Tables"]["events"]["Row"];
@@ -198,10 +198,14 @@ export type OperatingContextContactCreateInput = Pick<OperatingContextContactIns
198
198
  export type OperatingContextContactUpdateInput = Partial<Pick<OperatingContextContactCreateInput, "contact_id">> & LifecyclePatch;
199
199
  export type HerdOperatingPermissionCreateInput = Pick<HerdOperatingPermissionInsert, "herd_id" | "operating_permission_id">;
200
200
  export type HerdOperatingPermissionUpdateInput = Partial<Pick<HerdOperatingPermissionCreateInput, "operating_permission_id">> & LifecyclePatch;
201
- /** A part with its resolved product (parts.product_number -> product_numbers -> products). */
202
- export type IPartWithProduct = IPart & {
203
- product?: IProduct | null;
204
- };
201
+ /**
202
+ * Products stored once and shared by every part that references them
203
+ * (parts.product_number -> product_numbers -> products).
204
+ */
205
+ export interface IProductCatalog {
206
+ products: Record<number, IProduct>;
207
+ product_id_by_number: Record<string, number>;
208
+ }
205
209
  export type IUserCredential = Database["public"]["Tables"]["user_credentials"]["Row"];
206
210
  export type IUserLastActivity = Database["public"]["Tables"]["user_last_activity"]["Row"];
207
211
  export type IHerdCredential = Database["public"]["Tables"]["herd_credentials"]["Row"];
@@ -222,6 +226,10 @@ export type ISignedMediaUrls = {
222
226
  thumbnail_url?: string | null;
223
227
  proxy_url?: string | null;
224
228
  };
229
+ /** What a row holds, resolved from its file extension first and its stored media_type/modality second. */
230
+ export type MediaKind = MediaType | "unknown";
231
+ /** Availability of the media a view shows; `url_unavailable` means it exists but went unsigned. */
232
+ export type MediaStatus = "ready" | "processing" | "blocked" | "url_unavailable";
225
233
  export type IArtifactWithMediaUrl = IArtifact & ISignedMediaUrls;
226
234
  export type IEventWithMediaUrl = IEvent & ISignedMediaUrls;
227
235
  export type IVersionsSoftwareWithBuildUrl = IVersionsSoftware & {
@@ -370,6 +378,12 @@ export type IFeedItem = {
370
378
  event_data: (IEventAndTagsPrettyLocation & ISignedMediaUrls) | null;
371
379
  artifact_data: IArtifactWithMediaUrl | null;
372
380
  };
381
+ /** Lean merged events/artifacts row: media paths and a tag count, not `feed_item`'s arrays. */
382
+ export type IGalleryItemRow = Database["public"]["CompositeTypes"]["gallery_item"];
383
+ export type IGalleryItem = IGalleryItemRow & ISignedMediaUrls & {
384
+ media_kind: MediaKind;
385
+ media_status: MediaStatus;
386
+ };
373
387
  export type ISessionWithCoordinates = Database["public"]["CompositeTypes"]["session_with_coordinates"];
374
388
  export type IConnectivityWithCoordinates = Database["public"]["CompositeTypes"]["connectivity_with_coordinates"];
375
389
  export type IEmbeddingSearchResult = Database["public"]["CompositeTypes"]["embedding_search_result"];
@@ -1,4 +1,4 @@
1
- import { IDevice, IHerdPrettyLocation, IPlan, ILayer, IProvider, IUserAndRole, IZoneWithActions, ISessionSummary, ISessionUsageOverTime } from "../types/db";
1
+ import { IDevice, IHerdPrettyLocation, IPlan, ILayer, IProductCatalog, IProvider, IUserAndRole, IZoneWithActions, ISessionSummary, ISessionUsageOverTime } from "../types/db";
2
2
  import { EnumWebResponse } from "./requests";
3
3
  export declare enum EnumHerdModulesLoadingState {
4
4
  NOT_LOADING = "NOT_LOADING",
@@ -19,8 +19,18 @@ export interface IHerdModule {
19
19
  session_summaries: ISessionSummary | null;
20
20
  session_usage: ISessionUsageOverTime | null;
21
21
  }
22
+ /**
23
+ * Herd modules plus the single shared copy of every product their parts
24
+ * reference. Devices hold bare parts; resolve products with the
25
+ * `get_product_for_*` helpers instead of storing them per device.
26
+ */
27
+ export interface IHerdModulesSnapshot {
28
+ herd_modules: IHerdModule[];
29
+ product_catalog: IProductCatalog;
30
+ }
22
31
  export interface IHerdModulesResponse {
23
32
  data: IHerdModule[];
33
+ product_catalog: IProductCatalog;
24
34
  time_finished: number;
25
35
  server_processing_time_ms: number;
26
36
  content_hash: string;
@@ -29,6 +39,7 @@ export interface IHerdModulesResponseWithStatus {
29
39
  status: EnumWebResponse;
30
40
  msg: string;
31
41
  data: IHerdModule[] | null;
42
+ product_catalog: IProductCatalog;
32
43
  time_finished: number;
33
44
  time_sent: number;
34
45
  server_processing_time_ms: number;