@optifye/dashboard-core 6.4.2 → 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.mjs CHANGED
@@ -3,10 +3,9 @@ import React19__default, { createContext, useRef, useCallback, useState, useMemo
3
3
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
4
  import { useRouter } from 'next/router';
5
5
  import { toZonedTime, formatInTimeZone } from 'date-fns-tz';
6
- import { subDays, format, parseISO, isValid, isFuture, isToday } from 'date-fns';
6
+ import { subDays, format, parseISO, isValid, formatDistanceToNow, isFuture, isToday } from 'date-fns';
7
7
  import mixpanel from 'mixpanel-browser';
8
8
  import { EventEmitter } from 'events';
9
- import { S3Client, ListObjectsV2Command, GetObjectCommand } from '@aws-sdk/client-s3';
10
9
  import { REALTIME_SUBSCRIBE_STATES, createClient } from '@supabase/supabase-js';
11
10
  import Hls2 from 'hls.js';
12
11
  import useSWR from 'swr';
@@ -14,9 +13,9 @@ import { noop, warning, invariant, progress, secondsToMilliseconds, milliseconds
14
13
  import { getValueTransition, hover, press, isPrimaryPointer, GroupPlaybackControls, setDragLock, supportsLinearEasing, attachTimeline, isGenerator, calcGeneratorDuration, isWaapiSupportedEasing, mapEasingToNativeEasing, maxGeneratorDuration, generateLinearEasing, isBezierDefinition } from 'motion-dom';
15
14
  import { BarChart as BarChart$1, CartesianGrid, XAxis, YAxis, Tooltip, Legend, Bar, LabelList, ResponsiveContainer, LineChart as LineChart$1, Line, PieChart, Pie, Cell, ReferenceLine, ComposedChart, Area, ScatterChart, Scatter } from 'recharts';
16
15
  import { Slot } from '@radix-ui/react-slot';
17
- import { Camera, ChevronDown, ChevronUp, Check, ShieldCheck, Star, Award, X, Coffee, Plus, ArrowLeft, Clock, Calendar, Save, Minus, ArrowDown, ArrowUp, Settings2, CheckCircle2, Search, CheckCircle, AlertTriangle, Info, Share2, Trophy, Target, Download, User, XCircle, ChevronLeft, ChevronRight, AlertCircle, Sun, Moon, MessageSquare, Trash2, RefreshCw, Menu, Send, Copy, Edit2, UserCheck, LogOut, Package, Settings, LifeBuoy, EyeOff, Eye, Zap, UserCircle } from 'lucide-react';
16
+ import { Camera, ChevronDown, ChevronUp, Check, ShieldCheck, Star, Award, X, Coffee, Plus, ArrowLeft, Clock, Calendar, Save, Minus, ArrowDown, ArrowUp, Settings2, CheckCircle2, Search, Loader2, AlertCircle, Edit2, CheckCircle, AlertTriangle, Info, Share2, Trophy, Target, Download, User, XCircle, ChevronLeft, ChevronRight, WifiOff, Wifi, Grid3x3, List, Sun, Moon, MessageSquare, Trash2, RefreshCw, Menu, Send, Copy, UserCheck, LogOut, Package, TrendingUp, TrendingDown, Activity, Settings, LifeBuoy, EyeOff, Eye, Zap, UserCircle } from 'lucide-react';
18
17
  import { DayPicker, useNavigation as useNavigation$1 } from 'react-day-picker';
19
- import { XMarkIcon, ArrowRightIcon, HomeIcon, TrophyIcon, ChartBarIcon, AdjustmentsHorizontalIcon, ClockIcon, CubeIcon, SparklesIcon, QuestionMarkCircleIcon, UserCircleIcon, ExclamationCircleIcon, EnvelopeIcon, DocumentTextIcon, ChevronUpIcon, ChevronDownIcon, Bars3Icon, CheckCircleIcon, ChatBubbleLeftRightIcon, XCircleIcon, InformationCircleIcon, ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
18
+ import { XMarkIcon, ArrowRightIcon, HomeIcon, TrophyIcon, ChartBarIcon, AdjustmentsHorizontalIcon, ClockIcon, CubeIcon, SparklesIcon, QuestionMarkCircleIcon, HeartIcon, UserCircleIcon, ExclamationCircleIcon, EnvelopeIcon, DocumentTextIcon, ChevronUpIcon, ChevronDownIcon, Bars3Icon, CheckCircleIcon, ChatBubbleLeftRightIcon, XCircleIcon, InformationCircleIcon, ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
20
19
  import { CheckIcon } from '@heroicons/react/24/solid';
21
20
  import html2canvas from 'html2canvas';
22
21
  import jsPDF, { jsPDF as jsPDF$1 } from 'jspdf';
@@ -24,6 +23,7 @@ import * as SelectPrimitive from '@radix-ui/react-select';
24
23
  import videojs from 'video.js';
25
24
  import 'video.js/dist/video-js.css';
26
25
  import { toast } from 'sonner';
26
+ import { S3Client, ListObjectsV2Command, GetObjectCommand } from '@aws-sdk/client-s3';
27
27
  import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
28
28
  import { Readable } from 'stream';
29
29
 
@@ -1674,6 +1674,25 @@ var workspaceService = {
1674
1674
  this._workspaceDisplayNamesCache.clear();
1675
1675
  this._cacheTimestamp = 0;
1676
1676
  },
1677
+ /**
1678
+ * Updates the display name for a workspace
1679
+ * @param workspaceId - The workspace UUID
1680
+ * @param displayName - The new display name
1681
+ * @returns Promise<void>
1682
+ */
1683
+ async updateWorkspaceDisplayName(workspaceId, displayName) {
1684
+ const supabase = _getSupabaseInstance();
1685
+ if (!supabase) throw new Error("Supabase client not initialized");
1686
+ const config = _getDashboardConfigInstance();
1687
+ const dbConfig = config?.databaseConfig;
1688
+ const workspacesTable = getTable3(dbConfig, "workspaces");
1689
+ const { error } = await supabase.from(workspacesTable).update({ display_name: displayName.trim() }).eq("id", workspaceId);
1690
+ if (error) {
1691
+ console.error(`Error updating workspace display name for ${workspaceId}:`, error);
1692
+ throw error;
1693
+ }
1694
+ this.clearWorkspaceDisplayNamesCache();
1695
+ },
1677
1696
  async updateWorkspaceAction(updates) {
1678
1697
  const supabase = _getSupabaseInstance();
1679
1698
  if (!supabase) throw new Error("Supabase client not initialized");
@@ -1817,6 +1836,209 @@ var workspaceService = {
1817
1836
  }
1818
1837
  }
1819
1838
  };
1839
+ var WorkspaceHealthService = class _WorkspaceHealthService {
1840
+ constructor() {
1841
+ this.cache = /* @__PURE__ */ new Map();
1842
+ this.cacheExpiryMs = 30 * 1e3;
1843
+ }
1844
+ // 30 seconds cache
1845
+ static getInstance() {
1846
+ if (!_WorkspaceHealthService.instance) {
1847
+ _WorkspaceHealthService.instance = new _WorkspaceHealthService();
1848
+ }
1849
+ return _WorkspaceHealthService.instance;
1850
+ }
1851
+ getFromCache(key) {
1852
+ const cached = this.cache.get(key);
1853
+ if (cached && Date.now() - cached.timestamp < this.cacheExpiryMs) {
1854
+ return cached.data;
1855
+ }
1856
+ this.cache.delete(key);
1857
+ return null;
1858
+ }
1859
+ setCache(key, data) {
1860
+ this.cache.set(key, { data, timestamp: Date.now() });
1861
+ }
1862
+ async getWorkspaceHealthStatus(options = {}) {
1863
+ const supabase = _getSupabaseInstance();
1864
+ if (!supabase) throw new Error("Supabase client not initialized");
1865
+ let query = supabase.from("workspace_health_status").select("*").order("workspace_display_name", { ascending: true });
1866
+ if (options.lineId) {
1867
+ query = query.eq("line_id", options.lineId);
1868
+ }
1869
+ if (options.companyId) {
1870
+ query = query.eq("company_id", options.companyId);
1871
+ }
1872
+ const { data, error } = await query;
1873
+ if (error) {
1874
+ console.error("Error fetching workspace health status:", error);
1875
+ throw error;
1876
+ }
1877
+ const processedData = (data || []).map((item) => this.processHealthStatus(item));
1878
+ let filteredData = processedData;
1879
+ try {
1880
+ const { data: enabledWorkspaces, error: workspaceError } = await supabase.from("workspaces").select("workspace_id, display_name").eq("enable", true);
1881
+ if (!workspaceError && enabledWorkspaces && enabledWorkspaces.length > 0) {
1882
+ const enabledWorkspaceNames = /* @__PURE__ */ new Set();
1883
+ enabledWorkspaces.forEach((w) => {
1884
+ if (w.workspace_id) enabledWorkspaceNames.add(w.workspace_id);
1885
+ if (w.display_name) enabledWorkspaceNames.add(w.display_name);
1886
+ });
1887
+ filteredData = filteredData.filter((item) => {
1888
+ const displayName = item.workspace_display_name || "";
1889
+ return enabledWorkspaceNames.has(displayName);
1890
+ });
1891
+ } else if (!workspaceError && enabledWorkspaces && enabledWorkspaces.length === 0) {
1892
+ return [];
1893
+ } else if (workspaceError) {
1894
+ console.error("Error fetching enabled workspaces:", workspaceError);
1895
+ }
1896
+ } catch (e) {
1897
+ console.error("Error filtering workspaces:", e);
1898
+ }
1899
+ if (options.status) {
1900
+ filteredData = filteredData.filter((item) => item.status === options.status);
1901
+ }
1902
+ if (options.searchTerm) {
1903
+ const searchLower = options.searchTerm.toLowerCase();
1904
+ filteredData = filteredData.filter(
1905
+ (item) => item.workspace_display_name?.toLowerCase().includes(searchLower) || item.line_name?.toLowerCase().includes(searchLower) || item.company_name?.toLowerCase().includes(searchLower)
1906
+ );
1907
+ }
1908
+ if (options.sortBy) {
1909
+ filteredData.sort((a, b) => {
1910
+ let compareValue = 0;
1911
+ switch (options.sortBy) {
1912
+ case "name":
1913
+ compareValue = (a.workspace_display_name || "").localeCompare(b.workspace_display_name || "");
1914
+ break;
1915
+ case "status":
1916
+ compareValue = this.getStatusPriority(a.status) - this.getStatusPriority(b.status);
1917
+ break;
1918
+ case "lastUpdate":
1919
+ compareValue = new Date(b.last_heartbeat).getTime() - new Date(a.last_heartbeat).getTime();
1920
+ break;
1921
+ }
1922
+ return options.sortOrder === "desc" ? -compareValue : compareValue;
1923
+ });
1924
+ }
1925
+ return filteredData;
1926
+ }
1927
+ async getWorkspaceHealthById(workspaceId) {
1928
+ const cacheKey = `health-${workspaceId}`;
1929
+ const cached = this.getFromCache(cacheKey);
1930
+ if (cached) return cached;
1931
+ const supabase = _getSupabaseInstance();
1932
+ if (!supabase) throw new Error("Supabase client not initialized");
1933
+ const { data, error } = await supabase.from("workspace_health_status").select("*").eq("workspace_id", workspaceId).single();
1934
+ if (error) {
1935
+ if (error.code === "PGRST116") {
1936
+ return null;
1937
+ }
1938
+ console.error("Error fetching workspace health:", error);
1939
+ throw error;
1940
+ }
1941
+ const processedData = data ? this.processHealthStatus(data) : null;
1942
+ if (processedData) {
1943
+ this.setCache(cacheKey, processedData);
1944
+ }
1945
+ return processedData;
1946
+ }
1947
+ async getHealthSummary(lineId, companyId) {
1948
+ this.clearCache();
1949
+ const workspaces = await this.getWorkspaceHealthStatus({ lineId, companyId });
1950
+ const totalWorkspaces = workspaces.length;
1951
+ const healthyWorkspaces = workspaces.filter((w) => w.status === "healthy").length;
1952
+ const unhealthyWorkspaces = workspaces.filter((w) => w.status === "unhealthy").length;
1953
+ const warningWorkspaces = workspaces.filter((w) => w.status === "warning").length;
1954
+ const uptimePercentage = totalWorkspaces > 0 ? healthyWorkspaces / totalWorkspaces * 100 : 0;
1955
+ return {
1956
+ totalWorkspaces,
1957
+ healthyWorkspaces,
1958
+ unhealthyWorkspaces,
1959
+ warningWorkspaces,
1960
+ uptimePercentage,
1961
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
1962
+ };
1963
+ }
1964
+ async getHealthMetrics(workspaceId, startDate, endDate) {
1965
+ const supabase = _getSupabaseInstance();
1966
+ if (!supabase) throw new Error("Supabase client not initialized");
1967
+ return {
1968
+ avgResponseTime: 250,
1969
+ totalDowntime: 0,
1970
+ incidentCount: 0,
1971
+ mttr: 0
1972
+ };
1973
+ }
1974
+ processHealthStatus(data) {
1975
+ const now2 = /* @__PURE__ */ new Date();
1976
+ const lastHeartbeat = new Date(data.last_heartbeat);
1977
+ const minutesSinceUpdate = Math.floor((now2.getTime() - lastHeartbeat.getTime()) / (1e3 * 60));
1978
+ let status = "unknown";
1979
+ let isStale = false;
1980
+ if (data.is_healthy) {
1981
+ if (minutesSinceUpdate < 3) {
1982
+ status = "healthy";
1983
+ } else if (minutesSinceUpdate < 5) {
1984
+ status = "warning";
1985
+ isStale = true;
1986
+ } else {
1987
+ status = "unhealthy";
1988
+ isStale = true;
1989
+ }
1990
+ } else {
1991
+ status = "unhealthy";
1992
+ if (minutesSinceUpdate > 5) {
1993
+ isStale = true;
1994
+ }
1995
+ }
1996
+ const timeSinceLastUpdate = formatDistanceToNow(lastHeartbeat, { addSuffix: true });
1997
+ return {
1998
+ ...data,
1999
+ status,
2000
+ timeSinceLastUpdate,
2001
+ isStale
2002
+ };
2003
+ }
2004
+ getStatusPriority(status) {
2005
+ const priorities = {
2006
+ unhealthy: 0,
2007
+ warning: 1,
2008
+ unknown: 2,
2009
+ healthy: 3
2010
+ };
2011
+ return priorities[status];
2012
+ }
2013
+ subscribeToHealthUpdates(callback, filters) {
2014
+ const supabase = _getSupabaseInstance();
2015
+ if (!supabase) throw new Error("Supabase client not initialized");
2016
+ let subscription = supabase.channel("workspace-health-updates").on(
2017
+ "postgres_changes",
2018
+ {
2019
+ event: "*",
2020
+ schema: "public",
2021
+ table: "workspace_health_status"
2022
+ },
2023
+ (payload) => {
2024
+ if (payload.new) {
2025
+ const newData = payload.new;
2026
+ if (filters?.lineId && newData.line_id !== filters.lineId) return;
2027
+ if (filters?.companyId && newData.company_id !== filters.companyId) return;
2028
+ this.clearCache();
2029
+ callback(newData);
2030
+ }
2031
+ }
2032
+ ).subscribe();
2033
+ return () => {
2034
+ subscription.unsubscribe();
2035
+ };
2036
+ }
2037
+ clearCache() {
2038
+ this.cache.clear();
2039
+ }
2040
+ };
2041
+ var workspaceHealthService = WorkspaceHealthService.getInstance();
1820
2042
 
1821
2043
  // src/lib/services/skuService.ts
1822
2044
  var getTable4 = (dbConfig, tableName) => {
@@ -3211,6 +3433,14 @@ function parseS3Uri(s3Uri, sopCategories) {
3211
3433
  return null;
3212
3434
  }
3213
3435
  }
3436
+ function shuffleArray(array) {
3437
+ const shuffled = [...array];
3438
+ for (let i = shuffled.length - 1; i > 0; i--) {
3439
+ const j = Math.floor(Math.random() * (i + 1));
3440
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
3441
+ }
3442
+ return shuffled;
3443
+ }
3214
3444
 
3215
3445
  // src/lib/cache/clipsCache.ts
3216
3446
  var LRUCache = class _LRUCache {
@@ -3723,300 +3953,321 @@ if (typeof window !== "undefined") {
3723
3953
  });
3724
3954
  });
3725
3955
  }
3726
-
3727
- // src/lib/api/s3-clips.ts
3728
- var RequestDeduplicationCache = class {
3729
- constructor() {
3730
- this.pendingRequests = /* @__PURE__ */ new Map();
3731
- this.maxCacheSize = 1e3;
3732
- this.cleanupInterval = 3e5;
3733
- // 5 minutes
3734
- this.lastCleanup = Date.now();
3956
+ var getSupabaseClient = () => {
3957
+ const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
3958
+ const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
3959
+ if (!url || !key) {
3960
+ throw new Error("Supabase configuration missing");
3961
+ }
3962
+ return createClient(url, key);
3963
+ };
3964
+ var getAuthToken = async () => {
3965
+ try {
3966
+ const supabase = getSupabaseClient();
3967
+ const { data: { session } } = await supabase.auth.getSession();
3968
+ return session?.access_token || null;
3969
+ } catch (error) {
3970
+ console.error("[S3ClipsAPIClient] Error getting auth token:", error);
3971
+ return null;
3972
+ }
3973
+ };
3974
+ var S3ClipsAPIClient = class {
3975
+ constructor(sopCategories) {
3976
+ this.baseUrl = "/api/clips";
3977
+ this.requestCache = /* @__PURE__ */ new Map();
3978
+ this.sopCategories = sopCategories;
3979
+ console.log("[S3ClipsAPIClient] \u2705 Initialized - Using secure API routes (no direct S3 access)");
3735
3980
  }
3736
3981
  /**
3737
- * Get or create a deduplicated request
3982
+ * Fetch with authentication and error handling
3738
3983
  */
3739
- async deduplicate(key, factory, logPrefix = "Request") {
3740
- this.periodicCleanup();
3741
- const existingRequest = this.pendingRequests.get(key);
3742
- if (existingRequest) {
3743
- console.log(`[${logPrefix}] Deduplicating request for key: ${key}`);
3744
- return existingRequest;
3984
+ async fetchWithAuth(endpoint, body) {
3985
+ const token = await getAuthToken();
3986
+ if (!token) {
3987
+ throw new Error("Authentication required");
3988
+ }
3989
+ const response = await fetch(endpoint, {
3990
+ method: "POST",
3991
+ headers: {
3992
+ "Authorization": `Bearer ${token}`,
3993
+ "Content-Type": "application/json"
3994
+ },
3995
+ body: JSON.stringify(body)
3996
+ });
3997
+ if (!response.ok) {
3998
+ const error = await response.json().catch(() => ({ error: "Request failed" }));
3999
+ throw new Error(error.error || `API error: ${response.status}`);
4000
+ }
4001
+ return response.json();
4002
+ }
4003
+ /**
4004
+ * Deduplicate requests to prevent multiple API calls
4005
+ */
4006
+ async deduplicate(key, factory) {
4007
+ if (this.requestCache.has(key)) {
4008
+ console.log(`[S3ClipsAPIClient] Deduplicating request: ${key}`);
4009
+ return this.requestCache.get(key);
3745
4010
  }
3746
- console.log(`[${logPrefix}] Creating new request for key: ${key}`);
3747
4011
  const promise = factory().finally(() => {
3748
- this.pendingRequests.delete(key);
3749
- console.log(`[${logPrefix}] Completed and cleaned up request: ${key}`);
4012
+ this.requestCache.delete(key);
3750
4013
  });
3751
- this.pendingRequests.set(key, promise);
4014
+ this.requestCache.set(key, promise);
3752
4015
  return promise;
3753
4016
  }
3754
4017
  /**
3755
- * Clear all pending requests (useful for cleanup)
4018
+ * List S3 clips
3756
4019
  */
3757
- clear() {
3758
- console.log(`[RequestCache] Clearing ${this.pendingRequests.size} pending requests`);
3759
- this.pendingRequests.clear();
4020
+ async listS3Clips(params) {
4021
+ const cacheKey = `list:${JSON.stringify(params)}`;
4022
+ return this.deduplicate(cacheKey, async () => {
4023
+ const response = await this.fetchWithAuth(this.baseUrl, {
4024
+ action: "list",
4025
+ workspaceId: params.workspaceId,
4026
+ date: params.date,
4027
+ shift: params.shiftId,
4028
+ maxKeys: params.maxKeys
4029
+ });
4030
+ return response.clips.map((clip) => clip.originalUri);
4031
+ });
3760
4032
  }
3761
4033
  /**
3762
- * Get current cache stats
4034
+ * Get clip counts
3763
4035
  */
3764
- getStats() {
3765
- return {
3766
- pendingCount: this.pendingRequests.size,
3767
- maxSize: this.maxCacheSize
4036
+ async getClipCounts(workspaceId, date, shiftId) {
4037
+ const cacheKey = `counts:${workspaceId}:${date}:${shiftId}`;
4038
+ return this.deduplicate(cacheKey, async () => {
4039
+ const response = await this.fetchWithAuth(this.baseUrl, {
4040
+ action: "count",
4041
+ workspaceId,
4042
+ date,
4043
+ shift: shiftId.toString()
4044
+ });
4045
+ return response.counts;
4046
+ });
4047
+ }
4048
+ /**
4049
+ * Get clip counts with index (for compatibility)
4050
+ */
4051
+ async getClipCountsWithIndex(workspaceId, date, shiftId) {
4052
+ const counts = await this.getClipCounts(workspaceId, date, shiftId.toString());
4053
+ const videoIndex = {
4054
+ byCategory: /* @__PURE__ */ new Map(),
4055
+ allVideos: [],
4056
+ counts,
4057
+ workspaceId,
4058
+ date,
4059
+ shiftId: shiftId.toString(),
4060
+ lastUpdated: /* @__PURE__ */ new Date()
3768
4061
  };
4062
+ return { counts, videoIndex };
3769
4063
  }
3770
4064
  /**
3771
- * Periodic cleanup to prevent memory leaks
4065
+ * Get metadata for a video
3772
4066
  */
3773
- periodicCleanup() {
3774
- const now2 = Date.now();
3775
- if (now2 - this.lastCleanup > this.cleanupInterval) {
3776
- if (this.pendingRequests.size > this.maxCacheSize) {
3777
- console.warn(`[RequestCache] Cache size exceeded ${this.maxCacheSize}, clearing oldest entries`);
3778
- const entries = Array.from(this.pendingRequests.entries());
3779
- this.pendingRequests.clear();
3780
- entries.slice(-Math.floor(this.maxCacheSize / 2)).forEach(([key, promise]) => {
3781
- this.pendingRequests.set(key, promise);
3782
- });
4067
+ async getMetadata(playlistUri) {
4068
+ const cacheKey = `metadata:${playlistUri}`;
4069
+ return this.deduplicate(cacheKey, async () => {
4070
+ const response = await this.fetchWithAuth(this.baseUrl, {
4071
+ action: "metadata",
4072
+ playlistUri
4073
+ });
4074
+ return response.metadata;
4075
+ });
4076
+ }
4077
+ /**
4078
+ * Get metadata cycle time
4079
+ */
4080
+ async getMetadataCycleTime(playlistUri) {
4081
+ const metadata = await this.getMetadata(playlistUri);
4082
+ return metadata?.cycle_time_seconds || null;
4083
+ }
4084
+ /**
4085
+ * Get first clip for category
4086
+ */
4087
+ async getFirstClipForCategory(workspaceId, date, shiftId, category) {
4088
+ const cacheKey = `first:${workspaceId}:${date}:${shiftId}:${category}`;
4089
+ return this.deduplicate(cacheKey, async () => {
4090
+ const response = await this.fetchWithAuth(this.baseUrl, {
4091
+ action: "first",
4092
+ workspaceId,
4093
+ date,
4094
+ shift: shiftId.toString(),
4095
+ category,
4096
+ sopCategories: this.sopCategories
4097
+ });
4098
+ return response.video;
4099
+ });
4100
+ }
4101
+ /**
4102
+ * Get clip by index
4103
+ */
4104
+ async getClipByIndex(workspaceId, date, shiftId, category, index, includeCycleTime = true, includeMetadata = false) {
4105
+ const cacheKey = `by-index:${workspaceId}:${date}:${shiftId}:${category}:${index}`;
4106
+ return this.deduplicate(cacheKey, async () => {
4107
+ const response = await this.fetchWithAuth(this.baseUrl, {
4108
+ action: "by-index",
4109
+ workspaceId,
4110
+ date,
4111
+ shift: shiftId.toString(),
4112
+ category,
4113
+ index,
4114
+ sopCategories: this.sopCategories
4115
+ });
4116
+ const video = response.video;
4117
+ if (video && includeMetadata && video.originalUri) {
4118
+ try {
4119
+ const metadata = await this.getMetadata(video.originalUri);
4120
+ if (metadata) {
4121
+ video.cycle_time_seconds = metadata.cycle_time_seconds;
4122
+ video.creation_timestamp = metadata.creation_timestamp;
4123
+ }
4124
+ } catch (error) {
4125
+ console.warn("[S3ClipsAPIClient] Failed to fetch metadata:", error);
4126
+ }
3783
4127
  }
3784
- this.lastCleanup = now2;
3785
- }
4128
+ return video;
4129
+ });
4130
+ }
4131
+ /**
4132
+ * Get videos page with pagination
4133
+ */
4134
+ async getVideosPage(workspaceId, date, shiftId, category, pageSize = 5, startAfter) {
4135
+ const cacheKey = `page:${workspaceId}:${date}:${shiftId}:${category}:${pageSize}:${startAfter || "first"}`;
4136
+ return this.deduplicate(cacheKey, async () => {
4137
+ const response = await this.fetchWithAuth(this.baseUrl, {
4138
+ action: "page",
4139
+ workspaceId,
4140
+ date,
4141
+ shift: shiftId.toString(),
4142
+ category,
4143
+ pageSize,
4144
+ startAfter,
4145
+ sopCategories: this.sopCategories
4146
+ });
4147
+ return {
4148
+ videos: response.videos,
4149
+ nextToken: response.nextToken,
4150
+ hasMore: response.hasMore
4151
+ };
4152
+ });
4153
+ }
4154
+ /**
4155
+ * Convert S3 URI to CloudFront URL
4156
+ * In the API client, URLs are already signed from the server
4157
+ */
4158
+ s3UriToCloudfront(s3Uri) {
4159
+ return s3Uri;
4160
+ }
4161
+ /**
4162
+ * Clean up resources
4163
+ */
4164
+ dispose() {
4165
+ this.requestCache.clear();
4166
+ }
4167
+ /**
4168
+ * Get service statistics
4169
+ */
4170
+ getStats() {
4171
+ return {
4172
+ requestCache: {
4173
+ pendingCount: this.requestCache.size,
4174
+ maxSize: 1e3
4175
+ }
4176
+ };
3786
4177
  }
3787
4178
  };
4179
+
4180
+ // src/lib/api/s3-clips-secure.ts
3788
4181
  var S3ClipsService = class {
3789
4182
  constructor(config) {
3790
- // Request deduplication cache
3791
- this.requestCache = new RequestDeduplicationCache();
3792
- // Flag to prevent metadata fetching during index building
4183
+ // Flags for compatibility
3793
4184
  this.isIndexBuilding = false;
3794
- // Flag to prevent metadata fetching during entire prefetch operation
3795
4185
  this.isPrefetching = false;
3796
- // Global safeguard: limit concurrent metadata fetches to prevent flooding
3797
4186
  this.currentMetadataFetches = 0;
3798
4187
  this.MAX_CONCURRENT_METADATA = 3;
3799
4188
  this.config = config;
3800
4189
  if (!config.s3Config) {
3801
4190
  throw new Error("S3 configuration is required");
3802
4191
  }
4192
+ const sopCategories = config.s3Config.sopCategories?.default;
4193
+ this.apiClient = new S3ClipsAPIClient(sopCategories);
3803
4194
  const processing = config.s3Config.processing || {};
3804
4195
  this.defaultLimitPerCategory = processing.defaultLimitPerCategory || 30;
3805
4196
  this.maxLimitPerCategory = processing.maxLimitPerCategory || 1e3;
3806
4197
  this.concurrencyLimit = processing.concurrencyLimit || 10;
3807
4198
  this.maxInitialFetch = processing.maxInitialFetch || 60;
3808
- const region = this.validateAndSanitizeRegion(config.s3Config.region);
3809
- console.log(`S3ClipsService: Using AWS region: ${region}`);
3810
- this.s3Client = new S3Client({
3811
- region,
3812
- credentials: config.s3Config.credentials ? {
3813
- accessKeyId: config.s3Config.credentials.accessKeyId,
3814
- secretAccessKey: config.s3Config.credentials.secretAccessKey
3815
- } : void 0
3816
- });
4199
+ console.log("[S3ClipsService] \u2705 Initialized with secure API client - AWS credentials are now server-side only!");
3817
4200
  }
3818
4201
  /**
3819
- * Validates and sanitizes the AWS region
3820
- */
3821
- validateAndSanitizeRegion(region) {
3822
- const defaultRegion = "us-east-1";
3823
- if (!region || typeof region !== "string") {
3824
- console.warn(`S3ClipsService: Invalid region provided (${region}), using default: ${defaultRegion}`);
3825
- return defaultRegion;
3826
- }
3827
- const sanitizedRegion = region.trim().toLowerCase();
3828
- if (!sanitizedRegion) {
3829
- console.warn(`S3ClipsService: Empty region provided, using default: ${defaultRegion}`);
3830
- return defaultRegion;
3831
- }
3832
- const regionPattern = /^[a-z]{2,3}-[a-z]+-\d+$/;
3833
- if (!regionPattern.test(sanitizedRegion)) {
3834
- console.warn(`S3ClipsService: Invalid region format (${sanitizedRegion}), using default: ${defaultRegion}`);
3835
- return defaultRegion;
3836
- }
3837
- return sanitizedRegion;
3838
- }
3839
- /**
3840
- * Lists S3 clips for a workspace, date, and shift with request deduplication
4202
+ * Lists S3 clips using API
3841
4203
  */
3842
4204
  async listS3Clips(params) {
3843
- const { workspaceId, date, shiftId, maxKeys } = params;
4205
+ const { workspaceId, date, shiftId } = params;
3844
4206
  if (!isValidShiftId(shiftId)) {
3845
- console.error(`[S3ClipsService] Invalid shift ID: ${shiftId}. Must be 0 (day) or 1 (night)`);
4207
+ console.error(`[S3ClipsService] Invalid shift ID: ${shiftId}`);
3846
4208
  return [];
3847
4209
  }
3848
- const prefix = `sop_violations/${workspaceId}/${date}/${shiftId}/`;
3849
- console.log(`[S3ClipsService] Listing clips for workspace: ${workspaceId}, date: ${date}, shift: ${shiftId}`);
3850
- const deduplicationKey = `list-s3-clips:${prefix}:${maxKeys || "all"}`;
3851
- return this.requestCache.deduplicate(
3852
- deduplicationKey,
3853
- () => this.executeListS3Clips(params),
3854
- "ListS3Clips"
3855
- );
3856
- }
3857
- /**
3858
- * Internal implementation of S3 listing (called through deduplication)
3859
- */
3860
- async executeListS3Clips(params) {
3861
- const { workspaceId, date, shiftId, maxKeys } = params;
3862
- const prefix = `sop_violations/${workspaceId}/${date}/${shiftId}/`;
3863
- console.log(`[S3ClipsService] Executing S3 list for prefix: ${prefix}`);
4210
+ console.log(`[S3ClipsService] Listing clips via API for workspace: ${workspaceId}`);
3864
4211
  try {
3865
- let playlists = [];
3866
- let continuationToken = void 0;
3867
- do {
3868
- const command = new ListObjectsV2Command({
3869
- Bucket: this.config.s3Config.bucketName,
3870
- Prefix: prefix,
3871
- ContinuationToken: continuationToken,
3872
- MaxKeys: maxKeys ?? 1e3
3873
- });
3874
- const response = await this.s3Client.send(command);
3875
- console.log(`Got S3 response for ${prefix}:`, {
3876
- keyCount: response.KeyCount,
3877
- contentsLength: response.Contents?.length || 0,
3878
- hasContinuation: !!response.NextContinuationToken
3879
- });
3880
- if (response.Contents) {
3881
- if (response.Contents.length > 0) {
3882
- console.log("Sample Keys:", response.Contents.slice(0, 3).map((obj) => obj.Key));
3883
- }
3884
- for (const obj of response.Contents) {
3885
- if (obj.Key && obj.Key.endsWith("playlist.m3u8")) {
3886
- if (obj.Key.includes("missed_qchecks")) {
3887
- console.log(`Skipping missed_qchecks path: ${obj.Key}`);
3888
- continue;
3889
- }
3890
- playlists.push(`s3://${this.config.s3Config.bucketName}/${obj.Key}`);
3891
- }
3892
- }
3893
- }
3894
- continuationToken = response.NextContinuationToken;
3895
- if (maxKeys && playlists.length >= maxKeys) {
3896
- break;
3897
- }
3898
- } while (continuationToken && (!maxKeys || playlists.length < maxKeys));
3899
- console.log(`Found ${playlists.length} HLS playlists in ${prefix}`);
3900
- if (playlists.length > 0) {
3901
- console.log("First playlist URI:", playlists[0]);
3902
- }
3903
- return playlists;
4212
+ return await this.apiClient.listS3Clips(params);
3904
4213
  } catch (error) {
3905
- console.error(`Error listing S3 objects with prefix '${prefix}':`, error);
4214
+ console.error("[S3ClipsService] Error listing clips:", error);
3906
4215
  return [];
3907
4216
  }
3908
4217
  }
3909
4218
  /**
3910
- * Fetches and extracts cycle time from metadata.json with deduplication
4219
+ * Get metadata cycle time
3911
4220
  */
3912
4221
  async getMetadataCycleTime(playlistUri) {
3913
- const deduplicationKey = `metadata-cycle-time:${playlistUri}`;
3914
- return this.requestCache.deduplicate(
3915
- deduplicationKey,
3916
- () => this.executeGetMetadataCycleTime(playlistUri),
3917
- "MetadataCycleTime"
3918
- );
3919
- }
3920
- /**
3921
- * Internal implementation of metadata cycle time fetching
3922
- */
3923
- async executeGetMetadataCycleTime(playlistUri) {
3924
4222
  try {
3925
- const metadataUri = playlistUri.replace(/playlist\.m3u8$/, "metadata.json");
3926
- const url = new URL(metadataUri);
3927
- const bucket = url.hostname;
3928
- const key = url.pathname.substring(1);
3929
- console.log(`[S3ClipsService] Fetching metadata cycle time for: ${key}`);
3930
- const command = new GetObjectCommand({
3931
- Bucket: bucket,
3932
- Key: key
3933
- });
3934
- const response = await this.s3Client.send(command);
3935
- if (!response.Body) {
3936
- console.warn(`Empty response body for metadata file: ${key}`);
3937
- return null;
3938
- }
3939
- const metadataContent = await response.Body.transformToString();
3940
- const metadata = JSON.parse(metadataContent);
3941
- const cycleTimeFrames = metadata?.original_task_metadata?.cycle_time;
3942
- if (typeof cycleTimeFrames === "number") {
3943
- return cycleTimeFrames;
3944
- }
3945
- return null;
4223
+ return await this.apiClient.getMetadataCycleTime(playlistUri);
3946
4224
  } catch (error) {
3947
- console.error(`Error fetching or parsing metadata:`, error);
4225
+ console.error("[S3ClipsService] Error fetching metadata cycle time:", error);
3948
4226
  return null;
3949
4227
  }
3950
4228
  }
3951
4229
  /**
3952
- * Control prefetch mode to prevent metadata fetching during background operations
4230
+ * Control prefetch mode
3953
4231
  */
3954
4232
  setPrefetchMode(enabled) {
3955
4233
  this.isPrefetching = enabled;
3956
- console.log(`[S3ClipsService] Prefetch mode ${enabled ? "enabled" : "disabled"} - metadata fetching ${enabled ? "blocked" : "allowed"}`);
4234
+ console.log(`[S3ClipsService] Prefetch mode ${enabled ? "enabled" : "disabled"}`);
3957
4235
  }
3958
4236
  /**
3959
- * Fetches full metadata including timestamps with deduplication
4237
+ * Get full metadata
3960
4238
  */
3961
4239
  async getFullMetadata(playlistUri) {
3962
4240
  if (this.isIndexBuilding || this.isPrefetching) {
3963
- console.warn(`[S3ClipsService] Skipping metadata fetch - building: ${this.isIndexBuilding}, prefetching: ${this.isPrefetching}`);
4241
+ console.warn("[S3ClipsService] Skipping metadata - operation in progress");
3964
4242
  return null;
3965
4243
  }
3966
4244
  if (this.currentMetadataFetches >= this.MAX_CONCURRENT_METADATA) {
3967
- console.warn(`[S3ClipsService] Skipping metadata - max concurrent fetches (${this.MAX_CONCURRENT_METADATA}) reached`);
4245
+ console.warn("[S3ClipsService] Skipping metadata - max concurrent fetches reached");
3968
4246
  return null;
3969
4247
  }
3970
4248
  this.currentMetadataFetches++;
3971
4249
  try {
3972
- const deduplicationKey = `full-metadata:${playlistUri}`;
3973
- return await this.requestCache.deduplicate(
3974
- deduplicationKey,
3975
- () => this.executeGetFullMetadata(playlistUri),
3976
- "FullMetadata"
3977
- );
3978
- } finally {
3979
- this.currentMetadataFetches--;
3980
- }
3981
- }
3982
- /**
3983
- * Internal implementation of full metadata fetching
3984
- */
3985
- async executeGetFullMetadata(playlistUri) {
3986
- try {
3987
- const metadataUri = playlistUri.replace(/playlist\.m3u8$/, "metadata.json");
3988
- const url = new URL(metadataUri);
3989
- const bucket = url.hostname;
3990
- const key = url.pathname.substring(1);
3991
- console.log(`[S3ClipsService] Fetching full metadata for: ${key}`);
3992
- const command = new GetObjectCommand({
3993
- Bucket: bucket,
3994
- Key: key
3995
- });
3996
- const response = await this.s3Client.send(command);
3997
- if (!response.Body) {
3998
- console.warn(`Empty response body for metadata file: ${key}`);
3999
- return null;
4000
- }
4001
- const metadataContent = await response.Body.transformToString();
4002
- const metadata = JSON.parse(metadataContent);
4003
- return metadata;
4250
+ return await this.apiClient.getMetadata(playlistUri);
4004
4251
  } catch (error) {
4005
- console.error(`Error fetching or parsing metadata:`, error);
4252
+ console.error("[S3ClipsService] Error fetching metadata:", error);
4006
4253
  return null;
4254
+ } finally {
4255
+ this.currentMetadataFetches--;
4007
4256
  }
4008
4257
  }
4009
4258
  /**
4010
- * Converts S3 URI to CloudFront URL
4259
+ * Convert S3 URI to CloudFront URL
4260
+ * URLs from API are already signed
4011
4261
  */
4012
4262
  s3UriToCloudfront(s3Uri) {
4013
- const url = new URL(s3Uri);
4014
- const key = url.pathname.startsWith("/") ? url.pathname.substring(1) : url.pathname;
4015
- const cloudfrontUrl = `${this.config.s3Config.cloudFrontDomain}/${key}`;
4016
- return cloudfrontUrl;
4263
+ if (s3Uri.startsWith("http")) {
4264
+ return s3Uri;
4265
+ }
4266
+ console.warn("[S3ClipsService] Unexpected S3 URI in secure mode:", s3Uri);
4267
+ return s3Uri;
4017
4268
  }
4018
4269
  /**
4019
- * Gets SOP categories for a specific workspace
4270
+ * Get SOP categories for workspace
4020
4271
  */
4021
4272
  getSOPCategories(workspaceId) {
4022
4273
  const sopConfig = this.config.s3Config?.sopCategories;
@@ -4028,299 +4279,85 @@ var S3ClipsService = class {
4028
4279
  }
4029
4280
  async getClipCounts(workspaceId, date, shiftId, buildIndex) {
4030
4281
  if (!isValidShiftId(shiftId)) {
4031
- console.error(`[S3ClipsService] getClipCounts - Invalid shift ID: ${shiftId}. Must be 0 (day) or 1 (night)`);
4032
- return buildIndex ? { counts: {}, videoIndex: { byCategory: /* @__PURE__ */ new Map(), allVideos: [], counts: {}, workspaceId, date, shiftId: "0", lastUpdated: /* @__PURE__ */ new Date() } } : {};
4033
- }
4034
- const deduplicationKey = `clip-counts:${workspaceId}:${date}:${shiftId}:${buildIndex ? "with-index" : "counts-only"}`;
4035
- return this.requestCache.deduplicate(
4036
- deduplicationKey,
4037
- () => this.executeGetClipCounts(workspaceId, date, shiftId, buildIndex),
4038
- "ClipCounts"
4039
- );
4040
- }
4041
- /**
4042
- * Internal implementation of clip counts fetching
4043
- */
4044
- async executeGetClipCounts(workspaceId, date, shiftId, buildIndex) {
4045
- const effectiveBuildIndex = false;
4282
+ console.error(`[S3ClipsService] Invalid shift ID: ${shiftId}`);
4283
+ return buildIndex ? { counts: {}, videoIndex: this.createEmptyVideoIndex(workspaceId, date, shiftId.toString()) } : {};
4284
+ }
4046
4285
  try {
4047
- const basePrefix = `sop_violations/${workspaceId}/${date}/${shiftId}/`;
4048
- const counts = { total: 0 };
4049
- const categoryFolders = [
4050
- "idle_time",
4051
- "low_value",
4052
- "sop_deviation",
4053
- "missing_quality_check",
4054
- "best_cycle_time",
4055
- "worst_cycle_time",
4056
- "long_cycle_time",
4057
- "cycle_completion",
4058
- "bottleneck"
4059
- ];
4060
- const shiftName = shiftId === 0 || shiftId === "0" ? "Day" : "Night";
4061
- console.log(`[S3ClipsService] Fast counting clips for ${workspaceId} on ${date}, shift ${shiftId} (${shiftName} Shift)`);
4062
- const startTime = performance.now();
4063
- const videoIndex = effectiveBuildIndex ? {
4064
- byCategory: /* @__PURE__ */ new Map(),
4065
- allVideos: [],
4066
- counts: {},
4067
- workspaceId,
4068
- date,
4069
- shiftId: shiftId.toString(),
4070
- lastUpdated: /* @__PURE__ */ new Date(),
4071
- _debugId: `vid_${Date.now()}_${Math.random().toString(36).substring(7)}`
4072
- } : null;
4073
- const countPromises = categoryFolders.map(async (category) => {
4074
- const categoryPrefix = `${basePrefix}${category}/videos/`;
4075
- const categoryVideos = [];
4076
- try {
4077
- if (effectiveBuildIndex) ; else {
4078
- const command = new ListObjectsV2Command({
4079
- Bucket: this.config.s3Config.bucketName,
4080
- Prefix: categoryPrefix,
4081
- Delimiter: "/",
4082
- MaxKeys: 1e3
4083
- });
4084
- let folderCount = 0;
4085
- let continuationToken;
4086
- do {
4087
- if (continuationToken) {
4088
- command.input.ContinuationToken = continuationToken;
4089
- }
4090
- const response = await this.s3Client.send(command);
4091
- if (response.CommonPrefixes) {
4092
- folderCount += response.CommonPrefixes.length;
4093
- }
4094
- continuationToken = response.NextContinuationToken;
4095
- } while (continuationToken);
4096
- return { category, count: folderCount, videos: [] };
4097
- }
4098
- } catch (error) {
4099
- console.error(`Error ${buildIndex ? "building index for" : "counting folders for"} category ${category}:`, error);
4100
- return { category, count: 0, videos: [] };
4101
- }
4102
- });
4103
- const results = await Promise.all(countPromises);
4104
- for (const { category, count, videos } of results) {
4105
- counts[category] = count;
4106
- counts.total += count;
4107
- if (effectiveBuildIndex && videoIndex && videos) ;
4108
- }
4109
- if (effectiveBuildIndex && videoIndex) ;
4110
- const elapsed = performance.now() - startTime;
4111
- console.log(`[S3ClipsService] Clip counts completed in ${elapsed.toFixed(2)}ms - Total: ${counts.total}`);
4112
- if (effectiveBuildIndex && videoIndex) ;
4113
- return counts;
4114
- } finally {
4286
+ if (buildIndex) {
4287
+ const result = await this.apiClient.getClipCountsWithIndex(workspaceId, date, shiftId);
4288
+ const cacheKey = `clip-counts:${workspaceId}:${date}:${shiftId}`;
4289
+ await smartVideoCache.setClipCounts(cacheKey, result);
4290
+ return result;
4291
+ } else {
4292
+ const counts = await this.apiClient.getClipCounts(workspaceId, date, shiftId);
4293
+ return counts;
4294
+ }
4295
+ } catch (error) {
4296
+ console.error("[S3ClipsService] Error fetching clip counts:", error);
4297
+ return buildIndex ? { counts: {}, videoIndex: this.createEmptyVideoIndex(workspaceId, date, shiftId.toString()) } : {};
4115
4298
  }
4116
4299
  }
4117
4300
  async getClipCountsCacheFirst(workspaceId, date, shiftId, buildIndex) {
4118
4301
  const cacheKey = `clip-counts:${workspaceId}:${date}:${shiftId}`;
4119
4302
  const cachedResult = await smartVideoCache.getClipCounts(cacheKey);
4120
4303
  if (cachedResult) {
4121
- console.log(`[S3ClipsService] Using cached clip counts for ${workspaceId}`);
4122
- if (buildIndex) {
4123
- return cachedResult;
4124
- } else {
4125
- return cachedResult.counts;
4126
- }
4304
+ console.log("[S3ClipsService] Using cached clip counts");
4305
+ return buildIndex ? cachedResult : cachedResult.counts;
4127
4306
  }
4128
- console.log(`[S3ClipsService] Cache miss - fetching clip counts for ${workspaceId}`);
4307
+ console.log("[S3ClipsService] Cache miss - fetching from API");
4129
4308
  return buildIndex ? this.getClipCounts(workspaceId, date, shiftId, true) : this.getClipCounts(workspaceId, date, shiftId);
4130
4309
  }
4131
4310
  /**
4132
- * Get first clip for a specific category with deduplication
4311
+ * Get first clip for category
4133
4312
  */
4134
4313
  async getFirstClipForCategory(workspaceId, date, shiftId, category) {
4135
4314
  if (!isValidShiftId(shiftId)) {
4136
- console.error(`[S3ClipsService] getFirstClipForCategory - Invalid shift ID: ${shiftId}. Must be 0 (day) or 1 (night)`);
4315
+ console.error(`[S3ClipsService] Invalid shift ID: ${shiftId}`);
4137
4316
  return null;
4138
4317
  }
4139
- const deduplicationKey = `first-clip:${workspaceId}:${date}:${shiftId}:${category}`;
4140
- return this.requestCache.deduplicate(
4141
- deduplicationKey,
4142
- () => this.executeGetFirstClipForCategory(workspaceId, date, shiftId, category),
4143
- "FirstClip"
4144
- );
4145
- }
4146
- /**
4147
- * Internal implementation of first clip fetching
4148
- */
4149
- async executeGetFirstClipForCategory(workspaceId, date, shiftId, category) {
4150
- const categoryPrefix = `sop_violations/${workspaceId}/${date}/${shiftId}/${category}/videos/`;
4151
4318
  try {
4152
- const command = new ListObjectsV2Command({
4153
- Bucket: this.config.s3Config.bucketName,
4154
- Prefix: categoryPrefix,
4155
- MaxKeys: 10
4156
- // Small limit since we only need one
4157
- });
4158
- const response = await this.s3Client.send(command);
4159
- if (response.Contents) {
4160
- const playlistObj = response.Contents.find((obj) => obj.Key?.endsWith("playlist.m3u8"));
4161
- if (playlistObj && playlistObj.Key) {
4162
- const s3Uri = `s3://${this.config.s3Config.bucketName}/${playlistObj.Key}`;
4163
- const sopCategories = this.getSOPCategories(workspaceId);
4164
- const parsedInfo = parseS3Uri(s3Uri, sopCategories);
4165
- if (parsedInfo) {
4166
- const cloudfrontUrl = this.s3UriToCloudfront(s3Uri);
4167
- return {
4168
- id: `${workspaceId}-${date}-${shiftId}-${category}-first`,
4169
- src: cloudfrontUrl,
4170
- ...parsedInfo,
4171
- originalUri: s3Uri
4172
- };
4173
- }
4174
- }
4175
- }
4319
+ return await this.apiClient.getFirstClipForCategory(workspaceId, date, shiftId, category);
4176
4320
  } catch (error) {
4177
- console.error(`Error getting first clip for category ${category}:`, error);
4321
+ console.error("[S3ClipsService] Error fetching first clip:", error);
4322
+ return null;
4178
4323
  }
4179
- return null;
4180
4324
  }
4181
4325
  /**
4182
- * Get a specific video from the pre-built video index - O(1) lookup performance
4326
+ * Get video from index (for compatibility)
4183
4327
  */
4184
4328
  async getVideoFromIndex(videoIndex, category, index, includeCycleTime = true, includeMetadata = true) {
4329
+ const { workspaceId, date, shiftId } = videoIndex;
4330
+ return this.getClipByIndex(
4331
+ workspaceId,
4332
+ date,
4333
+ shiftId,
4334
+ category,
4335
+ index,
4336
+ includeCycleTime,
4337
+ includeMetadata
4338
+ );
4339
+ }
4340
+ /**
4341
+ * Get clip by index
4342
+ */
4343
+ async getClipByIndex(workspaceId, date, shiftId, category, index, includeCycleTime = true, includeMetadata = false) {
4185
4344
  try {
4186
- const categoryVideos = videoIndex.byCategory.get(category);
4187
- if (!categoryVideos || index < 0 || index >= categoryVideos.length) {
4188
- console.warn(`Invalid index ${index} for category '${category}'. Available: ${categoryVideos?.length || 0}`);
4189
- console.warn(`Video index ID: ${videoIndex._debugId || "NO_ID"}`);
4190
- console.warn(`Video index categories available: [${Array.from(videoIndex.byCategory.keys()).join(", ")}]`);
4191
- console.warn(`Video index total videos: ${videoIndex.allVideos.length}`);
4192
- return null;
4193
- }
4194
- const videoEntry = categoryVideos[index];
4195
- return this.processFullVideo(
4196
- videoEntry.uri,
4345
+ return await this.apiClient.getClipByIndex(
4346
+ workspaceId,
4347
+ date,
4348
+ shiftId,
4349
+ category,
4197
4350
  index,
4198
- videoEntry.workspaceId,
4199
- videoEntry.date,
4200
- videoEntry.shiftId,
4201
4351
  includeCycleTime,
4202
4352
  includeMetadata
4203
4353
  );
4204
4354
  } catch (error) {
4205
- console.error(`Error getting video from index at ${index} for category ${category}:`, error);
4355
+ console.error("[S3ClipsService] Error fetching clip by index:", error);
4206
4356
  return null;
4207
4357
  }
4208
4358
  }
4209
4359
  /**
4210
- * Get a specific clip by index for a category with deduplication
4211
- * @deprecated Use getVideoFromIndex with a pre-built VideoIndex for better performance
4212
- */
4213
- async getClipByIndex(workspaceId, date, shiftId, category, index, includeCycleTime = true, includeMetadata = false) {
4214
- const deduplicationKey = `clip-by-index:${workspaceId}:${date}:${shiftId}:${category}:${index}:${includeCycleTime}:${includeMetadata}`;
4215
- return this.requestCache.deduplicate(
4216
- deduplicationKey,
4217
- () => this.executeGetClipByIndex(workspaceId, date, shiftId, category, index, includeCycleTime, includeMetadata),
4218
- "ClipByIndex"
4219
- );
4220
- }
4221
- /**
4222
- * Internal implementation of clip by index fetching
4223
- */
4224
- async executeGetClipByIndex(workspaceId, date, shiftId, category, index, includeCycleTime = true, includeMetadata = false) {
4225
- const categoryPrefix = `sop_violations/${workspaceId}/${date}/${shiftId}/${category}/videos/`;
4226
- try {
4227
- const command = new ListObjectsV2Command({
4228
- Bucket: this.config.s3Config.bucketName,
4229
- Prefix: categoryPrefix,
4230
- MaxKeys: 1e3
4231
- });
4232
- let playlists = [];
4233
- let continuationToken = void 0;
4234
- do {
4235
- if (continuationToken) {
4236
- command.input.ContinuationToken = continuationToken;
4237
- }
4238
- const response = await this.s3Client.send(command);
4239
- if (response.Contents) {
4240
- for (const obj of response.Contents) {
4241
- if (obj.Key && obj.Key.endsWith("playlist.m3u8")) {
4242
- playlists.push(`s3://${this.config.s3Config.bucketName}/${obj.Key}`);
4243
- }
4244
- }
4245
- }
4246
- continuationToken = response.NextContinuationToken;
4247
- } while (continuationToken && playlists.length < index + 10);
4248
- playlists.sort((a, b) => {
4249
- const parsedA = parseS3Uri(a);
4250
- const parsedB = parseS3Uri(b);
4251
- if (!parsedA || !parsedB) return 0;
4252
- return parsedB.timestamp.localeCompare(parsedA.timestamp);
4253
- });
4254
- if (index < playlists.length) {
4255
- const uri = playlists[index];
4256
- return this.processFullVideo(
4257
- uri,
4258
- index,
4259
- workspaceId,
4260
- date,
4261
- shiftId.toString(),
4262
- includeCycleTime,
4263
- includeMetadata
4264
- );
4265
- }
4266
- } catch (error) {
4267
- console.error(`[getClipByIndex] Error getting clip at index ${index} for category ${category}:`, error);
4268
- }
4269
- return null;
4270
- }
4271
- /**
4272
- * Get one sample video from each category for preloading
4273
- */
4274
- async getSampleVideos(workspaceId, date, shiftId) {
4275
- const basePrefix = `sop_violations/${workspaceId}/${date}/${shiftId}/`;
4276
- const samples = {};
4277
- const sopCategories = this.getSOPCategories(workspaceId);
4278
- const categoriesToSample = sopCategories ? sopCategories.map((cat) => cat.id) : ["low_value", "best_cycle_time", "worst_cycle_time", "long_cycle_time", "cycle_completion"];
4279
- console.log(`[S3ClipsService] Getting sample videos for categories:`, categoriesToSample);
4280
- const samplePromises = categoriesToSample.map(async (category) => {
4281
- const categoryPrefix = `${basePrefix}${category}/`;
4282
- try {
4283
- const command = new ListObjectsV2Command({
4284
- Bucket: this.config.s3Config.bucketName,
4285
- Prefix: categoryPrefix,
4286
- MaxKeys: 20
4287
- // Small limit since we only need one
4288
- });
4289
- const response = await this.s3Client.send(command);
4290
- if (response.Contents) {
4291
- const playlistObj = response.Contents.find((obj) => obj.Key?.endsWith("playlist.m3u8"));
4292
- if (playlistObj && playlistObj.Key) {
4293
- const s3Uri = `s3://${this.config.s3Config.bucketName}/${playlistObj.Key}`;
4294
- const parsedInfo = parseS3Uri(s3Uri, sopCategories);
4295
- if (parsedInfo) {
4296
- return {
4297
- category,
4298
- sample: {
4299
- id: `${workspaceId}-${date}-${shiftId}-${category}-sample`,
4300
- src: this.s3UriToCloudfront(s3Uri),
4301
- // Pre-convert to CloudFront URL
4302
- ...parsedInfo,
4303
- originalUri: s3Uri
4304
- }
4305
- };
4306
- }
4307
- }
4308
- }
4309
- } catch (error) {
4310
- console.error(`Error getting sample for category ${category}:`, error);
4311
- }
4312
- return { category, sample: null };
4313
- });
4314
- const sampleResults = await Promise.all(samplePromises);
4315
- for (const { category, sample } of sampleResults) {
4316
- if (sample) {
4317
- samples[category] = sample;
4318
- }
4319
- }
4320
- return samples;
4321
- }
4322
- /**
4323
- * Processes a single video completely
4360
+ * Process full video (for compatibility)
4324
4361
  */
4325
4362
  async processFullVideo(uri, index, workspaceId, date, shiftId, includeCycleTime, includeMetadata = false) {
4326
4363
  const sopCategories = this.getSOPCategories(workspaceId);
@@ -4329,35 +4366,35 @@ var S3ClipsService = class {
4329
4366
  console.warn(`Skipping URI due to parsing failure: ${uri}`);
4330
4367
  return null;
4331
4368
  }
4332
- let cycleTimeSeconds = null;
4333
- let creationTimestamp = void 0;
4369
+ const video = {
4370
+ id: `${workspaceId}-${date}-${shiftId}-${index}`,
4371
+ src: uri,
4372
+ // Already signed from API
4373
+ ...parsedInfo,
4374
+ originalUri: uri
4375
+ };
4334
4376
  if (includeMetadata) {
4335
4377
  const metadata = await this.getFullMetadata(uri);
4336
4378
  if (metadata) {
4337
- if (metadata.original_task_metadata?.cycle_time) {
4338
- cycleTimeSeconds = metadata.original_task_metadata.cycle_time;
4339
- }
4340
- creationTimestamp = metadata.upload_timestamp || metadata.original_task_metadata?.timestamp || metadata.creation_timestamp || metadata[""];
4379
+ video.cycle_time_seconds = metadata.original_task_metadata?.cycle_time;
4380
+ video.creation_timestamp = metadata.upload_timestamp || metadata.original_task_metadata?.timestamp || metadata.creation_timestamp;
4341
4381
  }
4342
- } 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")) {
4343
- cycleTimeSeconds = null;
4344
4382
  }
4345
- const cloudfrontPlaylistUrl = this.s3UriToCloudfront(uri);
4346
- const { type: videoType, timestamp: videoTimestamp } = parsedInfo;
4347
- return {
4348
- id: `${workspaceId}-${date}-${shiftId}-${videoType}-${videoTimestamp.replace(/:/g, "")}-${index}`,
4349
- src: cloudfrontPlaylistUrl,
4350
- // Direct CloudFront playlist URL
4351
- ...parsedInfo,
4352
- cycle_time_seconds: cycleTimeSeconds || void 0,
4353
- creation_timestamp: creationTimestamp
4354
- };
4383
+ return video;
4355
4384
  }
4356
4385
  /**
4357
- * Simplified method to fetch clips based on parameters
4386
+ * Fetch clips (main method for compatibility)
4358
4387
  */
4359
4388
  async fetchClips(params) {
4360
- const { workspaceId, date: inputDate, shift, category, limit, offset = 0, mode, includeCycleTime, includeMetadata, timestampStart, timestampEnd } = params;
4389
+ const {
4390
+ workspaceId,
4391
+ date: inputDate,
4392
+ shift,
4393
+ category,
4394
+ limit,
4395
+ offset = 0,
4396
+ mode
4397
+ } = params;
4361
4398
  if (!workspaceId) {
4362
4399
  throw new Error("Valid Workspace ID is required");
4363
4400
  }
@@ -4375,157 +4412,66 @@ var S3ClipsService = class {
4375
4412
  const { shiftId: currentShiftId } = getCurrentShift2(this.config.dateTimeConfig?.defaultTimezone);
4376
4413
  shiftId = currentShiftId;
4377
4414
  }
4378
- console.log(`S3ClipsService: Fetching clips for workspace ${workspaceId} on date ${date}, shift ${shiftId}`);
4415
+ console.log(`[S3ClipsService] Fetching clips for workspace ${workspaceId}`);
4379
4416
  if (mode === "summary") {
4380
4417
  const counts = await this.getClipCounts(workspaceId, date, shiftId.toString());
4381
4418
  return { counts, samples: {} };
4382
4419
  }
4383
4420
  const effectiveLimit = limit || this.defaultLimitPerCategory;
4384
- const s3Uris = await this.listS3Clips({
4421
+ const result = await this.getVideosPage(
4385
4422
  workspaceId,
4386
4423
  date,
4387
4424
  shiftId,
4388
- maxKeys: category ? effectiveLimit * 2 : void 0
4389
- // Fetch extra for filtering
4390
- });
4391
- if (s3Uris.length === 0) {
4392
- console.log(`S3ClipsService: No HLS playlists found`);
4393
- return [];
4394
- }
4395
- let filteredUris = s3Uris;
4396
- if (category) {
4397
- filteredUris = s3Uris.filter((uri) => {
4398
- const parsedInfo = parseS3Uri(uri);
4399
- if (!parsedInfo) return false;
4400
- if (category === "long_cycle_time") {
4401
- return parsedInfo.type === "long_cycle_time" || parsedInfo.type === "bottleneck" && parsedInfo.description.toLowerCase().includes("cycle time");
4402
- }
4403
- return parsedInfo.type === category;
4404
- });
4405
- filteredUris = filteredUris.slice(offset, offset + effectiveLimit);
4406
- }
4407
- const videoPromises = filteredUris.map(async (uri, index) => {
4408
- return this.processFullVideo(
4409
- uri,
4410
- index,
4425
+ category || "all",
4426
+ effectiveLimit
4427
+ );
4428
+ return result.videos;
4429
+ }
4430
+ /**
4431
+ * Get videos page using pagination API
4432
+ */
4433
+ async getVideosPage(workspaceId, date, shiftId, category, pageSize = 5, startAfter) {
4434
+ try {
4435
+ return await this.apiClient.getVideosPage(
4411
4436
  workspaceId,
4412
4437
  date,
4413
- shiftId.toString(),
4414
- includeCycleTime || false,
4415
- includeMetadata || false
4416
- // Never fetch metadata for timestamp filtering to prevent flooding
4438
+ shiftId,
4439
+ category,
4440
+ pageSize,
4441
+ startAfter
4417
4442
  );
4418
- });
4419
- const videoResults = await Promise.all(videoPromises);
4420
- let videos = videoResults.filter((v) => v !== null);
4421
- if (timestampStart || timestampEnd) {
4422
- videos = videos.filter((video) => {
4423
- if (!video.creation_timestamp) return false;
4424
- const videoTimestamp = new Date(video.creation_timestamp).getTime();
4425
- if (timestampStart && timestampEnd) {
4426
- const start = new Date(timestampStart).getTime();
4427
- const end = new Date(timestampEnd).getTime();
4428
- return videoTimestamp >= start && videoTimestamp <= end;
4429
- } else if (timestampStart) {
4430
- const start = new Date(timestampStart).getTime();
4431
- return videoTimestamp >= start;
4432
- } else if (timestampEnd) {
4433
- const end = new Date(timestampEnd).getTime();
4434
- return videoTimestamp <= end;
4435
- }
4436
- return true;
4437
- });
4438
- }
4439
- videos.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
4440
- const cacheKey = `videos:${workspaceId}:${date}:${shiftId}:${category || "all"}`;
4441
- if (videos.length > 0) {
4442
- smartVideoCache.setVideos(cacheKey, videos);
4443
+ } catch (error) {
4444
+ console.error("[S3ClipsService] Error fetching videos page:", error);
4445
+ return { videos: [], hasMore: false };
4443
4446
  }
4444
- return videos;
4445
4447
  }
4446
4448
  /**
4447
- * Get a page of videos for a specific category using efficient pagination
4448
- * This method replaces the need for full video indexing
4449
- * @param workspaceId - Workspace ID
4450
- * @param date - Date in YYYY-MM-DD format
4451
- * @param shiftId - Shift ID (0 for day, 1 for night)
4452
- * @param category - Category to fetch videos from
4453
- * @param pageSize - Number of videos to fetch per page (default 5)
4454
- * @param startAfter - Optional key to start after for pagination
4455
- * @returns Page of videos with continuation token
4449
+ * Create empty video index for compatibility
4456
4450
  */
4457
- async getVideosPage(workspaceId, date, shiftId, category, pageSize = 5, startAfter) {
4458
- if (!isValidShiftId(shiftId)) {
4459
- console.error(`[S3ClipsService] getVideosPage - Invalid shift ID: ${shiftId}`);
4460
- return { videos: [], hasMore: false };
4461
- }
4462
- const categoryPrefix = `sop_violations/${workspaceId}/${date}/${shiftId}/${category}/videos/`;
4463
- const deduplicationKey = `videos-page:${categoryPrefix}:${pageSize}:${startAfter || "first"}`;
4464
- return this.requestCache.deduplicate(
4465
- deduplicationKey,
4466
- async () => {
4467
- try {
4468
- console.log(`[S3ClipsService] Fetching page of ${pageSize} videos for category '${category}'`);
4469
- const command = new ListObjectsV2Command({
4470
- Bucket: this.config.s3Config.bucketName,
4471
- Prefix: categoryPrefix,
4472
- MaxKeys: pageSize * 10,
4473
- // Fetch extra to account for non-playlist files
4474
- StartAfter: startAfter
4475
- });
4476
- const response = await this.s3Client.send(command);
4477
- const videos = [];
4478
- if (response.Contents) {
4479
- const sopCategories = this.getSOPCategories(workspaceId);
4480
- for (const obj of response.Contents) {
4481
- if (videos.length >= pageSize) break;
4482
- if (obj.Key && obj.Key.endsWith("playlist.m3u8")) {
4483
- if (obj.Key.includes("missed_qchecks")) continue;
4484
- const s3Uri = `s3://${this.config.s3Config.bucketName}/${obj.Key}`;
4485
- const parsedInfo = parseS3Uri(s3Uri, sopCategories);
4486
- if (parsedInfo) {
4487
- const cloudfrontUrl = this.s3UriToCloudfront(s3Uri);
4488
- videos.push({
4489
- id: `${workspaceId}-${date}-${shiftId}-${category}-${videos.length}`,
4490
- src: cloudfrontUrl,
4491
- ...parsedInfo,
4492
- originalUri: s3Uri
4493
- });
4494
- }
4495
- }
4496
- }
4497
- const lastKey = response.Contents[response.Contents.length - 1]?.Key;
4498
- const hasMore = !!response.IsTruncated || response.Contents.length === pageSize * 10;
4499
- console.log(`[S3ClipsService] Fetched ${videos.length} videos, hasMore: ${hasMore}`);
4500
- return {
4501
- videos,
4502
- nextToken: hasMore ? lastKey : void 0,
4503
- hasMore
4504
- };
4505
- }
4506
- return { videos: [], hasMore: false };
4507
- } catch (error) {
4508
- console.error(`[S3ClipsService] Error fetching videos page:`, error);
4509
- return { videos: [], hasMore: false };
4510
- }
4511
- },
4512
- "VideosPage"
4513
- );
4451
+ createEmptyVideoIndex(workspaceId, date, shiftId) {
4452
+ return {
4453
+ byCategory: /* @__PURE__ */ new Map(),
4454
+ allVideos: [],
4455
+ counts: {},
4456
+ workspaceId,
4457
+ date,
4458
+ shiftId,
4459
+ lastUpdated: /* @__PURE__ */ new Date(),
4460
+ _debugId: `empty_${Date.now()}`
4461
+ };
4514
4462
  }
4515
4463
  /**
4516
- * Cleanup method for proper resource management
4464
+ * Cleanup
4517
4465
  */
4518
4466
  dispose() {
4519
- console.log("[S3ClipsService] Disposing service and clearing request cache");
4520
- this.requestCache.clear();
4467
+ console.log("[S3ClipsService] Disposing service");
4468
+ this.apiClient.dispose();
4521
4469
  }
4522
4470
  /**
4523
- * Get service statistics for monitoring
4471
+ * Get statistics
4524
4472
  */
4525
4473
  getStats() {
4526
- return {
4527
- requestCache: this.requestCache.getStats()
4528
- };
4474
+ return this.apiClient.getStats();
4529
4475
  }
4530
4476
  };
4531
4477
 
@@ -12114,6 +12060,140 @@ function useWorkspaceNavigation() {
12114
12060
  getWorkspaceNavigationParams: getWorkspaceNavigationParams2
12115
12061
  };
12116
12062
  }
12063
+ function useWorkspaceHealth(options = {}) {
12064
+ const [workspaces, setWorkspaces] = useState([]);
12065
+ const [summary, setSummary] = useState(null);
12066
+ const [loading, setLoading] = useState(true);
12067
+ const [error, setError] = useState(null);
12068
+ const unsubscribeRef = useRef(null);
12069
+ const refreshIntervalRef = useRef(null);
12070
+ const {
12071
+ enableRealtime = true,
12072
+ refreshInterval = 3e4,
12073
+ // 30 seconds default
12074
+ ...filterOptions
12075
+ } = options;
12076
+ const fetchData = useCallback(async () => {
12077
+ try {
12078
+ setError(null);
12079
+ workspaceHealthService.clearCache();
12080
+ const [workspacesData, summaryData] = await Promise.all([
12081
+ workspaceHealthService.getWorkspaceHealthStatus(filterOptions),
12082
+ workspaceHealthService.getHealthSummary(filterOptions.lineId, filterOptions.companyId)
12083
+ ]);
12084
+ setWorkspaces(workspacesData);
12085
+ setSummary(summaryData);
12086
+ } catch (err) {
12087
+ console.error("Error fetching workspace health:", err);
12088
+ setError(err);
12089
+ } finally {
12090
+ setLoading(false);
12091
+ }
12092
+ }, [filterOptions.lineId, filterOptions.companyId, filterOptions.status, filterOptions.searchTerm, filterOptions.sortBy, filterOptions.sortOrder]);
12093
+ const handleRealtimeUpdate = useCallback(async (data) => {
12094
+ try {
12095
+ const [workspacesData, summaryData] = await Promise.all([
12096
+ workspaceHealthService.getWorkspaceHealthStatus(filterOptions),
12097
+ workspaceHealthService.getHealthSummary(filterOptions.lineId, filterOptions.companyId)
12098
+ ]);
12099
+ setWorkspaces(workspacesData);
12100
+ setSummary(summaryData);
12101
+ } catch (err) {
12102
+ console.error("Error updating real-time health data:", err);
12103
+ }
12104
+ }, [filterOptions]);
12105
+ useEffect(() => {
12106
+ fetchData();
12107
+ if (refreshInterval > 0) {
12108
+ refreshIntervalRef.current = setInterval(fetchData, 1e4);
12109
+ }
12110
+ if (enableRealtime) {
12111
+ unsubscribeRef.current = workspaceHealthService.subscribeToHealthUpdates(
12112
+ handleRealtimeUpdate,
12113
+ { lineId: filterOptions.lineId, companyId: filterOptions.companyId }
12114
+ );
12115
+ }
12116
+ return () => {
12117
+ if (refreshIntervalRef.current) {
12118
+ clearInterval(refreshIntervalRef.current);
12119
+ }
12120
+ if (unsubscribeRef.current) {
12121
+ unsubscribeRef.current();
12122
+ }
12123
+ };
12124
+ }, [fetchData, enableRealtime, refreshInterval, handleRealtimeUpdate, filterOptions.lineId, filterOptions.companyId]);
12125
+ return {
12126
+ workspaces,
12127
+ summary,
12128
+ loading,
12129
+ error,
12130
+ refetch: fetchData
12131
+ };
12132
+ }
12133
+ function useWorkspaceHealthById(workspaceId, options = {}) {
12134
+ const [workspace, setWorkspace] = useState(null);
12135
+ const [metrics2, setMetrics] = useState(null);
12136
+ const [loading, setLoading] = useState(true);
12137
+ const [error, setError] = useState(null);
12138
+ const unsubscribeRef = useRef(null);
12139
+ const refreshIntervalRef = useRef(null);
12140
+ const { enableRealtime = true, refreshInterval = 3e4 } = options;
12141
+ const fetchData = useCallback(async () => {
12142
+ if (!workspaceId) {
12143
+ setWorkspace(null);
12144
+ setMetrics(null);
12145
+ setLoading(false);
12146
+ return;
12147
+ }
12148
+ try {
12149
+ setLoading(true);
12150
+ setError(null);
12151
+ const [workspaceData, metricsData] = await Promise.all([
12152
+ workspaceHealthService.getWorkspaceHealthById(workspaceId),
12153
+ workspaceHealthService.getHealthMetrics(workspaceId)
12154
+ ]);
12155
+ setWorkspace(workspaceData);
12156
+ setMetrics(metricsData);
12157
+ } catch (err) {
12158
+ console.error("Error fetching workspace health by ID:", err);
12159
+ setError(err);
12160
+ } finally {
12161
+ setLoading(false);
12162
+ }
12163
+ }, [workspaceId]);
12164
+ const handleRealtimeUpdate = useCallback((data) => {
12165
+ if (data.workspace_id === workspaceId) {
12166
+ const updatedWorkspace = workspaceHealthService["processHealthStatus"](data);
12167
+ setWorkspace(updatedWorkspace);
12168
+ }
12169
+ }, [workspaceId]);
12170
+ useEffect(() => {
12171
+ fetchData();
12172
+ if (refreshInterval > 0) {
12173
+ refreshIntervalRef.current = setInterval(fetchData, 1e4);
12174
+ }
12175
+ if (enableRealtime && workspaceId) {
12176
+ unsubscribeRef.current = workspaceHealthService.subscribeToHealthUpdates(
12177
+ handleRealtimeUpdate
12178
+ );
12179
+ }
12180
+ return () => {
12181
+ if (refreshIntervalRef.current) {
12182
+ clearInterval(refreshIntervalRef.current);
12183
+ }
12184
+ if (unsubscribeRef.current) {
12185
+ unsubscribeRef.current();
12186
+ }
12187
+ };
12188
+ }, [fetchData, enableRealtime, refreshInterval, handleRealtimeUpdate, workspaceId]);
12189
+ return {
12190
+ workspace,
12191
+ metrics: metrics2,
12192
+ loading,
12193
+ error,
12194
+ refetch: fetchData
12195
+ };
12196
+ }
12117
12197
  function useDateFormatter() {
12118
12198
  const { defaultTimezone, defaultLocale, dateFormatOptions, timeFormatOptions, dateTimeFormatOptions } = useDateTimeConfig();
12119
12199
  const formatDate = useCallback(
@@ -23522,6 +23602,165 @@ var TicketHistory = ({ companyId }) => {
23522
23602
  ] });
23523
23603
  };
23524
23604
  var TicketHistory_default = TicketHistory;
23605
+ var HealthStatusIndicator = ({
23606
+ status,
23607
+ lastUpdated,
23608
+ showLabel = false,
23609
+ showTime = true,
23610
+ size = "md",
23611
+ className = "",
23612
+ inline = true,
23613
+ pulse = true
23614
+ }) => {
23615
+ const getStatusConfig = () => {
23616
+ switch (status) {
23617
+ case "healthy":
23618
+ return {
23619
+ color: "text-green-500",
23620
+ bgColor: "bg-green-500",
23621
+ borderColor: "border-green-500",
23622
+ label: "Healthy",
23623
+ icon: CheckCircle,
23624
+ shouldPulse: pulse
23625
+ };
23626
+ case "unhealthy":
23627
+ return {
23628
+ color: "text-red-500",
23629
+ bgColor: "bg-red-500",
23630
+ borderColor: "border-red-500",
23631
+ label: "Unhealthy",
23632
+ icon: XCircle,
23633
+ shouldPulse: false
23634
+ };
23635
+ case "warning":
23636
+ return {
23637
+ color: "text-yellow-500",
23638
+ bgColor: "bg-yellow-500",
23639
+ borderColor: "border-yellow-500",
23640
+ label: "Warning",
23641
+ icon: AlertTriangle,
23642
+ shouldPulse: true
23643
+ };
23644
+ case "unknown":
23645
+ default:
23646
+ return {
23647
+ color: "text-gray-400",
23648
+ bgColor: "bg-gray-400",
23649
+ borderColor: "border-gray-400",
23650
+ label: "Unknown",
23651
+ icon: Activity,
23652
+ shouldPulse: false
23653
+ };
23654
+ }
23655
+ };
23656
+ const config = getStatusConfig();
23657
+ config.icon;
23658
+ const sizeClasses = {
23659
+ sm: {
23660
+ dot: "h-2 w-2",
23661
+ icon: "h-3 w-3",
23662
+ text: "text-xs",
23663
+ spacing: "gap-1"
23664
+ },
23665
+ md: {
23666
+ dot: "h-3 w-3",
23667
+ icon: "h-4 w-4",
23668
+ text: "text-sm",
23669
+ spacing: "gap-1.5"
23670
+ },
23671
+ lg: {
23672
+ dot: "h-4 w-4",
23673
+ icon: "h-5 w-5",
23674
+ text: "text-base",
23675
+ spacing: "gap-2"
23676
+ }
23677
+ };
23678
+ const currentSize = sizeClasses[size];
23679
+ const containerClasses = clsx(
23680
+ "flex items-center",
23681
+ currentSize.spacing,
23682
+ inline ? "inline-flex" : "flex",
23683
+ className
23684
+ );
23685
+ const dotClasses = clsx(
23686
+ "rounded-full",
23687
+ currentSize.dot,
23688
+ config.bgColor,
23689
+ config.shouldPulse && "animate-pulse"
23690
+ );
23691
+ return /* @__PURE__ */ jsxs("div", { className: containerClasses, children: [
23692
+ /* @__PURE__ */ jsxs("div", { className: "relative flex items-center justify-center", children: [
23693
+ /* @__PURE__ */ jsx("div", { className: dotClasses }),
23694
+ config.shouldPulse && status === "healthy" && /* @__PURE__ */ jsx(
23695
+ "div",
23696
+ {
23697
+ className: clsx(
23698
+ "absolute rounded-full opacity-25",
23699
+ currentSize.dot,
23700
+ config.bgColor,
23701
+ "animate-ping"
23702
+ )
23703
+ }
23704
+ )
23705
+ ] }),
23706
+ showLabel && /* @__PURE__ */ jsx("span", { className: clsx(currentSize.text, "font-medium", config.color), children: config.label }),
23707
+ showTime && lastUpdated && /* @__PURE__ */ jsx("span", { className: clsx(currentSize.text, "text-gray-500 dark:text-gray-400"), children: lastUpdated })
23708
+ ] });
23709
+ };
23710
+ var DetailedHealthStatus = ({
23711
+ workspaceName,
23712
+ lineName,
23713
+ consecutiveMisses,
23714
+ showDetails = true,
23715
+ ...indicatorProps
23716
+ }) => {
23717
+ const getStatusConfig = () => {
23718
+ switch (indicatorProps.status) {
23719
+ case "healthy":
23720
+ return {
23721
+ bgClass: "bg-green-50 dark:bg-green-900/20",
23722
+ borderClass: "border-green-200 dark:border-green-800"
23723
+ };
23724
+ case "unhealthy":
23725
+ return {
23726
+ bgClass: "bg-red-50 dark:bg-red-900/20",
23727
+ borderClass: "border-red-200 dark:border-red-800"
23728
+ };
23729
+ case "warning":
23730
+ return {
23731
+ bgClass: "bg-yellow-50 dark:bg-yellow-900/20",
23732
+ borderClass: "border-yellow-200 dark:border-yellow-800"
23733
+ };
23734
+ default:
23735
+ return {
23736
+ bgClass: "bg-gray-50 dark:bg-gray-900/20",
23737
+ borderClass: "border-gray-200 dark:border-gray-800"
23738
+ };
23739
+ }
23740
+ };
23741
+ const config = getStatusConfig();
23742
+ return /* @__PURE__ */ jsx(
23743
+ "div",
23744
+ {
23745
+ className: clsx(
23746
+ "rounded-lg border p-3",
23747
+ config.bgClass,
23748
+ config.borderClass
23749
+ ),
23750
+ children: /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between", children: [
23751
+ /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
23752
+ showDetails && workspaceName && /* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: workspaceName }),
23753
+ showDetails && lineName && /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-0.5", children: lineName }),
23754
+ /* @__PURE__ */ jsx("div", { className: "mt-2", children: /* @__PURE__ */ jsx(HealthStatusIndicator, { ...indicatorProps, showLabel: true }) })
23755
+ ] }),
23756
+ showDetails && consecutiveMisses !== void 0 && consecutiveMisses > 0 && /* @__PURE__ */ jsxs("div", { className: "ml-3 text-right", children: [
23757
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: "Missed" }),
23758
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-bold text-gray-900 dark:text-gray-100", children: consecutiveMisses })
23759
+ ] })
23760
+ ] })
23761
+ }
23762
+ );
23763
+ };
23525
23764
  var LinePdfExportButton = ({
23526
23765
  targetElement,
23527
23766
  fileName = "line-export",
@@ -24644,6 +24883,8 @@ var WorkspaceCard = ({
24644
24883
  cycleTime,
24645
24884
  operators,
24646
24885
  status = "normal",
24886
+ healthStatus,
24887
+ healthLastUpdated,
24647
24888
  onCardClick,
24648
24889
  headerActions,
24649
24890
  footerContent,
@@ -24736,6 +24977,19 @@ var WorkspaceCard = ({
24736
24977
  ] })
24737
24978
  ] })
24738
24979
  ] }),
24980
+ healthStatus && /* @__PURE__ */ jsx("div", { className: "pt-2 mt-auto border-t dark:border-gray-700", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
24981
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-500 dark:text-gray-400", children: "System Health" }),
24982
+ /* @__PURE__ */ jsx(
24983
+ HealthStatusIndicator,
24984
+ {
24985
+ status: healthStatus,
24986
+ lastUpdated: healthLastUpdated,
24987
+ showTime: false,
24988
+ size: "sm",
24989
+ pulse: healthStatus === "healthy"
24990
+ }
24991
+ )
24992
+ ] }) }),
24739
24993
  chartData && chartData.data && chartData.data.length > 0 && /* @__PURE__ */ jsxs(
24740
24994
  "div",
24741
24995
  {
@@ -26394,6 +26648,194 @@ var BackButtonMinimal = ({
26394
26648
  }
26395
26649
  );
26396
26650
  };
26651
+ var InlineEditableText = ({
26652
+ value,
26653
+ onSave,
26654
+ placeholder = "Click to edit",
26655
+ className = "",
26656
+ editIconClassName = "",
26657
+ inputClassName = "",
26658
+ debounceDelay = 750,
26659
+ // 750ms for quick auto-save
26660
+ disabled = false
26661
+ }) => {
26662
+ const [isEditing, setIsEditing] = useState(false);
26663
+ const [editValue, setEditValue] = useState(value);
26664
+ const [saveStatus, setSaveStatus] = useState("idle");
26665
+ const [lastSavedValue, setLastSavedValue] = useState(value);
26666
+ const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
26667
+ const inputRef = useRef(null);
26668
+ const containerRef = useRef(null);
26669
+ const debounceTimeout = useRef(void 0);
26670
+ const saveStatusTimeout = useRef(void 0);
26671
+ useEffect(() => {
26672
+ if (!isEditing) {
26673
+ setEditValue(value);
26674
+ setLastSavedValue(value);
26675
+ }
26676
+ }, [value, isEditing]);
26677
+ useEffect(() => {
26678
+ if (isEditing && inputRef.current) {
26679
+ requestAnimationFrame(() => {
26680
+ inputRef.current?.focus();
26681
+ inputRef.current?.select();
26682
+ });
26683
+ }
26684
+ }, [isEditing]);
26685
+ useEffect(() => {
26686
+ return () => {
26687
+ if (debounceTimeout.current) clearTimeout(debounceTimeout.current);
26688
+ if (saveStatusTimeout.current) clearTimeout(saveStatusTimeout.current);
26689
+ };
26690
+ }, []);
26691
+ const performSave = useCallback(async (valueToSave, shouldClose = false) => {
26692
+ const trimmedValue = valueToSave.trim();
26693
+ if (trimmedValue === lastSavedValue.trim()) {
26694
+ setHasUnsavedChanges(false);
26695
+ if (shouldClose) {
26696
+ setIsEditing(false);
26697
+ setSaveStatus("idle");
26698
+ }
26699
+ return;
26700
+ }
26701
+ setSaveStatus("saving");
26702
+ setHasUnsavedChanges(false);
26703
+ try {
26704
+ await onSave(trimmedValue);
26705
+ setLastSavedValue(trimmedValue);
26706
+ setSaveStatus("saved");
26707
+ if (!shouldClose && inputRef.current) {
26708
+ requestAnimationFrame(() => {
26709
+ inputRef.current?.focus();
26710
+ });
26711
+ }
26712
+ if (saveStatusTimeout.current) clearTimeout(saveStatusTimeout.current);
26713
+ saveStatusTimeout.current = setTimeout(() => {
26714
+ setSaveStatus("idle");
26715
+ }, 2e3);
26716
+ if (shouldClose) {
26717
+ setIsEditing(false);
26718
+ }
26719
+ } catch (error) {
26720
+ console.error("Failed to save:", error);
26721
+ setSaveStatus("error");
26722
+ setHasUnsavedChanges(true);
26723
+ if (saveStatusTimeout.current) clearTimeout(saveStatusTimeout.current);
26724
+ saveStatusTimeout.current = setTimeout(() => {
26725
+ setSaveStatus("idle");
26726
+ }, 3e3);
26727
+ if (!shouldClose && inputRef.current) {
26728
+ requestAnimationFrame(() => {
26729
+ inputRef.current?.focus();
26730
+ });
26731
+ }
26732
+ }
26733
+ }, [lastSavedValue, onSave]);
26734
+ useEffect(() => {
26735
+ const handleClickOutside = (event) => {
26736
+ if (isEditing && containerRef.current && !containerRef.current.contains(event.target)) {
26737
+ if (debounceTimeout.current) {
26738
+ clearTimeout(debounceTimeout.current);
26739
+ }
26740
+ performSave(editValue, true);
26741
+ }
26742
+ };
26743
+ if (isEditing) {
26744
+ document.addEventListener("mousedown", handleClickOutside);
26745
+ return () => document.removeEventListener("mousedown", handleClickOutside);
26746
+ }
26747
+ }, [isEditing, editValue, performSave]);
26748
+ const handleInputChange = (e) => {
26749
+ const newValue = e.target.value;
26750
+ setEditValue(newValue);
26751
+ if (newValue.trim() !== lastSavedValue.trim()) {
26752
+ setHasUnsavedChanges(true);
26753
+ setSaveStatus("idle");
26754
+ } else {
26755
+ setHasUnsavedChanges(false);
26756
+ }
26757
+ if (debounceTimeout.current) {
26758
+ clearTimeout(debounceTimeout.current);
26759
+ }
26760
+ if (newValue.trim() !== lastSavedValue.trim()) {
26761
+ debounceTimeout.current = setTimeout(() => {
26762
+ performSave(newValue, false);
26763
+ }, debounceDelay);
26764
+ }
26765
+ };
26766
+ const handleKeyDown = (e) => {
26767
+ if (e.key === "Enter") {
26768
+ e.preventDefault();
26769
+ if (debounceTimeout.current) {
26770
+ clearTimeout(debounceTimeout.current);
26771
+ }
26772
+ performSave(editValue, true);
26773
+ } else if (e.key === "Escape") {
26774
+ e.preventDefault();
26775
+ if (debounceTimeout.current) {
26776
+ clearTimeout(debounceTimeout.current);
26777
+ }
26778
+ setEditValue(lastSavedValue);
26779
+ setHasUnsavedChanges(false);
26780
+ setSaveStatus("idle");
26781
+ setIsEditing(false);
26782
+ }
26783
+ };
26784
+ const handleClick = () => {
26785
+ if (!disabled && !isEditing) {
26786
+ setIsEditing(true);
26787
+ setSaveStatus("idle");
26788
+ }
26789
+ };
26790
+ if (isEditing) {
26791
+ return /* @__PURE__ */ jsx("div", { ref: containerRef, className: "inline-flex items-center gap-1.5", children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
26792
+ /* @__PURE__ */ jsx(
26793
+ "input",
26794
+ {
26795
+ ref: inputRef,
26796
+ type: "text",
26797
+ value: editValue,
26798
+ onChange: handleInputChange,
26799
+ onKeyDown: handleKeyDown,
26800
+ className: `px-2 py-1 pr-7 text-sm border rounded-md transition-colors duration-200
26801
+ ${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"}
26802
+ focus:outline-none focus:ring-2 bg-white
26803
+ ${inputClassName}`,
26804
+ placeholder,
26805
+ autoComplete: "off"
26806
+ }
26807
+ ),
26808
+ /* @__PURE__ */ jsxs("div", { className: "absolute right-1.5 top-1/2 -translate-y-1/2", children: [
26809
+ saveStatus === "saving" && /* @__PURE__ */ jsx(Loader2, { className: "w-3.5 h-3.5 text-blue-500 animate-spin" }),
26810
+ saveStatus === "saved" && /* @__PURE__ */ jsx(Check, { className: "w-3.5 h-3.5 text-green-500" }),
26811
+ saveStatus === "error" && /* @__PURE__ */ jsx(AlertCircle, { className: "w-3.5 h-3.5 text-red-500" }),
26812
+ saveStatus === "idle" && hasUnsavedChanges && /* @__PURE__ */ jsx("div", { className: "w-1.5 h-1.5 bg-yellow-400 rounded-full" })
26813
+ ] })
26814
+ ] }) });
26815
+ }
26816
+ return /* @__PURE__ */ jsxs(
26817
+ "div",
26818
+ {
26819
+ onClick: handleClick,
26820
+ className: `inline-flex items-center gap-1.5 cursor-pointer px-2 py-1 rounded-md
26821
+ transition-all duration-200 hover:bg-gray-50 group
26822
+ ${disabled ? "cursor-not-allowed opacity-50" : ""}
26823
+ ${className}`,
26824
+ children: [
26825
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-gray-900", children: editValue || placeholder }),
26826
+ /* @__PURE__ */ jsx(
26827
+ Edit2,
26828
+ {
26829
+ className: `w-3.5 h-3.5 text-gray-400 transition-opacity duration-200
26830
+ opacity-0 group-hover:opacity-100
26831
+ ${disabled ? "hidden" : ""}
26832
+ ${editIconClassName}`
26833
+ }
26834
+ )
26835
+ ]
26836
+ }
26837
+ );
26838
+ };
26397
26839
  var BottlenecksContent = ({
26398
26840
  workspaceId,
26399
26841
  workspaceName,
@@ -28139,6 +28581,451 @@ var KPISection = memo(({
28139
28581
  return true;
28140
28582
  });
28141
28583
  KPISection.displayName = "KPISection";
28584
+ var WorkspaceHealthCard = ({
28585
+ workspace,
28586
+ onClick,
28587
+ showDetails = true,
28588
+ className = ""
28589
+ }) => {
28590
+ const getStatusConfig = () => {
28591
+ switch (workspace.status) {
28592
+ case "healthy":
28593
+ return {
28594
+ gradient: "from-emerald-50 to-green-50 dark:from-emerald-950/30 dark:to-green-950/30",
28595
+ border: "border-emerald-200 dark:border-emerald-800",
28596
+ icon: CheckCircle2,
28597
+ iconColor: "text-emerald-600 dark:text-emerald-400",
28598
+ badge: "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-300",
28599
+ statusText: "Online",
28600
+ pulse: true
28601
+ };
28602
+ case "unhealthy":
28603
+ return {
28604
+ gradient: "from-rose-50 to-red-50 dark:from-rose-950/30 dark:to-red-950/30",
28605
+ border: "border-rose-200 dark:border-rose-800",
28606
+ icon: XCircle,
28607
+ iconColor: "text-rose-600 dark:text-rose-400",
28608
+ badge: "bg-rose-100 text-rose-700 dark:bg-rose-900/50 dark:text-rose-300",
28609
+ statusText: "Offline",
28610
+ pulse: false
28611
+ };
28612
+ case "warning":
28613
+ return {
28614
+ gradient: "from-amber-50 to-yellow-50 dark:from-amber-950/30 dark:to-yellow-950/30",
28615
+ border: "border-amber-200 dark:border-amber-800",
28616
+ icon: AlertTriangle,
28617
+ iconColor: "text-amber-600 dark:text-amber-400",
28618
+ badge: "bg-amber-100 text-amber-700 dark:bg-amber-900/50 dark:text-amber-300",
28619
+ statusText: "Degraded",
28620
+ pulse: true
28621
+ };
28622
+ default:
28623
+ return {
28624
+ gradient: "from-gray-50 to-slate-50 dark:from-gray-950/30 dark:to-slate-950/30",
28625
+ border: "border-gray-200 dark:border-gray-800",
28626
+ icon: Activity,
28627
+ iconColor: "text-gray-500 dark:text-gray-400",
28628
+ badge: "bg-gray-100 text-gray-700 dark:bg-gray-900/50 dark:text-gray-300",
28629
+ statusText: "Unknown",
28630
+ pulse: false
28631
+ };
28632
+ }
28633
+ };
28634
+ const config = getStatusConfig();
28635
+ const StatusIcon = config.icon;
28636
+ workspace.isStale ? WifiOff : Wifi;
28637
+ const handleClick = () => {
28638
+ if (onClick) {
28639
+ onClick(workspace);
28640
+ }
28641
+ };
28642
+ const handleKeyDown = (event) => {
28643
+ if (onClick && (event.key === "Enter" || event.key === " ")) {
28644
+ event.preventDefault();
28645
+ onClick(workspace);
28646
+ }
28647
+ };
28648
+ const formatTimeAgo = (timeString) => {
28649
+ return timeString.replace("about ", "").replace(" ago", "");
28650
+ };
28651
+ return /* @__PURE__ */ jsx(
28652
+ Card2,
28653
+ {
28654
+ className: clsx(
28655
+ "relative overflow-hidden transition-all duration-300",
28656
+ "bg-gradient-to-br",
28657
+ config.gradient,
28658
+ "border",
28659
+ config.border,
28660
+ "shadow-sm hover:shadow-md",
28661
+ onClick && "cursor-pointer hover:scale-[1.01]",
28662
+ workspace.isStale && "opacity-90",
28663
+ className
28664
+ ),
28665
+ onClick: handleClick,
28666
+ onKeyDown: handleKeyDown,
28667
+ tabIndex: onClick ? 0 : void 0,
28668
+ role: onClick ? "button" : void 0,
28669
+ "aria-label": `Workspace ${workspace.workspace_display_name} status: ${workspace.status}`,
28670
+ children: /* @__PURE__ */ jsxs("div", { className: "p-4", children: [
28671
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between mb-3", children: [
28672
+ /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
28673
+ /* @__PURE__ */ 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)}` }),
28674
+ showDetails && workspace.line_name && /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-600 dark:text-gray-400", children: workspace.line_name })
28675
+ ] }),
28676
+ /* @__PURE__ */ jsxs("div", { className: clsx(
28677
+ "flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium",
28678
+ config.badge
28679
+ ), children: [
28680
+ /* @__PURE__ */ jsx(StatusIcon, { className: "h-3.5 w-3.5" }),
28681
+ /* @__PURE__ */ jsx("span", { children: config.statusText })
28682
+ ] })
28683
+ ] }),
28684
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
28685
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
28686
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
28687
+ /* @__PURE__ */ jsx(Clock, { className: "h-3.5 w-3.5 text-gray-400" }),
28688
+ /* @__PURE__ */ jsxs("span", { className: "text-sm text-gray-600 dark:text-gray-400 whitespace-nowrap", children: [
28689
+ "Last seen: ",
28690
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: formatTimeAgo(workspace.timeSinceLastUpdate) })
28691
+ ] })
28692
+ ] }),
28693
+ workspace.isStale && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
28694
+ /* @__PURE__ */ jsx(WifiOff, { className: "h-3.5 w-3.5 text-amber-500" }),
28695
+ /* @__PURE__ */ jsx("span", { className: "text-amber-600 dark:text-amber-400 text-xs", children: "No recent updates" })
28696
+ ] })
28697
+ ] }),
28698
+ config.pulse && !workspace.isStale && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
28699
+ /* @__PURE__ */ jsxs("div", { className: "relative flex h-2 w-2", children: [
28700
+ /* @__PURE__ */ jsx("span", { className: clsx(
28701
+ "animate-ping absolute inline-flex h-full w-full rounded-full opacity-75",
28702
+ workspace.status === "healthy" ? "bg-emerald-400" : "bg-amber-400"
28703
+ ) }),
28704
+ /* @__PURE__ */ jsx("span", { className: clsx(
28705
+ "relative inline-flex rounded-full h-2 w-2",
28706
+ workspace.status === "healthy" ? "bg-emerald-500" : "bg-amber-500"
28707
+ ) })
28708
+ ] }),
28709
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-500 dark:text-gray-400", children: "Live" })
28710
+ ] })
28711
+ ] })
28712
+ ] })
28713
+ }
28714
+ );
28715
+ };
28716
+ var CompactWorkspaceHealthCard = ({
28717
+ workspace,
28718
+ onClick,
28719
+ className = ""
28720
+ }) => {
28721
+ const getStatusConfig = () => {
28722
+ switch (workspace.status) {
28723
+ case "healthy":
28724
+ return {
28725
+ dot: "bg-emerald-500",
28726
+ icon: CheckCircle2,
28727
+ iconColor: "text-emerald-600 dark:text-emerald-400",
28728
+ bg: "hover:bg-emerald-50 dark:hover:bg-emerald-950/20"
28729
+ };
28730
+ case "unhealthy":
28731
+ return {
28732
+ dot: "bg-rose-500",
28733
+ icon: XCircle,
28734
+ iconColor: "text-rose-600 dark:text-rose-400",
28735
+ bg: "hover:bg-rose-50 dark:hover:bg-rose-950/20"
28736
+ };
28737
+ case "warning":
28738
+ return {
28739
+ dot: "bg-amber-500",
28740
+ icon: AlertTriangle,
28741
+ iconColor: "text-amber-600 dark:text-amber-400",
28742
+ bg: "hover:bg-amber-50 dark:hover:bg-amber-950/20"
28743
+ };
28744
+ default:
28745
+ return {
28746
+ dot: "bg-gray-400",
28747
+ icon: Activity,
28748
+ iconColor: "text-gray-500 dark:text-gray-400",
28749
+ bg: "hover:bg-gray-50 dark:hover:bg-gray-950/20"
28750
+ };
28751
+ }
28752
+ };
28753
+ const config = getStatusConfig();
28754
+ const StatusIcon = config.icon;
28755
+ const handleClick = () => {
28756
+ if (onClick) {
28757
+ onClick(workspace);
28758
+ }
28759
+ };
28760
+ return /* @__PURE__ */ jsxs(
28761
+ "div",
28762
+ {
28763
+ className: clsx(
28764
+ "flex items-center justify-between px-4 py-3 rounded-lg border",
28765
+ "bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700",
28766
+ "transition-all duration-200",
28767
+ onClick && `cursor-pointer ${config.bg}`,
28768
+ className
28769
+ ),
28770
+ onClick: handleClick,
28771
+ role: onClick ? "button" : void 0,
28772
+ tabIndex: onClick ? 0 : void 0,
28773
+ children: [
28774
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
28775
+ /* @__PURE__ */ jsx(StatusIcon, { className: clsx("h-5 w-5", config.iconColor) }),
28776
+ /* @__PURE__ */ jsxs("div", { children: [
28777
+ /* @__PURE__ */ 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)}` }),
28778
+ workspace.line_name && /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: workspace.line_name })
28779
+ ] })
28780
+ ] }),
28781
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
28782
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: workspace.timeSinceLastUpdate }),
28783
+ /* @__PURE__ */ jsx("div", { className: clsx("h-2 w-2 rounded-full", config.dot) })
28784
+ ] })
28785
+ ]
28786
+ }
28787
+ );
28788
+ };
28789
+ var HealthStatusGrid = ({
28790
+ workspaces,
28791
+ onWorkspaceClick,
28792
+ viewMode: initialViewMode = "grid",
28793
+ showFilters = true,
28794
+ groupBy: initialGroupBy = "none",
28795
+ className = ""
28796
+ }) => {
28797
+ const [viewMode, setViewMode] = useState(initialViewMode);
28798
+ const [searchTerm, setSearchTerm] = useState("");
28799
+ const [statusFilter, setStatusFilter] = useState("all");
28800
+ const [groupBy, setGroupBy] = useState(initialGroupBy);
28801
+ const [expandedGroups, setExpandedGroups] = useState(/* @__PURE__ */ new Set());
28802
+ const filteredWorkspaces = useMemo(() => {
28803
+ let filtered = [...workspaces];
28804
+ if (searchTerm) {
28805
+ const search = searchTerm.toLowerCase();
28806
+ filtered = filtered.filter(
28807
+ (w) => w.workspace_display_name?.toLowerCase().includes(search) || w.line_name?.toLowerCase().includes(search) || w.company_name?.toLowerCase().includes(search)
28808
+ );
28809
+ }
28810
+ if (statusFilter !== "all") {
28811
+ filtered = filtered.filter((w) => w.status === statusFilter);
28812
+ }
28813
+ return filtered;
28814
+ }, [workspaces, searchTerm, statusFilter]);
28815
+ const groupedWorkspaces = useMemo(() => {
28816
+ if (groupBy === "none") {
28817
+ return { "All Workspaces": filteredWorkspaces };
28818
+ }
28819
+ const groups = {};
28820
+ filteredWorkspaces.forEach((workspace) => {
28821
+ let key = "Unknown";
28822
+ switch (groupBy) {
28823
+ case "line":
28824
+ key = workspace.line_name || "Unknown Line";
28825
+ break;
28826
+ case "status":
28827
+ key = workspace.status;
28828
+ break;
28829
+ }
28830
+ if (!groups[key]) {
28831
+ groups[key] = [];
28832
+ }
28833
+ groups[key].push(workspace);
28834
+ });
28835
+ const sortedGroups = {};
28836
+ Object.keys(groups).sort().forEach((key) => {
28837
+ sortedGroups[key] = groups[key];
28838
+ });
28839
+ return sortedGroups;
28840
+ }, [filteredWorkspaces, groupBy]);
28841
+ useEffect(() => {
28842
+ if (groupBy !== "none") {
28843
+ setExpandedGroups(new Set(Object.keys(groupedWorkspaces)));
28844
+ }
28845
+ }, [groupBy, groupedWorkspaces]);
28846
+ const toggleGroup = (groupName) => {
28847
+ const newExpanded = new Set(expandedGroups);
28848
+ if (newExpanded.has(groupName)) {
28849
+ newExpanded.delete(groupName);
28850
+ } else {
28851
+ newExpanded.add(groupName);
28852
+ }
28853
+ setExpandedGroups(newExpanded);
28854
+ };
28855
+ const getStatusCounts = () => {
28856
+ const counts = {
28857
+ healthy: 0,
28858
+ unhealthy: 0,
28859
+ warning: 0,
28860
+ unknown: 0
28861
+ };
28862
+ workspaces.forEach((w) => {
28863
+ counts[w.status]++;
28864
+ });
28865
+ return counts;
28866
+ };
28867
+ const statusCounts = getStatusCounts();
28868
+ return /* @__PURE__ */ jsxs("div", { className: clsx("space-y-4", className), children: [
28869
+ showFilters && /* @__PURE__ */ jsxs("div", { className: "bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4", children: [
28870
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col sm:flex-row gap-3 flex-wrap", children: [
28871
+ /* @__PURE__ */ jsx("div", { className: "flex-1 min-w-[200px]", children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
28872
+ /* @__PURE__ */ jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" }),
28873
+ /* @__PURE__ */ jsx(
28874
+ "input",
28875
+ {
28876
+ type: "text",
28877
+ placeholder: "Search workspaces...",
28878
+ value: searchTerm,
28879
+ onChange: (e) => setSearchTerm(e.target.value),
28880
+ 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"
28881
+ }
28882
+ )
28883
+ ] }) }),
28884
+ /* @__PURE__ */ jsxs(Select, { value: statusFilter, onValueChange: (value) => setStatusFilter(value), children: [
28885
+ /* @__PURE__ */ jsx(SelectTrigger, { className: "w-full sm:w-[180px] bg-white border-gray-200", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "All statuses" }) }),
28886
+ /* @__PURE__ */ jsxs(SelectContent, { children: [
28887
+ /* @__PURE__ */ jsxs(SelectItem, { value: "all", children: [
28888
+ "All (",
28889
+ workspaces.length,
28890
+ ")"
28891
+ ] }),
28892
+ /* @__PURE__ */ jsx(SelectItem, { value: "healthy", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
28893
+ /* @__PURE__ */ jsx("div", { className: "h-2 w-2 rounded-full bg-green-500" }),
28894
+ /* @__PURE__ */ jsxs("span", { children: [
28895
+ "Healthy (",
28896
+ statusCounts.healthy,
28897
+ ")"
28898
+ ] })
28899
+ ] }) }),
28900
+ /* @__PURE__ */ jsx(SelectItem, { value: "unhealthy", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
28901
+ /* @__PURE__ */ jsx("div", { className: "h-2 w-2 rounded-full bg-red-500" }),
28902
+ /* @__PURE__ */ jsxs("span", { children: [
28903
+ "Unhealthy (",
28904
+ statusCounts.unhealthy,
28905
+ ")"
28906
+ ] })
28907
+ ] }) }),
28908
+ /* @__PURE__ */ jsx(SelectItem, { value: "warning", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
28909
+ /* @__PURE__ */ jsx("div", { className: "h-2 w-2 rounded-full bg-yellow-500" }),
28910
+ /* @__PURE__ */ jsxs("span", { children: [
28911
+ "Warning (",
28912
+ statusCounts.warning,
28913
+ ")"
28914
+ ] })
28915
+ ] }) }),
28916
+ /* @__PURE__ */ jsx(SelectItem, { value: "unknown", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
28917
+ /* @__PURE__ */ jsx("div", { className: "h-2 w-2 rounded-full bg-gray-400" }),
28918
+ /* @__PURE__ */ jsxs("span", { children: [
28919
+ "Unknown (",
28920
+ statusCounts.unknown,
28921
+ ")"
28922
+ ] })
28923
+ ] }) })
28924
+ ] })
28925
+ ] }),
28926
+ /* @__PURE__ */ jsxs(Select, { value: groupBy, onValueChange: (value) => setGroupBy(value), children: [
28927
+ /* @__PURE__ */ jsx(SelectTrigger, { className: "w-full sm:w-[160px] bg-white border-gray-200", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Group by" }) }),
28928
+ /* @__PURE__ */ jsxs(SelectContent, { children: [
28929
+ /* @__PURE__ */ jsx(SelectItem, { value: "none", children: "No grouping" }),
28930
+ /* @__PURE__ */ jsx(SelectItem, { value: "line", children: "Group by Line" }),
28931
+ /* @__PURE__ */ jsx(SelectItem, { value: "status", children: "Group by Status" })
28932
+ ] })
28933
+ ] }),
28934
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
28935
+ /* @__PURE__ */ jsx(
28936
+ "button",
28937
+ {
28938
+ onClick: () => setViewMode("grid"),
28939
+ className: clsx(
28940
+ "p-2 rounded-lg transition-colors",
28941
+ 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"
28942
+ ),
28943
+ "aria-label": "Grid view",
28944
+ children: /* @__PURE__ */ jsx(Grid3x3, { className: "h-5 w-5" })
28945
+ }
28946
+ ),
28947
+ /* @__PURE__ */ jsx(
28948
+ "button",
28949
+ {
28950
+ onClick: () => setViewMode("list"),
28951
+ className: clsx(
28952
+ "p-2 rounded-lg transition-colors",
28953
+ 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"
28954
+ ),
28955
+ "aria-label": "List view",
28956
+ children: /* @__PURE__ */ jsx(List, { className: "h-5 w-5" })
28957
+ }
28958
+ )
28959
+ ] })
28960
+ ] }),
28961
+ /* @__PURE__ */ jsxs("div", { className: "mt-3 text-sm text-gray-500 dark:text-gray-400", children: [
28962
+ "Showing ",
28963
+ filteredWorkspaces.length,
28964
+ " of ",
28965
+ workspaces.length,
28966
+ " workspaces"
28967
+ ] })
28968
+ ] }),
28969
+ /* @__PURE__ */ jsx("div", { className: "space-y-6", children: Object.entries(groupedWorkspaces).map(([groupName, groupWorkspaces]) => {
28970
+ const isExpanded = groupBy === "none" || expandedGroups.has(groupName);
28971
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
28972
+ groupBy !== "none" && /* @__PURE__ */ jsxs(
28973
+ "div",
28974
+ {
28975
+ className: "flex items-center justify-between cursor-pointer group",
28976
+ onClick: () => toggleGroup(groupName),
28977
+ children: [
28978
+ /* @__PURE__ */ jsxs("h3", { className: "text-lg font-semibold text-gray-900 dark:text-gray-100 flex items-center gap-2", children: [
28979
+ groupName,
28980
+ /* @__PURE__ */ jsxs("span", { className: "text-sm font-normal text-gray-500 dark:text-gray-400", children: [
28981
+ "(",
28982
+ groupWorkspaces.length,
28983
+ ")"
28984
+ ] })
28985
+ ] }),
28986
+ /* @__PURE__ */ jsx(
28987
+ ChevronDown,
28988
+ {
28989
+ className: clsx(
28990
+ "h-5 w-5 text-gray-400 transition-transform",
28991
+ isExpanded && "rotate-180"
28992
+ )
28993
+ }
28994
+ )
28995
+ ]
28996
+ }
28997
+ ),
28998
+ isExpanded && /* @__PURE__ */ jsx(
28999
+ "div",
29000
+ {
29001
+ className: clsx(
29002
+ viewMode === "grid" ? "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4" : "space-y-2"
29003
+ ),
29004
+ children: groupWorkspaces.map(
29005
+ (workspace) => viewMode === "grid" ? /* @__PURE__ */ jsx(
29006
+ WorkspaceHealthCard,
29007
+ {
29008
+ workspace,
29009
+ onClick: onWorkspaceClick,
29010
+ showDetails: true
29011
+ },
29012
+ workspace.workspace_id
29013
+ ) : /* @__PURE__ */ jsx(
29014
+ CompactWorkspaceHealthCard,
29015
+ {
29016
+ workspace,
29017
+ onClick: onWorkspaceClick
29018
+ },
29019
+ workspace.workspace_id
29020
+ )
29021
+ )
29022
+ }
29023
+ )
29024
+ ] }, groupName);
29025
+ }) }),
29026
+ filteredWorkspaces.length === 0 && /* @__PURE__ */ jsx("div", { className: "text-center py-12", children: /* @__PURE__ */ jsx("p", { className: "text-gray-500 dark:text-gray-400", children: searchTerm || statusFilter !== "all" ? "No workspaces found matching your filters." : "No workspaces available." }) })
29027
+ ] });
29028
+ };
28142
29029
  var ISTTimer2 = ISTTimer_default;
28143
29030
  var DashboardHeader = memo(({ lineTitle, className = "", headerControls }) => {
28144
29031
  const getShiftName = () => {
@@ -28636,6 +29523,17 @@ var SideNavBar = memo(({
28636
29523
  });
28637
29524
  onMobileMenuClose?.();
28638
29525
  }, [navigate, onMobileMenuClose]);
29526
+ const handleHealthClick = useCallback(() => {
29527
+ navigate("/health", {
29528
+ trackingEvent: {
29529
+ name: "Health Status Page Clicked",
29530
+ properties: {
29531
+ source: "side_nav"
29532
+ }
29533
+ }
29534
+ });
29535
+ onMobileMenuClose?.();
29536
+ }, [navigate, onMobileMenuClose]);
28639
29537
  const handleLogoClick = useCallback(() => {
28640
29538
  navigate("/");
28641
29539
  onMobileMenuClose?.();
@@ -28649,6 +29547,7 @@ var SideNavBar = memo(({
28649
29547
  const profileButtonClasses = useMemo(() => getButtonClasses("/profile"), [getButtonClasses, pathname]);
28650
29548
  const helpButtonClasses = useMemo(() => getButtonClasses("/help"), [getButtonClasses, pathname]);
28651
29549
  const skusButtonClasses = useMemo(() => getButtonClasses("/skus"), [getButtonClasses, pathname]);
29550
+ const healthButtonClasses = useMemo(() => getButtonClasses("/health"), [getButtonClasses, pathname]);
28652
29551
  const NavigationContent = () => /* @__PURE__ */ jsxs(Fragment, { children: [
28653
29552
  /* @__PURE__ */ jsx("div", { className: "w-full py-6 px-4 flex-shrink-0", children: /* @__PURE__ */ jsx(
28654
29553
  "button",
@@ -28793,6 +29692,21 @@ var SideNavBar = memo(({
28793
29692
  /* @__PURE__ */ jsx("span", { className: "text-[10px] font-medium leading-tight", children: "Help" })
28794
29693
  ]
28795
29694
  }
29695
+ ),
29696
+ /* @__PURE__ */ jsxs(
29697
+ "button",
29698
+ {
29699
+ onClick: handleHealthClick,
29700
+ className: healthButtonClasses,
29701
+ "aria-label": "System Health",
29702
+ tabIndex: 0,
29703
+ role: "tab",
29704
+ "aria-selected": pathname === "/health" || pathname.startsWith("/health/"),
29705
+ children: [
29706
+ /* @__PURE__ */ jsx(HeartIcon, { className: "w-5 h-5 mb-1" }),
29707
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] font-medium leading-tight", children: "Health" })
29708
+ ]
29709
+ }
28796
29710
  )
28797
29711
  ] })
28798
29712
  ] }),
@@ -35418,6 +36332,7 @@ var TargetsViewUI = ({
35418
36332
  onSaveLine,
35419
36333
  onToggleBulkConfigure,
35420
36334
  onBulkConfigure,
36335
+ onUpdateWorkspaceDisplayName,
35421
36336
  // SKU props
35422
36337
  skuEnabled = false,
35423
36338
  skus = [],
@@ -35583,7 +36498,18 @@ var TargetsViewUI = ({
35583
36498
  {
35584
36499
  className: "px-6 py-4 hover:bg-gray-50 transition-all duration-200",
35585
36500
  children: /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-12 gap-6 items-center", children: [
35586
- /* @__PURE__ */ jsx("div", { className: "col-span-2", children: /* @__PURE__ */ jsx("span", { className: "font-medium text-gray-900", children: formattedName }) }),
36501
+ /* @__PURE__ */ jsx("div", { className: "col-span-2", children: onUpdateWorkspaceDisplayName ? /* @__PURE__ */ jsx(
36502
+ InlineEditableText,
36503
+ {
36504
+ value: formattedName,
36505
+ onSave: async (newName) => {
36506
+ await onUpdateWorkspaceDisplayName(workspace.id, newName);
36507
+ },
36508
+ placeholder: "Workspace name",
36509
+ className: "font-medium text-gray-900",
36510
+ inputClassName: "min-w-[120px]"
36511
+ }
36512
+ ) : /* @__PURE__ */ jsx("span", { className: "font-medium text-gray-900", children: formattedName }) }),
35587
36513
  /* @__PURE__ */ jsx("div", { className: "col-span-2", children: /* @__PURE__ */ jsxs(
35588
36514
  "select",
35589
36515
  {
@@ -36324,6 +37250,17 @@ var TargetsView = ({
36324
37250
  router.push("/");
36325
37251
  }
36326
37252
  };
37253
+ const handleUpdateWorkspaceDisplayName = useCallback(async (workspaceId, displayName) => {
37254
+ try {
37255
+ await workspaceService.updateWorkspaceDisplayName(workspaceId, displayName);
37256
+ await forceRefreshWorkspaceDisplayNames();
37257
+ toast.success("Workspace name updated successfully");
37258
+ } catch (error) {
37259
+ console.error("Error updating workspace display name:", error);
37260
+ toast.error("Failed to update workspace name");
37261
+ throw error;
37262
+ }
37263
+ }, []);
36327
37264
  return /* @__PURE__ */ jsx(
36328
37265
  TargetsViewUI_default,
36329
37266
  {
@@ -36346,6 +37283,7 @@ var TargetsView = ({
36346
37283
  onSaveLine: handleSaveLine,
36347
37284
  onToggleBulkConfigure: handleToggleBulkConfigure,
36348
37285
  onBulkConfigure: handleBulkConfigure,
37286
+ onUpdateWorkspaceDisplayName: handleUpdateWorkspaceDisplayName,
36349
37287
  skuEnabled,
36350
37288
  skus,
36351
37289
  onUpdateSelectedSKU: updateSelectedSKU,
@@ -36444,6 +37382,14 @@ var WorkspaceDetailView = ({
36444
37382
  const [usingFallbackData, setUsingFallbackData] = useState(false);
36445
37383
  const [showIdleTime, setShowIdleTime] = useState(false);
36446
37384
  const dashboardConfig = useDashboardConfig();
37385
+ const {
37386
+ workspace: workspaceHealth,
37387
+ loading: healthLoading,
37388
+ error: healthError
37389
+ } = useWorkspaceHealthById(workspaceId, {
37390
+ enableRealtime: true,
37391
+ refreshInterval: 3e4
37392
+ });
36447
37393
  const {
36448
37394
  status: prefetchStatus,
36449
37395
  data: prefetchData,
@@ -36796,10 +37742,7 @@ var WorkspaceDetailView = ({
36796
37742
  "aria-label": "Navigate back to previous page"
36797
37743
  }
36798
37744
  ) }),
36799
- /* @__PURE__ */ jsxs("div", { className: "absolute left-1/2 transform -translate-x-1/2 flex items-center gap-3", children: [
36800
- /* @__PURE__ */ jsx("h1", { className: "text-3xl font-semibold text-gray-900", children: formattedWorkspaceName }),
36801
- /* @__PURE__ */ jsx("div", { className: "h-2 w-2 rounded-full bg-green-500 animate-pulse ring-2 ring-green-500/30 ring-offset-1" })
36802
- ] }),
37745
+ /* @__PURE__ */ jsx("div", { className: "absolute left-1/2 transform -translate-x-1/2", children: /* @__PURE__ */ jsx("h1", { className: "text-3xl font-semibold text-gray-900", children: formattedWorkspaceName }) }),
36803
37746
  /* @__PURE__ */ jsx("div", { className: "w-full h-8" })
36804
37747
  ] }),
36805
37748
  activeTab !== "monthly_history" && /* @__PURE__ */ jsx("div", { className: "mt-3 bg-blue-50 px-3 py-2 rounded-lg", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-4", children: [
@@ -36827,6 +37770,19 @@ var WorkspaceDetailView = ({
36827
37770
  workspace.shift_type,
36828
37771
  " Shift"
36829
37772
  ] })
37773
+ ] }),
37774
+ workspaceHealth && /* @__PURE__ */ jsxs(Fragment, { children: [
37775
+ /* @__PURE__ */ jsx("div", { className: "w-px h-4 bg-blue-300" }),
37776
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
37777
+ /* @__PURE__ */ jsx("div", { className: clsx(
37778
+ "h-1.5 w-1.5 rounded-full",
37779
+ workspaceHealth.status === "healthy" ? "bg-green-600" : workspaceHealth.status === "unhealthy" ? "bg-red-600" : workspaceHealth.status === "warning" ? "bg-amber-600" : "bg-gray-500"
37780
+ ) }),
37781
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-blue-700", children: [
37782
+ "Last update: ",
37783
+ workspaceHealth.timeSinceLastUpdate
37784
+ ] })
37785
+ ] })
36830
37786
  ] })
36831
37787
  ] }) }),
36832
37788
  /* @__PURE__ */ jsxs("div", { className: "mt-1 sm:mt-1.5 lg:mt-2 flex items-center justify-between", children: [
@@ -37363,6 +38319,255 @@ var SKUManagementView = () => {
37363
38319
  )
37364
38320
  ] });
37365
38321
  };
38322
+ var WorkspaceHealthView = ({
38323
+ lineId,
38324
+ companyId,
38325
+ onNavigate,
38326
+ className = ""
38327
+ }) => {
38328
+ const router = useRouter();
38329
+ const [viewMode, setViewMode] = useState("grid");
38330
+ const [groupBy, setGroupBy] = useState("line");
38331
+ const operationalDate = getOperationalDate();
38332
+ const currentHour = (/* @__PURE__ */ new Date()).getHours();
38333
+ const isNightShift = currentHour >= 18 || currentHour < 6;
38334
+ const shiftType = isNightShift ? "Night" : "Day";
38335
+ const formatDate = (date) => {
38336
+ const d = new Date(date);
38337
+ return d.toLocaleDateString("en-IN", {
38338
+ month: "long",
38339
+ day: "numeric",
38340
+ year: "numeric",
38341
+ timeZone: "Asia/Kolkata"
38342
+ });
38343
+ };
38344
+ const getShiftIcon = (shift) => {
38345
+ return shift === "Night" ? /* @__PURE__ */ jsx(Moon, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(Sun, { className: "h-4 w-4" });
38346
+ };
38347
+ const {
38348
+ workspaces,
38349
+ summary,
38350
+ loading,
38351
+ error,
38352
+ refetch
38353
+ } = useWorkspaceHealth({
38354
+ lineId,
38355
+ companyId,
38356
+ enableRealtime: true,
38357
+ refreshInterval: 1e4
38358
+ // Refresh every 10 seconds for more responsive updates
38359
+ });
38360
+ const handleWorkspaceClick = useCallback(
38361
+ (workspace) => {
38362
+ const url = `/workspace/${workspace.workspace_id}`;
38363
+ if (onNavigate) {
38364
+ onNavigate(url);
38365
+ } else {
38366
+ router.push(url);
38367
+ }
38368
+ },
38369
+ [router, onNavigate]
38370
+ );
38371
+ const handleExport = useCallback(() => {
38372
+ const csv = [
38373
+ ["Workspace", "Line", "Company", "Status", "Last Heartbeat", "Consecutive Misses"],
38374
+ ...workspaces.map((w) => [
38375
+ w.workspace_display_name || "",
38376
+ w.line_name || "",
38377
+ w.company_name || "",
38378
+ w.status,
38379
+ w.last_heartbeat,
38380
+ w.consecutive_misses?.toString() || "0"
38381
+ ])
38382
+ ].map((row) => row.join(",")).join("\n");
38383
+ const blob = new Blob([csv], { type: "text/csv" });
38384
+ const url = window.URL.createObjectURL(blob);
38385
+ const a = document.createElement("a");
38386
+ a.href = url;
38387
+ a.download = `workspace-health-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.csv`;
38388
+ document.body.appendChild(a);
38389
+ a.click();
38390
+ document.body.removeChild(a);
38391
+ window.URL.revokeObjectURL(url);
38392
+ }, [workspaces]);
38393
+ const getStatusIcon = (status) => {
38394
+ switch (status) {
38395
+ case "healthy":
38396
+ return /* @__PURE__ */ jsx(CheckCircle, { className: "h-5 w-5 text-green-500" });
38397
+ case "unhealthy":
38398
+ return /* @__PURE__ */ jsx(XCircle, { className: "h-5 w-5 text-red-500" });
38399
+ case "warning":
38400
+ return /* @__PURE__ */ jsx(AlertTriangle, { className: "h-5 w-5 text-yellow-500" });
38401
+ default:
38402
+ return /* @__PURE__ */ jsx(Activity, { className: "h-5 w-5 text-gray-400" });
38403
+ }
38404
+ };
38405
+ const getUptimeColor = (percentage) => {
38406
+ if (percentage >= 99) return "text-green-600 dark:text-green-400";
38407
+ if (percentage >= 95) return "text-yellow-600 dark:text-yellow-400";
38408
+ return "text-red-600 dark:text-red-400";
38409
+ };
38410
+ if (loading && !summary) {
38411
+ return /* @__PURE__ */ jsx("div", { className: "min-h-screen bg-gray-50 dark:bg-gray-900 p-4", children: /* @__PURE__ */ jsx(LoadingState, {}) });
38412
+ }
38413
+ if (error) {
38414
+ return /* @__PURE__ */ jsx("div", { className: "min-h-screen bg-gray-50 dark:bg-gray-900 p-4", children: /* @__PURE__ */ jsx("div", { className: "max-w-7xl mx-auto", children: /* @__PURE__ */ jsx(Card2, { className: "border-red-200 dark:border-red-800", children: /* @__PURE__ */ jsxs(CardContent2, { className: "p-8 text-center", children: [
38415
+ /* @__PURE__ */ jsx(XCircle, { className: "h-12 w-12 text-red-500 mx-auto mb-4" }),
38416
+ /* @__PURE__ */ jsx("h2", { className: "text-xl font-semibold text-gray-900 dark:text-gray-100 mb-2", children: "Error Loading Health Status" }),
38417
+ /* @__PURE__ */ jsx("p", { className: "text-gray-600 dark:text-gray-400 mb-4", children: error.message || "Unable to load workspace health status" }),
38418
+ /* @__PURE__ */ jsx(
38419
+ "button",
38420
+ {
38421
+ onClick: () => refetch(),
38422
+ className: "px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors",
38423
+ children: "Try Again"
38424
+ }
38425
+ )
38426
+ ] }) }) }) });
38427
+ }
38428
+ return /* @__PURE__ */ jsxs("div", { className: clsx("min-h-screen bg-slate-50", className), children: [
38429
+ /* @__PURE__ */ 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: [
38430
+ /* @__PURE__ */ jsxs("div", { className: "relative flex items-center", children: [
38431
+ /* @__PURE__ */ jsx("div", { className: "absolute left-0 z-10", children: /* @__PURE__ */ jsx(
38432
+ BackButtonMinimal,
38433
+ {
38434
+ onClick: () => router.push("/"),
38435
+ text: "Back",
38436
+ size: "default",
38437
+ "aria-label": "Navigate back to dashboard"
38438
+ }
38439
+ ) }),
38440
+ /* @__PURE__ */ jsx("div", { className: "absolute left-1/2 transform -translate-x-1/2", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
38441
+ /* @__PURE__ */ jsx("h1", { className: "text-3xl font-semibold text-gray-900", children: "System Health" }),
38442
+ /* @__PURE__ */ jsxs("div", { className: "relative flex h-2.5 w-2.5", children: [
38443
+ /* @__PURE__ */ jsx("span", { className: "animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" }),
38444
+ /* @__PURE__ */ jsx("span", { className: "relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500" })
38445
+ ] })
38446
+ ] }) }),
38447
+ /* @__PURE__ */ jsxs("div", { className: "absolute right-0 flex gap-2", children: [
38448
+ /* @__PURE__ */ jsx(
38449
+ "button",
38450
+ {
38451
+ onClick: () => {
38452
+ refetch();
38453
+ },
38454
+ className: "p-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors",
38455
+ "aria-label": "Refresh",
38456
+ children: /* @__PURE__ */ jsx(RefreshCw, { className: "h-5 w-5" })
38457
+ }
38458
+ ),
38459
+ /* @__PURE__ */ jsx(
38460
+ "button",
38461
+ {
38462
+ onClick: handleExport,
38463
+ className: "p-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors",
38464
+ "aria-label": "Export CSV",
38465
+ children: /* @__PURE__ */ jsx(Download, { className: "h-5 w-5" })
38466
+ }
38467
+ )
38468
+ ] }),
38469
+ /* @__PURE__ */ jsx("div", { className: "w-full h-8" })
38470
+ ] }),
38471
+ /* @__PURE__ */ jsx("div", { className: "mt-3 bg-blue-50 px-3 py-2 rounded-lg", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-4", children: [
38472
+ /* @__PURE__ */ jsx("div", { className: "text-lg font-medium text-blue-600", children: /* @__PURE__ */ jsx(LiveTimer, {}) }),
38473
+ /* @__PURE__ */ jsx("div", { className: "w-px h-4 bg-blue-300" }),
38474
+ /* @__PURE__ */ jsx("span", { className: "text-base font-medium text-blue-600", children: formatDate(operationalDate) }),
38475
+ /* @__PURE__ */ jsx("div", { className: "w-px h-4 bg-blue-300" }),
38476
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
38477
+ /* @__PURE__ */ jsx("div", { className: "text-blue-600", children: getShiftIcon(shiftType) }),
38478
+ /* @__PURE__ */ jsxs("span", { className: "text-base font-medium text-blue-600", children: [
38479
+ shiftType,
38480
+ " Shift"
38481
+ ] })
38482
+ ] })
38483
+ ] }) })
38484
+ ] }),
38485
+ /* @__PURE__ */ jsxs("div", { className: "max-w-7xl mx-auto p-4 space-y-6", children: [
38486
+ summary && /* @__PURE__ */ jsxs(
38487
+ motion.div,
38488
+ {
38489
+ initial: { opacity: 0, y: 20 },
38490
+ animate: { opacity: 1, y: 0 },
38491
+ transition: { duration: 0.3, delay: 0.1 },
38492
+ className: "grid grid-cols-2 sm:grid-cols-2 md:grid-cols-5 gap-2 sm:gap-3 lg:gap-4",
38493
+ children: [
38494
+ /* @__PURE__ */ jsxs(Card2, { className: "col-span-2 sm:col-span-2 md:col-span-2", children: [
38495
+ /* @__PURE__ */ jsx(CardHeader2, { className: "pb-3", children: /* @__PURE__ */ jsx(CardTitle2, { className: "text-sm font-medium text-gray-500 dark:text-gray-400", children: "Overall System Status" }) }),
38496
+ /* @__PURE__ */ jsxs(CardContent2, { children: [
38497
+ /* @__PURE__ */ jsxs("div", { className: "flex items-baseline gap-2", children: [
38498
+ /* @__PURE__ */ jsxs("span", { className: clsx("text-3xl font-bold", getUptimeColor(summary.uptimePercentage)), children: [
38499
+ summary.uptimePercentage.toFixed(1),
38500
+ "%"
38501
+ ] }),
38502
+ summary.uptimePercentage >= 99 ? /* @__PURE__ */ jsx(TrendingUp, { className: "h-5 w-5 text-green-500" }) : /* @__PURE__ */ jsx(TrendingDown, { className: "h-5 w-5 text-red-500" })
38503
+ ] }),
38504
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: [
38505
+ summary.healthyWorkspaces,
38506
+ " of ",
38507
+ summary.totalWorkspaces,
38508
+ " workspaces healthy"
38509
+ ] })
38510
+ ] })
38511
+ ] }),
38512
+ /* @__PURE__ */ jsxs(Card2, { children: [
38513
+ /* @__PURE__ */ jsx(CardHeader2, { className: "pb-3", children: /* @__PURE__ */ jsxs(CardTitle2, { className: "text-sm font-medium text-gray-500 dark:text-gray-400 flex items-center gap-2", children: [
38514
+ getStatusIcon("healthy"),
38515
+ "Healthy"
38516
+ ] }) }),
38517
+ /* @__PURE__ */ jsxs(CardContent2, { children: [
38518
+ /* @__PURE__ */ jsx("p", { className: "text-2xl font-bold text-green-600 dark:text-green-400", children: summary.healthyWorkspaces }),
38519
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: "Operating normally" })
38520
+ ] })
38521
+ ] }),
38522
+ /* @__PURE__ */ jsxs(Card2, { children: [
38523
+ /* @__PURE__ */ jsx(CardHeader2, { className: "pb-3", children: /* @__PURE__ */ jsxs(CardTitle2, { className: "text-sm font-medium text-gray-500 dark:text-gray-400 flex items-center gap-2", children: [
38524
+ getStatusIcon("warning"),
38525
+ "Warning"
38526
+ ] }) }),
38527
+ /* @__PURE__ */ jsxs(CardContent2, { children: [
38528
+ /* @__PURE__ */ jsx("p", { className: "text-2xl font-bold text-yellow-600 dark:text-yellow-400", children: summary.warningWorkspaces }),
38529
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: "Delayed updates" })
38530
+ ] })
38531
+ ] }),
38532
+ /* @__PURE__ */ jsxs(Card2, { children: [
38533
+ /* @__PURE__ */ jsx(CardHeader2, { className: "pb-3", children: /* @__PURE__ */ jsxs(CardTitle2, { className: "text-sm font-medium text-gray-500 dark:text-gray-400 flex items-center gap-2", children: [
38534
+ getStatusIcon("unhealthy"),
38535
+ "Unhealthy"
38536
+ ] }) }),
38537
+ /* @__PURE__ */ jsxs(CardContent2, { children: [
38538
+ /* @__PURE__ */ jsx("p", { className: "text-2xl font-bold text-red-600 dark:text-red-400", children: summary.unhealthyWorkspaces }),
38539
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: "Requires attention" })
38540
+ ] })
38541
+ ] })
38542
+ ]
38543
+ }
38544
+ ),
38545
+ /* @__PURE__ */ jsx(
38546
+ motion.div,
38547
+ {
38548
+ initial: { opacity: 0, y: 20 },
38549
+ animate: { opacity: 1, y: 0 },
38550
+ transition: { duration: 0.3, delay: 0.2 },
38551
+ children: /* @__PURE__ */ jsx(
38552
+ HealthStatusGrid,
38553
+ {
38554
+ workspaces,
38555
+ onWorkspaceClick: handleWorkspaceClick,
38556
+ viewMode,
38557
+ showFilters: true,
38558
+ groupBy
38559
+ }
38560
+ )
38561
+ }
38562
+ )
38563
+ ] })
38564
+ ] });
38565
+ };
38566
+ var WorkspaceHealthView_default = withAuth(WorkspaceHealthView, {
38567
+ redirectTo: "/login",
38568
+ requireAuth: true
38569
+ });
38570
+ var AuthenticatedWorkspaceHealthView = WorkspaceHealthView;
37366
38571
  var S3Service = class {
37367
38572
  constructor(config) {
37368
38573
  this.s3Client = null;
@@ -37818,4 +39023,4 @@ var streamProxyConfig = {
37818
39023
  }
37819
39024
  };
37820
39025
 
37821
- export { ACTION_NAMES, AIAgentView_default as AIAgentView, AudioService, AuthCallback, AuthCallbackView_default as AuthCallbackView, AuthProvider, AuthenticatedFactoryView, AuthenticatedHelpView, AuthenticatedHomeView, AuthenticatedShiftsView, AuthenticatedTargetsView, BackButton, BackButtonMinimal, BarChart, BaseHistoryCalendar, BottlenecksContent, BreakNotificationPopup, CachePrefetchStatus, Card2 as Card, CardContent2 as CardContent, CardDescription2 as CardDescription, CardFooter2 as CardFooter, CardHeader2 as CardHeader, CardTitle2 as CardTitle, CongratulationsOverlay, CycleTimeChart, CycleTimeOverTimeChart, DEFAULT_ANALYTICS_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_CONFIG, DEFAULT_DATABASE_CONFIG, DEFAULT_DATE_TIME_CONFIG, DEFAULT_ENDPOINTS_CONFIG, DEFAULT_ENTITY_CONFIG, DEFAULT_SHIFT_CONFIG, DEFAULT_THEME_CONFIG, DEFAULT_VIDEO_CONFIG, DEFAULT_WORKSPACE_CONFIG, DEFAULT_WORKSPACE_POSITIONS, DashboardHeader, DashboardLayout, DashboardOverridesProvider, DashboardProvider, DateDisplay_default as DateDisplay, DateTimeDisplay, DebugAuth, DebugAuthView_default as DebugAuthView, EmptyStateMessage, EncouragementOverlay, FactoryView_default as FactoryView, GaugeChart, GridComponentsPlaceholder, HamburgerButton, Header, HelpView_default as HelpView, HomeView_default as HomeView, HourlyOutputChart2 as HourlyOutputChart, ISTTimer_default as ISTTimer, KPICard, KPIDetailView_default as KPIDetailView, KPIGrid, KPIHeader, KPISection, KPIsOverviewView_default as KPIsOverviewView, LINE_1_UUID, LINE_2_UUID, LargeOutputProgressChart, LeaderboardDetailView_default as LeaderboardDetailView, Legend6 as Legend, LineChart, LineHistoryCalendar, LineMonthlyHistory, LineMonthlyPdfGenerator, LinePdfExportButton, LinePdfGenerator, LineWhatsAppShareButton, LiveTimer, LoadingInline, LoadingOverlay_default as LoadingOverlay, LoadingPage_default as LoadingPage, LoadingSkeleton, LoadingState, LoginPage, LoginView_default as LoginView, MainLayout, MetricCard_default as MetricCard, NoWorkspaceData, OptifyeAgentClient, OptifyeLogoLoader_default as OptifyeLogoLoader, OutputProgressChart, PageHeader, PieChart4 as PieChart, PrefetchConfigurationError, PrefetchError, PrefetchEvents, PrefetchStatus, PrefetchTimeoutError, ProfileView_default as ProfileView, RegistryProvider, S3Service, SKUManagementView, SOPComplianceChart, SSEChatClient, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, ShiftDisplay_default as ShiftDisplay, ShiftsView_default as ShiftsView, SideNavBar, SingleVideoStream_default as SingleVideoStream, Skeleton, SubscriptionManager, SubscriptionManagerProvider, SupabaseProvider, TargetWorkspaceGrid, TargetsView_default as TargetsView, ThreadSidebar, TicketHistory_default as TicketHistory, TicketHistoryService, TimeDisplay_default as TimeDisplay, TimePickerDropdown, VideoCard, VideoGridView, VideoPlayer, VideoPreloader, WORKSPACE_POSITIONS, WhatsAppShareButton, WorkspaceCard, WorkspaceDetailView_default as WorkspaceDetailView, WorkspaceDisplayNameExample, WorkspaceGrid, WorkspaceGridItem, WorkspaceHistoryCalendar, WorkspaceMetricCards, WorkspaceMonthlyDataFetcher, WorkspaceMonthlyPdfGenerator, WorkspacePdfExportButton, WorkspacePdfGenerator, WorkspaceWhatsAppShareButton, actionService, apiUtils, authCoreService, authOTPService, authRateLimitService, checkRateLimit2 as checkRateLimit, clearAllRateLimits2 as clearAllRateLimits, clearRateLimit2 as clearRateLimit, clearS3VideoCache, clearS3VideoFromCache, clearWorkspaceDisplayNamesCache, cn, createStreamProxyHandler, createSupabaseClient, createThrottledReload, dashboardService, deleteThread, forceRefreshWorkspaceDisplayNames, formatDateInZone, formatDateTimeInZone, formatISTDate, formatIdleTime, formatTimeInZone, fromUrlFriendlyName, getAllLineDisplayNames, getAllThreadMessages, getAllWorkspaceDisplayNamesAsync, getAnonClient, getCameraNumber, getCompanyMetricsTableName, getConfigurableShortWorkspaceDisplayName, getConfigurableWorkspaceDisplayName, getConfiguredLineIds, getCoreSessionRecordingProperties, getCoreSessionReplayUrl, getCurrentShift, getCurrentTimeInZone, getDashboardHeaderTimeInZone, getDaysDifferenceInZone, getDefaultCameraStreamUrl, getDefaultLineId, getDefaultTabForWorkspace, getLineDisplayName, getManufacturingInsights, getMetricsTablePrefix, getOperationalDate, getS3SignedUrl, getS3VideoSrc, getShortWorkspaceDisplayName, getShortWorkspaceDisplayNameAsync, getStoredWorkspaceMappings, getSubscriptionManager, getThreadMessages, getUserThreads, getUserThreadsPaginated, getWorkspaceDisplayName, getWorkspaceDisplayNameAsync, getWorkspaceDisplayNamesMap, getWorkspaceFromUrl, getWorkspaceNavigationParams, identifyCoreUser, initializeCoreMixpanel, isLegacyConfiguration, isPrefetchError, isTransitionPeriod, isUrlPermanentlyFailed, isValidFactoryViewConfiguration, isValidLineInfoPayload, isValidPrefetchParams, isValidPrefetchStatus, isValidWorkspaceDetailedMetricsPayload, isValidWorkspaceMetricsPayload, isWorkspaceDisplayNamesLoaded, isWorkspaceDisplayNamesLoading, mergeWithDefaultConfig, migrateLegacyConfiguration, optifyeAgentClient, preInitializeWorkspaceDisplayNames, preloadS3Video, preloadS3VideoUrl, preloadS3VideosUrl, preloadVideoUrl, preloadVideosUrl, qualityService, realtimeService, refreshWorkspaceDisplayNames, resetCoreMixpanel, resetFailedUrl, resetSubscriptionManager, s3VideoPreloader, skuService, startCoreSessionRecording, stopCoreSessionRecording, storeWorkspaceMapping, streamProxyConfig, throttledReloadDashboard, toUrlFriendlyName, trackCoreEvent, trackCorePageView, updateThreadTitle, useActiveBreaks, useAllWorkspaceMetrics, useAnalyticsConfig, useAudioService, useAuth, useAuthConfig, useComponentOverride, useCustomConfig, useDashboardConfig, useDashboardMetrics, useDatabaseConfig, useDateFormatter, useDateTimeConfig, useEndpointsConfig, useEntityConfig, useFactoryOverviewMetrics, useFeatureFlags, useFormatNumber, useHistoricWorkspaceMetrics, useHlsStream, useHlsStreamWithCropping, useHookOverride, useHourEndTimer, useHourlyTargetAchievements, useHourlyTargetMisses, useLeaderboardMetrics, useLineDetailedMetrics, useLineKPIs, useLineMetrics, useLineWorkspaceMetrics, useMessages, useMetrics, useNavigation, useOverrides, usePageOverride, usePrefetchClipCounts, useRealtimeLineMetrics, useRegistry, useSKUs, useShiftConfig, useShifts, useSubscriptionManager, useSubscriptionManagerSafe, useSupabase, useSupabaseClient, useTargets, useTheme, useThemeConfig, useThreads, useTicketHistory, useVideoConfig, useWorkspaceConfig, useWorkspaceDetailedMetrics, useWorkspaceDisplayName, useWorkspaceDisplayNames, useWorkspaceDisplayNamesMap, useWorkspaceMetrics, useWorkspaceNavigation, useWorkspaceOperators, videoPrefetchManager, videoPreloader, whatsappService, withAuth, withRegistry, workspaceService };
39026
+ export { ACTION_NAMES, AIAgentView_default as AIAgentView, AudioService, AuthCallback, AuthCallbackView_default as AuthCallbackView, AuthProvider, AuthenticatedFactoryView, AuthenticatedHelpView, AuthenticatedHomeView, AuthenticatedShiftsView, AuthenticatedTargetsView, AuthenticatedWorkspaceHealthView, BackButton, BackButtonMinimal, BarChart, BaseHistoryCalendar, BottlenecksContent, BreakNotificationPopup, CachePrefetchStatus, Card2 as Card, CardContent2 as CardContent, CardDescription2 as CardDescription, CardFooter2 as CardFooter, CardHeader2 as CardHeader, CardTitle2 as CardTitle, CompactWorkspaceHealthCard, CongratulationsOverlay, CycleTimeChart, CycleTimeOverTimeChart, DEFAULT_ANALYTICS_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_CONFIG, DEFAULT_DATABASE_CONFIG, DEFAULT_DATE_TIME_CONFIG, DEFAULT_ENDPOINTS_CONFIG, DEFAULT_ENTITY_CONFIG, DEFAULT_SHIFT_CONFIG, DEFAULT_THEME_CONFIG, DEFAULT_VIDEO_CONFIG, DEFAULT_WORKSPACE_CONFIG, DEFAULT_WORKSPACE_POSITIONS, DashboardHeader, DashboardLayout, DashboardOverridesProvider, DashboardProvider, DateDisplay_default as DateDisplay, DateTimeDisplay, DebugAuth, DebugAuthView_default as DebugAuthView, DetailedHealthStatus, EmptyStateMessage, EncouragementOverlay, FactoryView_default as FactoryView, GaugeChart, GridComponentsPlaceholder, HamburgerButton, Header, HealthStatusGrid, HealthStatusIndicator, HelpView_default as HelpView, HomeView_default as HomeView, HourlyOutputChart2 as HourlyOutputChart, ISTTimer_default as ISTTimer, InlineEditableText, KPICard, KPIDetailView_default as KPIDetailView, KPIGrid, KPIHeader, KPISection, KPIsOverviewView_default as KPIsOverviewView, LINE_1_UUID, LINE_2_UUID, LargeOutputProgressChart, LeaderboardDetailView_default as LeaderboardDetailView, Legend6 as Legend, LineChart, LineHistoryCalendar, LineMonthlyHistory, LineMonthlyPdfGenerator, LinePdfExportButton, LinePdfGenerator, LineWhatsAppShareButton, LiveTimer, LoadingInline, LoadingOverlay_default as LoadingOverlay, LoadingPage_default as LoadingPage, LoadingSkeleton, LoadingState, LoginPage, LoginView_default as LoginView, MainLayout, MetricCard_default as MetricCard, NoWorkspaceData, OptifyeAgentClient, OptifyeLogoLoader_default as OptifyeLogoLoader, OutputProgressChart, PageHeader, PieChart4 as PieChart, PrefetchConfigurationError, PrefetchError, PrefetchEvents, PrefetchStatus, PrefetchTimeoutError, ProfileView_default as ProfileView, RegistryProvider, S3ClipsService, S3Service, SKUManagementView, SOPComplianceChart, SSEChatClient, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, ShiftDisplay_default as ShiftDisplay, ShiftsView_default as ShiftsView, SideNavBar, SingleVideoStream_default as SingleVideoStream, Skeleton, SubscriptionManager, SubscriptionManagerProvider, SupabaseProvider, TargetWorkspaceGrid, TargetsView_default as TargetsView, ThreadSidebar, TicketHistory_default as TicketHistory, TicketHistoryService, TimeDisplay_default as TimeDisplay, TimePickerDropdown, VideoCard, VideoGridView, VideoPlayer, VideoPreloader, WORKSPACE_POSITIONS, WhatsAppShareButton, WorkspaceCard, WorkspaceDetailView_default as WorkspaceDetailView, WorkspaceDisplayNameExample, WorkspaceGrid, WorkspaceGridItem, WorkspaceHealthCard, WorkspaceHealthView_default as WorkspaceHealthView, WorkspaceHistoryCalendar, WorkspaceMetricCards, WorkspaceMonthlyDataFetcher, WorkspaceMonthlyPdfGenerator, WorkspacePdfExportButton, WorkspacePdfGenerator, WorkspaceWhatsAppShareButton, actionService, apiUtils, authCoreService, authOTPService, authRateLimitService, checkRateLimit2 as checkRateLimit, clearAllRateLimits2 as clearAllRateLimits, clearRateLimit2 as clearRateLimit, clearS3VideoCache, clearS3VideoFromCache, clearWorkspaceDisplayNamesCache, cn, createStreamProxyHandler, createSupabaseClient, createThrottledReload, dashboardService, deleteThread, forceRefreshWorkspaceDisplayNames, formatDateInZone, formatDateTimeInZone, formatISTDate, formatIdleTime, formatTimeInZone, fromUrlFriendlyName, getAllLineDisplayNames, getAllThreadMessages, getAllWorkspaceDisplayNamesAsync, getAnonClient, getCameraNumber, getCompanyMetricsTableName, getConfigurableShortWorkspaceDisplayName, getConfigurableWorkspaceDisplayName, getConfiguredLineIds, getCoreSessionRecordingProperties, getCoreSessionReplayUrl, getCurrentShift, getCurrentTimeInZone, getDashboardHeaderTimeInZone, getDaysDifferenceInZone, getDefaultCameraStreamUrl, getDefaultLineId, getDefaultTabForWorkspace, getLineDisplayName, getManufacturingInsights, getMetricsTablePrefix, getOperationalDate, getS3SignedUrl, getS3VideoSrc, getShortWorkspaceDisplayName, getShortWorkspaceDisplayNameAsync, getStoredWorkspaceMappings, getSubscriptionManager, getThreadMessages, getUserThreads, getUserThreadsPaginated, getWorkspaceDisplayName, getWorkspaceDisplayNameAsync, getWorkspaceDisplayNamesMap, getWorkspaceFromUrl, getWorkspaceNavigationParams, identifyCoreUser, initializeCoreMixpanel, isLegacyConfiguration, isPrefetchError, isTransitionPeriod, isUrlPermanentlyFailed, isValidFactoryViewConfiguration, isValidLineInfoPayload, isValidPrefetchParams, isValidPrefetchStatus, isValidWorkspaceDetailedMetricsPayload, isValidWorkspaceMetricsPayload, isWorkspaceDisplayNamesLoaded, isWorkspaceDisplayNamesLoading, mergeWithDefaultConfig, migrateLegacyConfiguration, optifyeAgentClient, parseS3Uri, preInitializeWorkspaceDisplayNames, preloadS3Video, preloadS3VideoUrl, preloadS3VideosUrl, preloadVideoUrl, preloadVideosUrl, qualityService, realtimeService, refreshWorkspaceDisplayNames, resetCoreMixpanel, resetFailedUrl, resetSubscriptionManager, s3VideoPreloader, shuffleArray, skuService, startCoreSessionRecording, stopCoreSessionRecording, storeWorkspaceMapping, streamProxyConfig, throttledReloadDashboard, toUrlFriendlyName, trackCoreEvent, trackCorePageView, updateThreadTitle, useActiveBreaks, useAllWorkspaceMetrics, useAnalyticsConfig, useAudioService, useAuth, useAuthConfig, useComponentOverride, useCustomConfig, useDashboardConfig, useDashboardMetrics, useDatabaseConfig, useDateFormatter, useDateTimeConfig, useEndpointsConfig, useEntityConfig, useFactoryOverviewMetrics, useFeatureFlags, useFormatNumber, useHistoricWorkspaceMetrics, useHlsStream, useHlsStreamWithCropping, useHookOverride, useHourEndTimer, useHourlyTargetAchievements, useHourlyTargetMisses, useLeaderboardMetrics, useLineDetailedMetrics, useLineKPIs, useLineMetrics, useLineWorkspaceMetrics, useMessages, useMetrics, useNavigation, useOverrides, usePageOverride, usePrefetchClipCounts, useRealtimeLineMetrics, useRegistry, useSKUs, useShiftConfig, useShifts, useSubscriptionManager, useSubscriptionManagerSafe, useSupabase, useSupabaseClient, useTargets, useTheme, useThemeConfig, useThreads, useTicketHistory, useVideoConfig, useWorkspaceConfig, useWorkspaceDetailedMetrics, useWorkspaceDisplayName, useWorkspaceDisplayNames, useWorkspaceDisplayNamesMap, useWorkspaceHealth, useWorkspaceHealthById, useWorkspaceMetrics, useWorkspaceNavigation, useWorkspaceOperators, videoPrefetchManager, videoPreloader, whatsappService, withAuth, withRegistry, workspaceHealthService, workspaceService };