@optifye/dashboard-core 6.4.2 → 6.5.1

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/index.js CHANGED
@@ -7,7 +7,6 @@ var dateFnsTz = require('date-fns-tz');
7
7
  var dateFns = require('date-fns');
8
8
  var mixpanel = require('mixpanel-browser');
9
9
  var events = require('events');
10
- var clientS3 = require('@aws-sdk/client-s3');
11
10
  var supabaseJs = require('@supabase/supabase-js');
12
11
  var Hls2 = require('hls.js');
13
12
  var useSWR = require('swr');
@@ -25,6 +24,7 @@ var SelectPrimitive = require('@radix-ui/react-select');
25
24
  var videojs = require('video.js');
26
25
  require('video.js/dist/video-js.css');
27
26
  var sonner = require('sonner');
27
+ var clientS3 = require('@aws-sdk/client-s3');
28
28
  var s3RequestPresigner = require('@aws-sdk/s3-request-presigner');
29
29
  var stream = require('stream');
30
30
 
@@ -1704,6 +1704,25 @@ var workspaceService = {
1704
1704
  this._workspaceDisplayNamesCache.clear();
1705
1705
  this._cacheTimestamp = 0;
1706
1706
  },
1707
+ /**
1708
+ * Updates the display name for a workspace
1709
+ * @param workspaceId - The workspace UUID
1710
+ * @param displayName - The new display name
1711
+ * @returns Promise<void>
1712
+ */
1713
+ async updateWorkspaceDisplayName(workspaceId, displayName) {
1714
+ const supabase = _getSupabaseInstance();
1715
+ if (!supabase) throw new Error("Supabase client not initialized");
1716
+ const config = _getDashboardConfigInstance();
1717
+ const dbConfig = config?.databaseConfig;
1718
+ const workspacesTable = getTable3(dbConfig, "workspaces");
1719
+ const { error } = await supabase.from(workspacesTable).update({ display_name: displayName.trim() }).eq("id", workspaceId);
1720
+ if (error) {
1721
+ console.error(`Error updating workspace display name for ${workspaceId}:`, error);
1722
+ throw error;
1723
+ }
1724
+ this.clearWorkspaceDisplayNamesCache();
1725
+ },
1707
1726
  async updateWorkspaceAction(updates) {
1708
1727
  const supabase = _getSupabaseInstance();
1709
1728
  if (!supabase) throw new Error("Supabase client not initialized");
@@ -1847,6 +1866,209 @@ var workspaceService = {
1847
1866
  }
1848
1867
  }
1849
1868
  };
1869
+ var WorkspaceHealthService = class _WorkspaceHealthService {
1870
+ constructor() {
1871
+ this.cache = /* @__PURE__ */ new Map();
1872
+ this.cacheExpiryMs = 30 * 1e3;
1873
+ }
1874
+ // 30 seconds cache
1875
+ static getInstance() {
1876
+ if (!_WorkspaceHealthService.instance) {
1877
+ _WorkspaceHealthService.instance = new _WorkspaceHealthService();
1878
+ }
1879
+ return _WorkspaceHealthService.instance;
1880
+ }
1881
+ getFromCache(key) {
1882
+ const cached = this.cache.get(key);
1883
+ if (cached && Date.now() - cached.timestamp < this.cacheExpiryMs) {
1884
+ return cached.data;
1885
+ }
1886
+ this.cache.delete(key);
1887
+ return null;
1888
+ }
1889
+ setCache(key, data) {
1890
+ this.cache.set(key, { data, timestamp: Date.now() });
1891
+ }
1892
+ async getWorkspaceHealthStatus(options = {}) {
1893
+ const supabase = _getSupabaseInstance();
1894
+ if (!supabase) throw new Error("Supabase client not initialized");
1895
+ let query = supabase.from("workspace_health_status").select("*").order("workspace_display_name", { ascending: true });
1896
+ if (options.lineId) {
1897
+ query = query.eq("line_id", options.lineId);
1898
+ }
1899
+ if (options.companyId) {
1900
+ query = query.eq("company_id", options.companyId);
1901
+ }
1902
+ const { data, error } = await query;
1903
+ if (error) {
1904
+ console.error("Error fetching workspace health status:", error);
1905
+ throw error;
1906
+ }
1907
+ const processedData = (data || []).map((item) => this.processHealthStatus(item));
1908
+ let filteredData = processedData;
1909
+ try {
1910
+ const { data: enabledWorkspaces, error: workspaceError } = await supabase.from("workspaces").select("workspace_id, display_name").eq("enable", true);
1911
+ if (!workspaceError && enabledWorkspaces && enabledWorkspaces.length > 0) {
1912
+ const enabledWorkspaceNames = /* @__PURE__ */ new Set();
1913
+ enabledWorkspaces.forEach((w) => {
1914
+ if (w.workspace_id) enabledWorkspaceNames.add(w.workspace_id);
1915
+ if (w.display_name) enabledWorkspaceNames.add(w.display_name);
1916
+ });
1917
+ filteredData = filteredData.filter((item) => {
1918
+ const displayName = item.workspace_display_name || "";
1919
+ return enabledWorkspaceNames.has(displayName);
1920
+ });
1921
+ } else if (!workspaceError && enabledWorkspaces && enabledWorkspaces.length === 0) {
1922
+ return [];
1923
+ } else if (workspaceError) {
1924
+ console.error("Error fetching enabled workspaces:", workspaceError);
1925
+ }
1926
+ } catch (e) {
1927
+ console.error("Error filtering workspaces:", e);
1928
+ }
1929
+ if (options.status) {
1930
+ filteredData = filteredData.filter((item) => item.status === options.status);
1931
+ }
1932
+ if (options.searchTerm) {
1933
+ const searchLower = options.searchTerm.toLowerCase();
1934
+ filteredData = filteredData.filter(
1935
+ (item) => item.workspace_display_name?.toLowerCase().includes(searchLower) || item.line_name?.toLowerCase().includes(searchLower) || item.company_name?.toLowerCase().includes(searchLower)
1936
+ );
1937
+ }
1938
+ if (options.sortBy) {
1939
+ filteredData.sort((a, b) => {
1940
+ let compareValue = 0;
1941
+ switch (options.sortBy) {
1942
+ case "name":
1943
+ compareValue = (a.workspace_display_name || "").localeCompare(b.workspace_display_name || "");
1944
+ break;
1945
+ case "status":
1946
+ compareValue = this.getStatusPriority(a.status) - this.getStatusPriority(b.status);
1947
+ break;
1948
+ case "lastUpdate":
1949
+ compareValue = new Date(b.last_heartbeat).getTime() - new Date(a.last_heartbeat).getTime();
1950
+ break;
1951
+ }
1952
+ return options.sortOrder === "desc" ? -compareValue : compareValue;
1953
+ });
1954
+ }
1955
+ return filteredData;
1956
+ }
1957
+ async getWorkspaceHealthById(workspaceId) {
1958
+ const cacheKey = `health-${workspaceId}`;
1959
+ const cached = this.getFromCache(cacheKey);
1960
+ if (cached) return cached;
1961
+ const supabase = _getSupabaseInstance();
1962
+ if (!supabase) throw new Error("Supabase client not initialized");
1963
+ const { data, error } = await supabase.from("workspace_health_status").select("*").eq("workspace_id", workspaceId).single();
1964
+ if (error) {
1965
+ if (error.code === "PGRST116") {
1966
+ return null;
1967
+ }
1968
+ console.error("Error fetching workspace health:", error);
1969
+ throw error;
1970
+ }
1971
+ const processedData = data ? this.processHealthStatus(data) : null;
1972
+ if (processedData) {
1973
+ this.setCache(cacheKey, processedData);
1974
+ }
1975
+ return processedData;
1976
+ }
1977
+ async getHealthSummary(lineId, companyId) {
1978
+ this.clearCache();
1979
+ const workspaces = await this.getWorkspaceHealthStatus({ lineId, companyId });
1980
+ const totalWorkspaces = workspaces.length;
1981
+ const healthyWorkspaces = workspaces.filter((w) => w.status === "healthy").length;
1982
+ const unhealthyWorkspaces = workspaces.filter((w) => w.status === "unhealthy").length;
1983
+ const warningWorkspaces = workspaces.filter((w) => w.status === "warning").length;
1984
+ const uptimePercentage = totalWorkspaces > 0 ? healthyWorkspaces / totalWorkspaces * 100 : 0;
1985
+ return {
1986
+ totalWorkspaces,
1987
+ healthyWorkspaces,
1988
+ unhealthyWorkspaces,
1989
+ warningWorkspaces,
1990
+ uptimePercentage,
1991
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
1992
+ };
1993
+ }
1994
+ async getHealthMetrics(workspaceId, startDate, endDate) {
1995
+ const supabase = _getSupabaseInstance();
1996
+ if (!supabase) throw new Error("Supabase client not initialized");
1997
+ return {
1998
+ avgResponseTime: 250,
1999
+ totalDowntime: 0,
2000
+ incidentCount: 0,
2001
+ mttr: 0
2002
+ };
2003
+ }
2004
+ processHealthStatus(data) {
2005
+ const now2 = /* @__PURE__ */ new Date();
2006
+ const lastHeartbeat = new Date(data.last_heartbeat);
2007
+ const minutesSinceUpdate = Math.floor((now2.getTime() - lastHeartbeat.getTime()) / (1e3 * 60));
2008
+ let status = "unknown";
2009
+ let isStale = false;
2010
+ if (data.is_healthy) {
2011
+ if (minutesSinceUpdate < 3) {
2012
+ status = "healthy";
2013
+ } else if (minutesSinceUpdate < 5) {
2014
+ status = "warning";
2015
+ isStale = true;
2016
+ } else {
2017
+ status = "unhealthy";
2018
+ isStale = true;
2019
+ }
2020
+ } else {
2021
+ status = "unhealthy";
2022
+ if (minutesSinceUpdate > 5) {
2023
+ isStale = true;
2024
+ }
2025
+ }
2026
+ const timeSinceLastUpdate = dateFns.formatDistanceToNow(lastHeartbeat, { addSuffix: true });
2027
+ return {
2028
+ ...data,
2029
+ status,
2030
+ timeSinceLastUpdate,
2031
+ isStale
2032
+ };
2033
+ }
2034
+ getStatusPriority(status) {
2035
+ const priorities = {
2036
+ unhealthy: 0,
2037
+ warning: 1,
2038
+ unknown: 2,
2039
+ healthy: 3
2040
+ };
2041
+ return priorities[status];
2042
+ }
2043
+ subscribeToHealthUpdates(callback, filters) {
2044
+ const supabase = _getSupabaseInstance();
2045
+ if (!supabase) throw new Error("Supabase client not initialized");
2046
+ let subscription = supabase.channel("workspace-health-updates").on(
2047
+ "postgres_changes",
2048
+ {
2049
+ event: "*",
2050
+ schema: "public",
2051
+ table: "workspace_health_status"
2052
+ },
2053
+ (payload) => {
2054
+ if (payload.new) {
2055
+ const newData = payload.new;
2056
+ if (filters?.lineId && newData.line_id !== filters.lineId) return;
2057
+ if (filters?.companyId && newData.company_id !== filters.companyId) return;
2058
+ this.clearCache();
2059
+ callback(newData);
2060
+ }
2061
+ }
2062
+ ).subscribe();
2063
+ return () => {
2064
+ subscription.unsubscribe();
2065
+ };
2066
+ }
2067
+ clearCache() {
2068
+ this.cache.clear();
2069
+ }
2070
+ };
2071
+ var workspaceHealthService = WorkspaceHealthService.getInstance();
1850
2072
 
1851
2073
  // src/lib/services/skuService.ts
1852
2074
  var getTable4 = (dbConfig, tableName) => {
@@ -3064,35 +3286,6 @@ var useAudioService = () => {
3064
3286
  };
3065
3287
 
3066
3288
  // src/lib/utils/dateShiftUtils.ts
3067
- function getOperationalDate2(timezone = "Asia/Kolkata") {
3068
- const now2 = /* @__PURE__ */ new Date();
3069
- const localTime = new Date(now2.toLocaleString("en-US", { timeZone: timezone }));
3070
- const hour = localTime.getHours();
3071
- if (hour < 6) {
3072
- localTime.setDate(localTime.getDate() - 1);
3073
- }
3074
- return localTime.toISOString().split("T")[0];
3075
- }
3076
- function getCurrentShift2(timezone = "Asia/Kolkata") {
3077
- const now2 = /* @__PURE__ */ new Date();
3078
- const localTime = new Date(now2.toLocaleString("en-US", { timeZone: timezone }));
3079
- const hour = localTime.getHours();
3080
- if (hour >= 6 && hour < 18) {
3081
- return {
3082
- shiftId: 0,
3083
- shiftName: "Day Shift",
3084
- startTime: "06:00",
3085
- endTime: "18:00"
3086
- };
3087
- } else {
3088
- return {
3089
- shiftId: 1,
3090
- shiftName: "Night Shift",
3091
- startTime: "18:00",
3092
- endTime: "06:00"
3093
- };
3094
- }
3095
- }
3096
3289
  function isValidDateFormat(date) {
3097
3290
  return /^\d{4}-\d{2}-\d{2}$/.test(date);
3098
3291
  }
@@ -3241,6 +3434,14 @@ function parseS3Uri(s3Uri, sopCategories) {
3241
3434
  return null;
3242
3435
  }
3243
3436
  }
3437
+ function shuffleArray(array) {
3438
+ const shuffled = [...array];
3439
+ for (let i = shuffled.length - 1; i > 0; i--) {
3440
+ const j = Math.floor(Math.random() * (i + 1));
3441
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
3442
+ }
3443
+ return shuffled;
3444
+ }
3244
3445
 
3245
3446
  // src/lib/cache/clipsCache.ts
3246
3447
  var LRUCache = class _LRUCache {
@@ -3753,300 +3954,321 @@ if (typeof window !== "undefined") {
3753
3954
  });
3754
3955
  });
3755
3956
  }
3756
-
3757
- // src/lib/api/s3-clips.ts
3758
- var RequestDeduplicationCache = class {
3759
- constructor() {
3760
- this.pendingRequests = /* @__PURE__ */ new Map();
3761
- this.maxCacheSize = 1e3;
3762
- this.cleanupInterval = 3e5;
3763
- // 5 minutes
3764
- this.lastCleanup = Date.now();
3957
+ var getSupabaseClient = () => {
3958
+ const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
3959
+ const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
3960
+ if (!url || !key) {
3961
+ throw new Error("Supabase configuration missing");
3962
+ }
3963
+ return supabaseJs.createClient(url, key);
3964
+ };
3965
+ var getAuthToken = async () => {
3966
+ try {
3967
+ const supabase = getSupabaseClient();
3968
+ const { data: { session } } = await supabase.auth.getSession();
3969
+ return session?.access_token || null;
3970
+ } catch (error) {
3971
+ console.error("[S3ClipsAPIClient] Error getting auth token:", error);
3972
+ return null;
3973
+ }
3974
+ };
3975
+ var S3ClipsAPIClient = class {
3976
+ constructor(sopCategories) {
3977
+ this.baseUrl = "/api/clips";
3978
+ this.requestCache = /* @__PURE__ */ new Map();
3979
+ this.sopCategories = sopCategories;
3980
+ console.log("[S3ClipsAPIClient] \u2705 Initialized - Using secure API routes (no direct S3 access)");
3765
3981
  }
3766
3982
  /**
3767
- * Get or create a deduplicated request
3983
+ * Fetch with authentication and error handling
3768
3984
  */
3769
- async deduplicate(key, factory, logPrefix = "Request") {
3770
- this.periodicCleanup();
3771
- const existingRequest = this.pendingRequests.get(key);
3772
- if (existingRequest) {
3773
- console.log(`[${logPrefix}] Deduplicating request for key: ${key}`);
3774
- return existingRequest;
3985
+ async fetchWithAuth(endpoint, body) {
3986
+ const token = await getAuthToken();
3987
+ if (!token) {
3988
+ throw new Error("Authentication required");
3989
+ }
3990
+ const response = await fetch(endpoint, {
3991
+ method: "POST",
3992
+ headers: {
3993
+ "Authorization": `Bearer ${token}`,
3994
+ "Content-Type": "application/json"
3995
+ },
3996
+ body: JSON.stringify(body)
3997
+ });
3998
+ if (!response.ok) {
3999
+ const error = await response.json().catch(() => ({ error: "Request failed" }));
4000
+ throw new Error(error.error || `API error: ${response.status}`);
4001
+ }
4002
+ return response.json();
4003
+ }
4004
+ /**
4005
+ * Deduplicate requests to prevent multiple API calls
4006
+ */
4007
+ async deduplicate(key, factory) {
4008
+ if (this.requestCache.has(key)) {
4009
+ console.log(`[S3ClipsAPIClient] Deduplicating request: ${key}`);
4010
+ return this.requestCache.get(key);
3775
4011
  }
3776
- console.log(`[${logPrefix}] Creating new request for key: ${key}`);
3777
4012
  const promise = factory().finally(() => {
3778
- this.pendingRequests.delete(key);
3779
- console.log(`[${logPrefix}] Completed and cleaned up request: ${key}`);
4013
+ this.requestCache.delete(key);
3780
4014
  });
3781
- this.pendingRequests.set(key, promise);
4015
+ this.requestCache.set(key, promise);
3782
4016
  return promise;
3783
4017
  }
3784
4018
  /**
3785
- * Clear all pending requests (useful for cleanup)
4019
+ * List S3 clips
3786
4020
  */
3787
- clear() {
3788
- console.log(`[RequestCache] Clearing ${this.pendingRequests.size} pending requests`);
3789
- this.pendingRequests.clear();
4021
+ async listS3Clips(params) {
4022
+ const cacheKey = `list:${JSON.stringify(params)}`;
4023
+ return this.deduplicate(cacheKey, async () => {
4024
+ const response = await this.fetchWithAuth(this.baseUrl, {
4025
+ action: "list",
4026
+ workspaceId: params.workspaceId,
4027
+ date: params.date,
4028
+ shift: params.shiftId,
4029
+ maxKeys: params.maxKeys
4030
+ });
4031
+ return response.clips.map((clip) => clip.originalUri);
4032
+ });
3790
4033
  }
3791
4034
  /**
3792
- * Get current cache stats
4035
+ * Get clip counts
3793
4036
  */
3794
- getStats() {
3795
- return {
3796
- pendingCount: this.pendingRequests.size,
3797
- maxSize: this.maxCacheSize
4037
+ async getClipCounts(workspaceId, date, shiftId) {
4038
+ const cacheKey = `counts:${workspaceId}:${date}:${shiftId}`;
4039
+ return this.deduplicate(cacheKey, async () => {
4040
+ const response = await this.fetchWithAuth(this.baseUrl, {
4041
+ action: "count",
4042
+ workspaceId,
4043
+ date,
4044
+ shift: shiftId.toString()
4045
+ });
4046
+ return response.counts;
4047
+ });
4048
+ }
4049
+ /**
4050
+ * Get clip counts with index (for compatibility)
4051
+ */
4052
+ async getClipCountsWithIndex(workspaceId, date, shiftId) {
4053
+ const counts = await this.getClipCounts(workspaceId, date, shiftId.toString());
4054
+ const videoIndex = {
4055
+ byCategory: /* @__PURE__ */ new Map(),
4056
+ allVideos: [],
4057
+ counts,
4058
+ workspaceId,
4059
+ date,
4060
+ shiftId: shiftId.toString(),
4061
+ lastUpdated: /* @__PURE__ */ new Date()
3798
4062
  };
4063
+ return { counts, videoIndex };
3799
4064
  }
3800
4065
  /**
3801
- * Periodic cleanup to prevent memory leaks
4066
+ * Get metadata for a video
3802
4067
  */
3803
- periodicCleanup() {
3804
- const now2 = Date.now();
3805
- if (now2 - this.lastCleanup > this.cleanupInterval) {
3806
- if (this.pendingRequests.size > this.maxCacheSize) {
3807
- console.warn(`[RequestCache] Cache size exceeded ${this.maxCacheSize}, clearing oldest entries`);
3808
- const entries = Array.from(this.pendingRequests.entries());
3809
- this.pendingRequests.clear();
3810
- entries.slice(-Math.floor(this.maxCacheSize / 2)).forEach(([key, promise]) => {
3811
- this.pendingRequests.set(key, promise);
3812
- });
4068
+ async getMetadata(playlistUri) {
4069
+ const cacheKey = `metadata:${playlistUri}`;
4070
+ return this.deduplicate(cacheKey, async () => {
4071
+ const response = await this.fetchWithAuth(this.baseUrl, {
4072
+ action: "metadata",
4073
+ playlistUri
4074
+ });
4075
+ return response.metadata;
4076
+ });
4077
+ }
4078
+ /**
4079
+ * Get metadata cycle time
4080
+ */
4081
+ async getMetadataCycleTime(playlistUri) {
4082
+ const metadata = await this.getMetadata(playlistUri);
4083
+ return metadata?.cycle_time_seconds || null;
4084
+ }
4085
+ /**
4086
+ * Get first clip for category
4087
+ */
4088
+ async getFirstClipForCategory(workspaceId, date, shiftId, category) {
4089
+ const cacheKey = `first:${workspaceId}:${date}:${shiftId}:${category}`;
4090
+ return this.deduplicate(cacheKey, async () => {
4091
+ const response = await this.fetchWithAuth(this.baseUrl, {
4092
+ action: "first",
4093
+ workspaceId,
4094
+ date,
4095
+ shift: shiftId.toString(),
4096
+ category,
4097
+ sopCategories: this.sopCategories
4098
+ });
4099
+ return response.video;
4100
+ });
4101
+ }
4102
+ /**
4103
+ * Get clip by index
4104
+ */
4105
+ async getClipByIndex(workspaceId, date, shiftId, category, index, includeCycleTime = true, includeMetadata = false) {
4106
+ const cacheKey = `by-index:${workspaceId}:${date}:${shiftId}:${category}:${index}`;
4107
+ return this.deduplicate(cacheKey, async () => {
4108
+ const response = await this.fetchWithAuth(this.baseUrl, {
4109
+ action: "by-index",
4110
+ workspaceId,
4111
+ date,
4112
+ shift: shiftId.toString(),
4113
+ category,
4114
+ index,
4115
+ sopCategories: this.sopCategories
4116
+ });
4117
+ const video = response.video;
4118
+ if (video && includeMetadata && video.originalUri) {
4119
+ try {
4120
+ const metadata = await this.getMetadata(video.originalUri);
4121
+ if (metadata) {
4122
+ video.cycle_time_seconds = metadata.cycle_time_seconds;
4123
+ video.creation_timestamp = metadata.creation_timestamp;
4124
+ }
4125
+ } catch (error) {
4126
+ console.warn("[S3ClipsAPIClient] Failed to fetch metadata:", error);
4127
+ }
3813
4128
  }
3814
- this.lastCleanup = now2;
3815
- }
4129
+ return video;
4130
+ });
4131
+ }
4132
+ /**
4133
+ * Get videos page with pagination
4134
+ */
4135
+ async getVideosPage(workspaceId, date, shiftId, category, pageSize = 5, startAfter) {
4136
+ const cacheKey = `page:${workspaceId}:${date}:${shiftId}:${category}:${pageSize}:${startAfter || "first"}`;
4137
+ return this.deduplicate(cacheKey, async () => {
4138
+ const response = await this.fetchWithAuth(this.baseUrl, {
4139
+ action: "page",
4140
+ workspaceId,
4141
+ date,
4142
+ shift: shiftId.toString(),
4143
+ category,
4144
+ pageSize,
4145
+ startAfter,
4146
+ sopCategories: this.sopCategories
4147
+ });
4148
+ return {
4149
+ videos: response.videos,
4150
+ nextToken: response.nextToken,
4151
+ hasMore: response.hasMore
4152
+ };
4153
+ });
4154
+ }
4155
+ /**
4156
+ * Convert S3 URI to CloudFront URL
4157
+ * In the API client, URLs are already signed from the server
4158
+ */
4159
+ s3UriToCloudfront(s3Uri) {
4160
+ return s3Uri;
4161
+ }
4162
+ /**
4163
+ * Clean up resources
4164
+ */
4165
+ dispose() {
4166
+ this.requestCache.clear();
4167
+ }
4168
+ /**
4169
+ * Get service statistics
4170
+ */
4171
+ getStats() {
4172
+ return {
4173
+ requestCache: {
4174
+ pendingCount: this.requestCache.size,
4175
+ maxSize: 1e3
4176
+ }
4177
+ };
3816
4178
  }
3817
4179
  };
4180
+
4181
+ // src/lib/api/s3-clips-secure.ts
3818
4182
  var S3ClipsService = class {
3819
4183
  constructor(config) {
3820
- // Request deduplication cache
3821
- this.requestCache = new RequestDeduplicationCache();
3822
- // Flag to prevent metadata fetching during index building
4184
+ // Flags for compatibility
3823
4185
  this.isIndexBuilding = false;
3824
- // Flag to prevent metadata fetching during entire prefetch operation
3825
4186
  this.isPrefetching = false;
3826
- // Global safeguard: limit concurrent metadata fetches to prevent flooding
3827
4187
  this.currentMetadataFetches = 0;
3828
4188
  this.MAX_CONCURRENT_METADATA = 3;
3829
4189
  this.config = config;
3830
4190
  if (!config.s3Config) {
3831
4191
  throw new Error("S3 configuration is required");
3832
4192
  }
4193
+ const sopCategories = config.s3Config.sopCategories?.default;
4194
+ this.apiClient = new S3ClipsAPIClient(sopCategories);
3833
4195
  const processing = config.s3Config.processing || {};
3834
4196
  this.defaultLimitPerCategory = processing.defaultLimitPerCategory || 30;
3835
4197
  this.maxLimitPerCategory = processing.maxLimitPerCategory || 1e3;
3836
4198
  this.concurrencyLimit = processing.concurrencyLimit || 10;
3837
4199
  this.maxInitialFetch = processing.maxInitialFetch || 60;
3838
- const region = this.validateAndSanitizeRegion(config.s3Config.region);
3839
- console.log(`S3ClipsService: Using AWS region: ${region}`);
3840
- this.s3Client = new clientS3.S3Client({
3841
- region,
3842
- credentials: config.s3Config.credentials ? {
3843
- accessKeyId: config.s3Config.credentials.accessKeyId,
3844
- secretAccessKey: config.s3Config.credentials.secretAccessKey
3845
- } : void 0
3846
- });
3847
- }
3848
- /**
3849
- * Validates and sanitizes the AWS region
3850
- */
3851
- validateAndSanitizeRegion(region) {
3852
- const defaultRegion = "us-east-1";
3853
- if (!region || typeof region !== "string") {
3854
- console.warn(`S3ClipsService: Invalid region provided (${region}), using default: ${defaultRegion}`);
3855
- return defaultRegion;
3856
- }
3857
- const sanitizedRegion = region.trim().toLowerCase();
3858
- if (!sanitizedRegion) {
3859
- console.warn(`S3ClipsService: Empty region provided, using default: ${defaultRegion}`);
3860
- return defaultRegion;
3861
- }
3862
- const regionPattern = /^[a-z]{2,3}-[a-z]+-\d+$/;
3863
- if (!regionPattern.test(sanitizedRegion)) {
3864
- console.warn(`S3ClipsService: Invalid region format (${sanitizedRegion}), using default: ${defaultRegion}`);
3865
- return defaultRegion;
3866
- }
3867
- return sanitizedRegion;
4200
+ console.log("[S3ClipsService] \u2705 Initialized with secure API client - AWS credentials are now server-side only!");
3868
4201
  }
3869
4202
  /**
3870
- * Lists S3 clips for a workspace, date, and shift with request deduplication
4203
+ * Lists S3 clips using API
3871
4204
  */
3872
4205
  async listS3Clips(params) {
3873
- const { workspaceId, date, shiftId, maxKeys } = params;
4206
+ const { workspaceId, date, shiftId } = params;
3874
4207
  if (!isValidShiftId(shiftId)) {
3875
- console.error(`[S3ClipsService] Invalid shift ID: ${shiftId}. Must be 0 (day) or 1 (night)`);
4208
+ console.error(`[S3ClipsService] Invalid shift ID: ${shiftId}`);
3876
4209
  return [];
3877
4210
  }
3878
- const prefix = `sop_violations/${workspaceId}/${date}/${shiftId}/`;
3879
- console.log(`[S3ClipsService] Listing clips for workspace: ${workspaceId}, date: ${date}, shift: ${shiftId}`);
3880
- const deduplicationKey = `list-s3-clips:${prefix}:${maxKeys || "all"}`;
3881
- return this.requestCache.deduplicate(
3882
- deduplicationKey,
3883
- () => this.executeListS3Clips(params),
3884
- "ListS3Clips"
3885
- );
3886
- }
3887
- /**
3888
- * Internal implementation of S3 listing (called through deduplication)
3889
- */
3890
- async executeListS3Clips(params) {
3891
- const { workspaceId, date, shiftId, maxKeys } = params;
3892
- const prefix = `sop_violations/${workspaceId}/${date}/${shiftId}/`;
3893
- console.log(`[S3ClipsService] Executing S3 list for prefix: ${prefix}`);
4211
+ console.log(`[S3ClipsService] Listing clips via API for workspace: ${workspaceId}`);
3894
4212
  try {
3895
- let playlists = [];
3896
- let continuationToken = void 0;
3897
- do {
3898
- const command = new clientS3.ListObjectsV2Command({
3899
- Bucket: this.config.s3Config.bucketName,
3900
- Prefix: prefix,
3901
- ContinuationToken: continuationToken,
3902
- MaxKeys: maxKeys ?? 1e3
3903
- });
3904
- const response = await this.s3Client.send(command);
3905
- console.log(`Got S3 response for ${prefix}:`, {
3906
- keyCount: response.KeyCount,
3907
- contentsLength: response.Contents?.length || 0,
3908
- hasContinuation: !!response.NextContinuationToken
3909
- });
3910
- if (response.Contents) {
3911
- if (response.Contents.length > 0) {
3912
- console.log("Sample Keys:", response.Contents.slice(0, 3).map((obj) => obj.Key));
3913
- }
3914
- for (const obj of response.Contents) {
3915
- if (obj.Key && obj.Key.endsWith("playlist.m3u8")) {
3916
- if (obj.Key.includes("missed_qchecks")) {
3917
- console.log(`Skipping missed_qchecks path: ${obj.Key}`);
3918
- continue;
3919
- }
3920
- playlists.push(`s3://${this.config.s3Config.bucketName}/${obj.Key}`);
3921
- }
3922
- }
3923
- }
3924
- continuationToken = response.NextContinuationToken;
3925
- if (maxKeys && playlists.length >= maxKeys) {
3926
- break;
3927
- }
3928
- } while (continuationToken && (!maxKeys || playlists.length < maxKeys));
3929
- console.log(`Found ${playlists.length} HLS playlists in ${prefix}`);
3930
- if (playlists.length > 0) {
3931
- console.log("First playlist URI:", playlists[0]);
3932
- }
3933
- return playlists;
4213
+ return await this.apiClient.listS3Clips(params);
3934
4214
  } catch (error) {
3935
- console.error(`Error listing S3 objects with prefix '${prefix}':`, error);
4215
+ console.error("[S3ClipsService] Error listing clips:", error);
3936
4216
  return [];
3937
4217
  }
3938
4218
  }
3939
4219
  /**
3940
- * Fetches and extracts cycle time from metadata.json with deduplication
4220
+ * Get metadata cycle time
3941
4221
  */
3942
4222
  async getMetadataCycleTime(playlistUri) {
3943
- const deduplicationKey = `metadata-cycle-time:${playlistUri}`;
3944
- return this.requestCache.deduplicate(
3945
- deduplicationKey,
3946
- () => this.executeGetMetadataCycleTime(playlistUri),
3947
- "MetadataCycleTime"
3948
- );
3949
- }
3950
- /**
3951
- * Internal implementation of metadata cycle time fetching
3952
- */
3953
- async executeGetMetadataCycleTime(playlistUri) {
3954
4223
  try {
3955
- const metadataUri = playlistUri.replace(/playlist\.m3u8$/, "metadata.json");
3956
- const url = new URL(metadataUri);
3957
- const bucket = url.hostname;
3958
- const key = url.pathname.substring(1);
3959
- console.log(`[S3ClipsService] Fetching metadata cycle time for: ${key}`);
3960
- const command = new clientS3.GetObjectCommand({
3961
- Bucket: bucket,
3962
- Key: key
3963
- });
3964
- const response = await this.s3Client.send(command);
3965
- if (!response.Body) {
3966
- console.warn(`Empty response body for metadata file: ${key}`);
3967
- return null;
3968
- }
3969
- const metadataContent = await response.Body.transformToString();
3970
- const metadata = JSON.parse(metadataContent);
3971
- const cycleTimeFrames = metadata?.original_task_metadata?.cycle_time;
3972
- if (typeof cycleTimeFrames === "number") {
3973
- return cycleTimeFrames;
3974
- }
3975
- return null;
4224
+ return await this.apiClient.getMetadataCycleTime(playlistUri);
3976
4225
  } catch (error) {
3977
- console.error(`Error fetching or parsing metadata:`, error);
4226
+ console.error("[S3ClipsService] Error fetching metadata cycle time:", error);
3978
4227
  return null;
3979
4228
  }
3980
4229
  }
3981
4230
  /**
3982
- * Control prefetch mode to prevent metadata fetching during background operations
4231
+ * Control prefetch mode
3983
4232
  */
3984
4233
  setPrefetchMode(enabled) {
3985
4234
  this.isPrefetching = enabled;
3986
- console.log(`[S3ClipsService] Prefetch mode ${enabled ? "enabled" : "disabled"} - metadata fetching ${enabled ? "blocked" : "allowed"}`);
4235
+ console.log(`[S3ClipsService] Prefetch mode ${enabled ? "enabled" : "disabled"}`);
3987
4236
  }
3988
4237
  /**
3989
- * Fetches full metadata including timestamps with deduplication
4238
+ * Get full metadata
3990
4239
  */
3991
4240
  async getFullMetadata(playlistUri) {
3992
4241
  if (this.isIndexBuilding || this.isPrefetching) {
3993
- console.warn(`[S3ClipsService] Skipping metadata fetch - building: ${this.isIndexBuilding}, prefetching: ${this.isPrefetching}`);
4242
+ console.warn("[S3ClipsService] Skipping metadata - operation in progress");
3994
4243
  return null;
3995
4244
  }
3996
4245
  if (this.currentMetadataFetches >= this.MAX_CONCURRENT_METADATA) {
3997
- console.warn(`[S3ClipsService] Skipping metadata - max concurrent fetches (${this.MAX_CONCURRENT_METADATA}) reached`);
4246
+ console.warn("[S3ClipsService] Skipping metadata - max concurrent fetches reached");
3998
4247
  return null;
3999
4248
  }
4000
4249
  this.currentMetadataFetches++;
4001
4250
  try {
4002
- const deduplicationKey = `full-metadata:${playlistUri}`;
4003
- return await this.requestCache.deduplicate(
4004
- deduplicationKey,
4005
- () => this.executeGetFullMetadata(playlistUri),
4006
- "FullMetadata"
4007
- );
4008
- } finally {
4009
- this.currentMetadataFetches--;
4010
- }
4011
- }
4012
- /**
4013
- * Internal implementation of full metadata fetching
4014
- */
4015
- async executeGetFullMetadata(playlistUri) {
4016
- try {
4017
- const metadataUri = playlistUri.replace(/playlist\.m3u8$/, "metadata.json");
4018
- const url = new URL(metadataUri);
4019
- const bucket = url.hostname;
4020
- const key = url.pathname.substring(1);
4021
- console.log(`[S3ClipsService] Fetching full metadata for: ${key}`);
4022
- const command = new clientS3.GetObjectCommand({
4023
- Bucket: bucket,
4024
- Key: key
4025
- });
4026
- const response = await this.s3Client.send(command);
4027
- if (!response.Body) {
4028
- console.warn(`Empty response body for metadata file: ${key}`);
4029
- return null;
4030
- }
4031
- const metadataContent = await response.Body.transformToString();
4032
- const metadata = JSON.parse(metadataContent);
4033
- return metadata;
4251
+ return await this.apiClient.getMetadata(playlistUri);
4034
4252
  } catch (error) {
4035
- console.error(`Error fetching or parsing metadata:`, error);
4253
+ console.error("[S3ClipsService] Error fetching metadata:", error);
4036
4254
  return null;
4255
+ } finally {
4256
+ this.currentMetadataFetches--;
4037
4257
  }
4038
4258
  }
4039
4259
  /**
4040
- * Converts S3 URI to CloudFront URL
4260
+ * Convert S3 URI to CloudFront URL
4261
+ * URLs from API are already signed
4041
4262
  */
4042
4263
  s3UriToCloudfront(s3Uri) {
4043
- const url = new URL(s3Uri);
4044
- const key = url.pathname.startsWith("/") ? url.pathname.substring(1) : url.pathname;
4045
- const cloudfrontUrl = `${this.config.s3Config.cloudFrontDomain}/${key}`;
4046
- return cloudfrontUrl;
4264
+ if (s3Uri.startsWith("http")) {
4265
+ return s3Uri;
4266
+ }
4267
+ console.warn("[S3ClipsService] Unexpected S3 URI in secure mode:", s3Uri);
4268
+ return s3Uri;
4047
4269
  }
4048
4270
  /**
4049
- * Gets SOP categories for a specific workspace
4271
+ * Get SOP categories for workspace
4050
4272
  */
4051
4273
  getSOPCategories(workspaceId) {
4052
4274
  const sopConfig = this.config.s3Config?.sopCategories;
@@ -4058,299 +4280,85 @@ var S3ClipsService = class {
4058
4280
  }
4059
4281
  async getClipCounts(workspaceId, date, shiftId, buildIndex) {
4060
4282
  if (!isValidShiftId(shiftId)) {
4061
- console.error(`[S3ClipsService] getClipCounts - Invalid shift ID: ${shiftId}. Must be 0 (day) or 1 (night)`);
4062
- return buildIndex ? { counts: {}, videoIndex: { byCategory: /* @__PURE__ */ new Map(), allVideos: [], counts: {}, workspaceId, date, shiftId: "0", lastUpdated: /* @__PURE__ */ new Date() } } : {};
4063
- }
4064
- const deduplicationKey = `clip-counts:${workspaceId}:${date}:${shiftId}:${buildIndex ? "with-index" : "counts-only"}`;
4065
- return this.requestCache.deduplicate(
4066
- deduplicationKey,
4067
- () => this.executeGetClipCounts(workspaceId, date, shiftId, buildIndex),
4068
- "ClipCounts"
4069
- );
4070
- }
4071
- /**
4072
- * Internal implementation of clip counts fetching
4073
- */
4074
- async executeGetClipCounts(workspaceId, date, shiftId, buildIndex) {
4075
- const effectiveBuildIndex = false;
4283
+ console.error(`[S3ClipsService] Invalid shift ID: ${shiftId}`);
4284
+ return buildIndex ? { counts: {}, videoIndex: this.createEmptyVideoIndex(workspaceId, date, shiftId.toString()) } : {};
4285
+ }
4076
4286
  try {
4077
- const basePrefix = `sop_violations/${workspaceId}/${date}/${shiftId}/`;
4078
- const counts = { total: 0 };
4079
- const categoryFolders = [
4080
- "idle_time",
4081
- "low_value",
4082
- "sop_deviation",
4083
- "missing_quality_check",
4084
- "best_cycle_time",
4085
- "worst_cycle_time",
4086
- "long_cycle_time",
4087
- "cycle_completion",
4088
- "bottleneck"
4089
- ];
4090
- const shiftName = shiftId === 0 || shiftId === "0" ? "Day" : "Night";
4091
- console.log(`[S3ClipsService] Fast counting clips for ${workspaceId} on ${date}, shift ${shiftId} (${shiftName} Shift)`);
4092
- const startTime = performance.now();
4093
- const videoIndex = effectiveBuildIndex ? {
4094
- byCategory: /* @__PURE__ */ new Map(),
4095
- allVideos: [],
4096
- counts: {},
4097
- workspaceId,
4098
- date,
4099
- shiftId: shiftId.toString(),
4100
- lastUpdated: /* @__PURE__ */ new Date(),
4101
- _debugId: `vid_${Date.now()}_${Math.random().toString(36).substring(7)}`
4102
- } : null;
4103
- const countPromises = categoryFolders.map(async (category) => {
4104
- const categoryPrefix = `${basePrefix}${category}/videos/`;
4105
- const categoryVideos = [];
4106
- try {
4107
- if (effectiveBuildIndex) ; else {
4108
- const command = new clientS3.ListObjectsV2Command({
4109
- Bucket: this.config.s3Config.bucketName,
4110
- Prefix: categoryPrefix,
4111
- Delimiter: "/",
4112
- MaxKeys: 1e3
4113
- });
4114
- let folderCount = 0;
4115
- let continuationToken;
4116
- do {
4117
- if (continuationToken) {
4118
- command.input.ContinuationToken = continuationToken;
4119
- }
4120
- const response = await this.s3Client.send(command);
4121
- if (response.CommonPrefixes) {
4122
- folderCount += response.CommonPrefixes.length;
4123
- }
4124
- continuationToken = response.NextContinuationToken;
4125
- } while (continuationToken);
4126
- return { category, count: folderCount, videos: [] };
4127
- }
4128
- } catch (error) {
4129
- console.error(`Error ${buildIndex ? "building index for" : "counting folders for"} category ${category}:`, error);
4130
- return { category, count: 0, videos: [] };
4131
- }
4132
- });
4133
- const results = await Promise.all(countPromises);
4134
- for (const { category, count, videos } of results) {
4135
- counts[category] = count;
4136
- counts.total += count;
4137
- if (effectiveBuildIndex && videoIndex && videos) ;
4138
- }
4139
- if (effectiveBuildIndex && videoIndex) ;
4140
- const elapsed = performance.now() - startTime;
4141
- console.log(`[S3ClipsService] Clip counts completed in ${elapsed.toFixed(2)}ms - Total: ${counts.total}`);
4142
- if (effectiveBuildIndex && videoIndex) ;
4143
- return counts;
4144
- } finally {
4287
+ if (buildIndex) {
4288
+ const result = await this.apiClient.getClipCountsWithIndex(workspaceId, date, shiftId);
4289
+ const cacheKey = `clip-counts:${workspaceId}:${date}:${shiftId}`;
4290
+ await smartVideoCache.setClipCounts(cacheKey, result);
4291
+ return result;
4292
+ } else {
4293
+ const counts = await this.apiClient.getClipCounts(workspaceId, date, shiftId);
4294
+ return counts;
4295
+ }
4296
+ } catch (error) {
4297
+ console.error("[S3ClipsService] Error fetching clip counts:", error);
4298
+ return buildIndex ? { counts: {}, videoIndex: this.createEmptyVideoIndex(workspaceId, date, shiftId.toString()) } : {};
4145
4299
  }
4146
4300
  }
4147
4301
  async getClipCountsCacheFirst(workspaceId, date, shiftId, buildIndex) {
4148
4302
  const cacheKey = `clip-counts:${workspaceId}:${date}:${shiftId}`;
4149
4303
  const cachedResult = await smartVideoCache.getClipCounts(cacheKey);
4150
4304
  if (cachedResult) {
4151
- console.log(`[S3ClipsService] Using cached clip counts for ${workspaceId}`);
4152
- if (buildIndex) {
4153
- return cachedResult;
4154
- } else {
4155
- return cachedResult.counts;
4156
- }
4305
+ console.log("[S3ClipsService] Using cached clip counts");
4306
+ return buildIndex ? cachedResult : cachedResult.counts;
4157
4307
  }
4158
- console.log(`[S3ClipsService] Cache miss - fetching clip counts for ${workspaceId}`);
4308
+ console.log("[S3ClipsService] Cache miss - fetching from API");
4159
4309
  return buildIndex ? this.getClipCounts(workspaceId, date, shiftId, true) : this.getClipCounts(workspaceId, date, shiftId);
4160
4310
  }
4161
4311
  /**
4162
- * Get first clip for a specific category with deduplication
4312
+ * Get first clip for category
4163
4313
  */
4164
4314
  async getFirstClipForCategory(workspaceId, date, shiftId, category) {
4165
4315
  if (!isValidShiftId(shiftId)) {
4166
- console.error(`[S3ClipsService] getFirstClipForCategory - Invalid shift ID: ${shiftId}. Must be 0 (day) or 1 (night)`);
4316
+ console.error(`[S3ClipsService] Invalid shift ID: ${shiftId}`);
4167
4317
  return null;
4168
4318
  }
4169
- const deduplicationKey = `first-clip:${workspaceId}:${date}:${shiftId}:${category}`;
4170
- return this.requestCache.deduplicate(
4171
- deduplicationKey,
4172
- () => this.executeGetFirstClipForCategory(workspaceId, date, shiftId, category),
4173
- "FirstClip"
4174
- );
4175
- }
4176
- /**
4177
- * Internal implementation of first clip fetching
4178
- */
4179
- async executeGetFirstClipForCategory(workspaceId, date, shiftId, category) {
4180
- const categoryPrefix = `sop_violations/${workspaceId}/${date}/${shiftId}/${category}/videos/`;
4181
4319
  try {
4182
- const command = new clientS3.ListObjectsV2Command({
4183
- Bucket: this.config.s3Config.bucketName,
4184
- Prefix: categoryPrefix,
4185
- MaxKeys: 10
4186
- // Small limit since we only need one
4187
- });
4188
- const response = await this.s3Client.send(command);
4189
- if (response.Contents) {
4190
- const playlistObj = response.Contents.find((obj) => obj.Key?.endsWith("playlist.m3u8"));
4191
- if (playlistObj && playlistObj.Key) {
4192
- const s3Uri = `s3://${this.config.s3Config.bucketName}/${playlistObj.Key}`;
4193
- const sopCategories = this.getSOPCategories(workspaceId);
4194
- const parsedInfo = parseS3Uri(s3Uri, sopCategories);
4195
- if (parsedInfo) {
4196
- const cloudfrontUrl = this.s3UriToCloudfront(s3Uri);
4197
- return {
4198
- id: `${workspaceId}-${date}-${shiftId}-${category}-first`,
4199
- src: cloudfrontUrl,
4200
- ...parsedInfo,
4201
- originalUri: s3Uri
4202
- };
4203
- }
4204
- }
4205
- }
4320
+ return await this.apiClient.getFirstClipForCategory(workspaceId, date, shiftId, category);
4206
4321
  } catch (error) {
4207
- console.error(`Error getting first clip for category ${category}:`, error);
4322
+ console.error("[S3ClipsService] Error fetching first clip:", error);
4323
+ return null;
4208
4324
  }
4209
- return null;
4210
4325
  }
4211
4326
  /**
4212
- * Get a specific video from the pre-built video index - O(1) lookup performance
4327
+ * Get video from index (for compatibility)
4213
4328
  */
4214
4329
  async getVideoFromIndex(videoIndex, category, index, includeCycleTime = true, includeMetadata = true) {
4330
+ const { workspaceId, date, shiftId } = videoIndex;
4331
+ return this.getClipByIndex(
4332
+ workspaceId,
4333
+ date,
4334
+ shiftId,
4335
+ category,
4336
+ index,
4337
+ includeCycleTime,
4338
+ includeMetadata
4339
+ );
4340
+ }
4341
+ /**
4342
+ * Get clip by index
4343
+ */
4344
+ async getClipByIndex(workspaceId, date, shiftId, category, index, includeCycleTime = true, includeMetadata = false) {
4215
4345
  try {
4216
- const categoryVideos = videoIndex.byCategory.get(category);
4217
- if (!categoryVideos || index < 0 || index >= categoryVideos.length) {
4218
- console.warn(`Invalid index ${index} for category '${category}'. Available: ${categoryVideos?.length || 0}`);
4219
- console.warn(`Video index ID: ${videoIndex._debugId || "NO_ID"}`);
4220
- console.warn(`Video index categories available: [${Array.from(videoIndex.byCategory.keys()).join(", ")}]`);
4221
- console.warn(`Video index total videos: ${videoIndex.allVideos.length}`);
4222
- return null;
4223
- }
4224
- const videoEntry = categoryVideos[index];
4225
- return this.processFullVideo(
4226
- videoEntry.uri,
4346
+ return await this.apiClient.getClipByIndex(
4347
+ workspaceId,
4348
+ date,
4349
+ shiftId,
4350
+ category,
4227
4351
  index,
4228
- videoEntry.workspaceId,
4229
- videoEntry.date,
4230
- videoEntry.shiftId,
4231
4352
  includeCycleTime,
4232
4353
  includeMetadata
4233
4354
  );
4234
4355
  } catch (error) {
4235
- console.error(`Error getting video from index at ${index} for category ${category}:`, error);
4356
+ console.error("[S3ClipsService] Error fetching clip by index:", error);
4236
4357
  return null;
4237
4358
  }
4238
4359
  }
4239
4360
  /**
4240
- * Get a specific clip by index for a category with deduplication
4241
- * @deprecated Use getVideoFromIndex with a pre-built VideoIndex for better performance
4242
- */
4243
- async getClipByIndex(workspaceId, date, shiftId, category, index, includeCycleTime = true, includeMetadata = false) {
4244
- const deduplicationKey = `clip-by-index:${workspaceId}:${date}:${shiftId}:${category}:${index}:${includeCycleTime}:${includeMetadata}`;
4245
- return this.requestCache.deduplicate(
4246
- deduplicationKey,
4247
- () => this.executeGetClipByIndex(workspaceId, date, shiftId, category, index, includeCycleTime, includeMetadata),
4248
- "ClipByIndex"
4249
- );
4250
- }
4251
- /**
4252
- * Internal implementation of clip by index fetching
4253
- */
4254
- async executeGetClipByIndex(workspaceId, date, shiftId, category, index, includeCycleTime = true, includeMetadata = false) {
4255
- const categoryPrefix = `sop_violations/${workspaceId}/${date}/${shiftId}/${category}/videos/`;
4256
- try {
4257
- const command = new clientS3.ListObjectsV2Command({
4258
- Bucket: this.config.s3Config.bucketName,
4259
- Prefix: categoryPrefix,
4260
- MaxKeys: 1e3
4261
- });
4262
- let playlists = [];
4263
- let continuationToken = void 0;
4264
- do {
4265
- if (continuationToken) {
4266
- command.input.ContinuationToken = continuationToken;
4267
- }
4268
- const response = await this.s3Client.send(command);
4269
- if (response.Contents) {
4270
- for (const obj of response.Contents) {
4271
- if (obj.Key && obj.Key.endsWith("playlist.m3u8")) {
4272
- playlists.push(`s3://${this.config.s3Config.bucketName}/${obj.Key}`);
4273
- }
4274
- }
4275
- }
4276
- continuationToken = response.NextContinuationToken;
4277
- } while (continuationToken && playlists.length < index + 10);
4278
- playlists.sort((a, b) => {
4279
- const parsedA = parseS3Uri(a);
4280
- const parsedB = parseS3Uri(b);
4281
- if (!parsedA || !parsedB) return 0;
4282
- return parsedB.timestamp.localeCompare(parsedA.timestamp);
4283
- });
4284
- if (index < playlists.length) {
4285
- const uri = playlists[index];
4286
- return this.processFullVideo(
4287
- uri,
4288
- index,
4289
- workspaceId,
4290
- date,
4291
- shiftId.toString(),
4292
- includeCycleTime,
4293
- includeMetadata
4294
- );
4295
- }
4296
- } catch (error) {
4297
- console.error(`[getClipByIndex] Error getting clip at index ${index} for category ${category}:`, error);
4298
- }
4299
- return null;
4300
- }
4301
- /**
4302
- * Get one sample video from each category for preloading
4303
- */
4304
- async getSampleVideos(workspaceId, date, shiftId) {
4305
- const basePrefix = `sop_violations/${workspaceId}/${date}/${shiftId}/`;
4306
- const samples = {};
4307
- const sopCategories = this.getSOPCategories(workspaceId);
4308
- const categoriesToSample = sopCategories ? sopCategories.map((cat) => cat.id) : ["low_value", "best_cycle_time", "worst_cycle_time", "long_cycle_time", "cycle_completion"];
4309
- console.log(`[S3ClipsService] Getting sample videos for categories:`, categoriesToSample);
4310
- const samplePromises = categoriesToSample.map(async (category) => {
4311
- const categoryPrefix = `${basePrefix}${category}/`;
4312
- try {
4313
- const command = new clientS3.ListObjectsV2Command({
4314
- Bucket: this.config.s3Config.bucketName,
4315
- Prefix: categoryPrefix,
4316
- MaxKeys: 20
4317
- // Small limit since we only need one
4318
- });
4319
- const response = await this.s3Client.send(command);
4320
- if (response.Contents) {
4321
- const playlistObj = response.Contents.find((obj) => obj.Key?.endsWith("playlist.m3u8"));
4322
- if (playlistObj && playlistObj.Key) {
4323
- const s3Uri = `s3://${this.config.s3Config.bucketName}/${playlistObj.Key}`;
4324
- const parsedInfo = parseS3Uri(s3Uri, sopCategories);
4325
- if (parsedInfo) {
4326
- return {
4327
- category,
4328
- sample: {
4329
- id: `${workspaceId}-${date}-${shiftId}-${category}-sample`,
4330
- src: this.s3UriToCloudfront(s3Uri),
4331
- // Pre-convert to CloudFront URL
4332
- ...parsedInfo,
4333
- originalUri: s3Uri
4334
- }
4335
- };
4336
- }
4337
- }
4338
- }
4339
- } catch (error) {
4340
- console.error(`Error getting sample for category ${category}:`, error);
4341
- }
4342
- return { category, sample: null };
4343
- });
4344
- const sampleResults = await Promise.all(samplePromises);
4345
- for (const { category, sample } of sampleResults) {
4346
- if (sample) {
4347
- samples[category] = sample;
4348
- }
4349
- }
4350
- return samples;
4351
- }
4352
- /**
4353
- * Processes a single video completely
4361
+ * Process full video (for compatibility)
4354
4362
  */
4355
4363
  async processFullVideo(uri, index, workspaceId, date, shiftId, includeCycleTime, includeMetadata = false) {
4356
4364
  const sopCategories = this.getSOPCategories(workspaceId);
@@ -4359,39 +4367,43 @@ var S3ClipsService = class {
4359
4367
  console.warn(`Skipping URI due to parsing failure: ${uri}`);
4360
4368
  return null;
4361
4369
  }
4362
- let cycleTimeSeconds = null;
4363
- let creationTimestamp = void 0;
4370
+ const video = {
4371
+ id: `${workspaceId}-${date}-${shiftId}-${index}`,
4372
+ src: uri,
4373
+ // Already signed from API
4374
+ ...parsedInfo,
4375
+ originalUri: uri
4376
+ };
4364
4377
  if (includeMetadata) {
4365
4378
  const metadata = await this.getFullMetadata(uri);
4366
4379
  if (metadata) {
4367
- if (metadata.original_task_metadata?.cycle_time) {
4368
- cycleTimeSeconds = metadata.original_task_metadata.cycle_time;
4369
- }
4370
- creationTimestamp = metadata.upload_timestamp || metadata.original_task_metadata?.timestamp || metadata.creation_timestamp || metadata[""];
4380
+ video.cycle_time_seconds = metadata.original_task_metadata?.cycle_time;
4381
+ video.creation_timestamp = metadata.upload_timestamp || metadata.original_task_metadata?.timestamp || metadata.creation_timestamp;
4371
4382
  }
4372
- } else if (includeCycleTime && (parsedInfo.type === "bottleneck" && parsedInfo.description.toLowerCase().includes("cycle time") || parsedInfo.type === "best_cycle_time" || parsedInfo.type === "worst_cycle_time" || parsedInfo.type === "cycle_completion")) {
4373
- cycleTimeSeconds = null;
4374
4383
  }
4375
- const cloudfrontPlaylistUrl = this.s3UriToCloudfront(uri);
4376
- const { type: videoType, timestamp: videoTimestamp } = parsedInfo;
4377
- return {
4378
- id: `${workspaceId}-${date}-${shiftId}-${videoType}-${videoTimestamp.replace(/:/g, "")}-${index}`,
4379
- src: cloudfrontPlaylistUrl,
4380
- // Direct CloudFront playlist URL
4381
- ...parsedInfo,
4382
- cycle_time_seconds: cycleTimeSeconds || void 0,
4383
- creation_timestamp: creationTimestamp
4384
- };
4384
+ return video;
4385
4385
  }
4386
4386
  /**
4387
- * Simplified method to fetch clips based on parameters
4387
+ * Fetch clips (main method for compatibility)
4388
4388
  */
4389
4389
  async fetchClips(params) {
4390
- const { workspaceId, date: inputDate, shift, category, limit, offset = 0, mode, includeCycleTime, includeMetadata, timestampStart, timestampEnd } = params;
4390
+ const {
4391
+ workspaceId,
4392
+ date: inputDate,
4393
+ shift,
4394
+ category,
4395
+ limit,
4396
+ offset = 0,
4397
+ mode
4398
+ } = params;
4391
4399
  if (!workspaceId) {
4392
4400
  throw new Error("Valid Workspace ID is required");
4393
4401
  }
4394
- const date = inputDate || getOperationalDate2(this.config.dateTimeConfig?.defaultTimezone);
4402
+ const date = inputDate || getOperationalDate(
4403
+ this.config.dateTimeConfig?.defaultTimezone || "Asia/Kolkata",
4404
+ /* @__PURE__ */ new Date(),
4405
+ this.config.shiftConfig?.dayShift?.startTime || "06:00"
4406
+ );
4395
4407
  if (!isValidDateFormat(date)) {
4396
4408
  throw new Error("Invalid date format. Use YYYY-MM-DD.");
4397
4409
  }
@@ -4402,160 +4414,72 @@ var S3ClipsService = class {
4402
4414
  }
4403
4415
  shiftId = parseInt(shift, 10);
4404
4416
  } else {
4405
- const { shiftId: currentShiftId } = getCurrentShift2(this.config.dateTimeConfig?.defaultTimezone);
4417
+ const { shiftId: currentShiftId } = getCurrentShift(
4418
+ this.config.dateTimeConfig?.defaultTimezone || "Asia/Kolkata",
4419
+ this.config.shiftConfig
4420
+ );
4406
4421
  shiftId = currentShiftId;
4407
4422
  }
4408
- console.log(`S3ClipsService: Fetching clips for workspace ${workspaceId} on date ${date}, shift ${shiftId}`);
4423
+ console.log(`[S3ClipsService] Fetching clips for workspace ${workspaceId}`);
4409
4424
  if (mode === "summary") {
4410
4425
  const counts = await this.getClipCounts(workspaceId, date, shiftId.toString());
4411
4426
  return { counts, samples: {} };
4412
4427
  }
4413
4428
  const effectiveLimit = limit || this.defaultLimitPerCategory;
4414
- const s3Uris = await this.listS3Clips({
4429
+ const result = await this.getVideosPage(
4415
4430
  workspaceId,
4416
4431
  date,
4417
4432
  shiftId,
4418
- maxKeys: category ? effectiveLimit * 2 : void 0
4419
- // Fetch extra for filtering
4420
- });
4421
- if (s3Uris.length === 0) {
4422
- console.log(`S3ClipsService: No HLS playlists found`);
4423
- return [];
4424
- }
4425
- let filteredUris = s3Uris;
4426
- if (category) {
4427
- filteredUris = s3Uris.filter((uri) => {
4428
- const parsedInfo = parseS3Uri(uri);
4429
- if (!parsedInfo) return false;
4430
- if (category === "long_cycle_time") {
4431
- return parsedInfo.type === "long_cycle_time" || parsedInfo.type === "bottleneck" && parsedInfo.description.toLowerCase().includes("cycle time");
4432
- }
4433
- return parsedInfo.type === category;
4434
- });
4435
- filteredUris = filteredUris.slice(offset, offset + effectiveLimit);
4436
- }
4437
- const videoPromises = filteredUris.map(async (uri, index) => {
4438
- return this.processFullVideo(
4439
- uri,
4440
- index,
4433
+ category || "all",
4434
+ effectiveLimit
4435
+ );
4436
+ return result.videos;
4437
+ }
4438
+ /**
4439
+ * Get videos page using pagination API
4440
+ */
4441
+ async getVideosPage(workspaceId, date, shiftId, category, pageSize = 5, startAfter) {
4442
+ try {
4443
+ return await this.apiClient.getVideosPage(
4441
4444
  workspaceId,
4442
4445
  date,
4443
- shiftId.toString(),
4444
- includeCycleTime || false,
4445
- includeMetadata || false
4446
- // Never fetch metadata for timestamp filtering to prevent flooding
4446
+ shiftId,
4447
+ category,
4448
+ pageSize,
4449
+ startAfter
4447
4450
  );
4448
- });
4449
- const videoResults = await Promise.all(videoPromises);
4450
- let videos = videoResults.filter((v) => v !== null);
4451
- if (timestampStart || timestampEnd) {
4452
- videos = videos.filter((video) => {
4453
- if (!video.creation_timestamp) return false;
4454
- const videoTimestamp = new Date(video.creation_timestamp).getTime();
4455
- if (timestampStart && timestampEnd) {
4456
- const start = new Date(timestampStart).getTime();
4457
- const end = new Date(timestampEnd).getTime();
4458
- return videoTimestamp >= start && videoTimestamp <= end;
4459
- } else if (timestampStart) {
4460
- const start = new Date(timestampStart).getTime();
4461
- return videoTimestamp >= start;
4462
- } else if (timestampEnd) {
4463
- const end = new Date(timestampEnd).getTime();
4464
- return videoTimestamp <= end;
4465
- }
4466
- return true;
4467
- });
4468
- }
4469
- videos.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
4470
- const cacheKey = `videos:${workspaceId}:${date}:${shiftId}:${category || "all"}`;
4471
- if (videos.length > 0) {
4472
- smartVideoCache.setVideos(cacheKey, videos);
4451
+ } catch (error) {
4452
+ console.error("[S3ClipsService] Error fetching videos page:", error);
4453
+ return { videos: [], hasMore: false };
4473
4454
  }
4474
- return videos;
4475
4455
  }
4476
4456
  /**
4477
- * Get a page of videos for a specific category using efficient pagination
4478
- * This method replaces the need for full video indexing
4479
- * @param workspaceId - Workspace ID
4480
- * @param date - Date in YYYY-MM-DD format
4481
- * @param shiftId - Shift ID (0 for day, 1 for night)
4482
- * @param category - Category to fetch videos from
4483
- * @param pageSize - Number of videos to fetch per page (default 5)
4484
- * @param startAfter - Optional key to start after for pagination
4485
- * @returns Page of videos with continuation token
4457
+ * Create empty video index for compatibility
4486
4458
  */
4487
- async getVideosPage(workspaceId, date, shiftId, category, pageSize = 5, startAfter) {
4488
- if (!isValidShiftId(shiftId)) {
4489
- console.error(`[S3ClipsService] getVideosPage - Invalid shift ID: ${shiftId}`);
4490
- return { videos: [], hasMore: false };
4491
- }
4492
- const categoryPrefix = `sop_violations/${workspaceId}/${date}/${shiftId}/${category}/videos/`;
4493
- const deduplicationKey = `videos-page:${categoryPrefix}:${pageSize}:${startAfter || "first"}`;
4494
- return this.requestCache.deduplicate(
4495
- deduplicationKey,
4496
- async () => {
4497
- try {
4498
- console.log(`[S3ClipsService] Fetching page of ${pageSize} videos for category '${category}'`);
4499
- const command = new clientS3.ListObjectsV2Command({
4500
- Bucket: this.config.s3Config.bucketName,
4501
- Prefix: categoryPrefix,
4502
- MaxKeys: pageSize * 10,
4503
- // Fetch extra to account for non-playlist files
4504
- StartAfter: startAfter
4505
- });
4506
- const response = await this.s3Client.send(command);
4507
- const videos = [];
4508
- if (response.Contents) {
4509
- const sopCategories = this.getSOPCategories(workspaceId);
4510
- for (const obj of response.Contents) {
4511
- if (videos.length >= pageSize) break;
4512
- if (obj.Key && obj.Key.endsWith("playlist.m3u8")) {
4513
- if (obj.Key.includes("missed_qchecks")) continue;
4514
- const s3Uri = `s3://${this.config.s3Config.bucketName}/${obj.Key}`;
4515
- const parsedInfo = parseS3Uri(s3Uri, sopCategories);
4516
- if (parsedInfo) {
4517
- const cloudfrontUrl = this.s3UriToCloudfront(s3Uri);
4518
- videos.push({
4519
- id: `${workspaceId}-${date}-${shiftId}-${category}-${videos.length}`,
4520
- src: cloudfrontUrl,
4521
- ...parsedInfo,
4522
- originalUri: s3Uri
4523
- });
4524
- }
4525
- }
4526
- }
4527
- const lastKey = response.Contents[response.Contents.length - 1]?.Key;
4528
- const hasMore = !!response.IsTruncated || response.Contents.length === pageSize * 10;
4529
- console.log(`[S3ClipsService] Fetched ${videos.length} videos, hasMore: ${hasMore}`);
4530
- return {
4531
- videos,
4532
- nextToken: hasMore ? lastKey : void 0,
4533
- hasMore
4534
- };
4535
- }
4536
- return { videos: [], hasMore: false };
4537
- } catch (error) {
4538
- console.error(`[S3ClipsService] Error fetching videos page:`, error);
4539
- return { videos: [], hasMore: false };
4540
- }
4541
- },
4542
- "VideosPage"
4543
- );
4459
+ createEmptyVideoIndex(workspaceId, date, shiftId) {
4460
+ return {
4461
+ byCategory: /* @__PURE__ */ new Map(),
4462
+ allVideos: [],
4463
+ counts: {},
4464
+ workspaceId,
4465
+ date,
4466
+ shiftId,
4467
+ lastUpdated: /* @__PURE__ */ new Date(),
4468
+ _debugId: `empty_${Date.now()}`
4469
+ };
4544
4470
  }
4545
4471
  /**
4546
- * Cleanup method for proper resource management
4472
+ * Cleanup
4547
4473
  */
4548
4474
  dispose() {
4549
- console.log("[S3ClipsService] Disposing service and clearing request cache");
4550
- this.requestCache.clear();
4475
+ console.log("[S3ClipsService] Disposing service");
4476
+ this.apiClient.dispose();
4551
4477
  }
4552
4478
  /**
4553
- * Get service statistics for monitoring
4479
+ * Get statistics
4554
4480
  */
4555
4481
  getStats() {
4556
- return {
4557
- requestCache: this.requestCache.getStats()
4558
- };
4482
+ return this.apiClient.getStats();
4559
4483
  }
4560
4484
  };
4561
4485
 
@@ -8133,7 +8057,7 @@ var useActiveBreaks = (lineIds) => {
8133
8057
  }
8134
8058
  return { elapsedMinutes, remainingMinutes };
8135
8059
  };
8136
- const getCurrentShift3 = (currentMinutes, dayStart, nightStart) => {
8060
+ const getCurrentShift2 = (currentMinutes, dayStart, nightStart) => {
8137
8061
  const dayStartMinutes = parseTimeToMinutes2(dayStart);
8138
8062
  const nightStartMinutes = parseTimeToMinutes2(nightStart);
8139
8063
  if (nightStartMinutes < dayStartMinutes) {
@@ -8165,7 +8089,7 @@ var useActiveBreaks = (lineIds) => {
8165
8089
  const dayShift = dayShifts?.find((s) => s.line_id === lineId);
8166
8090
  const nightShift = nightShifts?.find((s) => s.line_id === lineId);
8167
8091
  if (!dayShift || !nightShift) continue;
8168
- const currentShift = getCurrentShift3(
8092
+ const currentShift = getCurrentShift2(
8169
8093
  currentMinutes,
8170
8094
  dayShift.start_time || "06:00",
8171
8095
  nightShift.start_time || "18:00"
@@ -11662,9 +11586,12 @@ var usePrefetchClipCounts = ({
11662
11586
  shiftStr = "0";
11663
11587
  console.log(`[usePrefetchClipCounts] No shift provided for historical date ${date}, defaulting to day shift (0)`);
11664
11588
  } else {
11665
- const currentShift = getCurrentShift2();
11589
+ const currentShift = getCurrentShift(
11590
+ dashboardConfig.dateTimeConfig?.defaultTimezone || "Asia/Kolkata",
11591
+ dashboardConfig.shiftConfig
11592
+ );
11666
11593
  shiftStr = currentShift.shiftId.toString();
11667
- console.log(`[usePrefetchClipCounts] Using current operational shift: ${shiftStr} (${currentShift.shiftName})`);
11594
+ console.log(`[usePrefetchClipCounts] Using current operational shift: ${shiftStr}`);
11668
11595
  }
11669
11596
  return {
11670
11597
  workspaceId: workspaceId || "",
@@ -12144,6 +12071,140 @@ function useWorkspaceNavigation() {
12144
12071
  getWorkspaceNavigationParams: getWorkspaceNavigationParams2
12145
12072
  };
12146
12073
  }
12074
+ function useWorkspaceHealth(options = {}) {
12075
+ const [workspaces, setWorkspaces] = React19.useState([]);
12076
+ const [summary, setSummary] = React19.useState(null);
12077
+ const [loading, setLoading] = React19.useState(true);
12078
+ const [error, setError] = React19.useState(null);
12079
+ const unsubscribeRef = React19.useRef(null);
12080
+ const refreshIntervalRef = React19.useRef(null);
12081
+ const {
12082
+ enableRealtime = true,
12083
+ refreshInterval = 3e4,
12084
+ // 30 seconds default
12085
+ ...filterOptions
12086
+ } = options;
12087
+ const fetchData = React19.useCallback(async () => {
12088
+ try {
12089
+ setError(null);
12090
+ workspaceHealthService.clearCache();
12091
+ const [workspacesData, summaryData] = await Promise.all([
12092
+ workspaceHealthService.getWorkspaceHealthStatus(filterOptions),
12093
+ workspaceHealthService.getHealthSummary(filterOptions.lineId, filterOptions.companyId)
12094
+ ]);
12095
+ setWorkspaces(workspacesData);
12096
+ setSummary(summaryData);
12097
+ } catch (err) {
12098
+ console.error("Error fetching workspace health:", err);
12099
+ setError(err);
12100
+ } finally {
12101
+ setLoading(false);
12102
+ }
12103
+ }, [filterOptions.lineId, filterOptions.companyId, filterOptions.status, filterOptions.searchTerm, filterOptions.sortBy, filterOptions.sortOrder]);
12104
+ const handleRealtimeUpdate = React19.useCallback(async (data) => {
12105
+ try {
12106
+ const [workspacesData, summaryData] = await Promise.all([
12107
+ workspaceHealthService.getWorkspaceHealthStatus(filterOptions),
12108
+ workspaceHealthService.getHealthSummary(filterOptions.lineId, filterOptions.companyId)
12109
+ ]);
12110
+ setWorkspaces(workspacesData);
12111
+ setSummary(summaryData);
12112
+ } catch (err) {
12113
+ console.error("Error updating real-time health data:", err);
12114
+ }
12115
+ }, [filterOptions]);
12116
+ React19.useEffect(() => {
12117
+ fetchData();
12118
+ if (refreshInterval > 0) {
12119
+ refreshIntervalRef.current = setInterval(fetchData, 1e4);
12120
+ }
12121
+ if (enableRealtime) {
12122
+ unsubscribeRef.current = workspaceHealthService.subscribeToHealthUpdates(
12123
+ handleRealtimeUpdate,
12124
+ { lineId: filterOptions.lineId, companyId: filterOptions.companyId }
12125
+ );
12126
+ }
12127
+ return () => {
12128
+ if (refreshIntervalRef.current) {
12129
+ clearInterval(refreshIntervalRef.current);
12130
+ }
12131
+ if (unsubscribeRef.current) {
12132
+ unsubscribeRef.current();
12133
+ }
12134
+ };
12135
+ }, [fetchData, enableRealtime, refreshInterval, handleRealtimeUpdate, filterOptions.lineId, filterOptions.companyId]);
12136
+ return {
12137
+ workspaces,
12138
+ summary,
12139
+ loading,
12140
+ error,
12141
+ refetch: fetchData
12142
+ };
12143
+ }
12144
+ function useWorkspaceHealthById(workspaceId, options = {}) {
12145
+ const [workspace, setWorkspace] = React19.useState(null);
12146
+ const [metrics2, setMetrics] = React19.useState(null);
12147
+ const [loading, setLoading] = React19.useState(true);
12148
+ const [error, setError] = React19.useState(null);
12149
+ const unsubscribeRef = React19.useRef(null);
12150
+ const refreshIntervalRef = React19.useRef(null);
12151
+ const { enableRealtime = true, refreshInterval = 3e4 } = options;
12152
+ const fetchData = React19.useCallback(async () => {
12153
+ if (!workspaceId) {
12154
+ setWorkspace(null);
12155
+ setMetrics(null);
12156
+ setLoading(false);
12157
+ return;
12158
+ }
12159
+ try {
12160
+ setLoading(true);
12161
+ setError(null);
12162
+ const [workspaceData, metricsData] = await Promise.all([
12163
+ workspaceHealthService.getWorkspaceHealthById(workspaceId),
12164
+ workspaceHealthService.getHealthMetrics(workspaceId)
12165
+ ]);
12166
+ setWorkspace(workspaceData);
12167
+ setMetrics(metricsData);
12168
+ } catch (err) {
12169
+ console.error("Error fetching workspace health by ID:", err);
12170
+ setError(err);
12171
+ } finally {
12172
+ setLoading(false);
12173
+ }
12174
+ }, [workspaceId]);
12175
+ const handleRealtimeUpdate = React19.useCallback((data) => {
12176
+ if (data.workspace_id === workspaceId) {
12177
+ const updatedWorkspace = workspaceHealthService["processHealthStatus"](data);
12178
+ setWorkspace(updatedWorkspace);
12179
+ }
12180
+ }, [workspaceId]);
12181
+ React19.useEffect(() => {
12182
+ fetchData();
12183
+ if (refreshInterval > 0) {
12184
+ refreshIntervalRef.current = setInterval(fetchData, 1e4);
12185
+ }
12186
+ if (enableRealtime && workspaceId) {
12187
+ unsubscribeRef.current = workspaceHealthService.subscribeToHealthUpdates(
12188
+ handleRealtimeUpdate
12189
+ );
12190
+ }
12191
+ return () => {
12192
+ if (refreshIntervalRef.current) {
12193
+ clearInterval(refreshIntervalRef.current);
12194
+ }
12195
+ if (unsubscribeRef.current) {
12196
+ unsubscribeRef.current();
12197
+ }
12198
+ };
12199
+ }, [fetchData, enableRealtime, refreshInterval, handleRealtimeUpdate, workspaceId]);
12200
+ return {
12201
+ workspace,
12202
+ metrics: metrics2,
12203
+ loading,
12204
+ error,
12205
+ refetch: fetchData
12206
+ };
12207
+ }
12147
12208
  function useDateFormatter() {
12148
12209
  const { defaultTimezone, defaultLocale, dateFormatOptions, timeFormatOptions, dateTimeFormatOptions } = useDateTimeConfig();
12149
12210
  const formatDate = React19.useCallback(
@@ -23552,6 +23613,165 @@ var TicketHistory = ({ companyId }) => {
23552
23613
  ] });
23553
23614
  };
23554
23615
  var TicketHistory_default = TicketHistory;
23616
+ var HealthStatusIndicator = ({
23617
+ status,
23618
+ lastUpdated,
23619
+ showLabel = false,
23620
+ showTime = true,
23621
+ size = "md",
23622
+ className = "",
23623
+ inline = true,
23624
+ pulse = true
23625
+ }) => {
23626
+ const getStatusConfig = () => {
23627
+ switch (status) {
23628
+ case "healthy":
23629
+ return {
23630
+ color: "text-green-500",
23631
+ bgColor: "bg-green-500",
23632
+ borderColor: "border-green-500",
23633
+ label: "Healthy",
23634
+ icon: lucideReact.CheckCircle,
23635
+ shouldPulse: pulse
23636
+ };
23637
+ case "unhealthy":
23638
+ return {
23639
+ color: "text-red-500",
23640
+ bgColor: "bg-red-500",
23641
+ borderColor: "border-red-500",
23642
+ label: "Unhealthy",
23643
+ icon: lucideReact.XCircle,
23644
+ shouldPulse: false
23645
+ };
23646
+ case "warning":
23647
+ return {
23648
+ color: "text-yellow-500",
23649
+ bgColor: "bg-yellow-500",
23650
+ borderColor: "border-yellow-500",
23651
+ label: "Warning",
23652
+ icon: lucideReact.AlertTriangle,
23653
+ shouldPulse: true
23654
+ };
23655
+ case "unknown":
23656
+ default:
23657
+ return {
23658
+ color: "text-gray-400",
23659
+ bgColor: "bg-gray-400",
23660
+ borderColor: "border-gray-400",
23661
+ label: "Unknown",
23662
+ icon: lucideReact.Activity,
23663
+ shouldPulse: false
23664
+ };
23665
+ }
23666
+ };
23667
+ const config = getStatusConfig();
23668
+ config.icon;
23669
+ const sizeClasses = {
23670
+ sm: {
23671
+ dot: "h-2 w-2",
23672
+ icon: "h-3 w-3",
23673
+ text: "text-xs",
23674
+ spacing: "gap-1"
23675
+ },
23676
+ md: {
23677
+ dot: "h-3 w-3",
23678
+ icon: "h-4 w-4",
23679
+ text: "text-sm",
23680
+ spacing: "gap-1.5"
23681
+ },
23682
+ lg: {
23683
+ dot: "h-4 w-4",
23684
+ icon: "h-5 w-5",
23685
+ text: "text-base",
23686
+ spacing: "gap-2"
23687
+ }
23688
+ };
23689
+ const currentSize = sizeClasses[size];
23690
+ const containerClasses = clsx(
23691
+ "flex items-center",
23692
+ currentSize.spacing,
23693
+ inline ? "inline-flex" : "flex",
23694
+ className
23695
+ );
23696
+ const dotClasses = clsx(
23697
+ "rounded-full",
23698
+ currentSize.dot,
23699
+ config.bgColor,
23700
+ config.shouldPulse && "animate-pulse"
23701
+ );
23702
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: containerClasses, children: [
23703
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex items-center justify-center", children: [
23704
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: dotClasses }),
23705
+ config.shouldPulse && status === "healthy" && /* @__PURE__ */ jsxRuntime.jsx(
23706
+ "div",
23707
+ {
23708
+ className: clsx(
23709
+ "absolute rounded-full opacity-25",
23710
+ currentSize.dot,
23711
+ config.bgColor,
23712
+ "animate-ping"
23713
+ )
23714
+ }
23715
+ )
23716
+ ] }),
23717
+ showLabel && /* @__PURE__ */ jsxRuntime.jsx("span", { className: clsx(currentSize.text, "font-medium", config.color), children: config.label }),
23718
+ showTime && lastUpdated && /* @__PURE__ */ jsxRuntime.jsx("span", { className: clsx(currentSize.text, "text-gray-500 dark:text-gray-400"), children: lastUpdated })
23719
+ ] });
23720
+ };
23721
+ var DetailedHealthStatus = ({
23722
+ workspaceName,
23723
+ lineName,
23724
+ consecutiveMisses,
23725
+ showDetails = true,
23726
+ ...indicatorProps
23727
+ }) => {
23728
+ const getStatusConfig = () => {
23729
+ switch (indicatorProps.status) {
23730
+ case "healthy":
23731
+ return {
23732
+ bgClass: "bg-green-50 dark:bg-green-900/20",
23733
+ borderClass: "border-green-200 dark:border-green-800"
23734
+ };
23735
+ case "unhealthy":
23736
+ return {
23737
+ bgClass: "bg-red-50 dark:bg-red-900/20",
23738
+ borderClass: "border-red-200 dark:border-red-800"
23739
+ };
23740
+ case "warning":
23741
+ return {
23742
+ bgClass: "bg-yellow-50 dark:bg-yellow-900/20",
23743
+ borderClass: "border-yellow-200 dark:border-yellow-800"
23744
+ };
23745
+ default:
23746
+ return {
23747
+ bgClass: "bg-gray-50 dark:bg-gray-900/20",
23748
+ borderClass: "border-gray-200 dark:border-gray-800"
23749
+ };
23750
+ }
23751
+ };
23752
+ const config = getStatusConfig();
23753
+ return /* @__PURE__ */ jsxRuntime.jsx(
23754
+ "div",
23755
+ {
23756
+ className: clsx(
23757
+ "rounded-lg border p-3",
23758
+ config.bgClass,
23759
+ config.borderClass
23760
+ ),
23761
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between", children: [
23762
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1", children: [
23763
+ showDetails && workspaceName && /* @__PURE__ */ jsxRuntime.jsx("h4", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: workspaceName }),
23764
+ showDetails && lineName && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-0.5", children: lineName }),
23765
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2", children: /* @__PURE__ */ jsxRuntime.jsx(HealthStatusIndicator, { ...indicatorProps, showLabel: true }) })
23766
+ ] }),
23767
+ showDetails && consecutiveMisses !== void 0 && consecutiveMisses > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ml-3 text-right", children: [
23768
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: "Missed" }),
23769
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-bold text-gray-900 dark:text-gray-100", children: consecutiveMisses })
23770
+ ] })
23771
+ ] })
23772
+ }
23773
+ );
23774
+ };
23555
23775
  var LinePdfExportButton = ({
23556
23776
  targetElement,
23557
23777
  fileName = "line-export",
@@ -24674,6 +24894,8 @@ var WorkspaceCard = ({
24674
24894
  cycleTime,
24675
24895
  operators,
24676
24896
  status = "normal",
24897
+ healthStatus,
24898
+ healthLastUpdated,
24677
24899
  onCardClick,
24678
24900
  headerActions,
24679
24901
  footerContent,
@@ -24766,6 +24988,19 @@ var WorkspaceCard = ({
24766
24988
  ] })
24767
24989
  ] })
24768
24990
  ] }),
24991
+ healthStatus && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pt-2 mt-auto border-t dark:border-gray-700", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
24992
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-500 dark:text-gray-400", children: "System Health" }),
24993
+ /* @__PURE__ */ jsxRuntime.jsx(
24994
+ HealthStatusIndicator,
24995
+ {
24996
+ status: healthStatus,
24997
+ lastUpdated: healthLastUpdated,
24998
+ showTime: false,
24999
+ size: "sm",
25000
+ pulse: healthStatus === "healthy"
25001
+ }
25002
+ )
25003
+ ] }) }),
24769
25004
  chartData && chartData.data && chartData.data.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
24770
25005
  "div",
24771
25006
  {
@@ -26424,6 +26659,194 @@ var BackButtonMinimal = ({
26424
26659
  }
26425
26660
  );
26426
26661
  };
26662
+ var InlineEditableText = ({
26663
+ value,
26664
+ onSave,
26665
+ placeholder = "Click to edit",
26666
+ className = "",
26667
+ editIconClassName = "",
26668
+ inputClassName = "",
26669
+ debounceDelay = 750,
26670
+ // 750ms for quick auto-save
26671
+ disabled = false
26672
+ }) => {
26673
+ const [isEditing, setIsEditing] = React19.useState(false);
26674
+ const [editValue, setEditValue] = React19.useState(value);
26675
+ const [saveStatus, setSaveStatus] = React19.useState("idle");
26676
+ const [lastSavedValue, setLastSavedValue] = React19.useState(value);
26677
+ const [hasUnsavedChanges, setHasUnsavedChanges] = React19.useState(false);
26678
+ const inputRef = React19.useRef(null);
26679
+ const containerRef = React19.useRef(null);
26680
+ const debounceTimeout = React19.useRef(void 0);
26681
+ const saveStatusTimeout = React19.useRef(void 0);
26682
+ React19.useEffect(() => {
26683
+ if (!isEditing) {
26684
+ setEditValue(value);
26685
+ setLastSavedValue(value);
26686
+ }
26687
+ }, [value, isEditing]);
26688
+ React19.useEffect(() => {
26689
+ if (isEditing && inputRef.current) {
26690
+ requestAnimationFrame(() => {
26691
+ inputRef.current?.focus();
26692
+ inputRef.current?.select();
26693
+ });
26694
+ }
26695
+ }, [isEditing]);
26696
+ React19.useEffect(() => {
26697
+ return () => {
26698
+ if (debounceTimeout.current) clearTimeout(debounceTimeout.current);
26699
+ if (saveStatusTimeout.current) clearTimeout(saveStatusTimeout.current);
26700
+ };
26701
+ }, []);
26702
+ const performSave = React19.useCallback(async (valueToSave, shouldClose = false) => {
26703
+ const trimmedValue = valueToSave.trim();
26704
+ if (trimmedValue === lastSavedValue.trim()) {
26705
+ setHasUnsavedChanges(false);
26706
+ if (shouldClose) {
26707
+ setIsEditing(false);
26708
+ setSaveStatus("idle");
26709
+ }
26710
+ return;
26711
+ }
26712
+ setSaveStatus("saving");
26713
+ setHasUnsavedChanges(false);
26714
+ try {
26715
+ await onSave(trimmedValue);
26716
+ setLastSavedValue(trimmedValue);
26717
+ setSaveStatus("saved");
26718
+ if (!shouldClose && inputRef.current) {
26719
+ requestAnimationFrame(() => {
26720
+ inputRef.current?.focus();
26721
+ });
26722
+ }
26723
+ if (saveStatusTimeout.current) clearTimeout(saveStatusTimeout.current);
26724
+ saveStatusTimeout.current = setTimeout(() => {
26725
+ setSaveStatus("idle");
26726
+ }, 2e3);
26727
+ if (shouldClose) {
26728
+ setIsEditing(false);
26729
+ }
26730
+ } catch (error) {
26731
+ console.error("Failed to save:", error);
26732
+ setSaveStatus("error");
26733
+ setHasUnsavedChanges(true);
26734
+ if (saveStatusTimeout.current) clearTimeout(saveStatusTimeout.current);
26735
+ saveStatusTimeout.current = setTimeout(() => {
26736
+ setSaveStatus("idle");
26737
+ }, 3e3);
26738
+ if (!shouldClose && inputRef.current) {
26739
+ requestAnimationFrame(() => {
26740
+ inputRef.current?.focus();
26741
+ });
26742
+ }
26743
+ }
26744
+ }, [lastSavedValue, onSave]);
26745
+ React19.useEffect(() => {
26746
+ const handleClickOutside = (event) => {
26747
+ if (isEditing && containerRef.current && !containerRef.current.contains(event.target)) {
26748
+ if (debounceTimeout.current) {
26749
+ clearTimeout(debounceTimeout.current);
26750
+ }
26751
+ performSave(editValue, true);
26752
+ }
26753
+ };
26754
+ if (isEditing) {
26755
+ document.addEventListener("mousedown", handleClickOutside);
26756
+ return () => document.removeEventListener("mousedown", handleClickOutside);
26757
+ }
26758
+ }, [isEditing, editValue, performSave]);
26759
+ const handleInputChange = (e) => {
26760
+ const newValue = e.target.value;
26761
+ setEditValue(newValue);
26762
+ if (newValue.trim() !== lastSavedValue.trim()) {
26763
+ setHasUnsavedChanges(true);
26764
+ setSaveStatus("idle");
26765
+ } else {
26766
+ setHasUnsavedChanges(false);
26767
+ }
26768
+ if (debounceTimeout.current) {
26769
+ clearTimeout(debounceTimeout.current);
26770
+ }
26771
+ if (newValue.trim() !== lastSavedValue.trim()) {
26772
+ debounceTimeout.current = setTimeout(() => {
26773
+ performSave(newValue, false);
26774
+ }, debounceDelay);
26775
+ }
26776
+ };
26777
+ const handleKeyDown = (e) => {
26778
+ if (e.key === "Enter") {
26779
+ e.preventDefault();
26780
+ if (debounceTimeout.current) {
26781
+ clearTimeout(debounceTimeout.current);
26782
+ }
26783
+ performSave(editValue, true);
26784
+ } else if (e.key === "Escape") {
26785
+ e.preventDefault();
26786
+ if (debounceTimeout.current) {
26787
+ clearTimeout(debounceTimeout.current);
26788
+ }
26789
+ setEditValue(lastSavedValue);
26790
+ setHasUnsavedChanges(false);
26791
+ setSaveStatus("idle");
26792
+ setIsEditing(false);
26793
+ }
26794
+ };
26795
+ const handleClick = () => {
26796
+ if (!disabled && !isEditing) {
26797
+ setIsEditing(true);
26798
+ setSaveStatus("idle");
26799
+ }
26800
+ };
26801
+ if (isEditing) {
26802
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, className: "inline-flex items-center gap-1.5", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
26803
+ /* @__PURE__ */ jsxRuntime.jsx(
26804
+ "input",
26805
+ {
26806
+ ref: inputRef,
26807
+ type: "text",
26808
+ value: editValue,
26809
+ onChange: handleInputChange,
26810
+ onKeyDown: handleKeyDown,
26811
+ className: `px-2 py-1 pr-7 text-sm border rounded-md transition-colors duration-200
26812
+ ${saveStatus === "error" ? "border-red-400 focus:ring-red-500 focus:border-red-500" : hasUnsavedChanges ? "border-yellow-400 focus:ring-yellow-500 focus:border-yellow-500" : saveStatus === "saved" ? "border-green-400 focus:ring-green-500 focus:border-green-500" : "border-blue-400 focus:ring-blue-500 focus:border-blue-500"}
26813
+ focus:outline-none focus:ring-2 bg-white
26814
+ ${inputClassName}`,
26815
+ placeholder,
26816
+ autoComplete: "off"
26817
+ }
26818
+ ),
26819
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute right-1.5 top-1/2 -translate-y-1/2", children: [
26820
+ saveStatus === "saving" && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "w-3.5 h-3.5 text-blue-500 animate-spin" }),
26821
+ saveStatus === "saved" && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "w-3.5 h-3.5 text-green-500" }),
26822
+ saveStatus === "error" && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircle, { className: "w-3.5 h-3.5 text-red-500" }),
26823
+ saveStatus === "idle" && hasUnsavedChanges && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1.5 h-1.5 bg-yellow-400 rounded-full" })
26824
+ ] })
26825
+ ] }) });
26826
+ }
26827
+ return /* @__PURE__ */ jsxRuntime.jsxs(
26828
+ "div",
26829
+ {
26830
+ onClick: handleClick,
26831
+ className: `inline-flex items-center gap-1.5 cursor-pointer px-2 py-1 rounded-md
26832
+ transition-all duration-200 hover:bg-gray-50 group
26833
+ ${disabled ? "cursor-not-allowed opacity-50" : ""}
26834
+ ${className}`,
26835
+ children: [
26836
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-900", children: editValue || placeholder }),
26837
+ /* @__PURE__ */ jsxRuntime.jsx(
26838
+ lucideReact.Edit2,
26839
+ {
26840
+ className: `w-3.5 h-3.5 text-gray-400 transition-opacity duration-200
26841
+ opacity-0 group-hover:opacity-100
26842
+ ${disabled ? "hidden" : ""}
26843
+ ${editIconClassName}`
26844
+ }
26845
+ )
26846
+ ]
26847
+ }
26848
+ );
26849
+ };
26427
26850
  var BottlenecksContent = ({
26428
26851
  workspaceId,
26429
26852
  workspaceName,
@@ -26505,11 +26928,14 @@ var BottlenecksContent = ({
26505
26928
  console.log(`[BottlenecksContent] No shift provided for historical date ${date}, defaulting to day shift (0)`);
26506
26929
  return "0";
26507
26930
  } else {
26508
- const currentShift = getCurrentShift2();
26509
- console.log(`[BottlenecksContent] Using current operational shift: ${currentShift.shiftId} (${currentShift.shiftName})`);
26931
+ const currentShift = getCurrentShift(
26932
+ dashboardConfig.dateTimeConfig?.defaultTimezone || "Asia/Kolkata",
26933
+ dashboardConfig.shiftConfig
26934
+ );
26935
+ console.log(`[BottlenecksContent] Using current operational shift: ${currentShift.shiftId}`);
26510
26936
  return currentShift.shiftId.toString();
26511
26937
  }
26512
- }, [shift, date]);
26938
+ }, [shift, date, dashboardConfig]);
26513
26939
  const {
26514
26940
  data: prefetchData,
26515
26941
  isFullyIndexed,
@@ -28169,6 +28595,388 @@ var KPISection = React19.memo(({
28169
28595
  return true;
28170
28596
  });
28171
28597
  KPISection.displayName = "KPISection";
28598
+ var WorkspaceHealthCard = ({
28599
+ workspace,
28600
+ onClick,
28601
+ showDetails = true,
28602
+ className = ""
28603
+ }) => {
28604
+ const getStatusConfig = () => {
28605
+ switch (workspace.status) {
28606
+ case "healthy":
28607
+ return {
28608
+ gradient: "from-emerald-50 to-green-50 dark:from-emerald-950/30 dark:to-green-950/30",
28609
+ border: "border-emerald-200 dark:border-emerald-800",
28610
+ icon: lucideReact.CheckCircle2,
28611
+ iconColor: "text-emerald-600 dark:text-emerald-400",
28612
+ badge: "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-300",
28613
+ statusText: "Online",
28614
+ pulse: true
28615
+ };
28616
+ case "unhealthy":
28617
+ return {
28618
+ gradient: "from-rose-50 to-red-50 dark:from-rose-950/30 dark:to-red-950/30",
28619
+ border: "border-rose-200 dark:border-rose-800",
28620
+ icon: lucideReact.XCircle,
28621
+ iconColor: "text-rose-600 dark:text-rose-400",
28622
+ badge: "bg-rose-100 text-rose-700 dark:bg-rose-900/50 dark:text-rose-300",
28623
+ statusText: "Offline",
28624
+ pulse: false
28625
+ };
28626
+ case "warning":
28627
+ return {
28628
+ gradient: "from-amber-50 to-yellow-50 dark:from-amber-950/30 dark:to-yellow-950/30",
28629
+ border: "border-amber-200 dark:border-amber-800",
28630
+ icon: lucideReact.AlertTriangle,
28631
+ iconColor: "text-amber-600 dark:text-amber-400",
28632
+ badge: "bg-amber-100 text-amber-700 dark:bg-amber-900/50 dark:text-amber-300",
28633
+ statusText: "Degraded",
28634
+ pulse: true
28635
+ };
28636
+ default:
28637
+ return {
28638
+ gradient: "from-gray-50 to-slate-50 dark:from-gray-950/30 dark:to-slate-950/30",
28639
+ border: "border-gray-200 dark:border-gray-800",
28640
+ icon: lucideReact.Activity,
28641
+ iconColor: "text-gray-500 dark:text-gray-400",
28642
+ badge: "bg-gray-100 text-gray-700 dark:bg-gray-900/50 dark:text-gray-300",
28643
+ statusText: "Unknown",
28644
+ pulse: false
28645
+ };
28646
+ }
28647
+ };
28648
+ const config = getStatusConfig();
28649
+ const StatusIcon = config.icon;
28650
+ const handleClick = () => {
28651
+ if (onClick) {
28652
+ onClick(workspace);
28653
+ }
28654
+ };
28655
+ const handleKeyDown = (event) => {
28656
+ if (onClick && (event.key === "Enter" || event.key === " ")) {
28657
+ event.preventDefault();
28658
+ onClick(workspace);
28659
+ }
28660
+ };
28661
+ const formatTimeAgo = (timeString) => {
28662
+ return timeString.replace("about ", "").replace(" ago", "");
28663
+ };
28664
+ return /* @__PURE__ */ jsxRuntime.jsx(
28665
+ Card2,
28666
+ {
28667
+ className: clsx(
28668
+ "relative overflow-hidden transition-all duration-300",
28669
+ "bg-gradient-to-br",
28670
+ config.gradient,
28671
+ "border",
28672
+ config.border,
28673
+ "shadow-sm hover:shadow-md",
28674
+ onClick && "cursor-pointer hover:scale-[1.01]",
28675
+ workspace.isStale && "opacity-90",
28676
+ className
28677
+ ),
28678
+ onClick: handleClick,
28679
+ onKeyDown: handleKeyDown,
28680
+ tabIndex: onClick ? 0 : void 0,
28681
+ role: onClick ? "button" : void 0,
28682
+ "aria-label": `Workspace ${workspace.workspace_display_name} status: ${workspace.status}`,
28683
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4", children: [
28684
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between mb-3", children: [
28685
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1", children: [
28686
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-semibold text-gray-900 dark:text-gray-100 mb-1", children: workspace.workspace_display_name || `Workspace ${workspace.workspace_id.slice(0, 8)}` }),
28687
+ showDetails && workspace.line_name && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-600 dark:text-gray-400", children: workspace.line_name })
28688
+ ] }),
28689
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: clsx(
28690
+ "flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium",
28691
+ config.badge
28692
+ ), children: [
28693
+ /* @__PURE__ */ jsxRuntime.jsx(StatusIcon, { className: "h-3.5 w-3.5" }),
28694
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: config.statusText })
28695
+ ] })
28696
+ ] }),
28697
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-between", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-1", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
28698
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Clock, { className: "h-3.5 w-3.5 text-gray-400" }),
28699
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm text-gray-600 dark:text-gray-400 whitespace-nowrap", children: [
28700
+ "Last seen: ",
28701
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: formatTimeAgo(workspace.timeSinceLastUpdate) })
28702
+ ] })
28703
+ ] }) }) })
28704
+ ] })
28705
+ }
28706
+ );
28707
+ };
28708
+ var CompactWorkspaceHealthCard = ({
28709
+ workspace,
28710
+ onClick,
28711
+ className = ""
28712
+ }) => {
28713
+ const getStatusConfig = () => {
28714
+ switch (workspace.status) {
28715
+ case "healthy":
28716
+ return {
28717
+ dot: "bg-emerald-500",
28718
+ icon: lucideReact.CheckCircle2,
28719
+ iconColor: "text-emerald-600 dark:text-emerald-400",
28720
+ bg: "hover:bg-emerald-50 dark:hover:bg-emerald-950/20"
28721
+ };
28722
+ case "unhealthy":
28723
+ return {
28724
+ dot: "bg-rose-500",
28725
+ icon: lucideReact.XCircle,
28726
+ iconColor: "text-rose-600 dark:text-rose-400",
28727
+ bg: "hover:bg-rose-50 dark:hover:bg-rose-950/20"
28728
+ };
28729
+ case "warning":
28730
+ return {
28731
+ dot: "bg-amber-500",
28732
+ icon: lucideReact.AlertTriangle,
28733
+ iconColor: "text-amber-600 dark:text-amber-400",
28734
+ bg: "hover:bg-amber-50 dark:hover:bg-amber-950/20"
28735
+ };
28736
+ default:
28737
+ return {
28738
+ dot: "bg-gray-400",
28739
+ icon: lucideReact.Activity,
28740
+ iconColor: "text-gray-500 dark:text-gray-400",
28741
+ bg: "hover:bg-gray-50 dark:hover:bg-gray-950/20"
28742
+ };
28743
+ }
28744
+ };
28745
+ const config = getStatusConfig();
28746
+ const StatusIcon = config.icon;
28747
+ const handleClick = () => {
28748
+ if (onClick) {
28749
+ onClick(workspace);
28750
+ }
28751
+ };
28752
+ return /* @__PURE__ */ jsxRuntime.jsxs(
28753
+ "div",
28754
+ {
28755
+ className: clsx(
28756
+ "flex items-center justify-between px-4 py-3 rounded-lg border",
28757
+ "bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700",
28758
+ "transition-all duration-200",
28759
+ onClick && `cursor-pointer ${config.bg}`,
28760
+ className
28761
+ ),
28762
+ onClick: handleClick,
28763
+ role: onClick ? "button" : void 0,
28764
+ tabIndex: onClick ? 0 : void 0,
28765
+ children: [
28766
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
28767
+ /* @__PURE__ */ jsxRuntime.jsx(StatusIcon, { className: clsx("h-5 w-5", config.iconColor) }),
28768
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
28769
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium text-gray-900 dark:text-gray-100", children: workspace.workspace_display_name || `WS-${workspace.workspace_id.slice(0, 6)}` }),
28770
+ workspace.line_name && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: workspace.line_name })
28771
+ ] })
28772
+ ] }),
28773
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
28774
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: workspace.timeSinceLastUpdate }),
28775
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: clsx("h-2 w-2 rounded-full", config.dot) })
28776
+ ] })
28777
+ ]
28778
+ }
28779
+ );
28780
+ };
28781
+ var HealthStatusGrid = ({
28782
+ workspaces,
28783
+ onWorkspaceClick,
28784
+ showFilters = true,
28785
+ groupBy: initialGroupBy = "none",
28786
+ className = ""
28787
+ }) => {
28788
+ const [searchTerm, setSearchTerm] = React19.useState("");
28789
+ const [statusFilter, setStatusFilter] = React19.useState("all");
28790
+ const [groupBy, setGroupBy] = React19.useState(initialGroupBy);
28791
+ const [expandedGroups, setExpandedGroups] = React19.useState(/* @__PURE__ */ new Set());
28792
+ const lastGroupByRef = React19.useRef(initialGroupBy);
28793
+ const hasInitializedGroupsRef = React19.useRef(false);
28794
+ const filteredWorkspaces = React19.useMemo(() => {
28795
+ let filtered = [...workspaces];
28796
+ if (searchTerm) {
28797
+ const search = searchTerm.toLowerCase();
28798
+ filtered = filtered.filter(
28799
+ (w) => w.workspace_display_name?.toLowerCase().includes(search) || w.line_name?.toLowerCase().includes(search) || w.company_name?.toLowerCase().includes(search)
28800
+ );
28801
+ }
28802
+ if (statusFilter !== "all") {
28803
+ filtered = filtered.filter((w) => w.status === statusFilter);
28804
+ }
28805
+ return filtered;
28806
+ }, [workspaces, searchTerm, statusFilter]);
28807
+ const groupedWorkspaces = React19.useMemo(() => {
28808
+ if (groupBy === "none") {
28809
+ return { "All Workspaces": filteredWorkspaces };
28810
+ }
28811
+ const groups = {};
28812
+ filteredWorkspaces.forEach((workspace) => {
28813
+ let key = "Unknown";
28814
+ switch (groupBy) {
28815
+ case "line":
28816
+ key = workspace.line_name || "Unknown Line";
28817
+ break;
28818
+ case "status":
28819
+ key = workspace.status;
28820
+ break;
28821
+ }
28822
+ if (!groups[key]) {
28823
+ groups[key] = [];
28824
+ }
28825
+ groups[key].push(workspace);
28826
+ });
28827
+ const sortedGroups = {};
28828
+ Object.keys(groups).sort().forEach((key) => {
28829
+ sortedGroups[key] = groups[key];
28830
+ });
28831
+ return sortedGroups;
28832
+ }, [filteredWorkspaces, groupBy]);
28833
+ React19.useEffect(() => {
28834
+ if (groupBy !== lastGroupByRef.current) {
28835
+ lastGroupByRef.current = groupBy;
28836
+ hasInitializedGroupsRef.current = false;
28837
+ if (groupBy === "none") {
28838
+ setExpandedGroups(/* @__PURE__ */ new Set());
28839
+ }
28840
+ }
28841
+ }, [groupBy]);
28842
+ React19.useEffect(() => {
28843
+ if (groupBy !== "none" && !hasInitializedGroupsRef.current && Object.keys(groupedWorkspaces).length > 0) {
28844
+ hasInitializedGroupsRef.current = true;
28845
+ setExpandedGroups(new Set(Object.keys(groupedWorkspaces)));
28846
+ }
28847
+ }, [groupBy, groupedWorkspaces]);
28848
+ const toggleGroup = (groupName) => {
28849
+ const newExpanded = new Set(expandedGroups);
28850
+ if (newExpanded.has(groupName)) {
28851
+ newExpanded.delete(groupName);
28852
+ } else {
28853
+ newExpanded.add(groupName);
28854
+ }
28855
+ setExpandedGroups(newExpanded);
28856
+ };
28857
+ const getStatusCounts = () => {
28858
+ const counts = {
28859
+ healthy: 0,
28860
+ unhealthy: 0,
28861
+ warning: 0,
28862
+ unknown: 0
28863
+ };
28864
+ workspaces.forEach((w) => {
28865
+ counts[w.status]++;
28866
+ });
28867
+ return counts;
28868
+ };
28869
+ const statusCounts = getStatusCounts();
28870
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: clsx("space-y-4", className), children: [
28871
+ showFilters && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4", children: [
28872
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col sm:flex-row gap-3 flex-wrap", children: [
28873
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 min-w-[200px]", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
28874
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" }),
28875
+ /* @__PURE__ */ jsxRuntime.jsx(
28876
+ "input",
28877
+ {
28878
+ type: "text",
28879
+ placeholder: "Search workspaces...",
28880
+ value: searchTerm,
28881
+ onChange: (e) => setSearchTerm(e.target.value),
28882
+ className: "w-full pl-10 pr-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100"
28883
+ }
28884
+ )
28885
+ ] }) }),
28886
+ /* @__PURE__ */ jsxRuntime.jsxs(Select, { value: statusFilter, onValueChange: (value) => setStatusFilter(value), children: [
28887
+ /* @__PURE__ */ jsxRuntime.jsx(SelectTrigger, { className: "w-full sm:w-[180px] bg-white border-gray-200", children: /* @__PURE__ */ jsxRuntime.jsx(SelectValue, { placeholder: "All statuses" }) }),
28888
+ /* @__PURE__ */ jsxRuntime.jsxs(SelectContent, { className: "bg-white border border-gray-200 shadow-lg", children: [
28889
+ /* @__PURE__ */ jsxRuntime.jsxs(SelectItem, { value: "all", className: "text-gray-700 hover:bg-gray-50 focus:bg-gray-50 border-b border-gray-100 last:border-b-0", children: [
28890
+ "All (",
28891
+ workspaces.length,
28892
+ ")"
28893
+ ] }),
28894
+ /* @__PURE__ */ jsxRuntime.jsx(SelectItem, { value: "healthy", className: "text-gray-700 hover:bg-gray-50 focus:bg-gray-50 border-b border-gray-100 last:border-b-0", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
28895
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-2 w-2 rounded-full bg-green-500" }),
28896
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
28897
+ "Healthy (",
28898
+ statusCounts.healthy,
28899
+ ")"
28900
+ ] })
28901
+ ] }) }),
28902
+ /* @__PURE__ */ jsxRuntime.jsx(SelectItem, { value: "unhealthy", className: "text-gray-700 hover:bg-gray-50 focus:bg-gray-50 border-b border-gray-100 last:border-b-0", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
28903
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-2 w-2 rounded-full bg-red-500" }),
28904
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
28905
+ "Unhealthy (",
28906
+ statusCounts.unhealthy,
28907
+ ")"
28908
+ ] })
28909
+ ] }) }),
28910
+ /* @__PURE__ */ jsxRuntime.jsx(SelectItem, { value: "warning", className: "text-gray-700 hover:bg-gray-50 focus:bg-gray-50 border-b border-gray-100 last:border-b-0", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
28911
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-2 w-2 rounded-full bg-yellow-500" }),
28912
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
28913
+ "Warning (",
28914
+ statusCounts.warning,
28915
+ ")"
28916
+ ] })
28917
+ ] }) })
28918
+ ] })
28919
+ ] }),
28920
+ /* @__PURE__ */ jsxRuntime.jsxs(Select, { value: groupBy, onValueChange: (value) => setGroupBy(value), children: [
28921
+ /* @__PURE__ */ jsxRuntime.jsx(SelectTrigger, { className: "w-full sm:w-[160px] bg-white border-gray-200", children: /* @__PURE__ */ jsxRuntime.jsx(SelectValue, { placeholder: "Group by" }) }),
28922
+ /* @__PURE__ */ jsxRuntime.jsxs(SelectContent, { className: "bg-white border border-gray-200 shadow-lg", children: [
28923
+ /* @__PURE__ */ jsxRuntime.jsx(SelectItem, { value: "none", className: "text-gray-700 hover:bg-gray-50 focus:bg-gray-50 border-b border-gray-100 last:border-b-0", children: "No grouping" }),
28924
+ /* @__PURE__ */ jsxRuntime.jsx(SelectItem, { value: "line", className: "text-gray-700 hover:bg-gray-50 focus:bg-gray-50 border-b border-gray-100 last:border-b-0", children: "Group by Line" }),
28925
+ /* @__PURE__ */ jsxRuntime.jsx(SelectItem, { value: "status", className: "text-gray-700 hover:bg-gray-50 focus:bg-gray-50 border-b border-gray-100 last:border-b-0", children: "Group by Status" })
28926
+ ] })
28927
+ ] })
28928
+ ] }),
28929
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-3 text-sm text-gray-500 dark:text-gray-400", children: [
28930
+ "Showing ",
28931
+ filteredWorkspaces.length,
28932
+ " of ",
28933
+ workspaces.length,
28934
+ " workspaces"
28935
+ ] })
28936
+ ] }),
28937
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-6", children: Object.entries(groupedWorkspaces).map(([groupName, groupWorkspaces]) => {
28938
+ const isExpanded = groupBy === "none" || expandedGroups.has(groupName);
28939
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
28940
+ groupBy !== "none" && /* @__PURE__ */ jsxRuntime.jsxs(
28941
+ "div",
28942
+ {
28943
+ className: "flex items-center justify-between cursor-pointer group",
28944
+ onClick: () => toggleGroup(groupName),
28945
+ children: [
28946
+ /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "text-lg font-semibold text-gray-900 dark:text-gray-100 flex items-center gap-2", children: [
28947
+ groupName,
28948
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm font-normal text-gray-500 dark:text-gray-400", children: [
28949
+ "(",
28950
+ groupWorkspaces.length,
28951
+ ")"
28952
+ ] })
28953
+ ] }),
28954
+ /* @__PURE__ */ jsxRuntime.jsx(
28955
+ lucideReact.ChevronDown,
28956
+ {
28957
+ className: clsx(
28958
+ "h-5 w-5 text-gray-400 transition-transform",
28959
+ isExpanded && "rotate-180"
28960
+ )
28961
+ }
28962
+ )
28963
+ ]
28964
+ }
28965
+ ),
28966
+ isExpanded && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4", children: groupWorkspaces.map((workspace) => /* @__PURE__ */ jsxRuntime.jsx(
28967
+ WorkspaceHealthCard,
28968
+ {
28969
+ workspace,
28970
+ onClick: onWorkspaceClick,
28971
+ showDetails: true
28972
+ },
28973
+ workspace.workspace_id
28974
+ )) })
28975
+ ] }, groupName);
28976
+ }) }),
28977
+ filteredWorkspaces.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-center py-12", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-500 dark:text-gray-400", children: searchTerm || statusFilter !== "all" ? "No workspaces found matching your filters." : "No workspaces available." }) })
28978
+ ] });
28979
+ };
28172
28980
  var ISTTimer2 = ISTTimer_default;
28173
28981
  var DashboardHeader = React19.memo(({ lineTitle, className = "", headerControls }) => {
28174
28982
  const getShiftName = () => {
@@ -28666,6 +29474,17 @@ var SideNavBar = React19.memo(({
28666
29474
  });
28667
29475
  onMobileMenuClose?.();
28668
29476
  }, [navigate, onMobileMenuClose]);
29477
+ const handleHealthClick = React19.useCallback(() => {
29478
+ navigate("/health", {
29479
+ trackingEvent: {
29480
+ name: "Health Status Page Clicked",
29481
+ properties: {
29482
+ source: "side_nav"
29483
+ }
29484
+ }
29485
+ });
29486
+ onMobileMenuClose?.();
29487
+ }, [navigate, onMobileMenuClose]);
28669
29488
  const handleLogoClick = React19.useCallback(() => {
28670
29489
  navigate("/");
28671
29490
  onMobileMenuClose?.();
@@ -28679,6 +29498,7 @@ var SideNavBar = React19.memo(({
28679
29498
  const profileButtonClasses = React19.useMemo(() => getButtonClasses("/profile"), [getButtonClasses, pathname]);
28680
29499
  const helpButtonClasses = React19.useMemo(() => getButtonClasses("/help"), [getButtonClasses, pathname]);
28681
29500
  const skusButtonClasses = React19.useMemo(() => getButtonClasses("/skus"), [getButtonClasses, pathname]);
29501
+ const healthButtonClasses = React19.useMemo(() => getButtonClasses("/health"), [getButtonClasses, pathname]);
28682
29502
  const NavigationContent = () => /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
28683
29503
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full py-6 px-4 flex-shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(
28684
29504
  "button",
@@ -28823,6 +29643,21 @@ var SideNavBar = React19.memo(({
28823
29643
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] font-medium leading-tight", children: "Help" })
28824
29644
  ]
28825
29645
  }
29646
+ ),
29647
+ /* @__PURE__ */ jsxRuntime.jsxs(
29648
+ "button",
29649
+ {
29650
+ onClick: handleHealthClick,
29651
+ className: healthButtonClasses,
29652
+ "aria-label": "System Health",
29653
+ tabIndex: 0,
29654
+ role: "tab",
29655
+ "aria-selected": pathname === "/health" || pathname.startsWith("/health/"),
29656
+ children: [
29657
+ /* @__PURE__ */ jsxRuntime.jsx(outline.HeartIcon, { className: "w-5 h-5 mb-1" }),
29658
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] font-medium leading-tight", children: "Health" })
29659
+ ]
29660
+ }
28826
29661
  )
28827
29662
  ] })
28828
29663
  ] }),
@@ -35448,6 +36283,7 @@ var TargetsViewUI = ({
35448
36283
  onSaveLine,
35449
36284
  onToggleBulkConfigure,
35450
36285
  onBulkConfigure,
36286
+ onUpdateWorkspaceDisplayName,
35451
36287
  // SKU props
35452
36288
  skuEnabled = false,
35453
36289
  skus = [],
@@ -35613,7 +36449,18 @@ var TargetsViewUI = ({
35613
36449
  {
35614
36450
  className: "px-6 py-4 hover:bg-gray-50 transition-all duration-200",
35615
36451
  children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-12 gap-6 items-center", children: [
35616
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-2", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-gray-900", children: formattedName }) }),
36452
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-2", children: onUpdateWorkspaceDisplayName ? /* @__PURE__ */ jsxRuntime.jsx(
36453
+ InlineEditableText,
36454
+ {
36455
+ value: formattedName,
36456
+ onSave: async (newName) => {
36457
+ await onUpdateWorkspaceDisplayName(workspace.id, newName);
36458
+ },
36459
+ placeholder: "Workspace name",
36460
+ className: "font-medium text-gray-900",
36461
+ inputClassName: "min-w-[120px]"
36462
+ }
36463
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-gray-900", children: formattedName }) }),
35617
36464
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-2", children: /* @__PURE__ */ jsxRuntime.jsxs(
35618
36465
  "select",
35619
36466
  {
@@ -36354,6 +37201,17 @@ var TargetsView = ({
36354
37201
  router.push("/");
36355
37202
  }
36356
37203
  };
37204
+ const handleUpdateWorkspaceDisplayName = React19.useCallback(async (workspaceId, displayName) => {
37205
+ try {
37206
+ await workspaceService.updateWorkspaceDisplayName(workspaceId, displayName);
37207
+ await forceRefreshWorkspaceDisplayNames();
37208
+ sonner.toast.success("Workspace name updated successfully");
37209
+ } catch (error) {
37210
+ console.error("Error updating workspace display name:", error);
37211
+ sonner.toast.error("Failed to update workspace name");
37212
+ throw error;
37213
+ }
37214
+ }, []);
36357
37215
  return /* @__PURE__ */ jsxRuntime.jsx(
36358
37216
  TargetsViewUI_default,
36359
37217
  {
@@ -36376,6 +37234,7 @@ var TargetsView = ({
36376
37234
  onSaveLine: handleSaveLine,
36377
37235
  onToggleBulkConfigure: handleToggleBulkConfigure,
36378
37236
  onBulkConfigure: handleBulkConfigure,
37237
+ onUpdateWorkspaceDisplayName: handleUpdateWorkspaceDisplayName,
36379
37238
  skuEnabled,
36380
37239
  skus,
36381
37240
  onUpdateSelectedSKU: updateSelectedSKU,
@@ -36474,6 +37333,14 @@ var WorkspaceDetailView = ({
36474
37333
  const [usingFallbackData, setUsingFallbackData] = React19.useState(false);
36475
37334
  const [showIdleTime, setShowIdleTime] = React19.useState(false);
36476
37335
  const dashboardConfig = useDashboardConfig();
37336
+ const {
37337
+ workspace: workspaceHealth,
37338
+ loading: healthLoading,
37339
+ error: healthError
37340
+ } = useWorkspaceHealthById(workspaceId, {
37341
+ enableRealtime: true,
37342
+ refreshInterval: 3e4
37343
+ });
36477
37344
  const {
36478
37345
  status: prefetchStatus,
36479
37346
  data: prefetchData,
@@ -36826,10 +37693,23 @@ var WorkspaceDetailView = ({
36826
37693
  "aria-label": "Navigate back to previous page"
36827
37694
  }
36828
37695
  ) }),
36829
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute left-1/2 transform -translate-x-1/2 flex items-center gap-3", children: [
37696
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute left-1/2 transform -translate-x-1/2", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
36830
37697
  /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-3xl font-semibold text-gray-900", children: formattedWorkspaceName }),
36831
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-2 w-2 rounded-full bg-green-500 animate-pulse ring-2 ring-green-500/30 ring-offset-1" })
36832
- ] }),
37698
+ workspaceHealth && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex h-2.5 w-2.5", children: [
37699
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: clsx(
37700
+ "animate-ping absolute inline-flex h-full w-full rounded-full opacity-75",
37701
+ workspaceHealth.status === "healthy" ? "bg-green-400" : "bg-red-400"
37702
+ ) }),
37703
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: clsx(
37704
+ "relative inline-flex rounded-full h-2.5 w-2.5",
37705
+ workspaceHealth.status === "healthy" ? "bg-green-500" : "bg-red-500"
37706
+ ) })
37707
+ ] })
37708
+ ] }) }),
37709
+ workspaceHealth && activeTab !== "monthly_history" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute right-0 top-0 flex items-center h-8", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-gray-500", children: [
37710
+ "Last update: ",
37711
+ workspaceHealth.timeSinceLastUpdate
37712
+ ] }) }),
36833
37713
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full h-8" })
36834
37714
  ] }),
36835
37715
  activeTab !== "monthly_history" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-3 bg-blue-50 px-3 py-2 rounded-lg", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-4", children: [
@@ -37393,6 +38273,253 @@ var SKUManagementView = () => {
37393
38273
  )
37394
38274
  ] });
37395
38275
  };
38276
+ var WorkspaceHealthView = ({
38277
+ lineId,
38278
+ companyId,
38279
+ onNavigate,
38280
+ className = ""
38281
+ }) => {
38282
+ const router$1 = router.useRouter();
38283
+ const [groupBy, setGroupBy] = React19.useState("line");
38284
+ const operationalDate = getOperationalDate();
38285
+ const currentHour = (/* @__PURE__ */ new Date()).getHours();
38286
+ const isNightShift = currentHour >= 18 || currentHour < 6;
38287
+ const shiftType = isNightShift ? "Night" : "Day";
38288
+ const formatDate = (date) => {
38289
+ const d = new Date(date);
38290
+ return d.toLocaleDateString("en-IN", {
38291
+ month: "long",
38292
+ day: "numeric",
38293
+ year: "numeric",
38294
+ timeZone: "Asia/Kolkata"
38295
+ });
38296
+ };
38297
+ const getShiftIcon = (shift) => {
38298
+ return shift === "Night" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Moon, { className: "h-4 w-4" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Sun, { className: "h-4 w-4" });
38299
+ };
38300
+ const {
38301
+ workspaces,
38302
+ summary,
38303
+ loading,
38304
+ error,
38305
+ refetch
38306
+ } = useWorkspaceHealth({
38307
+ lineId,
38308
+ companyId,
38309
+ enableRealtime: true,
38310
+ refreshInterval: 1e4
38311
+ // Refresh every 10 seconds for more responsive updates
38312
+ });
38313
+ const handleWorkspaceClick = React19.useCallback(
38314
+ (workspace) => {
38315
+ const url = `/workspace/${workspace.workspace_id}`;
38316
+ if (onNavigate) {
38317
+ onNavigate(url);
38318
+ } else {
38319
+ router$1.push(url);
38320
+ }
38321
+ },
38322
+ [router$1, onNavigate]
38323
+ );
38324
+ const handleExport = React19.useCallback(() => {
38325
+ const csv = [
38326
+ ["Workspace", "Line", "Company", "Status", "Last Heartbeat", "Consecutive Misses"],
38327
+ ...workspaces.map((w) => [
38328
+ w.workspace_display_name || "",
38329
+ w.line_name || "",
38330
+ w.company_name || "",
38331
+ w.status,
38332
+ w.last_heartbeat,
38333
+ w.consecutive_misses?.toString() || "0"
38334
+ ])
38335
+ ].map((row) => row.join(",")).join("\n");
38336
+ const blob = new Blob([csv], { type: "text/csv" });
38337
+ const url = window.URL.createObjectURL(blob);
38338
+ const a = document.createElement("a");
38339
+ a.href = url;
38340
+ a.download = `workspace-health-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.csv`;
38341
+ document.body.appendChild(a);
38342
+ a.click();
38343
+ document.body.removeChild(a);
38344
+ window.URL.revokeObjectURL(url);
38345
+ }, [workspaces]);
38346
+ const getStatusIcon = (status) => {
38347
+ switch (status) {
38348
+ case "healthy":
38349
+ return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CheckCircle, { className: "h-5 w-5 text-green-500" });
38350
+ case "unhealthy":
38351
+ return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.XCircle, { className: "h-5 w-5 text-red-500" });
38352
+ case "warning":
38353
+ return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { className: "h-5 w-5 text-yellow-500" });
38354
+ default:
38355
+ return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Activity, { className: "h-5 w-5 text-gray-400" });
38356
+ }
38357
+ };
38358
+ const getUptimeColor = (percentage) => {
38359
+ if (percentage >= 99) return "text-green-600 dark:text-green-400";
38360
+ if (percentage >= 95) return "text-yellow-600 dark:text-yellow-400";
38361
+ return "text-red-600 dark:text-red-400";
38362
+ };
38363
+ if (loading && !summary) {
38364
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-screen bg-gray-50 dark:bg-gray-900 p-4", children: /* @__PURE__ */ jsxRuntime.jsx(LoadingState, {}) });
38365
+ }
38366
+ if (error) {
38367
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-screen bg-gray-50 dark:bg-gray-900 p-4", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "max-w-7xl mx-auto", children: /* @__PURE__ */ jsxRuntime.jsx(Card2, { className: "border-red-200 dark:border-red-800", children: /* @__PURE__ */ jsxRuntime.jsxs(CardContent2, { className: "p-8 text-center", children: [
38368
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.XCircle, { className: "h-12 w-12 text-red-500 mx-auto mb-4" }),
38369
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-xl font-semibold text-gray-900 dark:text-gray-100 mb-2", children: "Error Loading Health Status" }),
38370
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-600 dark:text-gray-400 mb-4", children: error.message || "Unable to load workspace health status" }),
38371
+ /* @__PURE__ */ jsxRuntime.jsx(
38372
+ "button",
38373
+ {
38374
+ onClick: () => refetch(),
38375
+ className: "px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors",
38376
+ children: "Try Again"
38377
+ }
38378
+ )
38379
+ ] }) }) }) });
38380
+ }
38381
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: clsx("min-h-screen bg-slate-50", className), children: [
38382
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "sticky top-0 z-10 px-2 sm:px-2.5 lg:px-3 py-1.5 sm:py-2 lg:py-3 flex flex-col shadow-sm bg-white", children: [
38383
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex items-center", children: [
38384
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute left-0 z-10", children: /* @__PURE__ */ jsxRuntime.jsx(
38385
+ BackButtonMinimal,
38386
+ {
38387
+ onClick: () => router$1.push("/"),
38388
+ text: "Back",
38389
+ size: "default",
38390
+ "aria-label": "Navigate back to dashboard"
38391
+ }
38392
+ ) }),
38393
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute left-1/2 transform -translate-x-1/2", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
38394
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-3xl font-semibold text-gray-900", children: "System Health" }),
38395
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex h-2.5 w-2.5", children: [
38396
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" }),
38397
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500" })
38398
+ ] })
38399
+ ] }) }),
38400
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute right-0 flex gap-2", children: [
38401
+ /* @__PURE__ */ jsxRuntime.jsx(
38402
+ "button",
38403
+ {
38404
+ onClick: () => {
38405
+ refetch();
38406
+ },
38407
+ className: "p-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors",
38408
+ "aria-label": "Refresh",
38409
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { className: "h-5 w-5" })
38410
+ }
38411
+ ),
38412
+ /* @__PURE__ */ jsxRuntime.jsx(
38413
+ "button",
38414
+ {
38415
+ onClick: handleExport,
38416
+ className: "p-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors",
38417
+ "aria-label": "Export CSV",
38418
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { className: "h-5 w-5" })
38419
+ }
38420
+ )
38421
+ ] }),
38422
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full h-8" })
38423
+ ] }),
38424
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-3 bg-blue-50 px-3 py-2 rounded-lg", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-4", children: [
38425
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-lg font-medium text-blue-600", children: /* @__PURE__ */ jsxRuntime.jsx(LiveTimer, {}) }),
38426
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-px h-4 bg-blue-300" }),
38427
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-base font-medium text-blue-600", children: formatDate(operationalDate) }),
38428
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-px h-4 bg-blue-300" }),
38429
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
38430
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-blue-600", children: getShiftIcon(shiftType) }),
38431
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-base font-medium text-blue-600", children: [
38432
+ shiftType,
38433
+ " Shift"
38434
+ ] })
38435
+ ] })
38436
+ ] }) })
38437
+ ] }),
38438
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-w-7xl mx-auto p-4 space-y-6", children: [
38439
+ summary && /* @__PURE__ */ jsxRuntime.jsxs(
38440
+ motion.div,
38441
+ {
38442
+ initial: { opacity: 0, y: 20 },
38443
+ animate: { opacity: 1, y: 0 },
38444
+ transition: { duration: 0.3, delay: 0.1 },
38445
+ className: "grid grid-cols-2 sm:grid-cols-2 md:grid-cols-5 gap-2 sm:gap-3 lg:gap-4",
38446
+ children: [
38447
+ /* @__PURE__ */ jsxRuntime.jsxs(Card2, { className: "col-span-2 sm:col-span-2 md:col-span-2 bg-white", children: [
38448
+ /* @__PURE__ */ jsxRuntime.jsx(CardHeader2, { className: "pb-3", children: /* @__PURE__ */ jsxRuntime.jsx(CardTitle2, { className: "text-sm font-medium text-gray-500 dark:text-gray-400", children: "Overall System Status" }) }),
38449
+ /* @__PURE__ */ jsxRuntime.jsxs(CardContent2, { children: [
38450
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-baseline gap-2", children: [
38451
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: clsx("text-3xl font-bold", getUptimeColor(summary.uptimePercentage)), children: [
38452
+ summary.uptimePercentage.toFixed(1),
38453
+ "%"
38454
+ ] }),
38455
+ summary.uptimePercentage >= 99 ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.TrendingUp, { className: "h-5 w-5 text-green-500" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.TrendingDown, { className: "h-5 w-5 text-red-500" })
38456
+ ] }),
38457
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: [
38458
+ summary.healthyWorkspaces,
38459
+ " of ",
38460
+ summary.totalWorkspaces,
38461
+ " workspaces healthy"
38462
+ ] })
38463
+ ] })
38464
+ ] }),
38465
+ /* @__PURE__ */ jsxRuntime.jsxs(Card2, { className: "bg-white", children: [
38466
+ /* @__PURE__ */ jsxRuntime.jsx(CardHeader2, { className: "pb-3", children: /* @__PURE__ */ jsxRuntime.jsxs(CardTitle2, { className: "text-sm font-medium text-gray-500 dark:text-gray-400 flex items-center gap-2", children: [
38467
+ getStatusIcon("healthy"),
38468
+ "Healthy"
38469
+ ] }) }),
38470
+ /* @__PURE__ */ jsxRuntime.jsxs(CardContent2, { children: [
38471
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-2xl font-bold text-gray-900 dark:text-gray-50", children: summary.healthyWorkspaces }),
38472
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: "Operating normally" })
38473
+ ] })
38474
+ ] }),
38475
+ /* @__PURE__ */ jsxRuntime.jsxs(Card2, { className: "bg-white", children: [
38476
+ /* @__PURE__ */ jsxRuntime.jsx(CardHeader2, { className: "pb-3", children: /* @__PURE__ */ jsxRuntime.jsxs(CardTitle2, { className: "text-sm font-medium text-gray-500 dark:text-gray-400 flex items-center gap-2", children: [
38477
+ getStatusIcon("warning"),
38478
+ "Warning"
38479
+ ] }) }),
38480
+ /* @__PURE__ */ jsxRuntime.jsxs(CardContent2, { children: [
38481
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-2xl font-bold text-gray-900 dark:text-gray-50", children: summary.warningWorkspaces }),
38482
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: "Delayed updates" })
38483
+ ] })
38484
+ ] }),
38485
+ /* @__PURE__ */ jsxRuntime.jsxs(Card2, { className: "bg-white", children: [
38486
+ /* @__PURE__ */ jsxRuntime.jsx(CardHeader2, { className: "pb-3", children: /* @__PURE__ */ jsxRuntime.jsxs(CardTitle2, { className: "text-sm font-medium text-gray-500 dark:text-gray-400 flex items-center gap-2", children: [
38487
+ getStatusIcon("unhealthy"),
38488
+ "Unhealthy"
38489
+ ] }) }),
38490
+ /* @__PURE__ */ jsxRuntime.jsxs(CardContent2, { children: [
38491
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-2xl font-bold text-gray-900 dark:text-gray-50", children: summary.unhealthyWorkspaces }),
38492
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: "Requires attention" })
38493
+ ] })
38494
+ ] })
38495
+ ]
38496
+ }
38497
+ ),
38498
+ /* @__PURE__ */ jsxRuntime.jsx(
38499
+ motion.div,
38500
+ {
38501
+ initial: { opacity: 0, y: 20 },
38502
+ animate: { opacity: 1, y: 0 },
38503
+ transition: { duration: 0.3, delay: 0.2 },
38504
+ children: /* @__PURE__ */ jsxRuntime.jsx(
38505
+ HealthStatusGrid,
38506
+ {
38507
+ workspaces,
38508
+ onWorkspaceClick: handleWorkspaceClick,
38509
+ showFilters: true,
38510
+ groupBy
38511
+ }
38512
+ )
38513
+ }
38514
+ )
38515
+ ] })
38516
+ ] });
38517
+ };
38518
+ var WorkspaceHealthView_default = withAuth(WorkspaceHealthView, {
38519
+ redirectTo: "/login",
38520
+ requireAuth: true
38521
+ });
38522
+ var AuthenticatedWorkspaceHealthView = WorkspaceHealthView;
37396
38523
  var S3Service = class {
37397
38524
  constructor(config) {
37398
38525
  this.s3Client = null;
@@ -37859,6 +38986,7 @@ exports.AuthenticatedHelpView = AuthenticatedHelpView;
37859
38986
  exports.AuthenticatedHomeView = AuthenticatedHomeView;
37860
38987
  exports.AuthenticatedShiftsView = AuthenticatedShiftsView;
37861
38988
  exports.AuthenticatedTargetsView = AuthenticatedTargetsView;
38989
+ exports.AuthenticatedWorkspaceHealthView = AuthenticatedWorkspaceHealthView;
37862
38990
  exports.BackButton = BackButton;
37863
38991
  exports.BackButtonMinimal = BackButtonMinimal;
37864
38992
  exports.BarChart = BarChart;
@@ -37872,6 +39000,7 @@ exports.CardDescription = CardDescription2;
37872
39000
  exports.CardFooter = CardFooter2;
37873
39001
  exports.CardHeader = CardHeader2;
37874
39002
  exports.CardTitle = CardTitle2;
39003
+ exports.CompactWorkspaceHealthCard = CompactWorkspaceHealthCard;
37875
39004
  exports.CongratulationsOverlay = CongratulationsOverlay;
37876
39005
  exports.CycleTimeChart = CycleTimeChart;
37877
39006
  exports.CycleTimeOverTimeChart = CycleTimeOverTimeChart;
@@ -37895,6 +39024,7 @@ exports.DateDisplay = DateDisplay_default;
37895
39024
  exports.DateTimeDisplay = DateTimeDisplay;
37896
39025
  exports.DebugAuth = DebugAuth;
37897
39026
  exports.DebugAuthView = DebugAuthView_default;
39027
+ exports.DetailedHealthStatus = DetailedHealthStatus;
37898
39028
  exports.EmptyStateMessage = EmptyStateMessage;
37899
39029
  exports.EncouragementOverlay = EncouragementOverlay;
37900
39030
  exports.FactoryView = FactoryView_default;
@@ -37902,10 +39032,13 @@ exports.GaugeChart = GaugeChart;
37902
39032
  exports.GridComponentsPlaceholder = GridComponentsPlaceholder;
37903
39033
  exports.HamburgerButton = HamburgerButton;
37904
39034
  exports.Header = Header;
39035
+ exports.HealthStatusGrid = HealthStatusGrid;
39036
+ exports.HealthStatusIndicator = HealthStatusIndicator;
37905
39037
  exports.HelpView = HelpView_default;
37906
39038
  exports.HomeView = HomeView_default;
37907
39039
  exports.HourlyOutputChart = HourlyOutputChart2;
37908
39040
  exports.ISTTimer = ISTTimer_default;
39041
+ exports.InlineEditableText = InlineEditableText;
37909
39042
  exports.KPICard = KPICard;
37910
39043
  exports.KPIDetailView = KPIDetailView_default;
37911
39044
  exports.KPIGrid = KPIGrid;
@@ -37947,6 +39080,7 @@ exports.PrefetchStatus = PrefetchStatus;
37947
39080
  exports.PrefetchTimeoutError = PrefetchTimeoutError;
37948
39081
  exports.ProfileView = ProfileView_default;
37949
39082
  exports.RegistryProvider = RegistryProvider;
39083
+ exports.S3ClipsService = S3ClipsService;
37950
39084
  exports.S3Service = S3Service;
37951
39085
  exports.SKUManagementView = SKUManagementView;
37952
39086
  exports.SOPComplianceChart = SOPComplianceChart;
@@ -37987,6 +39121,8 @@ exports.WorkspaceDetailView = WorkspaceDetailView_default;
37987
39121
  exports.WorkspaceDisplayNameExample = WorkspaceDisplayNameExample;
37988
39122
  exports.WorkspaceGrid = WorkspaceGrid;
37989
39123
  exports.WorkspaceGridItem = WorkspaceGridItem;
39124
+ exports.WorkspaceHealthCard = WorkspaceHealthCard;
39125
+ exports.WorkspaceHealthView = WorkspaceHealthView_default;
37990
39126
  exports.WorkspaceHistoryCalendar = WorkspaceHistoryCalendar;
37991
39127
  exports.WorkspaceMetricCards = WorkspaceMetricCards;
37992
39128
  exports.WorkspaceMonthlyDataFetcher = WorkspaceMonthlyDataFetcher;
@@ -38071,6 +39207,7 @@ exports.isWorkspaceDisplayNamesLoading = isWorkspaceDisplayNamesLoading;
38071
39207
  exports.mergeWithDefaultConfig = mergeWithDefaultConfig;
38072
39208
  exports.migrateLegacyConfiguration = migrateLegacyConfiguration;
38073
39209
  exports.optifyeAgentClient = optifyeAgentClient;
39210
+ exports.parseS3Uri = parseS3Uri;
38074
39211
  exports.preInitializeWorkspaceDisplayNames = preInitializeWorkspaceDisplayNames;
38075
39212
  exports.preloadS3Video = preloadS3Video;
38076
39213
  exports.preloadS3VideoUrl = preloadS3VideoUrl;
@@ -38084,6 +39221,7 @@ exports.resetCoreMixpanel = resetCoreMixpanel;
38084
39221
  exports.resetFailedUrl = resetFailedUrl;
38085
39222
  exports.resetSubscriptionManager = resetSubscriptionManager;
38086
39223
  exports.s3VideoPreloader = s3VideoPreloader;
39224
+ exports.shuffleArray = shuffleArray;
38087
39225
  exports.skuService = skuService;
38088
39226
  exports.startCoreSessionRecording = startCoreSessionRecording;
38089
39227
  exports.stopCoreSessionRecording = stopCoreSessionRecording;
@@ -38150,6 +39288,8 @@ exports.useWorkspaceDetailedMetrics = useWorkspaceDetailedMetrics;
38150
39288
  exports.useWorkspaceDisplayName = useWorkspaceDisplayName;
38151
39289
  exports.useWorkspaceDisplayNames = useWorkspaceDisplayNames;
38152
39290
  exports.useWorkspaceDisplayNamesMap = useWorkspaceDisplayNamesMap;
39291
+ exports.useWorkspaceHealth = useWorkspaceHealth;
39292
+ exports.useWorkspaceHealthById = useWorkspaceHealthById;
38153
39293
  exports.useWorkspaceMetrics = useWorkspaceMetrics;
38154
39294
  exports.useWorkspaceNavigation = useWorkspaceNavigation;
38155
39295
  exports.useWorkspaceOperators = useWorkspaceOperators;
@@ -38158,4 +39298,5 @@ exports.videoPreloader = videoPreloader;
38158
39298
  exports.whatsappService = whatsappService;
38159
39299
  exports.withAuth = withAuth;
38160
39300
  exports.withRegistry = withRegistry;
39301
+ exports.workspaceHealthService = workspaceHealthService;
38161
39302
  exports.workspaceService = workspaceService;