@optifye/dashboard-core 6.4.1 → 6.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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');
@@ -18,12 +17,14 @@ var reactSlot = require('@radix-ui/react-slot');
18
17
  var lucideReact = require('lucide-react');
19
18
  var reactDayPicker = require('react-day-picker');
20
19
  var outline = require('@heroicons/react/24/outline');
20
+ var solid = require('@heroicons/react/24/solid');
21
21
  var html2canvas = require('html2canvas');
22
22
  var jsPDF = require('jspdf');
23
23
  var SelectPrimitive = require('@radix-ui/react-select');
24
24
  var videojs = require('video.js');
25
25
  require('video.js/dist/video-js.css');
26
26
  var sonner = require('sonner');
27
+ var clientS3 = require('@aws-sdk/client-s3');
27
28
  var s3RequestPresigner = require('@aws-sdk/s3-request-presigner');
28
29
  var stream = require('stream');
29
30
 
@@ -1703,6 +1704,25 @@ var workspaceService = {
1703
1704
  this._workspaceDisplayNamesCache.clear();
1704
1705
  this._cacheTimestamp = 0;
1705
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
+ },
1706
1726
  async updateWorkspaceAction(updates) {
1707
1727
  const supabase = _getSupabaseInstance();
1708
1728
  if (!supabase) throw new Error("Supabase client not initialized");
@@ -1846,6 +1866,209 @@ var workspaceService = {
1846
1866
  }
1847
1867
  }
1848
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();
1849
2072
 
1850
2073
  // src/lib/services/skuService.ts
1851
2074
  var getTable4 = (dbConfig, tableName) => {
@@ -2675,6 +2898,17 @@ var TicketHistoryService = class {
2675
2898
  }
2676
2899
  return data;
2677
2900
  }
2901
+ /**
2902
+ * Update ticket serviced status
2903
+ */
2904
+ static async updateTicketServicedStatus(ticketId, serviced) {
2905
+ const supabase = _getSupabaseInstance();
2906
+ const { data, error } = await supabase.from("support_ticket_history").update({ serviced, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", ticketId).select().single();
2907
+ if (error) {
2908
+ throw new Error(`Failed to update ticket serviced status: ${error.message}`);
2909
+ }
2910
+ return data;
2911
+ }
2678
2912
  };
2679
2913
 
2680
2914
  // src/lib/services/audioService.ts
@@ -3229,6 +3463,14 @@ function parseS3Uri(s3Uri, sopCategories) {
3229
3463
  return null;
3230
3464
  }
3231
3465
  }
3466
+ function shuffleArray(array) {
3467
+ const shuffled = [...array];
3468
+ for (let i = shuffled.length - 1; i > 0; i--) {
3469
+ const j = Math.floor(Math.random() * (i + 1));
3470
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
3471
+ }
3472
+ return shuffled;
3473
+ }
3232
3474
 
3233
3475
  // src/lib/cache/clipsCache.ts
3234
3476
  var LRUCache = class _LRUCache {
@@ -3741,300 +3983,321 @@ if (typeof window !== "undefined") {
3741
3983
  });
3742
3984
  });
3743
3985
  }
3744
-
3745
- // src/lib/api/s3-clips.ts
3746
- var RequestDeduplicationCache = class {
3747
- constructor() {
3748
- this.pendingRequests = /* @__PURE__ */ new Map();
3749
- this.maxCacheSize = 1e3;
3750
- this.cleanupInterval = 3e5;
3751
- // 5 minutes
3752
- this.lastCleanup = Date.now();
3986
+ var getSupabaseClient = () => {
3987
+ const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
3988
+ const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
3989
+ if (!url || !key) {
3990
+ throw new Error("Supabase configuration missing");
3991
+ }
3992
+ return supabaseJs.createClient(url, key);
3993
+ };
3994
+ var getAuthToken = async () => {
3995
+ try {
3996
+ const supabase = getSupabaseClient();
3997
+ const { data: { session } } = await supabase.auth.getSession();
3998
+ return session?.access_token || null;
3999
+ } catch (error) {
4000
+ console.error("[S3ClipsAPIClient] Error getting auth token:", error);
4001
+ return null;
4002
+ }
4003
+ };
4004
+ var S3ClipsAPIClient = class {
4005
+ constructor(sopCategories) {
4006
+ this.baseUrl = "/api/clips";
4007
+ this.requestCache = /* @__PURE__ */ new Map();
4008
+ this.sopCategories = sopCategories;
4009
+ console.log("[S3ClipsAPIClient] \u2705 Initialized - Using secure API routes (no direct S3 access)");
3753
4010
  }
3754
4011
  /**
3755
- * Get or create a deduplicated request
4012
+ * Fetch with authentication and error handling
3756
4013
  */
3757
- async deduplicate(key, factory, logPrefix = "Request") {
3758
- this.periodicCleanup();
3759
- const existingRequest = this.pendingRequests.get(key);
3760
- if (existingRequest) {
3761
- console.log(`[${logPrefix}] Deduplicating request for key: ${key}`);
3762
- return existingRequest;
4014
+ async fetchWithAuth(endpoint, body) {
4015
+ const token = await getAuthToken();
4016
+ if (!token) {
4017
+ throw new Error("Authentication required");
4018
+ }
4019
+ const response = await fetch(endpoint, {
4020
+ method: "POST",
4021
+ headers: {
4022
+ "Authorization": `Bearer ${token}`,
4023
+ "Content-Type": "application/json"
4024
+ },
4025
+ body: JSON.stringify(body)
4026
+ });
4027
+ if (!response.ok) {
4028
+ const error = await response.json().catch(() => ({ error: "Request failed" }));
4029
+ throw new Error(error.error || `API error: ${response.status}`);
4030
+ }
4031
+ return response.json();
4032
+ }
4033
+ /**
4034
+ * Deduplicate requests to prevent multiple API calls
4035
+ */
4036
+ async deduplicate(key, factory) {
4037
+ if (this.requestCache.has(key)) {
4038
+ console.log(`[S3ClipsAPIClient] Deduplicating request: ${key}`);
4039
+ return this.requestCache.get(key);
3763
4040
  }
3764
- console.log(`[${logPrefix}] Creating new request for key: ${key}`);
3765
4041
  const promise = factory().finally(() => {
3766
- this.pendingRequests.delete(key);
3767
- console.log(`[${logPrefix}] Completed and cleaned up request: ${key}`);
4042
+ this.requestCache.delete(key);
3768
4043
  });
3769
- this.pendingRequests.set(key, promise);
4044
+ this.requestCache.set(key, promise);
3770
4045
  return promise;
3771
4046
  }
3772
4047
  /**
3773
- * Clear all pending requests (useful for cleanup)
4048
+ * List S3 clips
3774
4049
  */
3775
- clear() {
3776
- console.log(`[RequestCache] Clearing ${this.pendingRequests.size} pending requests`);
3777
- this.pendingRequests.clear();
4050
+ async listS3Clips(params) {
4051
+ const cacheKey = `list:${JSON.stringify(params)}`;
4052
+ return this.deduplicate(cacheKey, async () => {
4053
+ const response = await this.fetchWithAuth(this.baseUrl, {
4054
+ action: "list",
4055
+ workspaceId: params.workspaceId,
4056
+ date: params.date,
4057
+ shift: params.shiftId,
4058
+ maxKeys: params.maxKeys
4059
+ });
4060
+ return response.clips.map((clip) => clip.originalUri);
4061
+ });
3778
4062
  }
3779
4063
  /**
3780
- * Get current cache stats
4064
+ * Get clip counts
3781
4065
  */
3782
- getStats() {
3783
- return {
3784
- pendingCount: this.pendingRequests.size,
3785
- maxSize: this.maxCacheSize
4066
+ async getClipCounts(workspaceId, date, shiftId) {
4067
+ const cacheKey = `counts:${workspaceId}:${date}:${shiftId}`;
4068
+ return this.deduplicate(cacheKey, async () => {
4069
+ const response = await this.fetchWithAuth(this.baseUrl, {
4070
+ action: "count",
4071
+ workspaceId,
4072
+ date,
4073
+ shift: shiftId.toString()
4074
+ });
4075
+ return response.counts;
4076
+ });
4077
+ }
4078
+ /**
4079
+ * Get clip counts with index (for compatibility)
4080
+ */
4081
+ async getClipCountsWithIndex(workspaceId, date, shiftId) {
4082
+ const counts = await this.getClipCounts(workspaceId, date, shiftId.toString());
4083
+ const videoIndex = {
4084
+ byCategory: /* @__PURE__ */ new Map(),
4085
+ allVideos: [],
4086
+ counts,
4087
+ workspaceId,
4088
+ date,
4089
+ shiftId: shiftId.toString(),
4090
+ lastUpdated: /* @__PURE__ */ new Date()
3786
4091
  };
4092
+ return { counts, videoIndex };
3787
4093
  }
3788
4094
  /**
3789
- * Periodic cleanup to prevent memory leaks
4095
+ * Get metadata for a video
3790
4096
  */
3791
- periodicCleanup() {
3792
- const now2 = Date.now();
3793
- if (now2 - this.lastCleanup > this.cleanupInterval) {
3794
- if (this.pendingRequests.size > this.maxCacheSize) {
3795
- console.warn(`[RequestCache] Cache size exceeded ${this.maxCacheSize}, clearing oldest entries`);
3796
- const entries = Array.from(this.pendingRequests.entries());
3797
- this.pendingRequests.clear();
3798
- entries.slice(-Math.floor(this.maxCacheSize / 2)).forEach(([key, promise]) => {
3799
- this.pendingRequests.set(key, promise);
3800
- });
4097
+ async getMetadata(playlistUri) {
4098
+ const cacheKey = `metadata:${playlistUri}`;
4099
+ return this.deduplicate(cacheKey, async () => {
4100
+ const response = await this.fetchWithAuth(this.baseUrl, {
4101
+ action: "metadata",
4102
+ playlistUri
4103
+ });
4104
+ return response.metadata;
4105
+ });
4106
+ }
4107
+ /**
4108
+ * Get metadata cycle time
4109
+ */
4110
+ async getMetadataCycleTime(playlistUri) {
4111
+ const metadata = await this.getMetadata(playlistUri);
4112
+ return metadata?.cycle_time_seconds || null;
4113
+ }
4114
+ /**
4115
+ * Get first clip for category
4116
+ */
4117
+ async getFirstClipForCategory(workspaceId, date, shiftId, category) {
4118
+ const cacheKey = `first:${workspaceId}:${date}:${shiftId}:${category}`;
4119
+ return this.deduplicate(cacheKey, async () => {
4120
+ const response = await this.fetchWithAuth(this.baseUrl, {
4121
+ action: "first",
4122
+ workspaceId,
4123
+ date,
4124
+ shift: shiftId.toString(),
4125
+ category,
4126
+ sopCategories: this.sopCategories
4127
+ });
4128
+ return response.video;
4129
+ });
4130
+ }
4131
+ /**
4132
+ * Get clip by index
4133
+ */
4134
+ async getClipByIndex(workspaceId, date, shiftId, category, index, includeCycleTime = true, includeMetadata = false) {
4135
+ const cacheKey = `by-index:${workspaceId}:${date}:${shiftId}:${category}:${index}`;
4136
+ return this.deduplicate(cacheKey, async () => {
4137
+ const response = await this.fetchWithAuth(this.baseUrl, {
4138
+ action: "by-index",
4139
+ workspaceId,
4140
+ date,
4141
+ shift: shiftId.toString(),
4142
+ category,
4143
+ index,
4144
+ sopCategories: this.sopCategories
4145
+ });
4146
+ const video = response.video;
4147
+ if (video && includeMetadata && video.originalUri) {
4148
+ try {
4149
+ const metadata = await this.getMetadata(video.originalUri);
4150
+ if (metadata) {
4151
+ video.cycle_time_seconds = metadata.cycle_time_seconds;
4152
+ video.creation_timestamp = metadata.creation_timestamp;
4153
+ }
4154
+ } catch (error) {
4155
+ console.warn("[S3ClipsAPIClient] Failed to fetch metadata:", error);
4156
+ }
3801
4157
  }
3802
- this.lastCleanup = now2;
3803
- }
4158
+ return video;
4159
+ });
4160
+ }
4161
+ /**
4162
+ * Get videos page with pagination
4163
+ */
4164
+ async getVideosPage(workspaceId, date, shiftId, category, pageSize = 5, startAfter) {
4165
+ const cacheKey = `page:${workspaceId}:${date}:${shiftId}:${category}:${pageSize}:${startAfter || "first"}`;
4166
+ return this.deduplicate(cacheKey, async () => {
4167
+ const response = await this.fetchWithAuth(this.baseUrl, {
4168
+ action: "page",
4169
+ workspaceId,
4170
+ date,
4171
+ shift: shiftId.toString(),
4172
+ category,
4173
+ pageSize,
4174
+ startAfter,
4175
+ sopCategories: this.sopCategories
4176
+ });
4177
+ return {
4178
+ videos: response.videos,
4179
+ nextToken: response.nextToken,
4180
+ hasMore: response.hasMore
4181
+ };
4182
+ });
4183
+ }
4184
+ /**
4185
+ * Convert S3 URI to CloudFront URL
4186
+ * In the API client, URLs are already signed from the server
4187
+ */
4188
+ s3UriToCloudfront(s3Uri) {
4189
+ return s3Uri;
4190
+ }
4191
+ /**
4192
+ * Clean up resources
4193
+ */
4194
+ dispose() {
4195
+ this.requestCache.clear();
4196
+ }
4197
+ /**
4198
+ * Get service statistics
4199
+ */
4200
+ getStats() {
4201
+ return {
4202
+ requestCache: {
4203
+ pendingCount: this.requestCache.size,
4204
+ maxSize: 1e3
4205
+ }
4206
+ };
3804
4207
  }
3805
4208
  };
4209
+
4210
+ // src/lib/api/s3-clips-secure.ts
3806
4211
  var S3ClipsService = class {
3807
4212
  constructor(config) {
3808
- // Request deduplication cache
3809
- this.requestCache = new RequestDeduplicationCache();
3810
- // Flag to prevent metadata fetching during index building
4213
+ // Flags for compatibility
3811
4214
  this.isIndexBuilding = false;
3812
- // Flag to prevent metadata fetching during entire prefetch operation
3813
4215
  this.isPrefetching = false;
3814
- // Global safeguard: limit concurrent metadata fetches to prevent flooding
3815
4216
  this.currentMetadataFetches = 0;
3816
4217
  this.MAX_CONCURRENT_METADATA = 3;
3817
4218
  this.config = config;
3818
4219
  if (!config.s3Config) {
3819
4220
  throw new Error("S3 configuration is required");
3820
4221
  }
4222
+ const sopCategories = config.s3Config.sopCategories?.default;
4223
+ this.apiClient = new S3ClipsAPIClient(sopCategories);
3821
4224
  const processing = config.s3Config.processing || {};
3822
4225
  this.defaultLimitPerCategory = processing.defaultLimitPerCategory || 30;
3823
4226
  this.maxLimitPerCategory = processing.maxLimitPerCategory || 1e3;
3824
4227
  this.concurrencyLimit = processing.concurrencyLimit || 10;
3825
4228
  this.maxInitialFetch = processing.maxInitialFetch || 60;
3826
- const region = this.validateAndSanitizeRegion(config.s3Config.region);
3827
- console.log(`S3ClipsService: Using AWS region: ${region}`);
3828
- this.s3Client = new clientS3.S3Client({
3829
- region,
3830
- credentials: config.s3Config.credentials ? {
3831
- accessKeyId: config.s3Config.credentials.accessKeyId,
3832
- secretAccessKey: config.s3Config.credentials.secretAccessKey
3833
- } : void 0
3834
- });
3835
- }
3836
- /**
3837
- * Validates and sanitizes the AWS region
3838
- */
3839
- validateAndSanitizeRegion(region) {
3840
- const defaultRegion = "us-east-1";
3841
- if (!region || typeof region !== "string") {
3842
- console.warn(`S3ClipsService: Invalid region provided (${region}), using default: ${defaultRegion}`);
3843
- return defaultRegion;
3844
- }
3845
- const sanitizedRegion = region.trim().toLowerCase();
3846
- if (!sanitizedRegion) {
3847
- console.warn(`S3ClipsService: Empty region provided, using default: ${defaultRegion}`);
3848
- return defaultRegion;
3849
- }
3850
- const regionPattern = /^[a-z]{2,3}-[a-z]+-\d+$/;
3851
- if (!regionPattern.test(sanitizedRegion)) {
3852
- console.warn(`S3ClipsService: Invalid region format (${sanitizedRegion}), using default: ${defaultRegion}`);
3853
- return defaultRegion;
3854
- }
3855
- return sanitizedRegion;
4229
+ console.log("[S3ClipsService] \u2705 Initialized with secure API client - AWS credentials are now server-side only!");
3856
4230
  }
3857
4231
  /**
3858
- * Lists S3 clips for a workspace, date, and shift with request deduplication
4232
+ * Lists S3 clips using API
3859
4233
  */
3860
4234
  async listS3Clips(params) {
3861
- const { workspaceId, date, shiftId, maxKeys } = params;
4235
+ const { workspaceId, date, shiftId } = params;
3862
4236
  if (!isValidShiftId(shiftId)) {
3863
- console.error(`[S3ClipsService] Invalid shift ID: ${shiftId}. Must be 0 (day) or 1 (night)`);
4237
+ console.error(`[S3ClipsService] Invalid shift ID: ${shiftId}`);
3864
4238
  return [];
3865
4239
  }
3866
- const prefix = `sop_violations/${workspaceId}/${date}/${shiftId}/`;
3867
- console.log(`[S3ClipsService] Listing clips for workspace: ${workspaceId}, date: ${date}, shift: ${shiftId}`);
3868
- const deduplicationKey = `list-s3-clips:${prefix}:${maxKeys || "all"}`;
3869
- return this.requestCache.deduplicate(
3870
- deduplicationKey,
3871
- () => this.executeListS3Clips(params),
3872
- "ListS3Clips"
3873
- );
3874
- }
3875
- /**
3876
- * Internal implementation of S3 listing (called through deduplication)
3877
- */
3878
- async executeListS3Clips(params) {
3879
- const { workspaceId, date, shiftId, maxKeys } = params;
3880
- const prefix = `sop_violations/${workspaceId}/${date}/${shiftId}/`;
3881
- console.log(`[S3ClipsService] Executing S3 list for prefix: ${prefix}`);
4240
+ console.log(`[S3ClipsService] Listing clips via API for workspace: ${workspaceId}`);
3882
4241
  try {
3883
- let playlists = [];
3884
- let continuationToken = void 0;
3885
- do {
3886
- const command = new clientS3.ListObjectsV2Command({
3887
- Bucket: this.config.s3Config.bucketName,
3888
- Prefix: prefix,
3889
- ContinuationToken: continuationToken,
3890
- MaxKeys: maxKeys ?? 1e3
3891
- });
3892
- const response = await this.s3Client.send(command);
3893
- console.log(`Got S3 response for ${prefix}:`, {
3894
- keyCount: response.KeyCount,
3895
- contentsLength: response.Contents?.length || 0,
3896
- hasContinuation: !!response.NextContinuationToken
3897
- });
3898
- if (response.Contents) {
3899
- if (response.Contents.length > 0) {
3900
- console.log("Sample Keys:", response.Contents.slice(0, 3).map((obj) => obj.Key));
3901
- }
3902
- for (const obj of response.Contents) {
3903
- if (obj.Key && obj.Key.endsWith("playlist.m3u8")) {
3904
- if (obj.Key.includes("missed_qchecks")) {
3905
- console.log(`Skipping missed_qchecks path: ${obj.Key}`);
3906
- continue;
3907
- }
3908
- playlists.push(`s3://${this.config.s3Config.bucketName}/${obj.Key}`);
3909
- }
3910
- }
3911
- }
3912
- continuationToken = response.NextContinuationToken;
3913
- if (maxKeys && playlists.length >= maxKeys) {
3914
- break;
3915
- }
3916
- } while (continuationToken && (!maxKeys || playlists.length < maxKeys));
3917
- console.log(`Found ${playlists.length} HLS playlists in ${prefix}`);
3918
- if (playlists.length > 0) {
3919
- console.log("First playlist URI:", playlists[0]);
3920
- }
3921
- return playlists;
4242
+ return await this.apiClient.listS3Clips(params);
3922
4243
  } catch (error) {
3923
- console.error(`Error listing S3 objects with prefix '${prefix}':`, error);
4244
+ console.error("[S3ClipsService] Error listing clips:", error);
3924
4245
  return [];
3925
4246
  }
3926
4247
  }
3927
4248
  /**
3928
- * Fetches and extracts cycle time from metadata.json with deduplication
4249
+ * Get metadata cycle time
3929
4250
  */
3930
4251
  async getMetadataCycleTime(playlistUri) {
3931
- const deduplicationKey = `metadata-cycle-time:${playlistUri}`;
3932
- return this.requestCache.deduplicate(
3933
- deduplicationKey,
3934
- () => this.executeGetMetadataCycleTime(playlistUri),
3935
- "MetadataCycleTime"
3936
- );
3937
- }
3938
- /**
3939
- * Internal implementation of metadata cycle time fetching
3940
- */
3941
- async executeGetMetadataCycleTime(playlistUri) {
3942
4252
  try {
3943
- const metadataUri = playlistUri.replace(/playlist\.m3u8$/, "metadata.json");
3944
- const url = new URL(metadataUri);
3945
- const bucket = url.hostname;
3946
- const key = url.pathname.substring(1);
3947
- console.log(`[S3ClipsService] Fetching metadata cycle time for: ${key}`);
3948
- const command = new clientS3.GetObjectCommand({
3949
- Bucket: bucket,
3950
- Key: key
3951
- });
3952
- const response = await this.s3Client.send(command);
3953
- if (!response.Body) {
3954
- console.warn(`Empty response body for metadata file: ${key}`);
3955
- return null;
3956
- }
3957
- const metadataContent = await response.Body.transformToString();
3958
- const metadata = JSON.parse(metadataContent);
3959
- const cycleTimeFrames = metadata?.original_task_metadata?.cycle_time;
3960
- if (typeof cycleTimeFrames === "number") {
3961
- return cycleTimeFrames;
3962
- }
3963
- return null;
4253
+ return await this.apiClient.getMetadataCycleTime(playlistUri);
3964
4254
  } catch (error) {
3965
- console.error(`Error fetching or parsing metadata:`, error);
4255
+ console.error("[S3ClipsService] Error fetching metadata cycle time:", error);
3966
4256
  return null;
3967
4257
  }
3968
4258
  }
3969
4259
  /**
3970
- * Control prefetch mode to prevent metadata fetching during background operations
4260
+ * Control prefetch mode
3971
4261
  */
3972
4262
  setPrefetchMode(enabled) {
3973
4263
  this.isPrefetching = enabled;
3974
- console.log(`[S3ClipsService] Prefetch mode ${enabled ? "enabled" : "disabled"} - metadata fetching ${enabled ? "blocked" : "allowed"}`);
4264
+ console.log(`[S3ClipsService] Prefetch mode ${enabled ? "enabled" : "disabled"}`);
3975
4265
  }
3976
4266
  /**
3977
- * Fetches full metadata including timestamps with deduplication
4267
+ * Get full metadata
3978
4268
  */
3979
4269
  async getFullMetadata(playlistUri) {
3980
4270
  if (this.isIndexBuilding || this.isPrefetching) {
3981
- console.warn(`[S3ClipsService] Skipping metadata fetch - building: ${this.isIndexBuilding}, prefetching: ${this.isPrefetching}`);
4271
+ console.warn("[S3ClipsService] Skipping metadata - operation in progress");
3982
4272
  return null;
3983
4273
  }
3984
4274
  if (this.currentMetadataFetches >= this.MAX_CONCURRENT_METADATA) {
3985
- console.warn(`[S3ClipsService] Skipping metadata - max concurrent fetches (${this.MAX_CONCURRENT_METADATA}) reached`);
4275
+ console.warn("[S3ClipsService] Skipping metadata - max concurrent fetches reached");
3986
4276
  return null;
3987
4277
  }
3988
4278
  this.currentMetadataFetches++;
3989
4279
  try {
3990
- const deduplicationKey = `full-metadata:${playlistUri}`;
3991
- return await this.requestCache.deduplicate(
3992
- deduplicationKey,
3993
- () => this.executeGetFullMetadata(playlistUri),
3994
- "FullMetadata"
3995
- );
3996
- } finally {
3997
- this.currentMetadataFetches--;
3998
- }
3999
- }
4000
- /**
4001
- * Internal implementation of full metadata fetching
4002
- */
4003
- async executeGetFullMetadata(playlistUri) {
4004
- try {
4005
- const metadataUri = playlistUri.replace(/playlist\.m3u8$/, "metadata.json");
4006
- const url = new URL(metadataUri);
4007
- const bucket = url.hostname;
4008
- const key = url.pathname.substring(1);
4009
- console.log(`[S3ClipsService] Fetching full metadata for: ${key}`);
4010
- const command = new clientS3.GetObjectCommand({
4011
- Bucket: bucket,
4012
- Key: key
4013
- });
4014
- const response = await this.s3Client.send(command);
4015
- if (!response.Body) {
4016
- console.warn(`Empty response body for metadata file: ${key}`);
4017
- return null;
4018
- }
4019
- const metadataContent = await response.Body.transformToString();
4020
- const metadata = JSON.parse(metadataContent);
4021
- return metadata;
4280
+ return await this.apiClient.getMetadata(playlistUri);
4022
4281
  } catch (error) {
4023
- console.error(`Error fetching or parsing metadata:`, error);
4282
+ console.error("[S3ClipsService] Error fetching metadata:", error);
4024
4283
  return null;
4284
+ } finally {
4285
+ this.currentMetadataFetches--;
4025
4286
  }
4026
4287
  }
4027
4288
  /**
4028
- * Converts S3 URI to CloudFront URL
4289
+ * Convert S3 URI to CloudFront URL
4290
+ * URLs from API are already signed
4029
4291
  */
4030
4292
  s3UriToCloudfront(s3Uri) {
4031
- const url = new URL(s3Uri);
4032
- const key = url.pathname.startsWith("/") ? url.pathname.substring(1) : url.pathname;
4033
- const cloudfrontUrl = `${this.config.s3Config.cloudFrontDomain}/${key}`;
4034
- return cloudfrontUrl;
4293
+ if (s3Uri.startsWith("http")) {
4294
+ return s3Uri;
4295
+ }
4296
+ console.warn("[S3ClipsService] Unexpected S3 URI in secure mode:", s3Uri);
4297
+ return s3Uri;
4035
4298
  }
4036
4299
  /**
4037
- * Gets SOP categories for a specific workspace
4300
+ * Get SOP categories for workspace
4038
4301
  */
4039
4302
  getSOPCategories(workspaceId) {
4040
4303
  const sopConfig = this.config.s3Config?.sopCategories;
@@ -4046,375 +4309,85 @@ var S3ClipsService = class {
4046
4309
  }
4047
4310
  async getClipCounts(workspaceId, date, shiftId, buildIndex) {
4048
4311
  if (!isValidShiftId(shiftId)) {
4049
- console.error(`[S3ClipsService] getClipCounts - Invalid shift ID: ${shiftId}. Must be 0 (day) or 1 (night)`);
4050
- return buildIndex ? { counts: {}, videoIndex: { byCategory: /* @__PURE__ */ new Map(), allVideos: [], counts: {}, workspaceId, date, shiftId: "0", lastUpdated: /* @__PURE__ */ new Date() } } : {};
4051
- }
4052
- const deduplicationKey = `clip-counts:${workspaceId}:${date}:${shiftId}:${buildIndex ? "with-index" : "counts-only"}`;
4053
- return this.requestCache.deduplicate(
4054
- deduplicationKey,
4055
- () => this.executeGetClipCounts(workspaceId, date, shiftId, buildIndex),
4056
- "ClipCounts"
4057
- );
4058
- }
4059
- /**
4060
- * Internal implementation of clip counts fetching
4061
- */
4062
- async executeGetClipCounts(workspaceId, date, shiftId, buildIndex) {
4063
- if (buildIndex) {
4064
- this.isIndexBuilding = true;
4065
- console.log(`[S3ClipsService] Starting index building - metadata fetching disabled`);
4312
+ console.error(`[S3ClipsService] Invalid shift ID: ${shiftId}`);
4313
+ return buildIndex ? { counts: {}, videoIndex: this.createEmptyVideoIndex(workspaceId, date, shiftId.toString()) } : {};
4066
4314
  }
4067
4315
  try {
4068
- const basePrefix = `sop_violations/${workspaceId}/${date}/${shiftId}/`;
4069
- const counts = { total: 0 };
4070
- const categoryFolders = [
4071
- "idle_time",
4072
- "low_value",
4073
- "sop_deviation",
4074
- "missing_quality_check",
4075
- "best_cycle_time",
4076
- "worst_cycle_time",
4077
- "long_cycle_time",
4078
- "cycle_completion",
4079
- "bottleneck"
4080
- ];
4081
- const shiftName = shiftId === 0 || shiftId === "0" ? "Day" : "Night";
4082
- console.log(`[S3ClipsService] ${buildIndex ? "Building video index and counting" : "Fast counting"} clips for ${workspaceId} on ${date}, shift ${shiftId} (${shiftName} Shift)`);
4083
- const startTime = performance.now();
4084
- const videoIndex = buildIndex ? {
4085
- byCategory: /* @__PURE__ */ new Map(),
4086
- allVideos: [],
4087
- counts: {},
4088
- workspaceId,
4089
- date,
4090
- shiftId: shiftId.toString(),
4091
- lastUpdated: /* @__PURE__ */ new Date(),
4092
- _debugId: `vid_${Date.now()}_${Math.random().toString(36).substring(7)}`
4093
- } : null;
4094
- const countPromises = categoryFolders.map(async (category) => {
4095
- const categoryPrefix = `${basePrefix}${category}/videos/`;
4096
- const categoryVideos = [];
4097
- try {
4098
- if (buildIndex) {
4099
- const command = new clientS3.ListObjectsV2Command({
4100
- Bucket: this.config.s3Config.bucketName,
4101
- Prefix: categoryPrefix,
4102
- MaxKeys: 1e3
4103
- });
4104
- let continuationToken;
4105
- do {
4106
- if (continuationToken) {
4107
- command.input.ContinuationToken = continuationToken;
4108
- }
4109
- const response = await this.s3Client.send(command);
4110
- if (response.Contents) {
4111
- for (const obj of response.Contents) {
4112
- if (obj.Key && obj.Key.endsWith("playlist.m3u8")) {
4113
- if (obj.Key.includes("missed_qchecks")) {
4114
- continue;
4115
- }
4116
- const s3Uri = `s3://${this.config.s3Config.bucketName}/${obj.Key}`;
4117
- const sopCategories = this.getSOPCategories(workspaceId);
4118
- const parsedInfo = parseS3Uri(s3Uri, sopCategories);
4119
- const belongsToCategory = parsedInfo && (parsedInfo.type === category || // Handle specific mismatches between folder names and parsed types
4120
- category === "cycle_completion" && parsedInfo.type === "cycle_completion" || category === "sop_deviation" && parsedInfo.type === "missing_quality_check" || category === "missing_quality_check" && parsedInfo.type === "missing_quality_check" || category === "idle_time" && parsedInfo.type === "low_value" || category === "low_value" && parsedInfo.type === "low_value");
4121
- if (belongsToCategory) {
4122
- const videoEntry = {
4123
- uri: s3Uri,
4124
- category: parsedInfo.type,
4125
- // Use the parsed type, not the folder name
4126
- timestamp: parsedInfo.timestamp,
4127
- videoId: `${workspaceId}-${parsedInfo.timestamp}`,
4128
- workspaceId,
4129
- date,
4130
- shiftId: shiftId.toString()
4131
- };
4132
- categoryVideos.push(videoEntry);
4133
- }
4134
- }
4135
- }
4136
- }
4137
- continuationToken = response.NextContinuationToken;
4138
- } while (continuationToken);
4139
- categoryVideos.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
4140
- if (categoryVideos.length > 0) {
4141
- console.log(`[S3ClipsService] Found ${categoryVideos.length} videos for category '${category}' (parsed types: ${[...new Set(categoryVideos.map((v) => v.category))].join(", ")})`);
4142
- }
4143
- return { category, count: categoryVideos.length, videos: categoryVideos };
4144
- } else {
4145
- const command = new clientS3.ListObjectsV2Command({
4146
- Bucket: this.config.s3Config.bucketName,
4147
- Prefix: categoryPrefix,
4148
- Delimiter: "/",
4149
- MaxKeys: 1e3
4150
- });
4151
- let folderCount = 0;
4152
- let continuationToken;
4153
- do {
4154
- if (continuationToken) {
4155
- command.input.ContinuationToken = continuationToken;
4156
- }
4157
- const response = await this.s3Client.send(command);
4158
- if (response.CommonPrefixes) {
4159
- folderCount += response.CommonPrefixes.length;
4160
- }
4161
- continuationToken = response.NextContinuationToken;
4162
- } while (continuationToken);
4163
- return { category, count: folderCount, videos: [] };
4164
- }
4165
- } catch (error) {
4166
- console.error(`Error ${buildIndex ? "building index for" : "counting folders for"} category ${category}:`, error);
4167
- return { category, count: 0, videos: [] };
4168
- }
4169
- });
4170
- const results = await Promise.all(countPromises);
4171
- for (const { category, count, videos } of results) {
4172
- counts[category] = count;
4173
- counts.total += count;
4174
- if (buildIndex && videoIndex && videos) {
4175
- if (videos.length > 0) {
4176
- const parsedType = videos[0].category;
4177
- videoIndex.byCategory.set(parsedType, videos);
4178
- console.log(`[S3ClipsService] Indexed ${videos.length} videos under parsed type '${parsedType}'`);
4179
- if (category !== parsedType) {
4180
- videoIndex.byCategory.set(category, videos);
4181
- console.log(`[S3ClipsService] Created alias: S3 folder '${category}' -> parsed type '${parsedType}' (${videos.length} videos)`);
4182
- }
4183
- }
4184
- videoIndex.allVideos.push(...videos);
4185
- }
4186
- }
4187
- if (buildIndex && videoIndex) {
4188
- videoIndex.allVideos.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
4189
- videoIndex.counts = { ...counts };
4190
- }
4191
- const elapsed = performance.now() - startTime;
4192
- console.log(`[S3ClipsService] ${buildIndex ? "Video index and counts" : "Clip counts"} completed in ${elapsed.toFixed(2)}ms - Total: ${counts.total}`);
4193
- if (buildIndex && videoIndex) {
4194
- console.log(`[S3ClipsService] Final video index summary:`);
4195
- console.log(` - VideoIndex ID: ${videoIndex._debugId}`);
4196
- console.log(` - Total videos in allVideos: ${videoIndex.allVideos.length}`);
4197
- console.log(` - Categories in byCategory Map: ${Array.from(videoIndex.byCategory.keys()).join(", ")}`);
4198
- for (const [cat, vids] of videoIndex.byCategory.entries()) {
4199
- console.log(` - '${cat}': ${vids.length} videos`);
4200
- }
4201
- return { counts, videoIndex };
4202
- }
4203
- return counts;
4204
- } finally {
4205
4316
  if (buildIndex) {
4206
- this.isIndexBuilding = false;
4207
- console.log(`[S3ClipsService] Index building complete - metadata fetching re-enabled`);
4317
+ const result = await this.apiClient.getClipCountsWithIndex(workspaceId, date, shiftId);
4318
+ const cacheKey = `clip-counts:${workspaceId}:${date}:${shiftId}`;
4319
+ await smartVideoCache.setClipCounts(cacheKey, result);
4320
+ return result;
4321
+ } else {
4322
+ const counts = await this.apiClient.getClipCounts(workspaceId, date, shiftId);
4323
+ return counts;
4208
4324
  }
4325
+ } catch (error) {
4326
+ console.error("[S3ClipsService] Error fetching clip counts:", error);
4327
+ return buildIndex ? { counts: {}, videoIndex: this.createEmptyVideoIndex(workspaceId, date, shiftId.toString()) } : {};
4209
4328
  }
4210
4329
  }
4211
4330
  async getClipCountsCacheFirst(workspaceId, date, shiftId, buildIndex) {
4212
4331
  const cacheKey = `clip-counts:${workspaceId}:${date}:${shiftId}`;
4213
4332
  const cachedResult = await smartVideoCache.getClipCounts(cacheKey);
4214
4333
  if (cachedResult) {
4215
- console.log(`[S3ClipsService] Using cached clip counts for ${workspaceId}`);
4216
- if (buildIndex) {
4217
- return cachedResult;
4218
- } else {
4219
- return cachedResult.counts;
4220
- }
4334
+ console.log("[S3ClipsService] Using cached clip counts");
4335
+ return buildIndex ? cachedResult : cachedResult.counts;
4221
4336
  }
4222
- console.log(`[S3ClipsService] Cache miss - fetching clip counts for ${workspaceId}`);
4337
+ console.log("[S3ClipsService] Cache miss - fetching from API");
4223
4338
  return buildIndex ? this.getClipCounts(workspaceId, date, shiftId, true) : this.getClipCounts(workspaceId, date, shiftId);
4224
4339
  }
4225
4340
  /**
4226
- * Get first clip for a specific category with deduplication
4341
+ * Get first clip for category
4227
4342
  */
4228
4343
  async getFirstClipForCategory(workspaceId, date, shiftId, category) {
4229
4344
  if (!isValidShiftId(shiftId)) {
4230
- console.error(`[S3ClipsService] getFirstClipForCategory - Invalid shift ID: ${shiftId}. Must be 0 (day) or 1 (night)`);
4345
+ console.error(`[S3ClipsService] Invalid shift ID: ${shiftId}`);
4231
4346
  return null;
4232
4347
  }
4233
- const deduplicationKey = `first-clip:${workspaceId}:${date}:${shiftId}:${category}`;
4234
- return this.requestCache.deduplicate(
4235
- deduplicationKey,
4236
- () => this.executeGetFirstClipForCategory(workspaceId, date, shiftId, category),
4237
- "FirstClip"
4238
- );
4239
- }
4240
- /**
4241
- * Internal implementation of first clip fetching
4242
- */
4243
- async executeGetFirstClipForCategory(workspaceId, date, shiftId, category) {
4244
- const categoryPrefix = `sop_violations/${workspaceId}/${date}/${shiftId}/${category}/videos/`;
4245
4348
  try {
4246
- const command = new clientS3.ListObjectsV2Command({
4247
- Bucket: this.config.s3Config.bucketName,
4248
- Prefix: categoryPrefix,
4249
- MaxKeys: 10
4250
- // Small limit since we only need one
4251
- });
4252
- const response = await this.s3Client.send(command);
4253
- if (response.Contents) {
4254
- const playlistObj = response.Contents.find((obj) => obj.Key?.endsWith("playlist.m3u8"));
4255
- if (playlistObj && playlistObj.Key) {
4256
- const s3Uri = `s3://${this.config.s3Config.bucketName}/${playlistObj.Key}`;
4257
- const sopCategories = this.getSOPCategories(workspaceId);
4258
- const parsedInfo = parseS3Uri(s3Uri, sopCategories);
4259
- if (parsedInfo) {
4260
- const cloudfrontUrl = this.s3UriToCloudfront(s3Uri);
4261
- return {
4262
- id: `${workspaceId}-${date}-${shiftId}-${category}-first`,
4263
- src: cloudfrontUrl,
4264
- ...parsedInfo,
4265
- originalUri: s3Uri
4266
- };
4267
- }
4268
- }
4269
- }
4349
+ return await this.apiClient.getFirstClipForCategory(workspaceId, date, shiftId, category);
4270
4350
  } catch (error) {
4271
- console.error(`Error getting first clip for category ${category}:`, error);
4351
+ console.error("[S3ClipsService] Error fetching first clip:", error);
4352
+ return null;
4272
4353
  }
4273
- return null;
4274
4354
  }
4275
4355
  /**
4276
- * Get a specific video from the pre-built video index - O(1) lookup performance
4356
+ * Get video from index (for compatibility)
4277
4357
  */
4278
4358
  async getVideoFromIndex(videoIndex, category, index, includeCycleTime = true, includeMetadata = true) {
4359
+ const { workspaceId, date, shiftId } = videoIndex;
4360
+ return this.getClipByIndex(
4361
+ workspaceId,
4362
+ date,
4363
+ shiftId,
4364
+ category,
4365
+ index,
4366
+ includeCycleTime,
4367
+ includeMetadata
4368
+ );
4369
+ }
4370
+ /**
4371
+ * Get clip by index
4372
+ */
4373
+ async getClipByIndex(workspaceId, date, shiftId, category, index, includeCycleTime = true, includeMetadata = false) {
4279
4374
  try {
4280
- const categoryVideos = videoIndex.byCategory.get(category);
4281
- if (!categoryVideos || index < 0 || index >= categoryVideos.length) {
4282
- console.warn(`Invalid index ${index} for category '${category}'. Available: ${categoryVideos?.length || 0}`);
4283
- console.warn(`Video index ID: ${videoIndex._debugId || "NO_ID"}`);
4284
- console.warn(`Video index categories available: [${Array.from(videoIndex.byCategory.keys()).join(", ")}]`);
4285
- console.warn(`Video index total videos: ${videoIndex.allVideos.length}`);
4286
- return null;
4287
- }
4288
- const videoEntry = categoryVideos[index];
4289
- return this.processFullVideo(
4290
- videoEntry.uri,
4375
+ return await this.apiClient.getClipByIndex(
4376
+ workspaceId,
4377
+ date,
4378
+ shiftId,
4379
+ category,
4291
4380
  index,
4292
- videoEntry.workspaceId,
4293
- videoEntry.date,
4294
- videoEntry.shiftId,
4295
4381
  includeCycleTime,
4296
4382
  includeMetadata
4297
4383
  );
4298
4384
  } catch (error) {
4299
- console.error(`Error getting video from index at ${index} for category ${category}:`, error);
4385
+ console.error("[S3ClipsService] Error fetching clip by index:", error);
4300
4386
  return null;
4301
4387
  }
4302
4388
  }
4303
4389
  /**
4304
- * Get a specific clip by index for a category with deduplication
4305
- * @deprecated Use getVideoFromIndex with a pre-built VideoIndex for better performance
4306
- */
4307
- async getClipByIndex(workspaceId, date, shiftId, category, index, includeCycleTime = true, includeMetadata = false) {
4308
- const deduplicationKey = `clip-by-index:${workspaceId}:${date}:${shiftId}:${category}:${index}:${includeCycleTime}:${includeMetadata}`;
4309
- return this.requestCache.deduplicate(
4310
- deduplicationKey,
4311
- () => this.executeGetClipByIndex(workspaceId, date, shiftId, category, index, includeCycleTime, includeMetadata),
4312
- "ClipByIndex"
4313
- );
4314
- }
4315
- /**
4316
- * Internal implementation of clip by index fetching
4317
- */
4318
- async executeGetClipByIndex(workspaceId, date, shiftId, category, index, includeCycleTime = true, includeMetadata = false) {
4319
- const categoryPrefix = `sop_violations/${workspaceId}/${date}/${shiftId}/${category}/videos/`;
4320
- try {
4321
- const command = new clientS3.ListObjectsV2Command({
4322
- Bucket: this.config.s3Config.bucketName,
4323
- Prefix: categoryPrefix,
4324
- MaxKeys: 1e3
4325
- });
4326
- let playlists = [];
4327
- let continuationToken = void 0;
4328
- do {
4329
- if (continuationToken) {
4330
- command.input.ContinuationToken = continuationToken;
4331
- }
4332
- const response = await this.s3Client.send(command);
4333
- if (response.Contents) {
4334
- for (const obj of response.Contents) {
4335
- if (obj.Key && obj.Key.endsWith("playlist.m3u8")) {
4336
- playlists.push(`s3://${this.config.s3Config.bucketName}/${obj.Key}`);
4337
- }
4338
- }
4339
- }
4340
- continuationToken = response.NextContinuationToken;
4341
- } while (continuationToken && playlists.length < index + 10);
4342
- playlists.sort((a, b) => {
4343
- const parsedA = parseS3Uri(a);
4344
- const parsedB = parseS3Uri(b);
4345
- if (!parsedA || !parsedB) return 0;
4346
- return parsedB.timestamp.localeCompare(parsedA.timestamp);
4347
- });
4348
- if (index < playlists.length) {
4349
- const uri = playlists[index];
4350
- return this.processFullVideo(
4351
- uri,
4352
- index,
4353
- workspaceId,
4354
- date,
4355
- shiftId.toString(),
4356
- includeCycleTime,
4357
- includeMetadata
4358
- );
4359
- }
4360
- } catch (error) {
4361
- console.error(`[getClipByIndex] Error getting clip at index ${index} for category ${category}:`, error);
4362
- }
4363
- return null;
4364
- }
4365
- /**
4366
- * Get one sample video from each category for preloading
4367
- */
4368
- async getSampleVideos(workspaceId, date, shiftId) {
4369
- const basePrefix = `sop_violations/${workspaceId}/${date}/${shiftId}/`;
4370
- const samples = {};
4371
- const sopCategories = this.getSOPCategories(workspaceId);
4372
- const categoriesToSample = sopCategories ? sopCategories.map((cat) => cat.id) : ["low_value", "best_cycle_time", "worst_cycle_time", "long_cycle_time", "cycle_completion"];
4373
- console.log(`[S3ClipsService] Getting sample videos for categories:`, categoriesToSample);
4374
- const samplePromises = categoriesToSample.map(async (category) => {
4375
- const categoryPrefix = `${basePrefix}${category}/`;
4376
- try {
4377
- const command = new clientS3.ListObjectsV2Command({
4378
- Bucket: this.config.s3Config.bucketName,
4379
- Prefix: categoryPrefix,
4380
- MaxKeys: 20
4381
- // Small limit since we only need one
4382
- });
4383
- const response = await this.s3Client.send(command);
4384
- if (response.Contents) {
4385
- const playlistObj = response.Contents.find((obj) => obj.Key?.endsWith("playlist.m3u8"));
4386
- if (playlistObj && playlistObj.Key) {
4387
- const s3Uri = `s3://${this.config.s3Config.bucketName}/${playlistObj.Key}`;
4388
- const parsedInfo = parseS3Uri(s3Uri, sopCategories);
4389
- if (parsedInfo) {
4390
- return {
4391
- category,
4392
- sample: {
4393
- id: `${workspaceId}-${date}-${shiftId}-${category}-sample`,
4394
- src: this.s3UriToCloudfront(s3Uri),
4395
- // Pre-convert to CloudFront URL
4396
- ...parsedInfo,
4397
- originalUri: s3Uri
4398
- }
4399
- };
4400
- }
4401
- }
4402
- }
4403
- } catch (error) {
4404
- console.error(`Error getting sample for category ${category}:`, error);
4405
- }
4406
- return { category, sample: null };
4407
- });
4408
- const sampleResults = await Promise.all(samplePromises);
4409
- for (const { category, sample } of sampleResults) {
4410
- if (sample) {
4411
- samples[category] = sample;
4412
- }
4413
- }
4414
- return samples;
4415
- }
4416
- /**
4417
- * Processes a single video completely
4390
+ * Process full video (for compatibility)
4418
4391
  */
4419
4392
  async processFullVideo(uri, index, workspaceId, date, shiftId, includeCycleTime, includeMetadata = false) {
4420
4393
  const sopCategories = this.getSOPCategories(workspaceId);
@@ -4423,35 +4396,35 @@ var S3ClipsService = class {
4423
4396
  console.warn(`Skipping URI due to parsing failure: ${uri}`);
4424
4397
  return null;
4425
4398
  }
4426
- let cycleTimeSeconds = null;
4427
- let creationTimestamp = void 0;
4399
+ const video = {
4400
+ id: `${workspaceId}-${date}-${shiftId}-${index}`,
4401
+ src: uri,
4402
+ // Already signed from API
4403
+ ...parsedInfo,
4404
+ originalUri: uri
4405
+ };
4428
4406
  if (includeMetadata) {
4429
4407
  const metadata = await this.getFullMetadata(uri);
4430
4408
  if (metadata) {
4431
- if (metadata.original_task_metadata?.cycle_time) {
4432
- cycleTimeSeconds = metadata.original_task_metadata.cycle_time;
4433
- }
4434
- creationTimestamp = metadata.upload_timestamp || metadata.original_task_metadata?.timestamp || metadata.creation_timestamp || metadata[""];
4409
+ video.cycle_time_seconds = metadata.original_task_metadata?.cycle_time;
4410
+ video.creation_timestamp = metadata.upload_timestamp || metadata.original_task_metadata?.timestamp || metadata.creation_timestamp;
4435
4411
  }
4436
- } 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")) {
4437
- cycleTimeSeconds = null;
4438
4412
  }
4439
- const cloudfrontPlaylistUrl = this.s3UriToCloudfront(uri);
4440
- const { type: videoType, timestamp: videoTimestamp } = parsedInfo;
4441
- return {
4442
- id: `${workspaceId}-${date}-${shiftId}-${videoType}-${videoTimestamp.replace(/:/g, "")}-${index}`,
4443
- src: cloudfrontPlaylistUrl,
4444
- // Direct CloudFront playlist URL
4445
- ...parsedInfo,
4446
- cycle_time_seconds: cycleTimeSeconds || void 0,
4447
- creation_timestamp: creationTimestamp
4448
- };
4413
+ return video;
4449
4414
  }
4450
4415
  /**
4451
- * Simplified method to fetch clips based on parameters
4416
+ * Fetch clips (main method for compatibility)
4452
4417
  */
4453
4418
  async fetchClips(params) {
4454
- const { workspaceId, date: inputDate, shift, category, limit, offset = 0, mode, includeCycleTime, includeMetadata, timestampStart, timestampEnd } = params;
4419
+ const {
4420
+ workspaceId,
4421
+ date: inputDate,
4422
+ shift,
4423
+ category,
4424
+ limit,
4425
+ offset = 0,
4426
+ mode
4427
+ } = params;
4455
4428
  if (!workspaceId) {
4456
4429
  throw new Error("Valid Workspace ID is required");
4457
4430
  }
@@ -4469,88 +4442,66 @@ var S3ClipsService = class {
4469
4442
  const { shiftId: currentShiftId } = getCurrentShift2(this.config.dateTimeConfig?.defaultTimezone);
4470
4443
  shiftId = currentShiftId;
4471
4444
  }
4472
- console.log(`S3ClipsService: Fetching clips for workspace ${workspaceId} on date ${date}, shift ${shiftId}`);
4445
+ console.log(`[S3ClipsService] Fetching clips for workspace ${workspaceId}`);
4473
4446
  if (mode === "summary") {
4474
4447
  const counts = await this.getClipCounts(workspaceId, date, shiftId.toString());
4475
4448
  return { counts, samples: {} };
4476
4449
  }
4477
4450
  const effectiveLimit = limit || this.defaultLimitPerCategory;
4478
- const s3Uris = await this.listS3Clips({
4451
+ const result = await this.getVideosPage(
4479
4452
  workspaceId,
4480
4453
  date,
4481
4454
  shiftId,
4482
- maxKeys: category ? effectiveLimit * 2 : void 0
4483
- // Fetch extra for filtering
4484
- });
4485
- if (s3Uris.length === 0) {
4486
- console.log(`S3ClipsService: No HLS playlists found`);
4487
- return [];
4488
- }
4489
- let filteredUris = s3Uris;
4490
- if (category) {
4491
- filteredUris = s3Uris.filter((uri) => {
4492
- const parsedInfo = parseS3Uri(uri);
4493
- if (!parsedInfo) return false;
4494
- if (category === "long_cycle_time") {
4495
- return parsedInfo.type === "long_cycle_time" || parsedInfo.type === "bottleneck" && parsedInfo.description.toLowerCase().includes("cycle time");
4496
- }
4497
- return parsedInfo.type === category;
4498
- });
4499
- filteredUris = filteredUris.slice(offset, offset + effectiveLimit);
4500
- }
4501
- const videoPromises = filteredUris.map(async (uri, index) => {
4502
- return this.processFullVideo(
4503
- uri,
4504
- index,
4455
+ category || "all",
4456
+ effectiveLimit
4457
+ );
4458
+ return result.videos;
4459
+ }
4460
+ /**
4461
+ * Get videos page using pagination API
4462
+ */
4463
+ async getVideosPage(workspaceId, date, shiftId, category, pageSize = 5, startAfter) {
4464
+ try {
4465
+ return await this.apiClient.getVideosPage(
4505
4466
  workspaceId,
4506
4467
  date,
4507
- shiftId.toString(),
4508
- includeCycleTime || false,
4509
- includeMetadata || false
4510
- // Never fetch metadata for timestamp filtering to prevent flooding
4468
+ shiftId,
4469
+ category,
4470
+ pageSize,
4471
+ startAfter
4511
4472
  );
4512
- });
4513
- const videoResults = await Promise.all(videoPromises);
4514
- let videos = videoResults.filter((v) => v !== null);
4515
- if (timestampStart || timestampEnd) {
4516
- videos = videos.filter((video) => {
4517
- if (!video.creation_timestamp) return false;
4518
- const videoTimestamp = new Date(video.creation_timestamp).getTime();
4519
- if (timestampStart && timestampEnd) {
4520
- const start = new Date(timestampStart).getTime();
4521
- const end = new Date(timestampEnd).getTime();
4522
- return videoTimestamp >= start && videoTimestamp <= end;
4523
- } else if (timestampStart) {
4524
- const start = new Date(timestampStart).getTime();
4525
- return videoTimestamp >= start;
4526
- } else if (timestampEnd) {
4527
- const end = new Date(timestampEnd).getTime();
4528
- return videoTimestamp <= end;
4529
- }
4530
- return true;
4531
- });
4532
- }
4533
- videos.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
4534
- const cacheKey = `videos:${workspaceId}:${date}:${shiftId}:${category || "all"}`;
4535
- if (videos.length > 0) {
4536
- smartVideoCache.setVideos(cacheKey, videos);
4473
+ } catch (error) {
4474
+ console.error("[S3ClipsService] Error fetching videos page:", error);
4475
+ return { videos: [], hasMore: false };
4537
4476
  }
4538
- return videos;
4539
4477
  }
4540
4478
  /**
4541
- * Cleanup method for proper resource management
4479
+ * Create empty video index for compatibility
4480
+ */
4481
+ createEmptyVideoIndex(workspaceId, date, shiftId) {
4482
+ return {
4483
+ byCategory: /* @__PURE__ */ new Map(),
4484
+ allVideos: [],
4485
+ counts: {},
4486
+ workspaceId,
4487
+ date,
4488
+ shiftId,
4489
+ lastUpdated: /* @__PURE__ */ new Date(),
4490
+ _debugId: `empty_${Date.now()}`
4491
+ };
4492
+ }
4493
+ /**
4494
+ * Cleanup
4542
4495
  */
4543
4496
  dispose() {
4544
- console.log("[S3ClipsService] Disposing service and clearing request cache");
4545
- this.requestCache.clear();
4497
+ console.log("[S3ClipsService] Disposing service");
4498
+ this.apiClient.dispose();
4546
4499
  }
4547
4500
  /**
4548
- * Get service statistics for monitoring
4501
+ * Get statistics
4549
4502
  */
4550
4503
  getStats() {
4551
- return {
4552
- requestCache: this.requestCache.getStats()
4553
- };
4504
+ return this.apiClient.getStats();
4554
4505
  }
4555
4506
  };
4556
4507
 
@@ -8465,6 +8416,16 @@ function useTicketHistory(companyId) {
8465
8416
  throw err;
8466
8417
  }
8467
8418
  }, [refreshTickets]);
8419
+ const updateTicketServicedStatus = React19.useCallback(async (ticketId, serviced) => {
8420
+ setError(null);
8421
+ try {
8422
+ await TicketHistoryService.updateTicketServicedStatus(ticketId, serviced);
8423
+ await refreshTickets();
8424
+ } catch (err) {
8425
+ setError(err instanceof Error ? err.message : "Failed to update ticket serviced status");
8426
+ throw err;
8427
+ }
8428
+ }, [refreshTickets]);
8468
8429
  React19.useEffect(() => {
8469
8430
  if (companyId) {
8470
8431
  refreshTickets();
@@ -8476,7 +8437,8 @@ function useTicketHistory(companyId) {
8476
8437
  error,
8477
8438
  createTicket,
8478
8439
  refreshTickets,
8479
- updateTicketStatus
8440
+ updateTicketStatus,
8441
+ updateTicketServicedStatus
8480
8442
  };
8481
8443
  }
8482
8444
 
@@ -11628,7 +11590,8 @@ var usePrefetchClipCounts = ({
11628
11590
  date,
11629
11591
  shift,
11630
11592
  enabled = true,
11631
- buildIndex = true,
11593
+ buildIndex = false,
11594
+ // Default to false for cost efficiency
11632
11595
  subscriberId
11633
11596
  }) => {
11634
11597
  const dashboardConfig = useDashboardConfig();
@@ -11716,12 +11679,12 @@ var usePrefetchClipCounts = ({
11716
11679
  }
11717
11680
  },
11718
11681
  onRenderReady: (key, newData) => {
11719
- console.log(`[usePrefetchClipCounts] Render ready with ${Object.values(newData.counts).reduce((sum, count) => sum + count, 0)} total clips`);
11682
+ console.log(`[usePrefetchClipCounts] Render ready with ${Object.values(newData.counts || {}).reduce((sum, count) => sum + count, 0)} total clips`);
11720
11683
  setData(newData);
11721
11684
  setError(null);
11722
11685
  },
11723
11686
  onFullyIndexed: (key, newData) => {
11724
- console.log(`[usePrefetchClipCounts] Fully indexed with ${newData.videoIndex.allVideos.length} videos`);
11687
+ console.log(`[usePrefetchClipCounts] Fully indexed with ${newData.videoIndex?.allVideos?.length || 0} videos`);
11725
11688
  setData(newData);
11726
11689
  setError(null);
11727
11690
  },
@@ -12127,6 +12090,140 @@ function useWorkspaceNavigation() {
12127
12090
  getWorkspaceNavigationParams: getWorkspaceNavigationParams2
12128
12091
  };
12129
12092
  }
12093
+ function useWorkspaceHealth(options = {}) {
12094
+ const [workspaces, setWorkspaces] = React19.useState([]);
12095
+ const [summary, setSummary] = React19.useState(null);
12096
+ const [loading, setLoading] = React19.useState(true);
12097
+ const [error, setError] = React19.useState(null);
12098
+ const unsubscribeRef = React19.useRef(null);
12099
+ const refreshIntervalRef = React19.useRef(null);
12100
+ const {
12101
+ enableRealtime = true,
12102
+ refreshInterval = 3e4,
12103
+ // 30 seconds default
12104
+ ...filterOptions
12105
+ } = options;
12106
+ const fetchData = React19.useCallback(async () => {
12107
+ try {
12108
+ setError(null);
12109
+ workspaceHealthService.clearCache();
12110
+ const [workspacesData, summaryData] = await Promise.all([
12111
+ workspaceHealthService.getWorkspaceHealthStatus(filterOptions),
12112
+ workspaceHealthService.getHealthSummary(filterOptions.lineId, filterOptions.companyId)
12113
+ ]);
12114
+ setWorkspaces(workspacesData);
12115
+ setSummary(summaryData);
12116
+ } catch (err) {
12117
+ console.error("Error fetching workspace health:", err);
12118
+ setError(err);
12119
+ } finally {
12120
+ setLoading(false);
12121
+ }
12122
+ }, [filterOptions.lineId, filterOptions.companyId, filterOptions.status, filterOptions.searchTerm, filterOptions.sortBy, filterOptions.sortOrder]);
12123
+ const handleRealtimeUpdate = React19.useCallback(async (data) => {
12124
+ try {
12125
+ const [workspacesData, summaryData] = await Promise.all([
12126
+ workspaceHealthService.getWorkspaceHealthStatus(filterOptions),
12127
+ workspaceHealthService.getHealthSummary(filterOptions.lineId, filterOptions.companyId)
12128
+ ]);
12129
+ setWorkspaces(workspacesData);
12130
+ setSummary(summaryData);
12131
+ } catch (err) {
12132
+ console.error("Error updating real-time health data:", err);
12133
+ }
12134
+ }, [filterOptions]);
12135
+ React19.useEffect(() => {
12136
+ fetchData();
12137
+ if (refreshInterval > 0) {
12138
+ refreshIntervalRef.current = setInterval(fetchData, 1e4);
12139
+ }
12140
+ if (enableRealtime) {
12141
+ unsubscribeRef.current = workspaceHealthService.subscribeToHealthUpdates(
12142
+ handleRealtimeUpdate,
12143
+ { lineId: filterOptions.lineId, companyId: filterOptions.companyId }
12144
+ );
12145
+ }
12146
+ return () => {
12147
+ if (refreshIntervalRef.current) {
12148
+ clearInterval(refreshIntervalRef.current);
12149
+ }
12150
+ if (unsubscribeRef.current) {
12151
+ unsubscribeRef.current();
12152
+ }
12153
+ };
12154
+ }, [fetchData, enableRealtime, refreshInterval, handleRealtimeUpdate, filterOptions.lineId, filterOptions.companyId]);
12155
+ return {
12156
+ workspaces,
12157
+ summary,
12158
+ loading,
12159
+ error,
12160
+ refetch: fetchData
12161
+ };
12162
+ }
12163
+ function useWorkspaceHealthById(workspaceId, options = {}) {
12164
+ const [workspace, setWorkspace] = React19.useState(null);
12165
+ const [metrics2, setMetrics] = React19.useState(null);
12166
+ const [loading, setLoading] = React19.useState(true);
12167
+ const [error, setError] = React19.useState(null);
12168
+ const unsubscribeRef = React19.useRef(null);
12169
+ const refreshIntervalRef = React19.useRef(null);
12170
+ const { enableRealtime = true, refreshInterval = 3e4 } = options;
12171
+ const fetchData = React19.useCallback(async () => {
12172
+ if (!workspaceId) {
12173
+ setWorkspace(null);
12174
+ setMetrics(null);
12175
+ setLoading(false);
12176
+ return;
12177
+ }
12178
+ try {
12179
+ setLoading(true);
12180
+ setError(null);
12181
+ const [workspaceData, metricsData] = await Promise.all([
12182
+ workspaceHealthService.getWorkspaceHealthById(workspaceId),
12183
+ workspaceHealthService.getHealthMetrics(workspaceId)
12184
+ ]);
12185
+ setWorkspace(workspaceData);
12186
+ setMetrics(metricsData);
12187
+ } catch (err) {
12188
+ console.error("Error fetching workspace health by ID:", err);
12189
+ setError(err);
12190
+ } finally {
12191
+ setLoading(false);
12192
+ }
12193
+ }, [workspaceId]);
12194
+ const handleRealtimeUpdate = React19.useCallback((data) => {
12195
+ if (data.workspace_id === workspaceId) {
12196
+ const updatedWorkspace = workspaceHealthService["processHealthStatus"](data);
12197
+ setWorkspace(updatedWorkspace);
12198
+ }
12199
+ }, [workspaceId]);
12200
+ React19.useEffect(() => {
12201
+ fetchData();
12202
+ if (refreshInterval > 0) {
12203
+ refreshIntervalRef.current = setInterval(fetchData, 1e4);
12204
+ }
12205
+ if (enableRealtime && workspaceId) {
12206
+ unsubscribeRef.current = workspaceHealthService.subscribeToHealthUpdates(
12207
+ handleRealtimeUpdate
12208
+ );
12209
+ }
12210
+ return () => {
12211
+ if (refreshIntervalRef.current) {
12212
+ clearInterval(refreshIntervalRef.current);
12213
+ }
12214
+ if (unsubscribeRef.current) {
12215
+ unsubscribeRef.current();
12216
+ }
12217
+ };
12218
+ }, [fetchData, enableRealtime, refreshInterval, handleRealtimeUpdate, workspaceId]);
12219
+ return {
12220
+ workspace,
12221
+ metrics: metrics2,
12222
+ loading,
12223
+ error,
12224
+ refetch: fetchData
12225
+ };
12226
+ }
12130
12227
  function useDateFormatter() {
12131
12228
  const { defaultTimezone, defaultLocale, dateFormatOptions, timeFormatOptions, dateTimeFormatOptions } = useDateTimeConfig();
12132
12229
  const formatDate = React19.useCallback(
@@ -23314,8 +23411,7 @@ var ISTTimer = React19.memo(() => {
23314
23411
  return /* @__PURE__ */ jsxRuntime.jsx(
23315
23412
  TimeDisplay2,
23316
23413
  {
23317
- variant: "minimal",
23318
- className: "text-sm font-medium text-gray-700"
23414
+ variant: "minimal"
23319
23415
  }
23320
23416
  );
23321
23417
  });
@@ -23347,6 +23443,7 @@ var CardFooter2 = (props) => {
23347
23443
  };
23348
23444
  var TicketHistory = ({ companyId }) => {
23349
23445
  const { tickets, loading, error } = useTicketHistory(companyId);
23446
+ const [expandedTickets, setExpandedTickets] = React19.useState(/* @__PURE__ */ new Set());
23350
23447
  const getCategoryIcon = (category) => {
23351
23448
  switch (category) {
23352
23449
  case "general":
@@ -23403,6 +23500,23 @@ var TicketHistory = ({ companyId }) => {
23403
23500
  return "text-gray-600 bg-gray-50";
23404
23501
  }
23405
23502
  };
23503
+ const toggleTicketExpansion = (ticketId) => {
23504
+ setExpandedTickets((prev) => {
23505
+ const newSet = new Set(prev);
23506
+ if (newSet.has(ticketId)) {
23507
+ newSet.delete(ticketId);
23508
+ } else {
23509
+ newSet.add(ticketId);
23510
+ }
23511
+ return newSet;
23512
+ });
23513
+ };
23514
+ const isTicketExpanded = (ticketId) => {
23515
+ return expandedTickets.has(ticketId);
23516
+ };
23517
+ const shouldShowExpandButton = (description) => {
23518
+ return description.length > 100 || description.includes("\n");
23519
+ };
23406
23520
  if (loading) {
23407
23521
  return /* @__PURE__ */ jsxRuntime.jsxs(Card2, { className: "h-full", children: [
23408
23522
  /* @__PURE__ */ jsxRuntime.jsx(CardHeader2, { children: /* @__PURE__ */ jsxRuntime.jsx(CardTitle2, { className: "text-lg font-semibold text-gray-800", children: "History of Tickets" }) }),
@@ -23443,12 +23557,14 @@ var TicketHistory = ({ companyId }) => {
23443
23557
  tickets.map((ticket, index) => {
23444
23558
  const CategoryIcon = getCategoryIcon(ticket.category);
23445
23559
  const StatusIcon = getStatusIcon(ticket.status);
23560
+ const isExpanded = isTicketExpanded(ticket.id);
23561
+ const showExpandButton = shouldShowExpandButton(ticket.description);
23446
23562
  return /* @__PURE__ */ jsxRuntime.jsxs(
23447
23563
  motion.div,
23448
23564
  {
23449
23565
  initial: { opacity: 0, y: 10 },
23450
23566
  animate: { opacity: 1, y: 0 },
23451
- className: `border-b border-gray-100 p-4 hover:bg-gray-50 transition-colors cursor-pointer ${index === 0 ? "border-t-0" : ""}`,
23567
+ className: `border-b border-gray-100 p-4 hover:bg-gray-50 transition-colors ${index === 0 ? "border-t-0" : ""}`,
23452
23568
  children: [
23453
23569
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-start justify-between mb-2", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 flex-1 min-w-0", children: [
23454
23570
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-1 bg-gray-100 rounded", children: /* @__PURE__ */ jsxRuntime.jsx(CategoryIcon, { className: "h-3 w-3 text-gray-600" }) }),
@@ -23459,9 +23575,42 @@ var TicketHistory = ({ companyId }) => {
23459
23575
  /* @__PURE__ */ jsxRuntime.jsx(StatusIcon, { className: "h-2.5 w-2.5 mr-1" }),
23460
23576
  ticket.status === "submitted" ? "New" : ticket.status.replace("_", " ")
23461
23577
  ] }),
23462
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: `inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${getPriorityColor(ticket.priority)}`, children: ticket.priority === "normal" ? "Standard" : ticket.priority })
23578
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: `inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${getPriorityColor(ticket.priority)}`, children: ticket.priority === "normal" ? "Standard" : ticket.priority }),
23579
+ ticket.serviced && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex items-center px-2 py-1 rounded-full text-xs font-medium text-green-700 bg-green-100 border border-green-200", children: [
23580
+ /* @__PURE__ */ jsxRuntime.jsx(solid.CheckIcon, { className: "h-2.5 w-2.5 mr-1" }),
23581
+ "Serviced"
23582
+ ] })
23583
+ ] }),
23584
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-3", children: [
23585
+ /* @__PURE__ */ jsxRuntime.jsx(AnimatePresence, { mode: "wait", children: /* @__PURE__ */ jsxRuntime.jsx(
23586
+ motion.div,
23587
+ {
23588
+ initial: { opacity: 0 },
23589
+ animate: { opacity: 1 },
23590
+ exit: { opacity: 0 },
23591
+ transition: { duration: 0.2 },
23592
+ children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: `text-xs text-gray-600 leading-relaxed whitespace-pre-wrap ${!isExpanded && showExpandButton ? "line-clamp-2" : ""}`, children: ticket.description })
23593
+ },
23594
+ isExpanded ? "expanded" : "collapsed"
23595
+ ) }),
23596
+ showExpandButton && /* @__PURE__ */ jsxRuntime.jsx(
23597
+ "button",
23598
+ {
23599
+ onClick: (e) => {
23600
+ e.stopPropagation();
23601
+ toggleTicketExpansion(ticket.id);
23602
+ },
23603
+ className: "mt-2 flex items-center gap-1 text-xs text-blue-600 hover:text-blue-700 font-medium transition-colors",
23604
+ children: isExpanded ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
23605
+ /* @__PURE__ */ jsxRuntime.jsx(outline.ChevronUpIcon, { className: "h-3 w-3" }),
23606
+ "Show less"
23607
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
23608
+ /* @__PURE__ */ jsxRuntime.jsx(outline.ChevronDownIcon, { className: "h-3 w-3" }),
23609
+ "Show more"
23610
+ ] })
23611
+ }
23612
+ )
23463
23613
  ] }),
23464
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-600 mb-3 line-clamp-2 leading-relaxed", children: ticket.description }),
23465
23614
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-xs", children: [
23466
23615
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-500 capitalize font-medium", children: ticket.category }),
23467
23616
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 text-gray-500", children: [
@@ -23483,6 +23632,165 @@ var TicketHistory = ({ companyId }) => {
23483
23632
  ] });
23484
23633
  };
23485
23634
  var TicketHistory_default = TicketHistory;
23635
+ var HealthStatusIndicator = ({
23636
+ status,
23637
+ lastUpdated,
23638
+ showLabel = false,
23639
+ showTime = true,
23640
+ size = "md",
23641
+ className = "",
23642
+ inline = true,
23643
+ pulse = true
23644
+ }) => {
23645
+ const getStatusConfig = () => {
23646
+ switch (status) {
23647
+ case "healthy":
23648
+ return {
23649
+ color: "text-green-500",
23650
+ bgColor: "bg-green-500",
23651
+ borderColor: "border-green-500",
23652
+ label: "Healthy",
23653
+ icon: lucideReact.CheckCircle,
23654
+ shouldPulse: pulse
23655
+ };
23656
+ case "unhealthy":
23657
+ return {
23658
+ color: "text-red-500",
23659
+ bgColor: "bg-red-500",
23660
+ borderColor: "border-red-500",
23661
+ label: "Unhealthy",
23662
+ icon: lucideReact.XCircle,
23663
+ shouldPulse: false
23664
+ };
23665
+ case "warning":
23666
+ return {
23667
+ color: "text-yellow-500",
23668
+ bgColor: "bg-yellow-500",
23669
+ borderColor: "border-yellow-500",
23670
+ label: "Warning",
23671
+ icon: lucideReact.AlertTriangle,
23672
+ shouldPulse: true
23673
+ };
23674
+ case "unknown":
23675
+ default:
23676
+ return {
23677
+ color: "text-gray-400",
23678
+ bgColor: "bg-gray-400",
23679
+ borderColor: "border-gray-400",
23680
+ label: "Unknown",
23681
+ icon: lucideReact.Activity,
23682
+ shouldPulse: false
23683
+ };
23684
+ }
23685
+ };
23686
+ const config = getStatusConfig();
23687
+ config.icon;
23688
+ const sizeClasses = {
23689
+ sm: {
23690
+ dot: "h-2 w-2",
23691
+ icon: "h-3 w-3",
23692
+ text: "text-xs",
23693
+ spacing: "gap-1"
23694
+ },
23695
+ md: {
23696
+ dot: "h-3 w-3",
23697
+ icon: "h-4 w-4",
23698
+ text: "text-sm",
23699
+ spacing: "gap-1.5"
23700
+ },
23701
+ lg: {
23702
+ dot: "h-4 w-4",
23703
+ icon: "h-5 w-5",
23704
+ text: "text-base",
23705
+ spacing: "gap-2"
23706
+ }
23707
+ };
23708
+ const currentSize = sizeClasses[size];
23709
+ const containerClasses = clsx(
23710
+ "flex items-center",
23711
+ currentSize.spacing,
23712
+ inline ? "inline-flex" : "flex",
23713
+ className
23714
+ );
23715
+ const dotClasses = clsx(
23716
+ "rounded-full",
23717
+ currentSize.dot,
23718
+ config.bgColor,
23719
+ config.shouldPulse && "animate-pulse"
23720
+ );
23721
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: containerClasses, children: [
23722
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex items-center justify-center", children: [
23723
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: dotClasses }),
23724
+ config.shouldPulse && status === "healthy" && /* @__PURE__ */ jsxRuntime.jsx(
23725
+ "div",
23726
+ {
23727
+ className: clsx(
23728
+ "absolute rounded-full opacity-25",
23729
+ currentSize.dot,
23730
+ config.bgColor,
23731
+ "animate-ping"
23732
+ )
23733
+ }
23734
+ )
23735
+ ] }),
23736
+ showLabel && /* @__PURE__ */ jsxRuntime.jsx("span", { className: clsx(currentSize.text, "font-medium", config.color), children: config.label }),
23737
+ showTime && lastUpdated && /* @__PURE__ */ jsxRuntime.jsx("span", { className: clsx(currentSize.text, "text-gray-500 dark:text-gray-400"), children: lastUpdated })
23738
+ ] });
23739
+ };
23740
+ var DetailedHealthStatus = ({
23741
+ workspaceName,
23742
+ lineName,
23743
+ consecutiveMisses,
23744
+ showDetails = true,
23745
+ ...indicatorProps
23746
+ }) => {
23747
+ const getStatusConfig = () => {
23748
+ switch (indicatorProps.status) {
23749
+ case "healthy":
23750
+ return {
23751
+ bgClass: "bg-green-50 dark:bg-green-900/20",
23752
+ borderClass: "border-green-200 dark:border-green-800"
23753
+ };
23754
+ case "unhealthy":
23755
+ return {
23756
+ bgClass: "bg-red-50 dark:bg-red-900/20",
23757
+ borderClass: "border-red-200 dark:border-red-800"
23758
+ };
23759
+ case "warning":
23760
+ return {
23761
+ bgClass: "bg-yellow-50 dark:bg-yellow-900/20",
23762
+ borderClass: "border-yellow-200 dark:border-yellow-800"
23763
+ };
23764
+ default:
23765
+ return {
23766
+ bgClass: "bg-gray-50 dark:bg-gray-900/20",
23767
+ borderClass: "border-gray-200 dark:border-gray-800"
23768
+ };
23769
+ }
23770
+ };
23771
+ const config = getStatusConfig();
23772
+ return /* @__PURE__ */ jsxRuntime.jsx(
23773
+ "div",
23774
+ {
23775
+ className: clsx(
23776
+ "rounded-lg border p-3",
23777
+ config.bgClass,
23778
+ config.borderClass
23779
+ ),
23780
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between", children: [
23781
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1", children: [
23782
+ showDetails && workspaceName && /* @__PURE__ */ jsxRuntime.jsx("h4", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: workspaceName }),
23783
+ showDetails && lineName && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-0.5", children: lineName }),
23784
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2", children: /* @__PURE__ */ jsxRuntime.jsx(HealthStatusIndicator, { ...indicatorProps, showLabel: true }) })
23785
+ ] }),
23786
+ showDetails && consecutiveMisses !== void 0 && consecutiveMisses > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ml-3 text-right", children: [
23787
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: "Missed" }),
23788
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-bold text-gray-900 dark:text-gray-100", children: consecutiveMisses })
23789
+ ] })
23790
+ ] })
23791
+ }
23792
+ );
23793
+ };
23486
23794
  var LinePdfExportButton = ({
23487
23795
  targetElement,
23488
23796
  fileName = "line-export",
@@ -24605,6 +24913,8 @@ var WorkspaceCard = ({
24605
24913
  cycleTime,
24606
24914
  operators,
24607
24915
  status = "normal",
24916
+ healthStatus,
24917
+ healthLastUpdated,
24608
24918
  onCardClick,
24609
24919
  headerActions,
24610
24920
  footerContent,
@@ -24697,6 +25007,19 @@ var WorkspaceCard = ({
24697
25007
  ] })
24698
25008
  ] })
24699
25009
  ] }),
25010
+ 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: [
25011
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-500 dark:text-gray-400", children: "System Health" }),
25012
+ /* @__PURE__ */ jsxRuntime.jsx(
25013
+ HealthStatusIndicator,
25014
+ {
25015
+ status: healthStatus,
25016
+ lastUpdated: healthLastUpdated,
25017
+ showTime: false,
25018
+ size: "sm",
25019
+ pulse: healthStatus === "healthy"
25020
+ }
25021
+ )
25022
+ ] }) }),
24700
25023
  chartData && chartData.data && chartData.data.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
24701
25024
  "div",
24702
25025
  {
@@ -26234,6 +26557,315 @@ var VideoPlayer = React19__namespace.default.forwardRef(({
26234
26557
  ] });
26235
26558
  });
26236
26559
  VideoPlayer.displayName = "VideoPlayer";
26560
+ var BackButton = ({
26561
+ onClick,
26562
+ text = "Back",
26563
+ className,
26564
+ size = "default",
26565
+ disabled = false,
26566
+ "aria-label": ariaLabel
26567
+ }) => {
26568
+ const sizeClasses = {
26569
+ sm: {
26570
+ container: "gap-1 px-2 py-1.5",
26571
+ icon: "w-3.5 h-3.5",
26572
+ text: "text-xs"
26573
+ },
26574
+ default: {
26575
+ container: "gap-2 px-3 py-2",
26576
+ icon: "w-4 h-4",
26577
+ text: "text-sm"
26578
+ },
26579
+ lg: {
26580
+ container: "gap-2 px-4 py-2.5",
26581
+ icon: "w-5 h-5",
26582
+ text: "text-base"
26583
+ }
26584
+ };
26585
+ const currentSize = sizeClasses[size];
26586
+ return /* @__PURE__ */ jsxRuntime.jsxs(
26587
+ "button",
26588
+ {
26589
+ onClick,
26590
+ disabled,
26591
+ "aria-label": ariaLabel || `${text} button`,
26592
+ className: cn(
26593
+ // Base styles
26594
+ "flex items-center font-medium rounded-lg transition-all duration-200",
26595
+ // Size-specific styles
26596
+ currentSize.container,
26597
+ // Color and interaction styles
26598
+ disabled ? "text-gray-400 cursor-not-allowed" : "text-gray-600 hover:text-gray-900 hover:bg-gray-50 active:bg-gray-100",
26599
+ // Focus styles for accessibility
26600
+ "focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2",
26601
+ className
26602
+ ),
26603
+ children: [
26604
+ /* @__PURE__ */ jsxRuntime.jsx(
26605
+ lucideReact.ArrowLeft,
26606
+ {
26607
+ className: cn(
26608
+ "flex-shrink-0",
26609
+ currentSize.icon,
26610
+ disabled && "opacity-50"
26611
+ )
26612
+ }
26613
+ ),
26614
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn(
26615
+ "font-medium select-none",
26616
+ currentSize.text,
26617
+ disabled && "opacity-50"
26618
+ ), children: text })
26619
+ ]
26620
+ }
26621
+ );
26622
+ };
26623
+ var BackButtonMinimal = ({
26624
+ onClick,
26625
+ text = "Back",
26626
+ className,
26627
+ size = "default",
26628
+ disabled = false,
26629
+ "aria-label": ariaLabel
26630
+ }) => {
26631
+ const sizeClasses = {
26632
+ sm: {
26633
+ icon: "w-3.5 h-3.5",
26634
+ text: "text-xs ml-1"
26635
+ },
26636
+ default: {
26637
+ icon: "w-4 h-4",
26638
+ text: "text-sm ml-2"
26639
+ },
26640
+ lg: {
26641
+ icon: "w-5 h-5",
26642
+ text: "text-base ml-2"
26643
+ }
26644
+ };
26645
+ const currentSize = sizeClasses[size];
26646
+ return /* @__PURE__ */ jsxRuntime.jsxs(
26647
+ "button",
26648
+ {
26649
+ onClick,
26650
+ disabled,
26651
+ "aria-label": ariaLabel || `${text} button`,
26652
+ className: cn(
26653
+ // Base styles - minimal padding for tight spaces
26654
+ "flex items-center transition-colors duration-200",
26655
+ // Color and interaction styles
26656
+ disabled ? "text-gray-400 cursor-not-allowed" : "text-gray-600 hover:text-gray-900",
26657
+ // Focus styles for accessibility
26658
+ "focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded",
26659
+ className
26660
+ ),
26661
+ children: [
26662
+ /* @__PURE__ */ jsxRuntime.jsx(
26663
+ lucideReact.ArrowLeft,
26664
+ {
26665
+ className: cn(
26666
+ "flex-shrink-0",
26667
+ currentSize.icon,
26668
+ disabled && "opacity-50"
26669
+ )
26670
+ }
26671
+ ),
26672
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn(
26673
+ "font-medium select-none",
26674
+ currentSize.text,
26675
+ disabled && "opacity-50"
26676
+ ), children: text })
26677
+ ]
26678
+ }
26679
+ );
26680
+ };
26681
+ var InlineEditableText = ({
26682
+ value,
26683
+ onSave,
26684
+ placeholder = "Click to edit",
26685
+ className = "",
26686
+ editIconClassName = "",
26687
+ inputClassName = "",
26688
+ debounceDelay = 750,
26689
+ // 750ms for quick auto-save
26690
+ disabled = false
26691
+ }) => {
26692
+ const [isEditing, setIsEditing] = React19.useState(false);
26693
+ const [editValue, setEditValue] = React19.useState(value);
26694
+ const [saveStatus, setSaveStatus] = React19.useState("idle");
26695
+ const [lastSavedValue, setLastSavedValue] = React19.useState(value);
26696
+ const [hasUnsavedChanges, setHasUnsavedChanges] = React19.useState(false);
26697
+ const inputRef = React19.useRef(null);
26698
+ const containerRef = React19.useRef(null);
26699
+ const debounceTimeout = React19.useRef(void 0);
26700
+ const saveStatusTimeout = React19.useRef(void 0);
26701
+ React19.useEffect(() => {
26702
+ if (!isEditing) {
26703
+ setEditValue(value);
26704
+ setLastSavedValue(value);
26705
+ }
26706
+ }, [value, isEditing]);
26707
+ React19.useEffect(() => {
26708
+ if (isEditing && inputRef.current) {
26709
+ requestAnimationFrame(() => {
26710
+ inputRef.current?.focus();
26711
+ inputRef.current?.select();
26712
+ });
26713
+ }
26714
+ }, [isEditing]);
26715
+ React19.useEffect(() => {
26716
+ return () => {
26717
+ if (debounceTimeout.current) clearTimeout(debounceTimeout.current);
26718
+ if (saveStatusTimeout.current) clearTimeout(saveStatusTimeout.current);
26719
+ };
26720
+ }, []);
26721
+ const performSave = React19.useCallback(async (valueToSave, shouldClose = false) => {
26722
+ const trimmedValue = valueToSave.trim();
26723
+ if (trimmedValue === lastSavedValue.trim()) {
26724
+ setHasUnsavedChanges(false);
26725
+ if (shouldClose) {
26726
+ setIsEditing(false);
26727
+ setSaveStatus("idle");
26728
+ }
26729
+ return;
26730
+ }
26731
+ setSaveStatus("saving");
26732
+ setHasUnsavedChanges(false);
26733
+ try {
26734
+ await onSave(trimmedValue);
26735
+ setLastSavedValue(trimmedValue);
26736
+ setSaveStatus("saved");
26737
+ if (!shouldClose && inputRef.current) {
26738
+ requestAnimationFrame(() => {
26739
+ inputRef.current?.focus();
26740
+ });
26741
+ }
26742
+ if (saveStatusTimeout.current) clearTimeout(saveStatusTimeout.current);
26743
+ saveStatusTimeout.current = setTimeout(() => {
26744
+ setSaveStatus("idle");
26745
+ }, 2e3);
26746
+ if (shouldClose) {
26747
+ setIsEditing(false);
26748
+ }
26749
+ } catch (error) {
26750
+ console.error("Failed to save:", error);
26751
+ setSaveStatus("error");
26752
+ setHasUnsavedChanges(true);
26753
+ if (saveStatusTimeout.current) clearTimeout(saveStatusTimeout.current);
26754
+ saveStatusTimeout.current = setTimeout(() => {
26755
+ setSaveStatus("idle");
26756
+ }, 3e3);
26757
+ if (!shouldClose && inputRef.current) {
26758
+ requestAnimationFrame(() => {
26759
+ inputRef.current?.focus();
26760
+ });
26761
+ }
26762
+ }
26763
+ }, [lastSavedValue, onSave]);
26764
+ React19.useEffect(() => {
26765
+ const handleClickOutside = (event) => {
26766
+ if (isEditing && containerRef.current && !containerRef.current.contains(event.target)) {
26767
+ if (debounceTimeout.current) {
26768
+ clearTimeout(debounceTimeout.current);
26769
+ }
26770
+ performSave(editValue, true);
26771
+ }
26772
+ };
26773
+ if (isEditing) {
26774
+ document.addEventListener("mousedown", handleClickOutside);
26775
+ return () => document.removeEventListener("mousedown", handleClickOutside);
26776
+ }
26777
+ }, [isEditing, editValue, performSave]);
26778
+ const handleInputChange = (e) => {
26779
+ const newValue = e.target.value;
26780
+ setEditValue(newValue);
26781
+ if (newValue.trim() !== lastSavedValue.trim()) {
26782
+ setHasUnsavedChanges(true);
26783
+ setSaveStatus("idle");
26784
+ } else {
26785
+ setHasUnsavedChanges(false);
26786
+ }
26787
+ if (debounceTimeout.current) {
26788
+ clearTimeout(debounceTimeout.current);
26789
+ }
26790
+ if (newValue.trim() !== lastSavedValue.trim()) {
26791
+ debounceTimeout.current = setTimeout(() => {
26792
+ performSave(newValue, false);
26793
+ }, debounceDelay);
26794
+ }
26795
+ };
26796
+ const handleKeyDown = (e) => {
26797
+ if (e.key === "Enter") {
26798
+ e.preventDefault();
26799
+ if (debounceTimeout.current) {
26800
+ clearTimeout(debounceTimeout.current);
26801
+ }
26802
+ performSave(editValue, true);
26803
+ } else if (e.key === "Escape") {
26804
+ e.preventDefault();
26805
+ if (debounceTimeout.current) {
26806
+ clearTimeout(debounceTimeout.current);
26807
+ }
26808
+ setEditValue(lastSavedValue);
26809
+ setHasUnsavedChanges(false);
26810
+ setSaveStatus("idle");
26811
+ setIsEditing(false);
26812
+ }
26813
+ };
26814
+ const handleClick = () => {
26815
+ if (!disabled && !isEditing) {
26816
+ setIsEditing(true);
26817
+ setSaveStatus("idle");
26818
+ }
26819
+ };
26820
+ if (isEditing) {
26821
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, className: "inline-flex items-center gap-1.5", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
26822
+ /* @__PURE__ */ jsxRuntime.jsx(
26823
+ "input",
26824
+ {
26825
+ ref: inputRef,
26826
+ type: "text",
26827
+ value: editValue,
26828
+ onChange: handleInputChange,
26829
+ onKeyDown: handleKeyDown,
26830
+ className: `px-2 py-1 pr-7 text-sm border rounded-md transition-colors duration-200
26831
+ ${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"}
26832
+ focus:outline-none focus:ring-2 bg-white
26833
+ ${inputClassName}`,
26834
+ placeholder,
26835
+ autoComplete: "off"
26836
+ }
26837
+ ),
26838
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute right-1.5 top-1/2 -translate-y-1/2", children: [
26839
+ saveStatus === "saving" && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "w-3.5 h-3.5 text-blue-500 animate-spin" }),
26840
+ saveStatus === "saved" && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "w-3.5 h-3.5 text-green-500" }),
26841
+ saveStatus === "error" && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircle, { className: "w-3.5 h-3.5 text-red-500" }),
26842
+ saveStatus === "idle" && hasUnsavedChanges && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1.5 h-1.5 bg-yellow-400 rounded-full" })
26843
+ ] })
26844
+ ] }) });
26845
+ }
26846
+ return /* @__PURE__ */ jsxRuntime.jsxs(
26847
+ "div",
26848
+ {
26849
+ onClick: handleClick,
26850
+ className: `inline-flex items-center gap-1.5 cursor-pointer px-2 py-1 rounded-md
26851
+ transition-all duration-200 hover:bg-gray-50 group
26852
+ ${disabled ? "cursor-not-allowed opacity-50" : ""}
26853
+ ${className}`,
26854
+ children: [
26855
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-900", children: editValue || placeholder }),
26856
+ /* @__PURE__ */ jsxRuntime.jsx(
26857
+ lucideReact.Edit2,
26858
+ {
26859
+ className: `w-3.5 h-3.5 text-gray-400 transition-opacity duration-200
26860
+ opacity-0 group-hover:opacity-100
26861
+ ${disabled ? "hidden" : ""}
26862
+ ${editIconClassName}`
26863
+ }
26864
+ )
26865
+ ]
26866
+ }
26867
+ );
26868
+ };
26237
26869
  var BottlenecksContent = ({
26238
26870
  workspaceId,
26239
26871
  workspaceName,
@@ -26253,7 +26885,6 @@ var BottlenecksContent = ({
26253
26885
  const videoRef = React19.useRef(null);
26254
26886
  const timestampFilterRef = React19.useRef(null);
26255
26887
  const initialFilter = sopCategories && sopCategories.length > 0 ? sopCategories[0].id : "low_value";
26256
- const videoIndexRef = React19.useRef(null);
26257
26888
  const currentIndexRef = React19.useRef(0);
26258
26889
  const activeFilterRef = React19.useRef(initialFilter);
26259
26890
  const isMountedRef = React19.useRef(true);
@@ -26271,15 +26902,6 @@ var BottlenecksContent = ({
26271
26902
  const [isNavigating, setIsNavigating] = React19.useState(false);
26272
26903
  const [error, setError] = React19.useState(null);
26273
26904
  const [clipCounts, setClipCounts] = React19.useState({});
26274
- const [videoIndex, setVideoIndex] = React19.useState(null);
26275
- const updateVideoIndex = React19.useCallback((newIndex) => {
26276
- console.log(`[BottlenecksContent] Updating video index - ID: ${newIndex?._debugId || "NO_ID"}, total videos: ${newIndex?.allVideos.length || 0}, categories: ${newIndex?.byCategory.size || 0}`);
26277
- if (newIndex) {
26278
- console.log(`[BottlenecksContent] VideoIndex categories: [${Array.from(newIndex.byCategory.keys()).join(", ")}]`);
26279
- }
26280
- setVideoIndex(newIndex);
26281
- videoIndexRef.current = newIndex;
26282
- }, []);
26283
26905
  const updateActiveFilter = React19.useCallback((newFilter) => {
26284
26906
  console.log(`[BottlenecksContent] Updating active filter: ${activeFilterRef.current} -> ${newFilter}`);
26285
26907
  setActiveFilter(newFilter);
@@ -26340,7 +26962,8 @@ var BottlenecksContent = ({
26340
26962
  date: date || getOperationalDate(),
26341
26963
  shift: effectiveShift,
26342
26964
  enabled: !!workspaceId && !!s3ClipsService,
26343
- buildIndex: true
26965
+ buildIndex: false
26966
+ // Disabled to reduce S3 costs - use pagination instead
26344
26967
  });
26345
26968
  const fetchClipCounts = React19.useCallback(async () => {
26346
26969
  if (!workspaceId || !s3ClipsService || !dashboardConfig?.s3Config || !isMountedRef.current) return;
@@ -26359,7 +26982,6 @@ var BottlenecksContent = ({
26359
26982
  if (cachedResult) {
26360
26983
  console.log(`[BottlenecksContent] Using cached clip counts`);
26361
26984
  updateClipCounts(cachedResult.counts);
26362
- updateVideoIndex(cachedResult.videoIndex);
26363
26985
  setIsLoading(false);
26364
26986
  setHasInitialLoad(true);
26365
26987
  return;
@@ -26368,19 +26990,13 @@ var BottlenecksContent = ({
26368
26990
  const fullResult = await s3ClipsService.getClipCounts(
26369
26991
  workspaceId,
26370
26992
  operationalDate,
26371
- shiftStr,
26372
- true
26373
- // Build index
26993
+ shiftStr
26994
+ // Don't build index - use pagination for cost efficiency
26374
26995
  );
26375
- if (fullResult && "counts" in fullResult && "videoIndex" in fullResult) {
26376
- updateClipCounts(fullResult.counts);
26377
- updateVideoIndex(fullResult.videoIndex);
26378
- await smartVideoCache.setClipCounts(cacheKey, fullResult, 5);
26379
- console.log(`[BottlenecksContent] Fetched and cached clip counts with ${fullResult.videoIndex.allVideos.length} videos`);
26380
- } else if (fullResult) {
26996
+ if (fullResult) {
26381
26997
  const counts = fullResult;
26382
26998
  updateClipCounts(counts);
26383
- console.log(`[BottlenecksContent] Fetched clip counts (no video index)`);
26999
+ console.log(`[BottlenecksContent] Fetched and cached clip counts`);
26384
27000
  }
26385
27001
  setIsLoading(false);
26386
27002
  setHasInitialLoad(true);
@@ -26393,7 +27009,7 @@ var BottlenecksContent = ({
26393
27009
  } finally {
26394
27010
  fetchInProgressRef.current.delete(operationKey);
26395
27011
  }
26396
- }, [workspaceId, date, s3ClipsService, effectiveShift, dashboardConfig, updateClipCounts, updateVideoIndex]);
27012
+ }, [workspaceId, date, s3ClipsService, effectiveShift, dashboardConfig, updateClipCounts]);
26397
27013
  const loadingCategoryRef = React19.useRef(null);
26398
27014
  const videoRetryCountRef = React19.useRef(0);
26399
27015
  const loadingVideosRef = React19.useRef(/* @__PURE__ */ new Set());
@@ -26401,7 +27017,6 @@ var BottlenecksContent = ({
26401
27017
  const ensureVideosLoaded = React19.useCallback(async (centerIndex) => {
26402
27018
  if (!s3ClipsService || !workspaceId || !isMountedRef.current) return;
26403
27019
  const currentFilter = activeFilterRef.current;
26404
- const currentVideoIndex = videoIndexRef.current;
26405
27020
  let effectiveFilter = currentFilter;
26406
27021
  if (sopCategories && sopCategories.length > 0) {
26407
27022
  const category = sopCategories.find((cat) => cat.id === currentFilter);
@@ -26428,17 +27043,6 @@ var BottlenecksContent = ({
26428
27043
  const loadPromises = indicesToLoad.map(async (index) => {
26429
27044
  try {
26430
27045
  let video = null;
26431
- if (currentVideoIndex && currentVideoIndex.byCategory && currentVideoIndex.allVideos.length > 0) {
26432
- video = await s3ClipsService.getVideoFromIndex(
26433
- currentVideoIndex,
26434
- effectiveFilter,
26435
- index,
26436
- true,
26437
- // includeCycleTime - OK for preloading
26438
- false
26439
- // includeMetadata - NO metadata during bulk preloading to prevent flooding
26440
- );
26441
- }
26442
27046
  if (!video) {
26443
27047
  const operationalDate = date || getOperationalDate();
26444
27048
  const shiftStr = effectiveShift;
@@ -26491,12 +27095,11 @@ var BottlenecksContent = ({
26491
27095
  try {
26492
27096
  const operationalDate = date || getOperationalDate();
26493
27097
  const shiftStr = effectiveShift;
26494
- if (!clipCounts[targetCategory] && !videoIndex) {
27098
+ if (!clipCounts[targetCategory]) {
26495
27099
  const cacheKey = `clip-counts:${workspaceId}:${operationalDate}:${shiftStr}`;
26496
27100
  const cachedResult = await smartVideoCache.getClipCounts(cacheKey);
26497
27101
  if (cachedResult && cachedResult.counts[targetCategory] > 0) {
26498
27102
  updateClipCounts(cachedResult.counts);
26499
- updateVideoIndex(cachedResult.videoIndex);
26500
27103
  setHasInitialLoad(true);
26501
27104
  console.log(`[BottlenecksContent] Used cached data for loadFirstVideoForCategory - ${targetCategory}`);
26502
27105
  }
@@ -26528,11 +27131,14 @@ var BottlenecksContent = ({
26528
27131
  } catch (directError) {
26529
27132
  console.warn(`[BottlenecksContent] Direct S3 loading failed, trying video index:`, directError);
26530
27133
  }
26531
- const currentVideoIndex = videoIndexRef.current;
26532
- if (clipCounts[targetCategory] > 0 && currentVideoIndex && currentVideoIndex.allVideos.length > 0) {
27134
+ if (clipCounts[targetCategory] > 0) {
26533
27135
  try {
26534
- const firstVideo = await s3ClipsService.getVideoFromIndex(
26535
- currentVideoIndex,
27136
+ const operationalDate2 = date || getOperationalDate();
27137
+ const shiftStr2 = effectiveShift;
27138
+ const firstVideo = await s3ClipsService.getClipByIndex(
27139
+ workspaceId,
27140
+ operationalDate2,
27141
+ shiftStr2,
26536
27142
  targetCategory,
26537
27143
  0,
26538
27144
  // First video (index 0)
@@ -26572,25 +27178,22 @@ var BottlenecksContent = ({
26572
27178
  loadingCategoryRef.current = null;
26573
27179
  fetchInProgressRef.current.delete(operationKey);
26574
27180
  }
26575
- }, [workspaceId, date, s3ClipsService, clipCounts, videoIndex, effectiveShift, updateClipCounts, updateVideoIndex]);
27181
+ }, [workspaceId, date, s3ClipsService, clipCounts, effectiveShift, updateClipCounts]);
26576
27182
  React19.useEffect(() => {
26577
27183
  if (s3ClipsService && !prefetchData) {
26578
27184
  fetchClipCounts();
26579
27185
  }
26580
- }, [workspaceId, date, effectiveShift, s3ClipsService, fetchClipCounts, updateClipCounts, updateVideoIndex, prefetchData]);
27186
+ }, [workspaceId, date, effectiveShift, s3ClipsService, fetchClipCounts, updateClipCounts, prefetchData]);
26581
27187
  React19.useEffect(() => {
26582
- if (prefetchData) {
26583
- console.log(`[BottlenecksContent] Received prefetch update - status: ${prefetchStatus}, videos: ${prefetchData.videoIndex.allVideos.length}, ID: ${prefetchData.videoIndex._debugId || "NO_ID"}`);
27188
+ if (prefetchData && prefetchData.counts) {
27189
+ console.log(`[BottlenecksContent] Received prefetch update - status: ${prefetchStatus}`);
26584
27190
  updateClipCounts(prefetchData.counts);
26585
- if (prefetchData.videoIndex.allVideos.length > 0) {
26586
- updateVideoIndex(prefetchData.videoIndex);
26587
- }
26588
- if (!hasInitialLoad && prefetchData.videoIndex.allVideos.length > 0) {
27191
+ if (!hasInitialLoad) {
26589
27192
  setIsLoading(false);
26590
27193
  setHasInitialLoad(true);
26591
27194
  }
26592
27195
  }
26593
- }, [prefetchData, prefetchStatus, updateClipCounts, updateVideoIndex, hasInitialLoad]);
27196
+ }, [prefetchData, prefetchStatus, updateClipCounts, hasInitialLoad]);
26594
27197
  React19.useEffect(() => {
26595
27198
  if (s3ClipsService && clipCounts[activeFilter] > 0) {
26596
27199
  const hasVideosForCurrentFilter = allVideos.some((video) => {
@@ -26630,7 +27233,7 @@ var BottlenecksContent = ({
26630
27233
  setIsCategoryLoading(false);
26631
27234
  }
26632
27235
  }
26633
- }, [activeFilter, s3ClipsService, videoIndex, clipCounts, loadFirstVideoForCategory, allVideos, sopCategories]);
27236
+ }, [activeFilter, s3ClipsService, clipCounts, loadFirstVideoForCategory, allVideos, sopCategories]);
26634
27237
  React19.useEffect(() => {
26635
27238
  if (previousFilterRef.current !== activeFilter) {
26636
27239
  console.log(`Filter changed from ${previousFilterRef.current} to ${activeFilter} - resetting to first video`);
@@ -26746,23 +27349,7 @@ var BottlenecksContent = ({
26746
27349
  }
26747
27350
  try {
26748
27351
  let video = null;
26749
- const currentVideoIndex = videoIndexRef.current;
26750
- if (currentVideoIndex && currentVideoIndex.byCategory && currentVideoIndex.allVideos.length > 0 && s3ClipsService) {
26751
- console.log(`[BottlenecksContent] Using video index for navigation - ID: ${currentVideoIndex._debugId || "NO_ID"}, total categories: ${currentVideoIndex.byCategory.size}, total videos: ${currentVideoIndex.allVideos.length}, filter: ${currentFilter}`);
26752
- console.log(`[BottlenecksContent] VideoIndex categories in handleNext: [${Array.from(currentVideoIndex.byCategory.keys()).join(", ")}]`);
26753
- video = await s3ClipsService.getVideoFromIndex(
26754
- currentVideoIndex,
26755
- effectiveFilter,
26756
- nextIndex,
26757
- true,
26758
- // includeCycleTime
26759
- false
26760
- // includeMetadata - DON'T fetch metadata during navigation to prevent flooding!
26761
- );
26762
- } else {
26763
- console.warn(`[BottlenecksContent] Video index not ready for navigation: ID: ${currentVideoIndex?._debugId || "NO_ID"}, byCategory exists = ${!!currentVideoIndex?.byCategory}, allVideos = ${currentVideoIndex?.allVideos?.length || 0}`);
26764
- }
26765
- if (!video && s3ClipsService) {
27352
+ if (s3ClipsService) {
26766
27353
  const operationalDate = date || getOperationalDate();
26767
27354
  const shiftStr = effectiveShift;
26768
27355
  video = await s3ClipsService.getClipByIndex(
@@ -28024,6 +28611,451 @@ var KPISection = React19.memo(({
28024
28611
  return true;
28025
28612
  });
28026
28613
  KPISection.displayName = "KPISection";
28614
+ var WorkspaceHealthCard = ({
28615
+ workspace,
28616
+ onClick,
28617
+ showDetails = true,
28618
+ className = ""
28619
+ }) => {
28620
+ const getStatusConfig = () => {
28621
+ switch (workspace.status) {
28622
+ case "healthy":
28623
+ return {
28624
+ gradient: "from-emerald-50 to-green-50 dark:from-emerald-950/30 dark:to-green-950/30",
28625
+ border: "border-emerald-200 dark:border-emerald-800",
28626
+ icon: lucideReact.CheckCircle2,
28627
+ iconColor: "text-emerald-600 dark:text-emerald-400",
28628
+ badge: "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-300",
28629
+ statusText: "Online",
28630
+ pulse: true
28631
+ };
28632
+ case "unhealthy":
28633
+ return {
28634
+ gradient: "from-rose-50 to-red-50 dark:from-rose-950/30 dark:to-red-950/30",
28635
+ border: "border-rose-200 dark:border-rose-800",
28636
+ icon: lucideReact.XCircle,
28637
+ iconColor: "text-rose-600 dark:text-rose-400",
28638
+ badge: "bg-rose-100 text-rose-700 dark:bg-rose-900/50 dark:text-rose-300",
28639
+ statusText: "Offline",
28640
+ pulse: false
28641
+ };
28642
+ case "warning":
28643
+ return {
28644
+ gradient: "from-amber-50 to-yellow-50 dark:from-amber-950/30 dark:to-yellow-950/30",
28645
+ border: "border-amber-200 dark:border-amber-800",
28646
+ icon: lucideReact.AlertTriangle,
28647
+ iconColor: "text-amber-600 dark:text-amber-400",
28648
+ badge: "bg-amber-100 text-amber-700 dark:bg-amber-900/50 dark:text-amber-300",
28649
+ statusText: "Degraded",
28650
+ pulse: true
28651
+ };
28652
+ default:
28653
+ return {
28654
+ gradient: "from-gray-50 to-slate-50 dark:from-gray-950/30 dark:to-slate-950/30",
28655
+ border: "border-gray-200 dark:border-gray-800",
28656
+ icon: lucideReact.Activity,
28657
+ iconColor: "text-gray-500 dark:text-gray-400",
28658
+ badge: "bg-gray-100 text-gray-700 dark:bg-gray-900/50 dark:text-gray-300",
28659
+ statusText: "Unknown",
28660
+ pulse: false
28661
+ };
28662
+ }
28663
+ };
28664
+ const config = getStatusConfig();
28665
+ const StatusIcon = config.icon;
28666
+ workspace.isStale ? lucideReact.WifiOff : lucideReact.Wifi;
28667
+ const handleClick = () => {
28668
+ if (onClick) {
28669
+ onClick(workspace);
28670
+ }
28671
+ };
28672
+ const handleKeyDown = (event) => {
28673
+ if (onClick && (event.key === "Enter" || event.key === " ")) {
28674
+ event.preventDefault();
28675
+ onClick(workspace);
28676
+ }
28677
+ };
28678
+ const formatTimeAgo = (timeString) => {
28679
+ return timeString.replace("about ", "").replace(" ago", "");
28680
+ };
28681
+ return /* @__PURE__ */ jsxRuntime.jsx(
28682
+ Card2,
28683
+ {
28684
+ className: clsx(
28685
+ "relative overflow-hidden transition-all duration-300",
28686
+ "bg-gradient-to-br",
28687
+ config.gradient,
28688
+ "border",
28689
+ config.border,
28690
+ "shadow-sm hover:shadow-md",
28691
+ onClick && "cursor-pointer hover:scale-[1.01]",
28692
+ workspace.isStale && "opacity-90",
28693
+ className
28694
+ ),
28695
+ onClick: handleClick,
28696
+ onKeyDown: handleKeyDown,
28697
+ tabIndex: onClick ? 0 : void 0,
28698
+ role: onClick ? "button" : void 0,
28699
+ "aria-label": `Workspace ${workspace.workspace_display_name} status: ${workspace.status}`,
28700
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4", children: [
28701
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between mb-3", children: [
28702
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1", children: [
28703
+ /* @__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)}` }),
28704
+ showDetails && workspace.line_name && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-600 dark:text-gray-400", children: workspace.line_name })
28705
+ ] }),
28706
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: clsx(
28707
+ "flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium",
28708
+ config.badge
28709
+ ), children: [
28710
+ /* @__PURE__ */ jsxRuntime.jsx(StatusIcon, { className: "h-3.5 w-3.5" }),
28711
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: config.statusText })
28712
+ ] })
28713
+ ] }),
28714
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
28715
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
28716
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
28717
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Clock, { className: "h-3.5 w-3.5 text-gray-400" }),
28718
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm text-gray-600 dark:text-gray-400 whitespace-nowrap", children: [
28719
+ "Last seen: ",
28720
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: formatTimeAgo(workspace.timeSinceLastUpdate) })
28721
+ ] })
28722
+ ] }),
28723
+ workspace.isStale && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
28724
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.WifiOff, { className: "h-3.5 w-3.5 text-amber-500" }),
28725
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-amber-600 dark:text-amber-400 text-xs", children: "No recent updates" })
28726
+ ] })
28727
+ ] }),
28728
+ config.pulse && !workspace.isStale && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
28729
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex h-2 w-2", children: [
28730
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: clsx(
28731
+ "animate-ping absolute inline-flex h-full w-full rounded-full opacity-75",
28732
+ workspace.status === "healthy" ? "bg-emerald-400" : "bg-amber-400"
28733
+ ) }),
28734
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: clsx(
28735
+ "relative inline-flex rounded-full h-2 w-2",
28736
+ workspace.status === "healthy" ? "bg-emerald-500" : "bg-amber-500"
28737
+ ) })
28738
+ ] }),
28739
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-500 dark:text-gray-400", children: "Live" })
28740
+ ] })
28741
+ ] })
28742
+ ] })
28743
+ }
28744
+ );
28745
+ };
28746
+ var CompactWorkspaceHealthCard = ({
28747
+ workspace,
28748
+ onClick,
28749
+ className = ""
28750
+ }) => {
28751
+ const getStatusConfig = () => {
28752
+ switch (workspace.status) {
28753
+ case "healthy":
28754
+ return {
28755
+ dot: "bg-emerald-500",
28756
+ icon: lucideReact.CheckCircle2,
28757
+ iconColor: "text-emerald-600 dark:text-emerald-400",
28758
+ bg: "hover:bg-emerald-50 dark:hover:bg-emerald-950/20"
28759
+ };
28760
+ case "unhealthy":
28761
+ return {
28762
+ dot: "bg-rose-500",
28763
+ icon: lucideReact.XCircle,
28764
+ iconColor: "text-rose-600 dark:text-rose-400",
28765
+ bg: "hover:bg-rose-50 dark:hover:bg-rose-950/20"
28766
+ };
28767
+ case "warning":
28768
+ return {
28769
+ dot: "bg-amber-500",
28770
+ icon: lucideReact.AlertTriangle,
28771
+ iconColor: "text-amber-600 dark:text-amber-400",
28772
+ bg: "hover:bg-amber-50 dark:hover:bg-amber-950/20"
28773
+ };
28774
+ default:
28775
+ return {
28776
+ dot: "bg-gray-400",
28777
+ icon: lucideReact.Activity,
28778
+ iconColor: "text-gray-500 dark:text-gray-400",
28779
+ bg: "hover:bg-gray-50 dark:hover:bg-gray-950/20"
28780
+ };
28781
+ }
28782
+ };
28783
+ const config = getStatusConfig();
28784
+ const StatusIcon = config.icon;
28785
+ const handleClick = () => {
28786
+ if (onClick) {
28787
+ onClick(workspace);
28788
+ }
28789
+ };
28790
+ return /* @__PURE__ */ jsxRuntime.jsxs(
28791
+ "div",
28792
+ {
28793
+ className: clsx(
28794
+ "flex items-center justify-between px-4 py-3 rounded-lg border",
28795
+ "bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700",
28796
+ "transition-all duration-200",
28797
+ onClick && `cursor-pointer ${config.bg}`,
28798
+ className
28799
+ ),
28800
+ onClick: handleClick,
28801
+ role: onClick ? "button" : void 0,
28802
+ tabIndex: onClick ? 0 : void 0,
28803
+ children: [
28804
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
28805
+ /* @__PURE__ */ jsxRuntime.jsx(StatusIcon, { className: clsx("h-5 w-5", config.iconColor) }),
28806
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
28807
+ /* @__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)}` }),
28808
+ workspace.line_name && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: workspace.line_name })
28809
+ ] })
28810
+ ] }),
28811
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
28812
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: workspace.timeSinceLastUpdate }),
28813
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: clsx("h-2 w-2 rounded-full", config.dot) })
28814
+ ] })
28815
+ ]
28816
+ }
28817
+ );
28818
+ };
28819
+ var HealthStatusGrid = ({
28820
+ workspaces,
28821
+ onWorkspaceClick,
28822
+ viewMode: initialViewMode = "grid",
28823
+ showFilters = true,
28824
+ groupBy: initialGroupBy = "none",
28825
+ className = ""
28826
+ }) => {
28827
+ const [viewMode, setViewMode] = React19.useState(initialViewMode);
28828
+ const [searchTerm, setSearchTerm] = React19.useState("");
28829
+ const [statusFilter, setStatusFilter] = React19.useState("all");
28830
+ const [groupBy, setGroupBy] = React19.useState(initialGroupBy);
28831
+ const [expandedGroups, setExpandedGroups] = React19.useState(/* @__PURE__ */ new Set());
28832
+ const filteredWorkspaces = React19.useMemo(() => {
28833
+ let filtered = [...workspaces];
28834
+ if (searchTerm) {
28835
+ const search = searchTerm.toLowerCase();
28836
+ filtered = filtered.filter(
28837
+ (w) => w.workspace_display_name?.toLowerCase().includes(search) || w.line_name?.toLowerCase().includes(search) || w.company_name?.toLowerCase().includes(search)
28838
+ );
28839
+ }
28840
+ if (statusFilter !== "all") {
28841
+ filtered = filtered.filter((w) => w.status === statusFilter);
28842
+ }
28843
+ return filtered;
28844
+ }, [workspaces, searchTerm, statusFilter]);
28845
+ const groupedWorkspaces = React19.useMemo(() => {
28846
+ if (groupBy === "none") {
28847
+ return { "All Workspaces": filteredWorkspaces };
28848
+ }
28849
+ const groups = {};
28850
+ filteredWorkspaces.forEach((workspace) => {
28851
+ let key = "Unknown";
28852
+ switch (groupBy) {
28853
+ case "line":
28854
+ key = workspace.line_name || "Unknown Line";
28855
+ break;
28856
+ case "status":
28857
+ key = workspace.status;
28858
+ break;
28859
+ }
28860
+ if (!groups[key]) {
28861
+ groups[key] = [];
28862
+ }
28863
+ groups[key].push(workspace);
28864
+ });
28865
+ const sortedGroups = {};
28866
+ Object.keys(groups).sort().forEach((key) => {
28867
+ sortedGroups[key] = groups[key];
28868
+ });
28869
+ return sortedGroups;
28870
+ }, [filteredWorkspaces, groupBy]);
28871
+ React19.useEffect(() => {
28872
+ if (groupBy !== "none") {
28873
+ setExpandedGroups(new Set(Object.keys(groupedWorkspaces)));
28874
+ }
28875
+ }, [groupBy, groupedWorkspaces]);
28876
+ const toggleGroup = (groupName) => {
28877
+ const newExpanded = new Set(expandedGroups);
28878
+ if (newExpanded.has(groupName)) {
28879
+ newExpanded.delete(groupName);
28880
+ } else {
28881
+ newExpanded.add(groupName);
28882
+ }
28883
+ setExpandedGroups(newExpanded);
28884
+ };
28885
+ const getStatusCounts = () => {
28886
+ const counts = {
28887
+ healthy: 0,
28888
+ unhealthy: 0,
28889
+ warning: 0,
28890
+ unknown: 0
28891
+ };
28892
+ workspaces.forEach((w) => {
28893
+ counts[w.status]++;
28894
+ });
28895
+ return counts;
28896
+ };
28897
+ const statusCounts = getStatusCounts();
28898
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: clsx("space-y-4", className), children: [
28899
+ 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: [
28900
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col sm:flex-row gap-3 flex-wrap", children: [
28901
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 min-w-[200px]", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
28902
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" }),
28903
+ /* @__PURE__ */ jsxRuntime.jsx(
28904
+ "input",
28905
+ {
28906
+ type: "text",
28907
+ placeholder: "Search workspaces...",
28908
+ value: searchTerm,
28909
+ onChange: (e) => setSearchTerm(e.target.value),
28910
+ 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"
28911
+ }
28912
+ )
28913
+ ] }) }),
28914
+ /* @__PURE__ */ jsxRuntime.jsxs(Select, { value: statusFilter, onValueChange: (value) => setStatusFilter(value), children: [
28915
+ /* @__PURE__ */ jsxRuntime.jsx(SelectTrigger, { className: "w-full sm:w-[180px] bg-white border-gray-200", children: /* @__PURE__ */ jsxRuntime.jsx(SelectValue, { placeholder: "All statuses" }) }),
28916
+ /* @__PURE__ */ jsxRuntime.jsxs(SelectContent, { children: [
28917
+ /* @__PURE__ */ jsxRuntime.jsxs(SelectItem, { value: "all", children: [
28918
+ "All (",
28919
+ workspaces.length,
28920
+ ")"
28921
+ ] }),
28922
+ /* @__PURE__ */ jsxRuntime.jsx(SelectItem, { value: "healthy", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
28923
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-2 w-2 rounded-full bg-green-500" }),
28924
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
28925
+ "Healthy (",
28926
+ statusCounts.healthy,
28927
+ ")"
28928
+ ] })
28929
+ ] }) }),
28930
+ /* @__PURE__ */ jsxRuntime.jsx(SelectItem, { value: "unhealthy", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
28931
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-2 w-2 rounded-full bg-red-500" }),
28932
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
28933
+ "Unhealthy (",
28934
+ statusCounts.unhealthy,
28935
+ ")"
28936
+ ] })
28937
+ ] }) }),
28938
+ /* @__PURE__ */ jsxRuntime.jsx(SelectItem, { value: "warning", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
28939
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-2 w-2 rounded-full bg-yellow-500" }),
28940
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
28941
+ "Warning (",
28942
+ statusCounts.warning,
28943
+ ")"
28944
+ ] })
28945
+ ] }) }),
28946
+ /* @__PURE__ */ jsxRuntime.jsx(SelectItem, { value: "unknown", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
28947
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-2 w-2 rounded-full bg-gray-400" }),
28948
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
28949
+ "Unknown (",
28950
+ statusCounts.unknown,
28951
+ ")"
28952
+ ] })
28953
+ ] }) })
28954
+ ] })
28955
+ ] }),
28956
+ /* @__PURE__ */ jsxRuntime.jsxs(Select, { value: groupBy, onValueChange: (value) => setGroupBy(value), children: [
28957
+ /* @__PURE__ */ jsxRuntime.jsx(SelectTrigger, { className: "w-full sm:w-[160px] bg-white border-gray-200", children: /* @__PURE__ */ jsxRuntime.jsx(SelectValue, { placeholder: "Group by" }) }),
28958
+ /* @__PURE__ */ jsxRuntime.jsxs(SelectContent, { children: [
28959
+ /* @__PURE__ */ jsxRuntime.jsx(SelectItem, { value: "none", children: "No grouping" }),
28960
+ /* @__PURE__ */ jsxRuntime.jsx(SelectItem, { value: "line", children: "Group by Line" }),
28961
+ /* @__PURE__ */ jsxRuntime.jsx(SelectItem, { value: "status", children: "Group by Status" })
28962
+ ] })
28963
+ ] }),
28964
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
28965
+ /* @__PURE__ */ jsxRuntime.jsx(
28966
+ "button",
28967
+ {
28968
+ onClick: () => setViewMode("grid"),
28969
+ className: clsx(
28970
+ "p-2 rounded-lg transition-colors",
28971
+ viewMode === "grid" ? "bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700"
28972
+ ),
28973
+ "aria-label": "Grid view",
28974
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Grid3x3, { className: "h-5 w-5" })
28975
+ }
28976
+ ),
28977
+ /* @__PURE__ */ jsxRuntime.jsx(
28978
+ "button",
28979
+ {
28980
+ onClick: () => setViewMode("list"),
28981
+ className: clsx(
28982
+ "p-2 rounded-lg transition-colors",
28983
+ viewMode === "list" ? "bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700"
28984
+ ),
28985
+ "aria-label": "List view",
28986
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.List, { className: "h-5 w-5" })
28987
+ }
28988
+ )
28989
+ ] })
28990
+ ] }),
28991
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-3 text-sm text-gray-500 dark:text-gray-400", children: [
28992
+ "Showing ",
28993
+ filteredWorkspaces.length,
28994
+ " of ",
28995
+ workspaces.length,
28996
+ " workspaces"
28997
+ ] })
28998
+ ] }),
28999
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-6", children: Object.entries(groupedWorkspaces).map(([groupName, groupWorkspaces]) => {
29000
+ const isExpanded = groupBy === "none" || expandedGroups.has(groupName);
29001
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
29002
+ groupBy !== "none" && /* @__PURE__ */ jsxRuntime.jsxs(
29003
+ "div",
29004
+ {
29005
+ className: "flex items-center justify-between cursor-pointer group",
29006
+ onClick: () => toggleGroup(groupName),
29007
+ children: [
29008
+ /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "text-lg font-semibold text-gray-900 dark:text-gray-100 flex items-center gap-2", children: [
29009
+ groupName,
29010
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm font-normal text-gray-500 dark:text-gray-400", children: [
29011
+ "(",
29012
+ groupWorkspaces.length,
29013
+ ")"
29014
+ ] })
29015
+ ] }),
29016
+ /* @__PURE__ */ jsxRuntime.jsx(
29017
+ lucideReact.ChevronDown,
29018
+ {
29019
+ className: clsx(
29020
+ "h-5 w-5 text-gray-400 transition-transform",
29021
+ isExpanded && "rotate-180"
29022
+ )
29023
+ }
29024
+ )
29025
+ ]
29026
+ }
29027
+ ),
29028
+ isExpanded && /* @__PURE__ */ jsxRuntime.jsx(
29029
+ "div",
29030
+ {
29031
+ className: clsx(
29032
+ viewMode === "grid" ? "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4" : "space-y-2"
29033
+ ),
29034
+ children: groupWorkspaces.map(
29035
+ (workspace) => viewMode === "grid" ? /* @__PURE__ */ jsxRuntime.jsx(
29036
+ WorkspaceHealthCard,
29037
+ {
29038
+ workspace,
29039
+ onClick: onWorkspaceClick,
29040
+ showDetails: true
29041
+ },
29042
+ workspace.workspace_id
29043
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
29044
+ CompactWorkspaceHealthCard,
29045
+ {
29046
+ workspace,
29047
+ onClick: onWorkspaceClick
29048
+ },
29049
+ workspace.workspace_id
29050
+ )
29051
+ )
29052
+ }
29053
+ )
29054
+ ] }, groupName);
29055
+ }) }),
29056
+ 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." }) })
29057
+ ] });
29058
+ };
28027
29059
  var ISTTimer2 = ISTTimer_default;
28028
29060
  var DashboardHeader = React19.memo(({ lineTitle, className = "", headerControls }) => {
28029
29061
  const getShiftName = () => {
@@ -28521,6 +29553,17 @@ var SideNavBar = React19.memo(({
28521
29553
  });
28522
29554
  onMobileMenuClose?.();
28523
29555
  }, [navigate, onMobileMenuClose]);
29556
+ const handleHealthClick = React19.useCallback(() => {
29557
+ navigate("/health", {
29558
+ trackingEvent: {
29559
+ name: "Health Status Page Clicked",
29560
+ properties: {
29561
+ source: "side_nav"
29562
+ }
29563
+ }
29564
+ });
29565
+ onMobileMenuClose?.();
29566
+ }, [navigate, onMobileMenuClose]);
28524
29567
  const handleLogoClick = React19.useCallback(() => {
28525
29568
  navigate("/");
28526
29569
  onMobileMenuClose?.();
@@ -28534,6 +29577,7 @@ var SideNavBar = React19.memo(({
28534
29577
  const profileButtonClasses = React19.useMemo(() => getButtonClasses("/profile"), [getButtonClasses, pathname]);
28535
29578
  const helpButtonClasses = React19.useMemo(() => getButtonClasses("/help"), [getButtonClasses, pathname]);
28536
29579
  const skusButtonClasses = React19.useMemo(() => getButtonClasses("/skus"), [getButtonClasses, pathname]);
29580
+ const healthButtonClasses = React19.useMemo(() => getButtonClasses("/health"), [getButtonClasses, pathname]);
28537
29581
  const NavigationContent = () => /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
28538
29582
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full py-6 px-4 flex-shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(
28539
29583
  "button",
@@ -28678,6 +29722,21 @@ var SideNavBar = React19.memo(({
28678
29722
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] font-medium leading-tight", children: "Help" })
28679
29723
  ]
28680
29724
  }
29725
+ ),
29726
+ /* @__PURE__ */ jsxRuntime.jsxs(
29727
+ "button",
29728
+ {
29729
+ onClick: handleHealthClick,
29730
+ className: healthButtonClasses,
29731
+ "aria-label": "System Health",
29732
+ tabIndex: 0,
29733
+ role: "tab",
29734
+ "aria-selected": pathname === "/health" || pathname.startsWith("/health/"),
29735
+ children: [
29736
+ /* @__PURE__ */ jsxRuntime.jsx(outline.HeartIcon, { className: "w-5 h-5 mb-1" }),
29737
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] font-medium leading-tight", children: "Health" })
29738
+ ]
29739
+ }
28681
29740
  )
28682
29741
  ] })
28683
29742
  ] }),
@@ -30820,16 +31879,13 @@ var AIAgentView = () => {
30820
31879
  } }),
30821
31880
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex-1 flex flex-col h-screen transition-all duration-300 ${isSidebarOpen ? "mr-80" : "mr-0"}`, children: [
30822
31881
  /* @__PURE__ */ jsxRuntime.jsx("header", { className: "flex-shrink-0 bg-white px-8 py-6 shadow-sm border-b border-gray-200/80 sticky top-0 z-10", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between relative", children: [
30823
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute left-0", children: /* @__PURE__ */ jsxRuntime.jsxs(
30824
- "button",
31882
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute left-0", children: /* @__PURE__ */ jsxRuntime.jsx(
31883
+ BackButtonMinimal,
30825
31884
  {
30826
31885
  onClick: () => navigate("/"),
30827
- className: "flex items-center text-gray-600 hover:text-gray-900",
30828
- "aria-label": "Go back",
30829
- children: [
30830
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowLeft, { className: "h-5 w-5" }),
30831
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-2", children: "Back" })
30832
- ]
31886
+ text: "Back",
31887
+ size: "default",
31888
+ "aria-label": "Navigate back to dashboard"
30833
31889
  }
30834
31890
  ) }),
30835
31891
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 text-center mx-auto", children: [
@@ -31498,15 +32554,13 @@ var HelpView = ({
31498
32554
  transition: { duration: 0.3 },
31499
32555
  children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col", children: [
31500
32556
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-white px-8 py-6 shadow-sm border-b border-gray-200/80", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between relative", children: [
31501
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute left-0", children: /* @__PURE__ */ jsxRuntime.jsxs(
31502
- "button",
32557
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute left-0", children: /* @__PURE__ */ jsxRuntime.jsx(
32558
+ BackButtonMinimal,
31503
32559
  {
31504
32560
  onClick: handleBackClick,
31505
- className: "flex items-center text-gray-600 hover:text-gray-900",
31506
- children: [
31507
- /* @__PURE__ */ jsxRuntime.jsx(outline.ArrowLeftIcon, { className: "h-5 w-5" }),
31508
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-2", children: "Back" })
31509
- ]
32561
+ text: "Back",
32562
+ size: "default",
32563
+ "aria-label": "Navigate back to dashboard"
31510
32564
  }
31511
32565
  ) }),
31512
32566
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 text-center mx-auto", children: [
@@ -31536,10 +32590,7 @@ var HelpView = ({
31536
32590
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "xl:col-span-3 order-1", children: /* @__PURE__ */ jsxRuntime.jsxs(Card2, { className: "shadow-lg border-gray-200 bg-white", children: [
31537
32591
  /* @__PURE__ */ jsxRuntime.jsx(CardHeader2, { className: "bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-100 p-4 sm:p-6", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 sm:gap-3", children: [
31538
32592
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-1.5 sm:p-2 bg-blue-100 rounded-lg", children: /* @__PURE__ */ jsxRuntime.jsx(outline.DocumentTextIcon, { className: "h-4 w-4 sm:h-5 sm:w-5 text-blue-600" }) }),
31539
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
31540
- /* @__PURE__ */ jsxRuntime.jsx(CardTitle2, { className: "text-lg sm:text-xl font-bold text-gray-900", children: "Submit Support Request" }),
31541
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs sm:text-sm text-gray-600 mt-1", children: "Direct line to our engineering team \u2022 Avg. response time: <30 minutes" })
31542
- ] })
32593
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(CardTitle2, { className: "text-lg sm:text-xl font-bold text-gray-900", children: "Submit Support Request" }) })
31543
32594
  ] }) }),
31544
32595
  /* @__PURE__ */ jsxRuntime.jsx(CardContent2, { className: "p-4 sm:p-6", children: /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: handleSubmit, className: "space-y-4 sm:space-y-5", children: [
31545
32596
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4 sm:space-y-5", children: [
@@ -32633,17 +33684,15 @@ var KPIDetailView = ({
32633
33684
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-h-screen bg-gray-50 flex flex-col", children: [
32634
33685
  /* @__PURE__ */ jsxRuntime.jsx("header", { className: "sticky top-0 z-10 bg-white border-b flex-shrink-0", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-3", children: [
32635
33686
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center relative", children: [
32636
- /* @__PURE__ */ jsxRuntime.jsxs(
32637
- "div",
33687
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute left-0", children: /* @__PURE__ */ jsxRuntime.jsx(
33688
+ BackButtonMinimal,
32638
33689
  {
32639
33690
  onClick: handleBackClick,
32640
- className: "absolute left-0 flex items-center text-gray-600 hover:text-gray-900 cursor-pointer",
32641
- children: [
32642
- /* @__PURE__ */ jsxRuntime.jsx(outline.ArrowLeftIcon, { className: "h-5 w-5" }),
32643
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-2", children: "Back" })
32644
- ]
33691
+ text: "Back",
33692
+ size: "default",
33693
+ "aria-label": "Navigate back to previous page"
32645
33694
  }
32646
- ),
33695
+ ) }),
32647
33696
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 flex justify-center", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
32648
33697
  /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-3xl font-semibold text-gray-900", children: lineInfo?.line_name || "Line" }),
32649
33698
  /* @__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" })
@@ -33005,17 +34054,15 @@ var KPIsOverviewView = ({
33005
34054
  if (error) {
33006
34055
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-h-screen bg-gray-50 flex flex-col", children: [
33007
34056
  /* @__PURE__ */ jsxRuntime.jsx("header", { className: "sticky top-0 z-10 bg-white border-b flex-shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center relative", children: [
33008
- /* @__PURE__ */ jsxRuntime.jsxs(
33009
- "div",
34057
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute left-0", children: /* @__PURE__ */ jsxRuntime.jsx(
34058
+ BackButtonMinimal,
33010
34059
  {
33011
34060
  onClick: handleBackClick,
33012
- className: "absolute left-0 flex items-center text-gray-600 hover:text-gray-900 cursor-pointer",
33013
- children: [
33014
- /* @__PURE__ */ jsxRuntime.jsx(outline.ArrowLeftIcon, { className: "h-5 w-5" }),
33015
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-2", children: "Back" })
33016
- ]
34061
+ text: "Back",
34062
+ size: "default",
34063
+ "aria-label": "Navigate back to previous page"
33017
34064
  }
33018
- ),
34065
+ ) }),
33019
34066
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 flex justify-center", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
33020
34067
  /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-3xl font-semibold text-gray-900", children: "Shop-floor overview" }),
33021
34068
  /* @__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" })
@@ -33027,17 +34074,15 @@ var KPIsOverviewView = ({
33027
34074
  if (lines.length === 0) {
33028
34075
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-h-screen bg-gray-50 flex flex-col", children: [
33029
34076
  /* @__PURE__ */ jsxRuntime.jsx("header", { className: "sticky top-0 z-10 bg-white border-b flex-shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center relative", children: [
33030
- /* @__PURE__ */ jsxRuntime.jsxs(
33031
- "div",
34077
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute left-0", children: /* @__PURE__ */ jsxRuntime.jsx(
34078
+ BackButtonMinimal,
33032
34079
  {
33033
34080
  onClick: handleBackClick,
33034
- className: "absolute left-0 flex items-center text-gray-600 hover:text-gray-900 cursor-pointer",
33035
- children: [
33036
- /* @__PURE__ */ jsxRuntime.jsx(outline.ArrowLeftIcon, { className: "h-5 w-5" }),
33037
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-2", children: "Back" })
33038
- ]
34081
+ text: "Back",
34082
+ size: "default",
34083
+ "aria-label": "Navigate back to previous page"
33039
34084
  }
33040
- ),
34085
+ ) }),
33041
34086
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 flex justify-center", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
33042
34087
  /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-3xl font-semibold text-gray-900", children: "Shop-floor overview" }),
33043
34088
  /* @__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" })
@@ -33052,17 +34097,15 @@ var KPIsOverviewView = ({
33052
34097
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-h-screen bg-gray-50 flex flex-col", children: [
33053
34098
  /* @__PURE__ */ jsxRuntime.jsx("header", { className: "sticky top-0 z-10 bg-white border-b flex-shrink-0", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-3", children: [
33054
34099
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center relative", children: [
33055
- /* @__PURE__ */ jsxRuntime.jsxs(
33056
- "div",
34100
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute left-0", children: /* @__PURE__ */ jsxRuntime.jsx(
34101
+ BackButtonMinimal,
33057
34102
  {
33058
34103
  onClick: handleBackClick,
33059
- className: "absolute left-0 flex items-center text-gray-600 hover:text-gray-900 cursor-pointer",
33060
- children: [
33061
- /* @__PURE__ */ jsxRuntime.jsx(outline.ArrowLeftIcon, { className: "h-5 w-5" }),
33062
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-2", children: "Back" })
33063
- ]
34104
+ text: "Back",
34105
+ size: "default",
34106
+ "aria-label": "Navigate back to previous page"
33064
34107
  }
33065
- ),
34108
+ ) }),
33066
34109
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 flex justify-center", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
33067
34110
  /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-3xl font-semibold text-gray-900", children: "Shop-floor overview" }),
33068
34111
  /* @__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" })
@@ -33346,18 +34389,13 @@ var LeaderboardDetailView = React19.memo(({
33346
34389
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `min-h-screen bg-slate-50 flex flex-col ${className}`, children: [
33347
34390
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sticky top-0 z-20 bg-white shadow-sm border-b border-gray-200/80", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-3 sm:px-8 py-2 sm:py-2.5", children: [
33348
34391
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
33349
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-auto sm:w-32", children: /* @__PURE__ */ jsxRuntime.jsxs(
33350
- "button",
34392
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-auto sm:w-32", children: /* @__PURE__ */ jsxRuntime.jsx(
34393
+ BackButtonMinimal,
33351
34394
  {
33352
34395
  onClick: handleBackClick,
33353
- className: "flex items-center gap-1 sm:gap-2 px-2 sm:px-3 py-1.5 sm:py-2 text-gray-600 hover:text-gray-900 transition-colors",
33354
- children: [
33355
- /* @__PURE__ */ jsxRuntime.jsxs("svg", { className: "w-4 h-4", xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
33356
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M19 12H5" }),
33357
- /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "12 19 5 12 12 5" })
33358
- ] }),
33359
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs sm:text-sm font-medium", children: "Back" })
33360
- ]
34396
+ text: "Back",
34397
+ size: "default",
34398
+ "aria-label": "Navigate back to previous page"
33361
34399
  }
33362
34400
  ) }),
33363
34401
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 sm:gap-3", children: [
@@ -34418,18 +35456,15 @@ var ShiftsView = ({
34418
35456
  }, [lineConfigs, supabase, showToast]);
34419
35457
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `min-h-screen bg-slate-50 ${className}`, children: [
34420
35458
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sticky top-0 z-10 bg-white border-b border-gray-200/80 shadow-sm", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 sm:px-8 py-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center relative", children: [
34421
- /* @__PURE__ */ jsxRuntime.jsxs(
34422
- "button",
35459
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute left-0", children: /* @__PURE__ */ jsxRuntime.jsx(
35460
+ BackButtonMinimal,
34423
35461
  {
34424
35462
  onClick: () => onBackClick ? onBackClick() : window.history.back(),
34425
- className: "absolute left-0 flex items-center gap-2 text-gray-600 hover:text-gray-900 transition-colors",
34426
- "aria-label": "Go back",
34427
- children: [
34428
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowLeft, { className: "w-5 h-5" }),
34429
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium", children: "Back" })
34430
- ]
35463
+ text: "Back",
35464
+ size: "default",
35465
+ "aria-label": "Navigate back to previous page"
34431
35466
  }
34432
- ),
35467
+ ) }),
34433
35468
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 flex flex-col items-center", children: [
34434
35469
  /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-3xl font-semibold text-gray-900", children: "Shift Management" }),
34435
35470
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-500 mt-1", children: "Configure day and night shift timings and breaks for each production line" })
@@ -35327,6 +36362,7 @@ var TargetsViewUI = ({
35327
36362
  onSaveLine,
35328
36363
  onToggleBulkConfigure,
35329
36364
  onBulkConfigure,
36365
+ onUpdateWorkspaceDisplayName,
35330
36366
  // SKU props
35331
36367
  skuEnabled = false,
35332
36368
  skus = [],
@@ -35338,15 +36374,13 @@ var TargetsViewUI = ({
35338
36374
  }
35339
36375
  return /* @__PURE__ */ jsxRuntime.jsxs("main", { className: "min-h-screen flex-1 bg-gray-50", children: [
35340
36376
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-white px-8 py-6 shadow-sm border-b border-gray-200/80", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between relative", children: [
35341
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute left-0", children: /* @__PURE__ */ jsxRuntime.jsxs(
35342
- "button",
36377
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute left-0", children: /* @__PURE__ */ jsxRuntime.jsx(
36378
+ BackButtonMinimal,
35343
36379
  {
35344
36380
  onClick: onBack,
35345
- className: "flex items-center text-gray-600 hover:text-gray-900",
35346
- children: [
35347
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowLeftIcon, { className: "h-5 w-5" }),
35348
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-2", children: "Back" })
35349
- ]
36381
+ text: "Back",
36382
+ size: "default",
36383
+ "aria-label": "Navigate back to previous page"
35350
36384
  }
35351
36385
  ) }),
35352
36386
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute right-0", children: /* @__PURE__ */ jsxRuntime.jsxs(
@@ -35494,7 +36528,18 @@ var TargetsViewUI = ({
35494
36528
  {
35495
36529
  className: "px-6 py-4 hover:bg-gray-50 transition-all duration-200",
35496
36530
  children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-12 gap-6 items-center", children: [
35497
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-2", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-gray-900", children: formattedName }) }),
36531
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-2", children: onUpdateWorkspaceDisplayName ? /* @__PURE__ */ jsxRuntime.jsx(
36532
+ InlineEditableText,
36533
+ {
36534
+ value: formattedName,
36535
+ onSave: async (newName) => {
36536
+ await onUpdateWorkspaceDisplayName(workspace.id, newName);
36537
+ },
36538
+ placeholder: "Workspace name",
36539
+ className: "font-medium text-gray-900",
36540
+ inputClassName: "min-w-[120px]"
36541
+ }
36542
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-gray-900", children: formattedName }) }),
35498
36543
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-2", children: /* @__PURE__ */ jsxRuntime.jsxs(
35499
36544
  "select",
35500
36545
  {
@@ -36235,6 +37280,17 @@ var TargetsView = ({
36235
37280
  router.push("/");
36236
37281
  }
36237
37282
  };
37283
+ const handleUpdateWorkspaceDisplayName = React19.useCallback(async (workspaceId, displayName) => {
37284
+ try {
37285
+ await workspaceService.updateWorkspaceDisplayName(workspaceId, displayName);
37286
+ await forceRefreshWorkspaceDisplayNames();
37287
+ sonner.toast.success("Workspace name updated successfully");
37288
+ } catch (error) {
37289
+ console.error("Error updating workspace display name:", error);
37290
+ sonner.toast.error("Failed to update workspace name");
37291
+ throw error;
37292
+ }
37293
+ }, []);
36238
37294
  return /* @__PURE__ */ jsxRuntime.jsx(
36239
37295
  TargetsViewUI_default,
36240
37296
  {
@@ -36257,6 +37313,7 @@ var TargetsView = ({
36257
37313
  onSaveLine: handleSaveLine,
36258
37314
  onToggleBulkConfigure: handleToggleBulkConfigure,
36259
37315
  onBulkConfigure: handleBulkConfigure,
37316
+ onUpdateWorkspaceDisplayName: handleUpdateWorkspaceDisplayName,
36260
37317
  skuEnabled,
36261
37318
  skus,
36262
37319
  onUpdateSelectedSKU: updateSelectedSKU,
@@ -36355,6 +37412,14 @@ var WorkspaceDetailView = ({
36355
37412
  const [usingFallbackData, setUsingFallbackData] = React19.useState(false);
36356
37413
  const [showIdleTime, setShowIdleTime] = React19.useState(false);
36357
37414
  const dashboardConfig = useDashboardConfig();
37415
+ const {
37416
+ workspace: workspaceHealth,
37417
+ loading: healthLoading,
37418
+ error: healthError
37419
+ } = useWorkspaceHealthById(workspaceId, {
37420
+ enableRealtime: true,
37421
+ refreshInterval: 3e4
37422
+ });
36358
37423
  const {
36359
37424
  status: prefetchStatus,
36360
37425
  data: prefetchData,
@@ -36663,15 +37728,13 @@ var WorkspaceDetailView = ({
36663
37728
  "Error: ",
36664
37729
  error.message
36665
37730
  ] }),
36666
- /* @__PURE__ */ jsxRuntime.jsxs(
36667
- "button",
37731
+ /* @__PURE__ */ jsxRuntime.jsx(
37732
+ BackButton,
36668
37733
  {
36669
37734
  onClick: () => onNavigate && onNavigate("/"),
36670
- className: "inline-flex items-center text-gray-600 hover:text-gray-900",
36671
- children: [
36672
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowLeft, { className: "w-5 h-5 mr-2" }),
36673
- "Return to Dashboard"
36674
- ]
37735
+ text: "Return to Dashboard",
37736
+ size: "default",
37737
+ "aria-label": "Return to dashboard"
36675
37738
  }
36676
37739
  )
36677
37740
  ] });
@@ -36679,15 +37742,13 @@ var WorkspaceDetailView = ({
36679
37742
  if (!workspace) {
36680
37743
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-h-screen p-8 bg-slate-50", children: [
36681
37744
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-4 text-xl text-gray-600", children: "Workspace not found" }),
36682
- /* @__PURE__ */ jsxRuntime.jsxs(
36683
- "button",
37745
+ /* @__PURE__ */ jsxRuntime.jsx(
37746
+ BackButton,
36684
37747
  {
36685
37748
  onClick: () => onNavigate && onNavigate("/"),
36686
- className: "inline-flex items-center text-gray-600 hover:text-gray-900",
36687
- children: [
36688
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowLeft, { className: "w-5 h-5 mr-2" }),
36689
- "Return to Dashboard"
36690
- ]
37749
+ text: "Return to Dashboard",
37750
+ size: "default",
37751
+ "aria-label": "Return to dashboard"
36691
37752
  }
36692
37753
  )
36693
37754
  ] });
@@ -36702,21 +37763,16 @@ var WorkspaceDetailView = ({
36702
37763
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-h-screen w-full flex flex-col bg-slate-50", children: [
36703
37764
  /* @__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: [
36704
37765
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex items-center", children: [
36705
- /* @__PURE__ */ jsxRuntime.jsxs(
36706
- "button",
37766
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute left-0 z-10", children: /* @__PURE__ */ jsxRuntime.jsx(
37767
+ BackButtonMinimal,
36707
37768
  {
36708
37769
  onClick: handleBackNavigation,
36709
- className: "absolute left-0 flex items-center text-gray-600 hover:text-gray-900 z-10",
36710
- children: [
36711
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowLeft, { className: "w-3 h-3 sm:w-3.5 lg:w-4 sm:h-3.5 lg:h-4 mr-1" }),
36712
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm sm:text-sm lg:text-base", children: previousView === "line_monthly_history" ? "Back to Line History" : returnUrl && returnUrl.includes("monthly_history") ? "Back to Line History" : returnUrl && returnUrl.includes("/kpis/") ? "Back to KPIs" : returnUrl && returnUrl.includes("/leaderboard/") ? "Back to Leaderboard" : date || shift ? "Back to Monthly History" : "Back" })
36713
- ]
37770
+ text: previousView === "line_monthly_history" ? "Back to Line History" : returnUrl && returnUrl.includes("monthly_history") ? "Back to Line History" : returnUrl && returnUrl.includes("/kpis/") ? "Back to KPIs" : returnUrl && returnUrl.includes("/leaderboard/") ? "Back to Leaderboard" : date || shift ? "Back to Monthly History" : "Back",
37771
+ size: "default",
37772
+ "aria-label": "Navigate back to previous page"
36714
37773
  }
36715
- ),
36716
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute left-1/2 transform -translate-x-1/2 flex items-center gap-3", children: [
36717
- /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-3xl font-semibold text-gray-900", children: formattedWorkspaceName }),
36718
- /* @__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" })
36719
- ] }),
37774
+ ) }),
37775
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute left-1/2 transform -translate-x-1/2", children: /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-3xl font-semibold text-gray-900", children: formattedWorkspaceName }) }),
36720
37776
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full h-8" })
36721
37777
  ] }),
36722
37778
  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: [
@@ -36744,6 +37800,19 @@ var WorkspaceDetailView = ({
36744
37800
  workspace.shift_type,
36745
37801
  " Shift"
36746
37802
  ] })
37803
+ ] }),
37804
+ workspaceHealth && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
37805
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-px h-4 bg-blue-300" }),
37806
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
37807
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: clsx(
37808
+ "h-1.5 w-1.5 rounded-full",
37809
+ workspaceHealth.status === "healthy" ? "bg-green-600" : workspaceHealth.status === "unhealthy" ? "bg-red-600" : workspaceHealth.status === "warning" ? "bg-amber-600" : "bg-gray-500"
37810
+ ) }),
37811
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-blue-700", children: [
37812
+ "Last update: ",
37813
+ workspaceHealth.timeSinceLastUpdate
37814
+ ] })
37815
+ ] })
36747
37816
  ] })
36748
37817
  ] }) }),
36749
37818
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-1 sm:mt-1.5 lg:mt-2 flex items-center justify-between", children: [
@@ -37220,17 +38289,15 @@ var SKUManagementView = () => {
37220
38289
  }
37221
38290
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-h-screen bg-slate-50", children: [
37222
38291
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sticky top-0 z-10 bg-white border-b border-gray-200/80 shadow-sm", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 sm:px-8 py-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center relative", children: [
37223
- /* @__PURE__ */ jsxRuntime.jsxs(
37224
- "button",
38292
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute left-0", children: /* @__PURE__ */ jsxRuntime.jsx(
38293
+ BackButtonMinimal,
37225
38294
  {
37226
38295
  onClick: handleBack,
37227
- className: "absolute left-0 flex items-center gap-2 text-gray-600 hover:text-gray-900 transition-colors",
37228
- children: [
37229
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowLeft, { className: "h-5 w-5" }),
37230
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Back" })
37231
- ]
38296
+ text: "Back",
38297
+ size: "default",
38298
+ "aria-label": "Navigate back to previous page"
37232
38299
  }
37233
- ),
38300
+ ) }),
37234
38301
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 text-center mx-auto", children: [
37235
38302
  /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-3xl font-semibold text-gray-900", children: "SKU Management" }),
37236
38303
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-sm text-gray-500", children: "Manage Stock Keeping Units (SKUs) for production planning" })
@@ -37282,6 +38349,255 @@ var SKUManagementView = () => {
37282
38349
  )
37283
38350
  ] });
37284
38351
  };
38352
+ var WorkspaceHealthView = ({
38353
+ lineId,
38354
+ companyId,
38355
+ onNavigate,
38356
+ className = ""
38357
+ }) => {
38358
+ const router$1 = router.useRouter();
38359
+ const [viewMode, setViewMode] = React19.useState("grid");
38360
+ const [groupBy, setGroupBy] = React19.useState("line");
38361
+ const operationalDate = getOperationalDate();
38362
+ const currentHour = (/* @__PURE__ */ new Date()).getHours();
38363
+ const isNightShift = currentHour >= 18 || currentHour < 6;
38364
+ const shiftType = isNightShift ? "Night" : "Day";
38365
+ const formatDate = (date) => {
38366
+ const d = new Date(date);
38367
+ return d.toLocaleDateString("en-IN", {
38368
+ month: "long",
38369
+ day: "numeric",
38370
+ year: "numeric",
38371
+ timeZone: "Asia/Kolkata"
38372
+ });
38373
+ };
38374
+ const getShiftIcon = (shift) => {
38375
+ return shift === "Night" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Moon, { className: "h-4 w-4" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Sun, { className: "h-4 w-4" });
38376
+ };
38377
+ const {
38378
+ workspaces,
38379
+ summary,
38380
+ loading,
38381
+ error,
38382
+ refetch
38383
+ } = useWorkspaceHealth({
38384
+ lineId,
38385
+ companyId,
38386
+ enableRealtime: true,
38387
+ refreshInterval: 1e4
38388
+ // Refresh every 10 seconds for more responsive updates
38389
+ });
38390
+ const handleWorkspaceClick = React19.useCallback(
38391
+ (workspace) => {
38392
+ const url = `/workspace/${workspace.workspace_id}`;
38393
+ if (onNavigate) {
38394
+ onNavigate(url);
38395
+ } else {
38396
+ router$1.push(url);
38397
+ }
38398
+ },
38399
+ [router$1, onNavigate]
38400
+ );
38401
+ const handleExport = React19.useCallback(() => {
38402
+ const csv = [
38403
+ ["Workspace", "Line", "Company", "Status", "Last Heartbeat", "Consecutive Misses"],
38404
+ ...workspaces.map((w) => [
38405
+ w.workspace_display_name || "",
38406
+ w.line_name || "",
38407
+ w.company_name || "",
38408
+ w.status,
38409
+ w.last_heartbeat,
38410
+ w.consecutive_misses?.toString() || "0"
38411
+ ])
38412
+ ].map((row) => row.join(",")).join("\n");
38413
+ const blob = new Blob([csv], { type: "text/csv" });
38414
+ const url = window.URL.createObjectURL(blob);
38415
+ const a = document.createElement("a");
38416
+ a.href = url;
38417
+ a.download = `workspace-health-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.csv`;
38418
+ document.body.appendChild(a);
38419
+ a.click();
38420
+ document.body.removeChild(a);
38421
+ window.URL.revokeObjectURL(url);
38422
+ }, [workspaces]);
38423
+ const getStatusIcon = (status) => {
38424
+ switch (status) {
38425
+ case "healthy":
38426
+ return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CheckCircle, { className: "h-5 w-5 text-green-500" });
38427
+ case "unhealthy":
38428
+ return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.XCircle, { className: "h-5 w-5 text-red-500" });
38429
+ case "warning":
38430
+ return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { className: "h-5 w-5 text-yellow-500" });
38431
+ default:
38432
+ return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Activity, { className: "h-5 w-5 text-gray-400" });
38433
+ }
38434
+ };
38435
+ const getUptimeColor = (percentage) => {
38436
+ if (percentage >= 99) return "text-green-600 dark:text-green-400";
38437
+ if (percentage >= 95) return "text-yellow-600 dark:text-yellow-400";
38438
+ return "text-red-600 dark:text-red-400";
38439
+ };
38440
+ if (loading && !summary) {
38441
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-screen bg-gray-50 dark:bg-gray-900 p-4", children: /* @__PURE__ */ jsxRuntime.jsx(LoadingState, {}) });
38442
+ }
38443
+ if (error) {
38444
+ 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: [
38445
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.XCircle, { className: "h-12 w-12 text-red-500 mx-auto mb-4" }),
38446
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-xl font-semibold text-gray-900 dark:text-gray-100 mb-2", children: "Error Loading Health Status" }),
38447
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-600 dark:text-gray-400 mb-4", children: error.message || "Unable to load workspace health status" }),
38448
+ /* @__PURE__ */ jsxRuntime.jsx(
38449
+ "button",
38450
+ {
38451
+ onClick: () => refetch(),
38452
+ className: "px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors",
38453
+ children: "Try Again"
38454
+ }
38455
+ )
38456
+ ] }) }) }) });
38457
+ }
38458
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: clsx("min-h-screen bg-slate-50", className), children: [
38459
+ /* @__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: [
38460
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex items-center", children: [
38461
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute left-0 z-10", children: /* @__PURE__ */ jsxRuntime.jsx(
38462
+ BackButtonMinimal,
38463
+ {
38464
+ onClick: () => router$1.push("/"),
38465
+ text: "Back",
38466
+ size: "default",
38467
+ "aria-label": "Navigate back to dashboard"
38468
+ }
38469
+ ) }),
38470
+ /* @__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: [
38471
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-3xl font-semibold text-gray-900", children: "System Health" }),
38472
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex h-2.5 w-2.5", children: [
38473
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" }),
38474
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500" })
38475
+ ] })
38476
+ ] }) }),
38477
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute right-0 flex gap-2", children: [
38478
+ /* @__PURE__ */ jsxRuntime.jsx(
38479
+ "button",
38480
+ {
38481
+ onClick: () => {
38482
+ refetch();
38483
+ },
38484
+ className: "p-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors",
38485
+ "aria-label": "Refresh",
38486
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { className: "h-5 w-5" })
38487
+ }
38488
+ ),
38489
+ /* @__PURE__ */ jsxRuntime.jsx(
38490
+ "button",
38491
+ {
38492
+ onClick: handleExport,
38493
+ className: "p-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors",
38494
+ "aria-label": "Export CSV",
38495
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { className: "h-5 w-5" })
38496
+ }
38497
+ )
38498
+ ] }),
38499
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full h-8" })
38500
+ ] }),
38501
+ /* @__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: [
38502
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-lg font-medium text-blue-600", children: /* @__PURE__ */ jsxRuntime.jsx(LiveTimer, {}) }),
38503
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-px h-4 bg-blue-300" }),
38504
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-base font-medium text-blue-600", children: formatDate(operationalDate) }),
38505
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-px h-4 bg-blue-300" }),
38506
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
38507
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-blue-600", children: getShiftIcon(shiftType) }),
38508
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-base font-medium text-blue-600", children: [
38509
+ shiftType,
38510
+ " Shift"
38511
+ ] })
38512
+ ] })
38513
+ ] }) })
38514
+ ] }),
38515
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-w-7xl mx-auto p-4 space-y-6", children: [
38516
+ summary && /* @__PURE__ */ jsxRuntime.jsxs(
38517
+ motion.div,
38518
+ {
38519
+ initial: { opacity: 0, y: 20 },
38520
+ animate: { opacity: 1, y: 0 },
38521
+ transition: { duration: 0.3, delay: 0.1 },
38522
+ className: "grid grid-cols-2 sm:grid-cols-2 md:grid-cols-5 gap-2 sm:gap-3 lg:gap-4",
38523
+ children: [
38524
+ /* @__PURE__ */ jsxRuntime.jsxs(Card2, { className: "col-span-2 sm:col-span-2 md:col-span-2", children: [
38525
+ /* @__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" }) }),
38526
+ /* @__PURE__ */ jsxRuntime.jsxs(CardContent2, { children: [
38527
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-baseline gap-2", children: [
38528
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: clsx("text-3xl font-bold", getUptimeColor(summary.uptimePercentage)), children: [
38529
+ summary.uptimePercentage.toFixed(1),
38530
+ "%"
38531
+ ] }),
38532
+ 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" })
38533
+ ] }),
38534
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: [
38535
+ summary.healthyWorkspaces,
38536
+ " of ",
38537
+ summary.totalWorkspaces,
38538
+ " workspaces healthy"
38539
+ ] })
38540
+ ] })
38541
+ ] }),
38542
+ /* @__PURE__ */ jsxRuntime.jsxs(Card2, { children: [
38543
+ /* @__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: [
38544
+ getStatusIcon("healthy"),
38545
+ "Healthy"
38546
+ ] }) }),
38547
+ /* @__PURE__ */ jsxRuntime.jsxs(CardContent2, { children: [
38548
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-2xl font-bold text-green-600 dark:text-green-400", children: summary.healthyWorkspaces }),
38549
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: "Operating normally" })
38550
+ ] })
38551
+ ] }),
38552
+ /* @__PURE__ */ jsxRuntime.jsxs(Card2, { children: [
38553
+ /* @__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: [
38554
+ getStatusIcon("warning"),
38555
+ "Warning"
38556
+ ] }) }),
38557
+ /* @__PURE__ */ jsxRuntime.jsxs(CardContent2, { children: [
38558
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-2xl font-bold text-yellow-600 dark:text-yellow-400", children: summary.warningWorkspaces }),
38559
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: "Delayed updates" })
38560
+ ] })
38561
+ ] }),
38562
+ /* @__PURE__ */ jsxRuntime.jsxs(Card2, { children: [
38563
+ /* @__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: [
38564
+ getStatusIcon("unhealthy"),
38565
+ "Unhealthy"
38566
+ ] }) }),
38567
+ /* @__PURE__ */ jsxRuntime.jsxs(CardContent2, { children: [
38568
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-2xl font-bold text-red-600 dark:text-red-400", children: summary.unhealthyWorkspaces }),
38569
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: "Requires attention" })
38570
+ ] })
38571
+ ] })
38572
+ ]
38573
+ }
38574
+ ),
38575
+ /* @__PURE__ */ jsxRuntime.jsx(
38576
+ motion.div,
38577
+ {
38578
+ initial: { opacity: 0, y: 20 },
38579
+ animate: { opacity: 1, y: 0 },
38580
+ transition: { duration: 0.3, delay: 0.2 },
38581
+ children: /* @__PURE__ */ jsxRuntime.jsx(
38582
+ HealthStatusGrid,
38583
+ {
38584
+ workspaces,
38585
+ onWorkspaceClick: handleWorkspaceClick,
38586
+ viewMode,
38587
+ showFilters: true,
38588
+ groupBy
38589
+ }
38590
+ )
38591
+ }
38592
+ )
38593
+ ] })
38594
+ ] });
38595
+ };
38596
+ var WorkspaceHealthView_default = withAuth(WorkspaceHealthView, {
38597
+ redirectTo: "/login",
38598
+ requireAuth: true
38599
+ });
38600
+ var AuthenticatedWorkspaceHealthView = WorkspaceHealthView;
37285
38601
  var S3Service = class {
37286
38602
  constructor(config) {
37287
38603
  this.s3Client = null;
@@ -37748,6 +39064,9 @@ exports.AuthenticatedHelpView = AuthenticatedHelpView;
37748
39064
  exports.AuthenticatedHomeView = AuthenticatedHomeView;
37749
39065
  exports.AuthenticatedShiftsView = AuthenticatedShiftsView;
37750
39066
  exports.AuthenticatedTargetsView = AuthenticatedTargetsView;
39067
+ exports.AuthenticatedWorkspaceHealthView = AuthenticatedWorkspaceHealthView;
39068
+ exports.BackButton = BackButton;
39069
+ exports.BackButtonMinimal = BackButtonMinimal;
37751
39070
  exports.BarChart = BarChart;
37752
39071
  exports.BaseHistoryCalendar = BaseHistoryCalendar;
37753
39072
  exports.BottlenecksContent = BottlenecksContent;
@@ -37759,6 +39078,7 @@ exports.CardDescription = CardDescription2;
37759
39078
  exports.CardFooter = CardFooter2;
37760
39079
  exports.CardHeader = CardHeader2;
37761
39080
  exports.CardTitle = CardTitle2;
39081
+ exports.CompactWorkspaceHealthCard = CompactWorkspaceHealthCard;
37762
39082
  exports.CongratulationsOverlay = CongratulationsOverlay;
37763
39083
  exports.CycleTimeChart = CycleTimeChart;
37764
39084
  exports.CycleTimeOverTimeChart = CycleTimeOverTimeChart;
@@ -37782,6 +39102,7 @@ exports.DateDisplay = DateDisplay_default;
37782
39102
  exports.DateTimeDisplay = DateTimeDisplay;
37783
39103
  exports.DebugAuth = DebugAuth;
37784
39104
  exports.DebugAuthView = DebugAuthView_default;
39105
+ exports.DetailedHealthStatus = DetailedHealthStatus;
37785
39106
  exports.EmptyStateMessage = EmptyStateMessage;
37786
39107
  exports.EncouragementOverlay = EncouragementOverlay;
37787
39108
  exports.FactoryView = FactoryView_default;
@@ -37789,10 +39110,13 @@ exports.GaugeChart = GaugeChart;
37789
39110
  exports.GridComponentsPlaceholder = GridComponentsPlaceholder;
37790
39111
  exports.HamburgerButton = HamburgerButton;
37791
39112
  exports.Header = Header;
39113
+ exports.HealthStatusGrid = HealthStatusGrid;
39114
+ exports.HealthStatusIndicator = HealthStatusIndicator;
37792
39115
  exports.HelpView = HelpView_default;
37793
39116
  exports.HomeView = HomeView_default;
37794
39117
  exports.HourlyOutputChart = HourlyOutputChart2;
37795
39118
  exports.ISTTimer = ISTTimer_default;
39119
+ exports.InlineEditableText = InlineEditableText;
37796
39120
  exports.KPICard = KPICard;
37797
39121
  exports.KPIDetailView = KPIDetailView_default;
37798
39122
  exports.KPIGrid = KPIGrid;
@@ -37834,6 +39158,7 @@ exports.PrefetchStatus = PrefetchStatus;
37834
39158
  exports.PrefetchTimeoutError = PrefetchTimeoutError;
37835
39159
  exports.ProfileView = ProfileView_default;
37836
39160
  exports.RegistryProvider = RegistryProvider;
39161
+ exports.S3ClipsService = S3ClipsService;
37837
39162
  exports.S3Service = S3Service;
37838
39163
  exports.SKUManagementView = SKUManagementView;
37839
39164
  exports.SOPComplianceChart = SOPComplianceChart;
@@ -37874,6 +39199,8 @@ exports.WorkspaceDetailView = WorkspaceDetailView_default;
37874
39199
  exports.WorkspaceDisplayNameExample = WorkspaceDisplayNameExample;
37875
39200
  exports.WorkspaceGrid = WorkspaceGrid;
37876
39201
  exports.WorkspaceGridItem = WorkspaceGridItem;
39202
+ exports.WorkspaceHealthCard = WorkspaceHealthCard;
39203
+ exports.WorkspaceHealthView = WorkspaceHealthView_default;
37877
39204
  exports.WorkspaceHistoryCalendar = WorkspaceHistoryCalendar;
37878
39205
  exports.WorkspaceMetricCards = WorkspaceMetricCards;
37879
39206
  exports.WorkspaceMonthlyDataFetcher = WorkspaceMonthlyDataFetcher;
@@ -37958,6 +39285,7 @@ exports.isWorkspaceDisplayNamesLoading = isWorkspaceDisplayNamesLoading;
37958
39285
  exports.mergeWithDefaultConfig = mergeWithDefaultConfig;
37959
39286
  exports.migrateLegacyConfiguration = migrateLegacyConfiguration;
37960
39287
  exports.optifyeAgentClient = optifyeAgentClient;
39288
+ exports.parseS3Uri = parseS3Uri;
37961
39289
  exports.preInitializeWorkspaceDisplayNames = preInitializeWorkspaceDisplayNames;
37962
39290
  exports.preloadS3Video = preloadS3Video;
37963
39291
  exports.preloadS3VideoUrl = preloadS3VideoUrl;
@@ -37971,6 +39299,7 @@ exports.resetCoreMixpanel = resetCoreMixpanel;
37971
39299
  exports.resetFailedUrl = resetFailedUrl;
37972
39300
  exports.resetSubscriptionManager = resetSubscriptionManager;
37973
39301
  exports.s3VideoPreloader = s3VideoPreloader;
39302
+ exports.shuffleArray = shuffleArray;
37974
39303
  exports.skuService = skuService;
37975
39304
  exports.startCoreSessionRecording = startCoreSessionRecording;
37976
39305
  exports.stopCoreSessionRecording = stopCoreSessionRecording;
@@ -38037,6 +39366,8 @@ exports.useWorkspaceDetailedMetrics = useWorkspaceDetailedMetrics;
38037
39366
  exports.useWorkspaceDisplayName = useWorkspaceDisplayName;
38038
39367
  exports.useWorkspaceDisplayNames = useWorkspaceDisplayNames;
38039
39368
  exports.useWorkspaceDisplayNamesMap = useWorkspaceDisplayNamesMap;
39369
+ exports.useWorkspaceHealth = useWorkspaceHealth;
39370
+ exports.useWorkspaceHealthById = useWorkspaceHealthById;
38040
39371
  exports.useWorkspaceMetrics = useWorkspaceMetrics;
38041
39372
  exports.useWorkspaceNavigation = useWorkspaceNavigation;
38042
39373
  exports.useWorkspaceOperators = useWorkspaceOperators;
@@ -38045,4 +39376,5 @@ exports.videoPreloader = videoPreloader;
38045
39376
  exports.whatsappService = whatsappService;
38046
39377
  exports.withAuth = withAuth;
38047
39378
  exports.withRegistry = withRegistry;
39379
+ exports.workspaceHealthService = workspaceHealthService;
38048
39380
  exports.workspaceService = workspaceService;