@adventurelabs/scout-core 1.4.67 → 1.4.68

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.
@@ -2866,6 +2866,7 @@ export declare function useSupabase(): SupabaseClient<Database, "public", "publi
2866
2866
  description: string | null;
2867
2867
  latitude: number | null;
2868
2868
  longitude: number | null;
2869
+ color: string | null;
2869
2870
  };
2870
2871
  embedding_match: {
2871
2872
  id: number | null;
@@ -2951,6 +2951,7 @@ export type Database = {
2951
2951
  description: string | null;
2952
2952
  latitude: number | null;
2953
2953
  longitude: number | null;
2954
+ color: string | null;
2954
2955
  };
2955
2956
  embedding_match: {
2956
2957
  id: number | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adventurelabs/scout-core",
3
- "version": "1.4.67",
3
+ "version": "1.4.68",
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",
@@ -1,14 +0,0 @@
1
- import { Database } from "../types/supabase";
2
- import { IComponent, ComponentInsert } from "../types/db";
3
- import { IWebResponseCompatible } from "../types/requests";
4
- import { SupabaseClient } from "@supabase/supabase-js";
5
- export declare function get_components_by_device_id(client: SupabaseClient<Database>, device_id: number): Promise<IWebResponseCompatible<IComponent[]>>;
6
- export declare function get_component_by_id(client: SupabaseClient<Database>, component_id: number): Promise<IWebResponseCompatible<IComponent | null>>;
7
- export declare function get_components_by_serial_number(client: SupabaseClient<Database>, serial_number: string): Promise<IWebResponseCompatible<IComponent[]>>;
8
- export declare function get_components_by_product_number(client: SupabaseClient<Database>, product_number: string): Promise<IWebResponseCompatible<IComponent[]>>;
9
- export declare function get_components_by_status(client: SupabaseClient<Database>, status: Database["public"]["Enums"]["component_status"]): Promise<IWebResponseCompatible<IComponent[]>>;
10
- export declare function create_component(client: SupabaseClient<Database>, newComponent: ComponentInsert): Promise<IWebResponseCompatible<IComponent | null>>;
11
- export declare function update_component(client: SupabaseClient<Database>, component_id: number, updatedComponent: Partial<ComponentInsert>): Promise<IWebResponseCompatible<IComponent | null>>;
12
- export declare function delete_component(client: SupabaseClient<Database>, component_id: number): Promise<IWebResponseCompatible<IComponent | null>>;
13
- export declare function update_component_status(client: SupabaseClient<Database>, component_id: number, status: Database["public"]["Enums"]["component_status"]): Promise<IWebResponseCompatible<IComponent | null>>;
14
- export declare function get_components_by_certificate_id(client: SupabaseClient<Database>, certificate_id: number): Promise<IWebResponseCompatible<IComponent[]>>;
@@ -1,155 +0,0 @@
1
- import { IWebResponse } from "../types/requests";
2
- export async function get_components_by_device_id(client, device_id) {
3
- const { data, error } = await client
4
- .from("components")
5
- .select("*")
6
- .eq("device_id", device_id)
7
- .order("created_at", { ascending: false });
8
- if (error) {
9
- return IWebResponse.error(error.message).to_compatible();
10
- }
11
- if (!data) {
12
- return IWebResponse.error("No components found for device").to_compatible();
13
- }
14
- return IWebResponse.success(data).to_compatible();
15
- }
16
- export async function get_component_by_id(client, component_id) {
17
- const { data, error } = await client
18
- .from("components")
19
- .select("*")
20
- .eq("id", component_id)
21
- .single();
22
- if (error) {
23
- return IWebResponse.error(error.message).to_compatible();
24
- }
25
- if (!data) {
26
- return IWebResponse.error("Component not found").to_compatible();
27
- }
28
- return IWebResponse.success(data).to_compatible();
29
- }
30
- export async function get_components_by_serial_number(client, serial_number) {
31
- const { data, error } = await client
32
- .from("components")
33
- .select("*")
34
- .eq("serial_number", serial_number)
35
- .order("created_at", { ascending: false });
36
- if (error) {
37
- return IWebResponse.error(error.message).to_compatible();
38
- }
39
- if (!data) {
40
- return IWebResponse.error(`No components found with serial number: ${serial_number}`).to_compatible();
41
- }
42
- return IWebResponse.success(data).to_compatible();
43
- }
44
- export async function get_components_by_product_number(client, product_number) {
45
- const { data, error } = await client
46
- .from("components")
47
- .select("*")
48
- .eq("product_number", product_number)
49
- .order("created_at", { ascending: false });
50
- if (error) {
51
- return IWebResponse.error(error.message).to_compatible();
52
- }
53
- if (!data) {
54
- return IWebResponse.error(`No components found with product number: ${product_number}`).to_compatible();
55
- }
56
- return IWebResponse.success(data).to_compatible();
57
- }
58
- export async function get_components_by_status(client, status) {
59
- const { data, error } = await client
60
- .from("components")
61
- .select("*")
62
- .eq("status", status)
63
- .order("created_at", { ascending: false });
64
- if (error) {
65
- return IWebResponse.error(error.message).to_compatible();
66
- }
67
- if (!data) {
68
- return IWebResponse.error(`No components found with status: ${status}`).to_compatible();
69
- }
70
- return IWebResponse.success(data).to_compatible();
71
- }
72
- export async function create_component(client, newComponent) {
73
- // Validate required fields
74
- if (!newComponent.device_id) {
75
- return IWebResponse.error("Device ID is required").to_compatible();
76
- }
77
- if (!newComponent.serial_number) {
78
- return IWebResponse.error("Serial number is required").to_compatible();
79
- }
80
- const { data, error } = await client
81
- .from("components")
82
- .insert([newComponent])
83
- .select("*")
84
- .single();
85
- if (error) {
86
- return IWebResponse.error(error.message).to_compatible();
87
- }
88
- if (!data) {
89
- return IWebResponse.error("Failed to create component").to_compatible();
90
- }
91
- return IWebResponse.success(data).to_compatible();
92
- }
93
- export async function update_component(client, component_id, updatedComponent) {
94
- // Remove fields that shouldn't be updated
95
- const updateData = { ...updatedComponent };
96
- delete updateData.id;
97
- delete updateData.created_at;
98
- const { data, error } = await client
99
- .from("components")
100
- .update(updateData)
101
- .eq("id", component_id)
102
- .select("*")
103
- .single();
104
- if (error) {
105
- return IWebResponse.error(error.message).to_compatible();
106
- }
107
- if (!data) {
108
- return IWebResponse.error("Component not found or update failed").to_compatible();
109
- }
110
- return IWebResponse.success(data).to_compatible();
111
- }
112
- export async function delete_component(client, component_id) {
113
- const { data, error } = await client
114
- .from("components")
115
- .delete()
116
- .eq("id", component_id)
117
- .select("*")
118
- .single();
119
- if (error) {
120
- return IWebResponse.error(error.message).to_compatible();
121
- }
122
- if (!data) {
123
- return IWebResponse.error("Component not found or deletion failed").to_compatible();
124
- }
125
- return IWebResponse.success(data).to_compatible();
126
- }
127
- export async function update_component_status(client, component_id, status) {
128
- const { data, error } = await client
129
- .from("components")
130
- .update({ status })
131
- .eq("id", component_id)
132
- .select("*")
133
- .single();
134
- if (error) {
135
- return IWebResponse.error(error.message).to_compatible();
136
- }
137
- if (!data) {
138
- return IWebResponse.error("Component not found or status update failed").to_compatible();
139
- }
140
- return IWebResponse.success(data).to_compatible();
141
- }
142
- export async function get_components_by_certificate_id(client, certificate_id) {
143
- const { data, error } = await client
144
- .from("components")
145
- .select("*")
146
- .eq("certificate_id", certificate_id)
147
- .order("created_at", { ascending: false });
148
- if (error) {
149
- return IWebResponse.error(error.message).to_compatible();
150
- }
151
- if (!data) {
152
- return IWebResponse.error(`No components found with certificate ID: ${certificate_id}`).to_compatible();
153
- }
154
- return IWebResponse.success(data).to_compatible();
155
- }
@@ -1,5 +0,0 @@
1
- import { IWebResponseCompatible } from "../types/requests";
2
- import { IOperator } from "../types/db";
3
- export declare function server_get_operators_by_session_id(sessionId: number): Promise<IWebResponseCompatible<IOperator[]>>;
4
- export declare function server_get_operators_by_user_id(userId: string): Promise<IWebResponseCompatible<IOperator[]>>;
5
- export declare function server_get_operators_by_session_id_filtered(sessionId: number, action?: string, timestampAfter?: string): Promise<IWebResponseCompatible<IOperator[]>>;
@@ -1,64 +0,0 @@
1
- "use server";
2
- import { newServerClient } from "../supabase/server";
3
- import { EnumWebResponse, IWebResponse, } from "../types/requests";
4
- // Get operators by session id (server id)
5
- export async function server_get_operators_by_session_id(sessionId) {
6
- const supabase = await newServerClient();
7
- const { data, error } = await supabase
8
- .from("operators")
9
- .select("*")
10
- .eq("session_id", sessionId)
11
- .order("created_at", { ascending: false });
12
- if (error) {
13
- console.warn("Error fetching operators by session id:", error.message);
14
- return {
15
- status: EnumWebResponse.ERROR,
16
- msg: error.message,
17
- data: [],
18
- };
19
- }
20
- return IWebResponse.success(data || []).to_compatible();
21
- }
22
- // Get all operators for a specific user
23
- export async function server_get_operators_by_user_id(userId) {
24
- const supabase = await newServerClient();
25
- const { data, error } = await supabase
26
- .from("operators")
27
- .select("*")
28
- .eq("user_id", userId)
29
- .order("created_at", { ascending: false });
30
- if (error) {
31
- console.warn("Error fetching operators by user id:", error.message);
32
- return {
33
- status: EnumWebResponse.ERROR,
34
- msg: error.message,
35
- data: [],
36
- };
37
- }
38
- return IWebResponse.success(data || []).to_compatible();
39
- }
40
- // Get operators by session id with additional filters
41
- export async function server_get_operators_by_session_id_filtered(sessionId, action, timestampAfter) {
42
- const supabase = await newServerClient();
43
- let query = supabase
44
- .from("operators")
45
- .select("*")
46
- .eq("session_id", sessionId);
47
- // Apply optional filters
48
- if (action) {
49
- query = query.eq("action", action);
50
- }
51
- if (timestampAfter) {
52
- query = query.gte("timestamp", timestampAfter);
53
- }
54
- const { data, error } = await query.order("timestamp", { ascending: false });
55
- if (error) {
56
- console.warn("Error fetching filtered operators by session id:", error.message);
57
- return {
58
- status: EnumWebResponse.ERROR,
59
- msg: error.message,
60
- data: [],
61
- };
62
- }
63
- return IWebResponse.success(data || []).to_compatible();
64
- }
@@ -1,167 +0,0 @@
1
- import { SupabaseClient } from "@supabase/supabase-js";
2
- import { useInfiniteSessionsByHerd, useInfiniteEventsByHerd, useInfiniteArtifactsByHerd } from "./useInfiniteQuery";
3
- import { IHerdModule } from "../types/herd_module";
4
- import { ISessionSummary } from "../types/db";
5
- interface UseHerdDataOptions {
6
- sessionsLimit?: number;
7
- eventsLimit?: number;
8
- artifactsLimit?: number;
9
- enableSessions?: boolean;
10
- enableEvents?: boolean;
11
- enableArtifacts?: boolean;
12
- supabase: SupabaseClient;
13
- }
14
- interface UseHerdDataReturn {
15
- herdModule: IHerdModule | undefined;
16
- herd: IHerdModule["herd"] | undefined;
17
- devices: IHerdModule["devices"];
18
- zones: IHerdModule["zones"];
19
- plans: IHerdModule["plans"];
20
- layers: IHerdModule["layers"];
21
- providers: IHerdModule["providers"];
22
- labels: IHerdModule["labels"];
23
- userRoles: IHerdModule["user_roles"];
24
- sessionSummaries: ISessionSummary | null;
25
- timestampLastRefreshed: number | null;
26
- sessions: {
27
- items: ReturnType<typeof useInfiniteSessionsByHerd>["items"];
28
- isLoading: boolean;
29
- isLoadingMore: boolean;
30
- hasMore: boolean;
31
- loadMore: () => void;
32
- refetch: () => void;
33
- error: any;
34
- };
35
- events: {
36
- items: ReturnType<typeof useInfiniteEventsByHerd>["items"];
37
- isLoading: boolean;
38
- isLoadingMore: boolean;
39
- hasMore: boolean;
40
- loadMore: () => void;
41
- refetch: () => void;
42
- error: any;
43
- };
44
- artifacts: {
45
- items: ReturnType<typeof useInfiniteArtifactsByHerd>["items"];
46
- isLoading: boolean;
47
- isLoadingMore: boolean;
48
- hasMore: boolean;
49
- loadMore: () => void;
50
- refetch: () => void;
51
- error: any;
52
- };
53
- isInitialLoading: boolean;
54
- hasAnyError: boolean;
55
- }
56
- /**
57
- * Comprehensive hook for accessing herd data from both global store and RTK Query
58
- *
59
- * @param herdId - The herd ID to fetch data for
60
- * @param options - Configuration options for data fetching
61
- * @returns Complete herd data with both structural and paginated content
62
- *
63
- * @example
64
- * ```tsx
65
- * const {
66
- * herd,
67
- * devices,
68
- * events,
69
- * sessions,
70
- * artifacts,
71
- * sessionSummaries
72
- * } = useHerdData(123, {
73
- * supabase,
74
- * eventsLimit: 20,
75
- * enableSessions: true
76
- * });
77
- *
78
- * // Use structural data from global store
79
- * console.log(herd.name, devices.length);
80
- *
81
- * // Use paginated data from RTK Query
82
- * console.log(events.items.length, events.hasMore);
83
- * if (events.hasMore) {
84
- * events.loadMore();
85
- * }
86
- *
87
- * // Use session summaries
88
- * console.log(sessionSummaries?.total_sessions, sessionSummaries?.total_distance_meters);
89
- * ```
90
- */
91
- export declare const useHerdData: (herdId: number, options: UseHerdDataOptions) => UseHerdDataReturn;
92
- /**
93
- * Hook for getting just the structural herd data from global store
94
- * Useful when you only need basic herd info without paginated content
95
- */
96
- export declare const useHerdStructure: (herdId: number) => {
97
- herdModule: IHerdModule | undefined;
98
- herd: {
99
- created_by: string;
100
- description: string;
101
- earthranger_domain: string | null;
102
- earthranger_token: string | null;
103
- id: number;
104
- inserted_at: string;
105
- is_public: boolean;
106
- slug: string;
107
- video_publisher_token: string | null;
108
- video_server_url: string | null;
109
- video_subscriber_token: string | null;
110
- } | undefined;
111
- devices: import("../types/db").IDevice[];
112
- zones: import("../types/db").IZoneWithActions[];
113
- plans: {
114
- herd_id: number;
115
- id: number;
116
- inserted_at: string | null;
117
- instructions: string;
118
- name: string;
119
- plan_type: import("..").Database["public"]["Enums"]["plan_type"];
120
- }[];
121
- layers: {
122
- created_at: string;
123
- features: import("..").Json;
124
- herd_id: number;
125
- id: number;
126
- }[];
127
- providers: {
128
- created_at: string;
129
- herd_id: number;
130
- id: number;
131
- key: string | null;
132
- source: string;
133
- type: string;
134
- }[];
135
- labels: string[];
136
- userRoles: import("../types/db").IUserAndRole[] | null;
137
- sessionSummaries: ISessionSummary | null;
138
- timestampLastRefreshed: number | null;
139
- };
140
- /**
141
- * Hook for getting all herd structures (without paginated data)
142
- * Useful for dashboard/overview components
143
- */
144
- export declare const useAllHerds: () => {
145
- herdModules: IHerdModule[];
146
- herds: {
147
- created_by: string;
148
- description: string;
149
- earthranger_domain: string | null;
150
- earthranger_token: string | null;
151
- id: number;
152
- inserted_at: string;
153
- is_public: boolean;
154
- slug: string;
155
- video_publisher_token: string | null;
156
- video_server_url: string | null;
157
- video_subscriber_token: string | null;
158
- }[];
159
- totalHerds: number;
160
- totalDevices: number;
161
- sessionSummaries: {
162
- herdId: number;
163
- herdSlug: string;
164
- summaries: ISessionSummary | null;
165
- }[];
166
- };
167
- export {};
@@ -1,153 +0,0 @@
1
- import { useSelector } from "react-redux";
2
- import { useInfiniteSessionsByHerd, useInfiniteEventsByHerd, useInfiniteArtifactsByHerd, } from "./useInfiniteQuery";
3
- /**
4
- * Comprehensive hook for accessing herd data from both global store and RTK Query
5
- *
6
- * @param herdId - The herd ID to fetch data for
7
- * @param options - Configuration options for data fetching
8
- * @returns Complete herd data with both structural and paginated content
9
- *
10
- * @example
11
- * ```tsx
12
- * const {
13
- * herd,
14
- * devices,
15
- * events,
16
- * sessions,
17
- * artifacts,
18
- * sessionSummaries
19
- * } = useHerdData(123, {
20
- * supabase,
21
- * eventsLimit: 20,
22
- * enableSessions: true
23
- * });
24
- *
25
- * // Use structural data from global store
26
- * console.log(herd.name, devices.length);
27
- *
28
- * // Use paginated data from RTK Query
29
- * console.log(events.items.length, events.hasMore);
30
- * if (events.hasMore) {
31
- * events.loadMore();
32
- * }
33
- *
34
- * // Use session summaries
35
- * console.log(sessionSummaries?.total_sessions, sessionSummaries?.total_distance_meters);
36
- * ```
37
- */
38
- export const useHerdData = (herdId, options) => {
39
- const { sessionsLimit = 20, eventsLimit = 20, artifactsLimit = 20, enableSessions = true, enableEvents = true, enableArtifacts = true, supabase, } = options;
40
- // Get structural data from global store
41
- const herdModule = useSelector((state) => state.scout.herd_modules.find((hm) => hm.herd.id === herdId));
42
- // RTK Query infinite data hooks
43
- const sessionsQuery = useInfiniteSessionsByHerd(herdId, {
44
- limit: sessionsLimit,
45
- enabled: enableSessions,
46
- supabase,
47
- });
48
- const eventsQuery = useInfiniteEventsByHerd(herdId, {
49
- limit: eventsLimit,
50
- enabled: enableEvents,
51
- supabase,
52
- });
53
- const artifactsQuery = useInfiniteArtifactsByHerd(herdId, {
54
- limit: artifactsLimit,
55
- enabled: enableArtifacts,
56
- supabase,
57
- });
58
- // Overall loading state - true if any enabled query is loading for the first time
59
- const isInitialLoading = (enableSessions &&
60
- sessionsQuery.isLoading &&
61
- sessionsQuery.items.length === 0) ||
62
- (enableEvents && eventsQuery.isLoading && eventsQuery.items.length === 0) ||
63
- (enableArtifacts &&
64
- artifactsQuery.isLoading &&
65
- artifactsQuery.items.length === 0);
66
- // Overall error state - true if any enabled query has an error
67
- const hasAnyError = (enableSessions && !!sessionsQuery.error) ||
68
- (enableEvents && !!eventsQuery.error) ||
69
- (enableArtifacts && !!artifactsQuery.error);
70
- return {
71
- // Global store structural data
72
- herdModule,
73
- herd: herdModule?.herd,
74
- devices: herdModule?.devices || [],
75
- zones: herdModule?.zones || [],
76
- plans: herdModule?.plans || [],
77
- layers: herdModule?.layers || [],
78
- providers: herdModule?.providers || [],
79
- labels: herdModule?.labels || [],
80
- userRoles: herdModule?.user_roles || null,
81
- sessionSummaries: herdModule?.session_summaries || null,
82
- timestampLastRefreshed: herdModule?.timestamp_last_refreshed || null,
83
- // RTK Query paginated data
84
- sessions: {
85
- items: sessionsQuery.items,
86
- isLoading: sessionsQuery.isLoading,
87
- isLoadingMore: sessionsQuery.isLoadingMore,
88
- hasMore: sessionsQuery.hasMore,
89
- loadMore: sessionsQuery.loadMore,
90
- refetch: sessionsQuery.refetch,
91
- error: sessionsQuery.error,
92
- },
93
- events: {
94
- items: eventsQuery.items,
95
- isLoading: eventsQuery.isLoading,
96
- isLoadingMore: eventsQuery.isLoadingMore,
97
- hasMore: eventsQuery.hasMore,
98
- loadMore: eventsQuery.loadMore,
99
- refetch: eventsQuery.refetch,
100
- error: eventsQuery.error,
101
- },
102
- artifacts: {
103
- items: artifactsQuery.items,
104
- isLoading: artifactsQuery.isLoading,
105
- isLoadingMore: artifactsQuery.isLoadingMore,
106
- hasMore: artifactsQuery.hasMore,
107
- loadMore: artifactsQuery.loadMore,
108
- refetch: artifactsQuery.refetch,
109
- error: artifactsQuery.error,
110
- },
111
- // Overall states
112
- isInitialLoading,
113
- hasAnyError,
114
- };
115
- };
116
- /**
117
- * Hook for getting just the structural herd data from global store
118
- * Useful when you only need basic herd info without paginated content
119
- */
120
- export const useHerdStructure = (herdId) => {
121
- const herdModule = useSelector((state) => state.scout.herd_modules.find((hm) => hm.herd.id === herdId));
122
- return {
123
- herdModule,
124
- herd: herdModule?.herd,
125
- devices: herdModule?.devices || [],
126
- zones: herdModule?.zones || [],
127
- plans: herdModule?.plans || [],
128
- layers: herdModule?.layers || [],
129
- providers: herdModule?.providers || [],
130
- labels: herdModule?.labels || [],
131
- userRoles: herdModule?.user_roles || null,
132
- sessionSummaries: herdModule?.session_summaries || null,
133
- timestampLastRefreshed: herdModule?.timestamp_last_refreshed || null,
134
- };
135
- };
136
- /**
137
- * Hook for getting all herd structures (without paginated data)
138
- * Useful for dashboard/overview components
139
- */
140
- export const useAllHerds = () => {
141
- const herdModules = useSelector((state) => state.scout.herd_modules);
142
- return {
143
- herdModules,
144
- herds: herdModules.map((hm) => hm.herd),
145
- totalHerds: herdModules.length,
146
- totalDevices: herdModules.reduce((sum, hm) => sum + hm.devices.length, 0),
147
- sessionSummaries: herdModules.map((hm) => ({
148
- herdId: hm.herd.id,
149
- herdSlug: hm.herd.slug,
150
- summaries: hm.session_summaries,
151
- })),
152
- };
153
- };
@@ -1,11 +0,0 @@
1
- import { SupabaseClient } from "@supabase/supabase-js";
2
- import { Database } from "../types/supabase";
3
- /**
4
- * Creates one browser Supabase client (reads NEXT_PUBLIC_* env each call).
5
- * Prefer {@link useScoutBrowserClient} in components so creation runs once via lazy state init.
6
- */
7
- export declare function createScoutBrowserClient(): SupabaseClient<Database>;
8
- /**
9
- * One browser Supabase client per component instance, created on first render (lazy state initializer).
10
- */
11
- export declare function useScoutBrowserClient(): SupabaseClient<Database>;
@@ -1,17 +0,0 @@
1
- "use client";
2
- import { useState } from "react";
3
- import { createBrowserClient } from "@supabase/ssr";
4
- /**
5
- * Creates one browser Supabase client (reads NEXT_PUBLIC_* env each call).
6
- * Prefer {@link useScoutBrowserClient} in components so creation runs once via lazy state init.
7
- */
8
- export function createScoutBrowserClient() {
9
- return createBrowserClient(process.env.NEXT_PUBLIC_SUPABASE_URL || "", process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "");
10
- }
11
- /**
12
- * One browser Supabase client per component instance, created on first render (lazy state initializer).
13
- */
14
- export function useScoutBrowserClient() {
15
- const [client] = useState(createScoutBrowserClient);
16
- return client;
17
- }
@@ -1,3 +0,0 @@
1
- import { SupabaseClient } from "@supabase/supabase-js";
2
- import { Database } from "../types/supabase";
3
- export declare function useScoutDbListener(scoutSupabase: SupabaseClient<Database>): void;
@@ -1,134 +0,0 @@
1
- "use client";
2
- import { useAppDispatch } from "../store/hooks";
3
- import { useEffect, useRef } from "react";
4
- import { addDevice, addPlan, addTag, addSessionToStore, deleteDevice, deletePlan, deleteSessionFromStore, deleteTag, updateDevice, updatePlan, updateSessionInStore, updateTag, } from "../store/scout";
5
- export function useScoutDbListener(scoutSupabase) {
6
- const supabase = useRef(null);
7
- const channels = useRef([]);
8
- const dispatch = useAppDispatch();
9
- function handleTagInserts(payload) {
10
- console.log("[DB Listener] Tag INSERT received:", payload.new);
11
- dispatch(addTag(payload.new));
12
- }
13
- function handleTagDeletes(payload) {
14
- console.log("[DB Listener] Tag DELETE received:", payload.old);
15
- if (!payload.old || !payload.old.id) {
16
- console.error("[DB Listener] Tag DELETE - Invalid payload, missing tag data");
17
- return;
18
- }
19
- dispatch(deleteTag(payload.old));
20
- }
21
- function handleTagUpdates(payload) {
22
- console.log("[DB Listener] Tag UPDATE received:", payload.new);
23
- dispatch(updateTag(payload.new));
24
- }
25
- function handleDeviceInserts(payload) {
26
- console.log("[DB Listener] Device INSERT received:", payload.new);
27
- dispatch(addDevice(payload.new));
28
- }
29
- function handleDeviceDeletes(payload) {
30
- console.log("[DB Listener] Device DELETE received:", payload.old);
31
- dispatch(deleteDevice(payload.old));
32
- }
33
- function handleDeviceUpdates(payload) {
34
- console.log("[DB Listener] Device UPDATE received:", payload.new);
35
- dispatch(updateDevice(payload.new));
36
- }
37
- function handlePlanInserts(payload) {
38
- console.log("[DB Listener] Plan INSERT received:", payload.new);
39
- dispatch(addPlan(payload.new));
40
- }
41
- function handlePlanDeletes(payload) {
42
- console.log("[DB Listener] Plan DELETE received:", payload.old);
43
- dispatch(deletePlan(payload.old));
44
- }
45
- function handlePlanUpdates(payload) {
46
- console.log("[DB Listener] Plan UPDATE received:", payload.new);
47
- dispatch(updatePlan(payload.new));
48
- }
49
- function handleSessionInserts(payload) {
50
- console.log("[DB Listener] Session INSERT received:", payload.new);
51
- dispatch(addSessionToStore(payload.new));
52
- }
53
- function handleSessionDeletes(payload) {
54
- console.log("[DB Listener] Session DELETE received:", payload.old);
55
- if (!payload.old || !payload.old.id) {
56
- console.error("[DB Listener] Session DELETE - Invalid payload, missing session data");
57
- return;
58
- }
59
- dispatch(deleteSessionFromStore(payload.old));
60
- }
61
- function handleSessionUpdates(payload) {
62
- console.log("[DB Listener] Session UPDATE received:", payload.new);
63
- dispatch(updateSessionInStore(payload.new));
64
- }
65
- function handleConnectivityInserts(payload) {
66
- console.log("[DB Listener] Connectivity INSERT received:", payload.new);
67
- }
68
- function handleConnectivityDeletes(payload) {
69
- console.log("[DB Listener] Connectivity DELETE received:", payload.old);
70
- }
71
- function handleConnectivityUpdates(payload) {
72
- console.log("[DB Listener] Connectivity UPDATE received:", payload.new);
73
- }
74
- // Clean up all channels
75
- const cleanupChannels = () => {
76
- channels.current.forEach((channel) => {
77
- if (channel) {
78
- scoutSupabase.removeChannel(channel);
79
- }
80
- });
81
- channels.current = [];
82
- };
83
- // Setup channel with event handlers
84
- const setupChannel = () => {
85
- if (!scoutSupabase)
86
- return null;
87
- const channelId = `scout_realtime_${Date.now()}_${Math.random()
88
- .toString(36)
89
- .substr(2, 9)}`;
90
- const mainChannel = scoutSupabase.channel(channelId);
91
- console.log(`[DB Listener] Creating channel: ${channelId}`);
92
- // Subscribe to all events
93
- mainChannel
94
- .on("postgres_changes", { event: "INSERT", schema: "public", table: "plans" }, handlePlanInserts)
95
- .on("postgres_changes", { event: "DELETE", schema: "public", table: "plans" }, handlePlanDeletes)
96
- .on("postgres_changes", { event: "UPDATE", schema: "public", table: "plans" }, handlePlanUpdates)
97
- .on("postgres_changes", { event: "INSERT", schema: "public", table: "devices" }, handleDeviceInserts)
98
- .on("postgres_changes", { event: "DELETE", schema: "public", table: "devices" }, handleDeviceDeletes)
99
- .on("postgres_changes", { event: "UPDATE", schema: "public", table: "devices" }, handleDeviceUpdates)
100
- .on("postgres_changes", { event: "INSERT", schema: "public", table: "tags" }, handleTagInserts)
101
- .on("postgres_changes", { event: "DELETE", schema: "public", table: "tags" }, handleTagDeletes)
102
- .on("postgres_changes", { event: "UPDATE", schema: "public", table: "tags" }, handleTagUpdates)
103
- .on("postgres_changes", { event: "INSERT", schema: "public", table: "connectivity" }, handleConnectivityInserts)
104
- .on("postgres_changes", { event: "DELETE", schema: "public", table: "connectivity" }, handleConnectivityDeletes)
105
- .on("postgres_changes", { event: "UPDATE", schema: "public", table: "connectivity" }, handleConnectivityUpdates)
106
- .on("postgres_changes", { event: "INSERT", schema: "public", table: "sessions" }, handleSessionInserts)
107
- .on("postgres_changes", { event: "DELETE", schema: "public", table: "sessions" }, handleSessionDeletes)
108
- .on("postgres_changes", { event: "UPDATE", schema: "public", table: "sessions" }, handleSessionUpdates)
109
- .subscribe((status) => {
110
- console.log("[DB Listener] Subscription status:", status);
111
- if (status === "SUBSCRIBED") {
112
- console.log("[DB Listener] ✅ Successfully subscribed to real-time updates");
113
- }
114
- });
115
- return mainChannel;
116
- };
117
- useEffect(() => {
118
- if (!scoutSupabase) {
119
- console.error("[DB Listener] No Supabase client available");
120
- return;
121
- }
122
- supabase.current = scoutSupabase;
123
- // Initial channel setup
124
- const mainChannel = setupChannel();
125
- if (mainChannel) {
126
- channels.current.push(mainChannel);
127
- }
128
- // Cleanup function
129
- return () => {
130
- console.log("[DB Listener] 🧹 Cleaning up channels");
131
- cleanupChannels();
132
- };
133
- }, [scoutSupabase, dispatch]);
134
- }
@@ -1,3 +0,0 @@
1
- import { SupabaseClient } from "@supabase/supabase-js";
2
- import { Database } from "../types/supabase";
3
- export declare function useScoutRealtimeConnectivity(scoutSupabase: SupabaseClient<Database>): void;
@@ -1,159 +0,0 @@
1
- "use client";
2
- import { useAppDispatch } from "../store/hooks";
3
- import { useSelector } from "react-redux";
4
- import { useEffect, useRef, useCallback, useMemo } from "react";
5
- import { setActiveHerdGpsTrackersConnectivity } from "../store/scout";
6
- import { server_get_connectivity_by_device_id } from "../helpers/connectivity";
7
- import { EnumWebResponse } from "../types/requests";
8
- import { getHoursAgoTimestamp } from "../helpers/time";
9
- export function useScoutRealtimeConnectivity(scoutSupabase) {
10
- const channels = useRef([]);
11
- const dispatch = useAppDispatch();
12
- const activeHerdId = useSelector((state) => state.scout.active_herd_id);
13
- const connectivity = useSelector((state) => state.scout.active_herd_gps_trackers_connectivity);
14
- const herdModules = useSelector((state) => state.scout.herd_modules);
15
- // Create stable reference for GPS device IDs to prevent unnecessary refetching
16
- const gpsDeviceIds = useMemo(() => {
17
- if (!activeHerdId)
18
- return "";
19
- const activeHerdModule = herdModules.find((hm) => hm.herd.id.toString() === activeHerdId);
20
- if (!activeHerdModule)
21
- return "";
22
- const gpsDevices = activeHerdModule.devices.filter((device) => device.device_type &&
23
- ["gps_tracker", "gps_tracker_vehicle", "gps_tracker_person"].includes(device.device_type));
24
- return gpsDevices
25
- .map((d) => d.id)
26
- .filter(Boolean)
27
- .sort()
28
- .join(",");
29
- }, [activeHerdId, herdModules]);
30
- // Handle connectivity broadcasts
31
- const handleConnectivityBroadcast = useCallback((payload) => {
32
- const { event, payload: data } = payload;
33
- const connectivityData = data.record || data.old_record;
34
- // Only process GPS tracker data (no session_id)
35
- if (!connectivityData?.device_id || connectivityData.session_id) {
36
- return;
37
- }
38
- const deviceId = connectivityData.device_id;
39
- const updatedConnectivity = { ...connectivity };
40
- switch (data.operation) {
41
- case "INSERT":
42
- console.log(`[CONNECTIVITY] INSERT for ${deviceId}, ${JSON.stringify(connectivityData)}`);
43
- if (!updatedConnectivity[deviceId]) {
44
- updatedConnectivity[deviceId] = {
45
- most_recent: connectivityData,
46
- history: [],
47
- };
48
- }
49
- else {
50
- const newHistory = [
51
- updatedConnectivity[deviceId].most_recent,
52
- ...updatedConnectivity[deviceId].history,
53
- ].slice(0, 99);
54
- updatedConnectivity[deviceId] = {
55
- most_recent: connectivityData,
56
- history: newHistory,
57
- };
58
- }
59
- break;
60
- case "UPDATE":
61
- if (updatedConnectivity[deviceId]) {
62
- if (updatedConnectivity[deviceId].most_recent.id ===
63
- connectivityData.id) {
64
- updatedConnectivity[deviceId] = {
65
- ...updatedConnectivity[deviceId],
66
- most_recent: connectivityData,
67
- };
68
- }
69
- else {
70
- const historyIndex = updatedConnectivity[deviceId].history.findIndex((c) => c.id === connectivityData.id);
71
- if (historyIndex >= 0) {
72
- const newHistory = [...updatedConnectivity[deviceId].history];
73
- newHistory[historyIndex] = connectivityData;
74
- updatedConnectivity[deviceId] = {
75
- ...updatedConnectivity[deviceId],
76
- history: newHistory,
77
- };
78
- }
79
- }
80
- }
81
- break;
82
- case "DELETE":
83
- if (updatedConnectivity[deviceId]) {
84
- if (updatedConnectivity[deviceId].most_recent.id ===
85
- connectivityData.id) {
86
- if (updatedConnectivity[deviceId].history.length === 0) {
87
- delete updatedConnectivity[deviceId];
88
- }
89
- else {
90
- updatedConnectivity[deviceId] = {
91
- most_recent: updatedConnectivity[deviceId].history[0],
92
- history: updatedConnectivity[deviceId].history.slice(1),
93
- };
94
- }
95
- }
96
- else {
97
- updatedConnectivity[deviceId] = {
98
- ...updatedConnectivity[deviceId],
99
- history: updatedConnectivity[deviceId].history.filter((c) => c.id !== connectivityData.id),
100
- };
101
- }
102
- }
103
- break;
104
- }
105
- dispatch(setActiveHerdGpsTrackersConnectivity(updatedConnectivity));
106
- }, [connectivity, dispatch]);
107
- // Fetch initial connectivity data
108
- const fetchInitialData = useCallback(async () => {
109
- if (!gpsDeviceIds)
110
- return;
111
- const deviceIds = gpsDeviceIds.split(",").filter(Boolean).map(Number);
112
- if (deviceIds.length === 0) {
113
- return;
114
- }
115
- const timestampFilter = getHoursAgoTimestamp(1);
116
- const connectivityData = {};
117
- await Promise.all(deviceIds.map(async (deviceId) => {
118
- try {
119
- const response = await server_get_connectivity_by_device_id(deviceId, timestampFilter);
120
- if (response.status === EnumWebResponse.SUCCESS && response.data) {
121
- const trackerData = response.data.filter((conn) => !conn.session_id);
122
- if (trackerData.length > 0) {
123
- const sortedData = trackerData
124
- .sort((a, b) => new Date(b.timestamp_start || 0).getTime() -
125
- new Date(a.timestamp_start || 0).getTime())
126
- .slice(0, 100);
127
- connectivityData[deviceId] = {
128
- most_recent: sortedData[0],
129
- history: sortedData.slice(1), // Exclude the most recent item
130
- };
131
- }
132
- }
133
- }
134
- catch (error) {
135
- // Silent error handling
136
- }
137
- }));
138
- dispatch(setActiveHerdGpsTrackersConnectivity(connectivityData));
139
- }, [gpsDeviceIds, dispatch]);
140
- useEffect(() => {
141
- if (!scoutSupabase || gpsDeviceIds === "")
142
- return;
143
- // Clean up existing channels
144
- channels.current.forEach((channel) => scoutSupabase.removeChannel(channel));
145
- channels.current = [];
146
- // Create connectivity channel
147
- const channel = scoutSupabase
148
- .channel(`${activeHerdId}-connectivity`, { config: { private: true } })
149
- .on("broadcast", { event: "*" }, handleConnectivityBroadcast)
150
- .subscribe();
151
- channels.current.push(channel);
152
- // Fetch initial data
153
- fetchInitialData();
154
- return () => {
155
- channels.current.forEach((ch) => scoutSupabase.removeChannel(ch));
156
- channels.current = [];
157
- };
158
- }, [scoutSupabase, gpsDeviceIds, activeHerdId, handleConnectivityBroadcast]);
159
- }
@@ -1,3 +0,0 @@
1
- import { SupabaseClient } from "@supabase/supabase-js";
2
- import { Database } from "../types/supabase";
3
- export declare function useScoutRealtimeDevices(scoutSupabase: SupabaseClient<Database>): void;
@@ -1,55 +0,0 @@
1
- "use client";
2
- import { useAppDispatch } from "../store/hooks";
3
- import { useSelector } from "react-redux";
4
- import { useEffect, useRef, useCallback } from "react";
5
- import { addDevice, deleteDevice, updateDevice } from "../store/scout";
6
- export function useScoutRealtimeDevices(scoutSupabase) {
7
- const channels = useRef([]);
8
- const dispatch = useAppDispatch();
9
- const activeHerdId = useSelector((state) => state.scout.active_herd_id);
10
- // Device broadcast handler
11
- const handleDeviceBroadcast = useCallback((payload) => {
12
- console.log("[Devices] Broadcast received:", payload.payload.operation);
13
- const data = payload.payload;
14
- switch (data.operation) {
15
- case "INSERT":
16
- if (data.record)
17
- dispatch(addDevice(data.record));
18
- break;
19
- case "UPDATE":
20
- if (data.record)
21
- dispatch(updateDevice(data.record));
22
- break;
23
- case "DELETE":
24
- if (data.old_record)
25
- dispatch(deleteDevice(data.old_record));
26
- break;
27
- }
28
- }, [dispatch]);
29
- const cleanupChannels = () => {
30
- channels.current.forEach((channel) => scoutSupabase.removeChannel(channel));
31
- channels.current = [];
32
- };
33
- const createDevicesChannel = (herdId) => {
34
- return scoutSupabase
35
- .channel(`${herdId}-devices`, { config: { private: true } })
36
- .on("broadcast", { event: "*" }, handleDeviceBroadcast)
37
- .subscribe((status) => {
38
- if (status === "SUBSCRIBED") {
39
- console.log(`[Devices] ✅ Connected to herd ${herdId}`);
40
- }
41
- else if (status === "CHANNEL_ERROR") {
42
- console.warn(`[Devices] 🟡 Failed to connect to herd ${herdId}`);
43
- }
44
- });
45
- };
46
- useEffect(() => {
47
- cleanupChannels();
48
- // Create devices channel for active herd
49
- if (activeHerdId) {
50
- const channel = createDevicesChannel(activeHerdId);
51
- channels.current.push(channel);
52
- }
53
- return cleanupChannels;
54
- }, [activeHerdId]);
55
- }
@@ -1,4 +0,0 @@
1
- export type HistoricalData<T> = {
2
- most_recent: T;
3
- history: T[];
4
- };
@@ -1 +0,0 @@
1
- export {};