@optifye/dashboard-core 6.12.50 → 6.12.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6085,6 +6085,10 @@ var workspaceService = {
6085
6085
  _workspaceCameraIpsInFlight: /* @__PURE__ */ new Map(),
6086
6086
  _workspaceCameraIpsCacheExpiryMs: 5 * 60 * 1e3,
6087
6087
  // 5 minutes cache
6088
+ // Cache for workspace bulb IP configs
6089
+ _workspaceLightConfigsCache: /* @__PURE__ */ new Map(),
6090
+ _workspaceLightConfigsInFlight: /* @__PURE__ */ new Map(),
6091
+ _workspaceLightConfigsCacheExpiryMs: 5 * 60 * 1e3,
6088
6092
  async getWorkspaces(lineId, options) {
6089
6093
  const enabledOnly = options?.enabledOnly ?? false;
6090
6094
  const force = options?.force ?? false;
@@ -6262,6 +6266,61 @@ var workspaceService = {
6262
6266
  this._workspaceCameraIpsInFlight.set(cacheKey, fetchPromise);
6263
6267
  return fetchPromise;
6264
6268
  },
6269
+ async getWorkspaceLightConfigs(params) {
6270
+ const workspaceIds = (params.workspaceIds || []).filter(Boolean);
6271
+ const lineIds = (params.lineIds || []).filter(Boolean);
6272
+ const force = params.force ?? false;
6273
+ if (!workspaceIds.length && !lineIds.length) {
6274
+ return {};
6275
+ }
6276
+ const workspaceKey = workspaceIds.slice().sort().join(",");
6277
+ const lineKey = lineIds.slice().sort().join(",");
6278
+ const cacheKey = workspaceKey ? `workspaces:${workspaceKey}` : `lines:${lineKey}`;
6279
+ const now4 = Date.now();
6280
+ const cached = this._workspaceLightConfigsCache.get(cacheKey);
6281
+ if (!force && cached && now4 - cached.timestamp < this._workspaceLightConfigsCacheExpiryMs) {
6282
+ return cached.lightConfigs;
6283
+ }
6284
+ const inFlight = this._workspaceLightConfigsInFlight.get(cacheKey);
6285
+ if (!force && inFlight) {
6286
+ return inFlight;
6287
+ }
6288
+ const fetchPromise = (async () => {
6289
+ try {
6290
+ const token = await getAuthToken2();
6291
+ const apiUrl = getBackendUrl2();
6292
+ const response = await fetch(`${apiUrl}/api/workspaces/light-configs`, {
6293
+ method: "POST",
6294
+ headers: {
6295
+ "Authorization": `Bearer ${token}`,
6296
+ "Content-Type": "application/json"
6297
+ },
6298
+ body: JSON.stringify({
6299
+ workspace_ids: workspaceIds.length ? workspaceIds : void 0,
6300
+ line_ids: lineIds.length ? lineIds : void 0
6301
+ })
6302
+ });
6303
+ if (!response.ok) {
6304
+ const errorText = await response.text();
6305
+ throw new Error(`Backend API error (${response.status}): ${errorText}`);
6306
+ }
6307
+ const data = await response.json();
6308
+ const lightConfigs = data.light_configs || {};
6309
+ this._workspaceLightConfigsCache.set(cacheKey, {
6310
+ lightConfigs,
6311
+ timestamp: Date.now()
6312
+ });
6313
+ return lightConfigs;
6314
+ } catch (error) {
6315
+ console.error("Error fetching workspace light configs:", error);
6316
+ throw error;
6317
+ } finally {
6318
+ this._workspaceLightConfigsInFlight.delete(cacheKey);
6319
+ }
6320
+ })();
6321
+ this._workspaceLightConfigsInFlight.set(cacheKey, fetchPromise);
6322
+ return fetchPromise;
6323
+ },
6265
6324
  /**
6266
6325
  * Fetches workspace display names from the database
6267
6326
  * Returns a map of workspace_id -> display_name
@@ -6682,6 +6741,138 @@ var computeWorkspaceHealthSummary = (data, lastUpdated = (/* @__PURE__ */ new Da
6682
6741
  lastUpdated
6683
6742
  };
6684
6743
  };
6744
+ var normalizeHourBucket = (bucket) => {
6745
+ if (Array.isArray(bucket)) return bucket;
6746
+ if (bucket && typeof bucket === "object" && "values" in bucket && Array.isArray(bucket.values)) {
6747
+ return bucket.values;
6748
+ }
6749
+ return void 0;
6750
+ };
6751
+ var normalizeOutputHourly = (outputHourlyRaw) => outputHourlyRaw && typeof outputHourlyRaw === "object" ? Object.fromEntries(
6752
+ Object.entries(outputHourlyRaw).map(([key, value]) => [key, normalizeHourBucket(value) || []])
6753
+ ) : {};
6754
+ var interpretUptimeValue = (value) => {
6755
+ if (value === null || value === void 0) return "down";
6756
+ if (typeof value === "string") {
6757
+ return value.trim().toLowerCase() === "x" ? "down" : "up";
6758
+ }
6759
+ return "up";
6760
+ };
6761
+ var deriveStatusForMinute = (minuteOffset, minuteDate, outputHourly, outputArray, timezone) => {
6762
+ const hourKey = dateFnsTz.formatInTimeZone(minuteDate, timezone, "H");
6763
+ const minuteKey = Number.parseInt(dateFnsTz.formatInTimeZone(minuteDate, timezone, "m"), 10);
6764
+ const hourBucket = outputHourly[hourKey];
6765
+ if (Array.isArray(hourBucket)) {
6766
+ const value = hourBucket[minuteKey];
6767
+ if (value !== void 0) {
6768
+ return interpretUptimeValue(value);
6769
+ }
6770
+ }
6771
+ if (minuteOffset < outputArray.length) {
6772
+ return interpretUptimeValue(outputArray[minuteOffset]);
6773
+ }
6774
+ return "down";
6775
+ };
6776
+ var hasCompletedTimelineData = (completedMinutes, shiftStartDate, outputHourly, outputArray, timezone) => {
6777
+ if (completedMinutes <= 0) {
6778
+ return false;
6779
+ }
6780
+ for (let minuteIndex = 0; minuteIndex < completedMinutes; minuteIndex += 1) {
6781
+ const minuteDate = dateFns.addMinutes(shiftStartDate, minuteIndex);
6782
+ const hourKey = dateFnsTz.formatInTimeZone(minuteDate, timezone, "H");
6783
+ const minuteKey = Number.parseInt(dateFnsTz.formatInTimeZone(minuteDate, timezone, "m"), 10);
6784
+ const hourBucket = outputHourly[hourKey];
6785
+ if (Array.isArray(hourBucket) && hourBucket[minuteKey] !== void 0) {
6786
+ return true;
6787
+ }
6788
+ if (minuteIndex < outputArray.length) {
6789
+ return true;
6790
+ }
6791
+ }
6792
+ return false;
6793
+ };
6794
+ var computeWorkspaceUptime = ({
6795
+ totalMinutes,
6796
+ completedMinutes,
6797
+ shiftStartDate,
6798
+ outputHourly,
6799
+ outputArray,
6800
+ timezone
6801
+ }) => {
6802
+ const hasData = hasCompletedTimelineData(
6803
+ completedMinutes,
6804
+ shiftStartDate,
6805
+ outputHourly,
6806
+ outputArray,
6807
+ timezone
6808
+ );
6809
+ if (!hasData) {
6810
+ return {
6811
+ hasData: false,
6812
+ uptimeMinutes: 0,
6813
+ downtimeMinutes: 0,
6814
+ uptimePercentage: 0,
6815
+ points: [],
6816
+ downtimeSegments: []
6817
+ };
6818
+ }
6819
+ const points = [];
6820
+ let uptimeMinutes = 0;
6821
+ let downtimeMinutes = 0;
6822
+ for (let minuteIndex = 0; minuteIndex < totalMinutes; minuteIndex += 1) {
6823
+ const minuteDate = dateFns.addMinutes(shiftStartDate, minuteIndex);
6824
+ const timestamp = dateFnsTz.formatInTimeZone(minuteDate, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX");
6825
+ const status = minuteIndex < completedMinutes ? deriveStatusForMinute(minuteIndex, minuteDate, outputHourly, outputArray, timezone) : "pending";
6826
+ if (status === "up") {
6827
+ uptimeMinutes += 1;
6828
+ } else if (status === "down") {
6829
+ downtimeMinutes += 1;
6830
+ }
6831
+ points.push({
6832
+ minuteIndex,
6833
+ timestamp,
6834
+ status
6835
+ });
6836
+ }
6837
+ const downtimeSegments = [];
6838
+ let currentSegmentStart = null;
6839
+ const pushSegment = (startIndex, endIndex) => {
6840
+ if (endIndex <= startIndex) return;
6841
+ const segmentStartDate = dateFns.addMinutes(shiftStartDate, startIndex);
6842
+ const segmentEndDate = dateFns.addMinutes(shiftStartDate, endIndex);
6843
+ downtimeSegments.push({
6844
+ startMinuteIndex: startIndex,
6845
+ endMinuteIndex: endIndex,
6846
+ startTime: dateFnsTz.formatInTimeZone(segmentStartDate, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX"),
6847
+ endTime: dateFnsTz.formatInTimeZone(segmentEndDate, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX"),
6848
+ durationMinutes: endIndex - startIndex
6849
+ });
6850
+ };
6851
+ for (let minuteIndex = 0; minuteIndex < completedMinutes; minuteIndex += 1) {
6852
+ const point = points[minuteIndex];
6853
+ if (point.status === "down") {
6854
+ if (currentSegmentStart === null) {
6855
+ currentSegmentStart = minuteIndex;
6856
+ }
6857
+ } else if (currentSegmentStart !== null) {
6858
+ pushSegment(currentSegmentStart, minuteIndex);
6859
+ currentSegmentStart = null;
6860
+ }
6861
+ }
6862
+ if (currentSegmentStart !== null) {
6863
+ pushSegment(currentSegmentStart, completedMinutes);
6864
+ }
6865
+ const completedWindow = Math.max(1, uptimeMinutes + downtimeMinutes);
6866
+ const uptimePercentage = completedMinutes > 0 ? Number((uptimeMinutes / completedWindow * 100).toFixed(1)) : 100;
6867
+ return {
6868
+ hasData: true,
6869
+ uptimeMinutes,
6870
+ downtimeMinutes,
6871
+ uptimePercentage,
6872
+ points,
6873
+ downtimeSegments
6874
+ };
6875
+ };
6685
6876
 
6686
6877
  // src/lib/services/workspaceHealthService.ts
6687
6878
  var DATA_PROCESSING_DELAY_MINUTES = 5;
@@ -6708,6 +6899,92 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
6708
6899
  setCache(key, data) {
6709
6900
  this.cache.set(key, { data, timestamp: Date.now() });
6710
6901
  }
6902
+ hasBackendUrl() {
6903
+ return Boolean(process.env.NEXT_PUBLIC_BACKEND_URL);
6904
+ }
6905
+ async fetchBackendWorkspaceUptimeSummaries(companyId, lineIds, date, shiftId, timezone) {
6906
+ if (!this.hasBackendUrl()) {
6907
+ return null;
6908
+ }
6909
+ const supabase = _getSupabaseInstance();
6910
+ if (!supabase) throw new Error("Supabase client not initialized");
6911
+ const params = new URLSearchParams({
6912
+ company_id: companyId,
6913
+ date,
6914
+ shift_id: String(shiftId),
6915
+ timezone
6916
+ });
6917
+ const filteredLineIds = (lineIds || []).filter(Boolean);
6918
+ if (filteredLineIds.length > 0) {
6919
+ params.set("line_ids", filteredLineIds.join(","));
6920
+ }
6921
+ const cacheKey = `backend-uptime-summary:${companyId}:${filteredLineIds.join(",") || "all"}:${date}:${shiftId}:${timezone}`;
6922
+ const cached = this.getFromCache(cacheKey);
6923
+ if (cached) {
6924
+ return cached;
6925
+ }
6926
+ try {
6927
+ const response = await fetchBackendJson(
6928
+ supabase,
6929
+ `/api/dashboard/workspace-uptime?${params.toString()}`,
6930
+ {
6931
+ retries: 1,
6932
+ timeoutMs: 15e3,
6933
+ sentry: {
6934
+ capture: true,
6935
+ surface: "workspace_health",
6936
+ route: "GET /api/dashboard/workspace-uptime",
6937
+ severity: "warning",
6938
+ quotaKey: "workspace-health-uptime-summary"
6939
+ }
6940
+ }
6941
+ );
6942
+ const uptimeMap = /* @__PURE__ */ new Map();
6943
+ for (const entry of response.uptimes || []) {
6944
+ if (!entry.workspaceId || !entry.uptimeDetails) continue;
6945
+ uptimeMap.set(this.getUptimeMapKey(entry.lineId, entry.workspaceId), entry.uptimeDetails);
6946
+ }
6947
+ this.setCache(cacheKey, uptimeMap);
6948
+ return uptimeMap;
6949
+ } catch (error) {
6950
+ console.warn("[workspaceHealthService] Backend uptime summary failed; using frontend fallback", error);
6951
+ return null;
6952
+ }
6953
+ }
6954
+ async fetchBackendWorkspaceUptimeTimeline(workspaceId, companyId, lineId, date, shiftId, timezone) {
6955
+ if (!this.hasBackendUrl()) {
6956
+ return null;
6957
+ }
6958
+ const supabase = _getSupabaseInstance();
6959
+ if (!supabase) throw new Error("Supabase client not initialized");
6960
+ const params = new URLSearchParams({
6961
+ company_id: companyId,
6962
+ line_id: lineId,
6963
+ date,
6964
+ shift_id: String(shiftId),
6965
+ timezone
6966
+ });
6967
+ try {
6968
+ return await fetchBackendJson(
6969
+ supabase,
6970
+ `/api/dashboard/workspace/${workspaceId}/uptime-timeline?${params.toString()}`,
6971
+ {
6972
+ retries: 1,
6973
+ timeoutMs: 15e3,
6974
+ sentry: {
6975
+ capture: true,
6976
+ surface: "workspace_health",
6977
+ route: "GET /api/dashboard/workspace/{workspace_id}/uptime-timeline",
6978
+ severity: "warning",
6979
+ quotaKey: "workspace-health-uptime-timeline"
6980
+ }
6981
+ }
6982
+ );
6983
+ } catch (error) {
6984
+ console.warn("[workspaceHealthService] Backend uptime timeline failed; using frontend fallback", error);
6985
+ return null;
6986
+ }
6987
+ }
6711
6988
  getShiftTiming(timezone, shiftConfig) {
6712
6989
  const currentShift = getCurrentShift(timezone, shiftConfig);
6713
6990
  const { shiftId, date, shiftName, startTime, endTime } = currentShift;
@@ -6791,15 +7068,21 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
6791
7068
  pendingMinutes
6792
7069
  };
6793
7070
  }
6794
- normalizeHourBucket(bucket) {
6795
- if (Array.isArray(bucket)) return bucket;
6796
- if (bucket && Array.isArray(bucket.values)) return bucket.values;
6797
- return void 0;
6798
- }
6799
- normalizeOutputHourly(outputHourlyRaw) {
6800
- return outputHourlyRaw && typeof outputHourlyRaw === "object" ? Object.fromEntries(
6801
- Object.entries(outputHourlyRaw).map(([key, value]) => [key, this.normalizeHourBucket(value) || []])
6802
- ) : {};
7071
+ mergeOutputArrayValues(values) {
7072
+ const numericValues = values.filter(
7073
+ (value) => typeof value === "number" && Number.isFinite(value)
7074
+ );
7075
+ if (numericValues.length > 0) {
7076
+ const total = numericValues.reduce((sum, value) => sum + value, 0);
7077
+ return Number.isInteger(total) ? total : total;
7078
+ }
7079
+ if (values.length > 0 && values.every((value) => typeof value === "string" && value.trim().toLowerCase() === "x")) {
7080
+ return "x";
7081
+ }
7082
+ if (values.some((value) => value === null || value === void 0)) {
7083
+ return null;
7084
+ }
7085
+ return 0;
6803
7086
  }
6804
7087
  /**
6805
7088
  * Group multi-SKU `performance_metrics` rows by workspace and merge their
@@ -6833,21 +7116,16 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
6833
7116
  const primary = group[0];
6834
7117
  const mergedOutputHourly = mergeOutputHourly(
6835
7118
  group.map((record) => ({
6836
- output_hourly: this.normalizeOutputHourly(record.output_hourly || {})
7119
+ output_hourly: normalizeOutputHourly(record.output_hourly || {})
6837
7120
  }))
6838
7121
  );
6839
7122
  const arrays = group.map((record) => Array.isArray(record.output_array) ? record.output_array : []).filter((arr) => arr.length > 0);
6840
7123
  let mergedArray = [];
6841
7124
  if (arrays.length > 0) {
6842
7125
  const maxLen = arrays.reduce((acc, arr) => Math.max(acc, arr.length), 0);
6843
- mergedArray = new Array(maxLen).fill(0);
6844
7126
  for (let i = 0; i < maxLen; i += 1) {
6845
- let sum = 0;
6846
- for (const arr of arrays) {
6847
- const value = i < arr.length ? arr[i] : 0;
6848
- if (typeof value === "number" && Number.isFinite(value)) sum += value;
6849
- }
6850
- mergedArray[i] = sum;
7127
+ const values = arrays.filter((arr) => i < arr.length).map((arr) => arr[i]);
7128
+ mergedArray[i] = this.mergeOutputArrayValues(values);
6851
7129
  }
6852
7130
  }
6853
7131
  return {
@@ -6857,45 +7135,392 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
6857
7135
  };
6858
7136
  });
6859
7137
  }
6860
- interpretUptimeValue(value) {
6861
- if (value === null || value === void 0) return "down";
6862
- if (typeof value === "string") {
6863
- return value.trim().toLowerCase() === "x" ? "down" : "up";
7138
+ parseShiftTime(value) {
7139
+ const [hourPart = "0", minutePart = "0"] = String(value || "00:00").split(":");
7140
+ const hour = Number.parseInt(hourPart, 10);
7141
+ const minute = Number.parseInt(minutePart, 10);
7142
+ return {
7143
+ hour: Number.isFinite(hour) ? hour : 0,
7144
+ minute: Number.isFinite(minute) ? minute : 0
7145
+ };
7146
+ }
7147
+ getShiftDurationMinutes(startTime, endTime) {
7148
+ const start = this.parseShiftTime(startTime);
7149
+ const end = this.parseShiftTime(endTime);
7150
+ let duration = end.hour * 60 + end.minute - (start.hour * 60 + start.minute);
7151
+ if (duration <= 0) {
7152
+ duration += 24 * 60;
6864
7153
  }
6865
- return "up";
7154
+ return duration;
6866
7155
  }
6867
- deriveStatusForMinute(minuteOffset, minuteDate, outputHourly, outputArray, timezone) {
6868
- const hourKey = dateFnsTz.formatInTimeZone(minuteDate, timezone, "H");
6869
- const minuteKey = Number.parseInt(dateFnsTz.formatInTimeZone(minuteDate, timezone, "m"), 10);
6870
- const hourBucket = outputHourly[hourKey];
6871
- if (Array.isArray(hourBucket)) {
6872
- const value = hourBucket[minuteKey];
6873
- if (value !== void 0) {
6874
- return this.interpretUptimeValue(value);
7156
+ resolveLightShiftWindow(shiftConfig, timezone, overrideDate, overrideShiftId, now4 = /* @__PURE__ */ new Date()) {
7157
+ const currentShift = getCurrentShift(timezone, shiftConfig);
7158
+ const queryDate = overrideDate ?? currentShift.date;
7159
+ const queryShiftId = overrideShiftId ?? currentShift.shiftId;
7160
+ const targetShiftConfig = shiftConfig?.shifts?.find((shift) => shift.shiftId === queryShiftId);
7161
+ const startTime = targetShiftConfig?.startTime || currentShift.startTime || "06:00";
7162
+ const endTime = targetShiftConfig?.endTime || currentShift.endTime || "18:00";
7163
+ const shiftLabel = targetShiftConfig?.shiftName || targetShiftConfig?.name || currentShift.shiftName || `Shift ${queryShiftId}`;
7164
+ const shiftStartDate = dateFnsTz.fromZonedTime(`${queryDate}T${startTime}:00`, timezone);
7165
+ const totalMinutes = this.getShiftDurationMinutes(startTime, endTime);
7166
+ const shiftEndDate = dateFns.addMinutes(shiftStartDate, totalMinutes);
7167
+ const boundedWindowEnd = new Date(Math.min(Math.max(now4.getTime(), shiftStartDate.getTime()), shiftEndDate.getTime()));
7168
+ const completedMinutes = Math.max(
7169
+ 0,
7170
+ Math.min(totalMinutes, Math.floor((boundedWindowEnd.getTime() - shiftStartDate.getTime()) / 6e4))
7171
+ );
7172
+ return {
7173
+ shiftId: queryShiftId,
7174
+ shiftLabel,
7175
+ shiftStartDate,
7176
+ shiftEndDate,
7177
+ windowEndDate: boundedWindowEnd,
7178
+ totalMinutes,
7179
+ completedMinutes,
7180
+ pendingMinutes: Math.max(0, totalMinutes - completedMinutes)
7181
+ };
7182
+ }
7183
+ isLightStatus(status) {
7184
+ return status === "up" || status === "down" || status === "unknown";
7185
+ }
7186
+ secondsBetween(start, end) {
7187
+ return Math.max(0, Math.round((end.getTime() - start.getTime()) / 1e3));
7188
+ }
7189
+ roundPercent(value) {
7190
+ return Number(value.toFixed(1));
7191
+ }
7192
+ emptyLightTimeline(shiftWindow, timezone, bulbIp = null) {
7193
+ return {
7194
+ shiftId: shiftWindow.shiftId,
7195
+ shiftLabel: shiftWindow.shiftLabel,
7196
+ shiftStart: dateFnsTz.formatInTimeZone(shiftWindow.shiftStartDate, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX"),
7197
+ shiftEnd: dateFnsTz.formatInTimeZone(shiftWindow.shiftEndDate, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX"),
7198
+ totalMinutes: shiftWindow.totalMinutes,
7199
+ completedMinutes: shiftWindow.completedMinutes,
7200
+ upSeconds: 0,
7201
+ downSeconds: 0,
7202
+ unknownSeconds: 0,
7203
+ totalObservedSeconds: 0,
7204
+ downtimeSeconds: 0,
7205
+ uptimePercentage: null,
7206
+ hasData: false,
7207
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
7208
+ bulbIp,
7209
+ points: [],
7210
+ statusSegments: []
7211
+ };
7212
+ }
7213
+ async resolveWorkspaceLightConfigs(workspaces) {
7214
+ const supabase = _getSupabaseInstance();
7215
+ if (!supabase) throw new Error("Supabase client not initialized");
7216
+ const visibleWorkspaces = workspaces.filter((workspace) => workspace.workspace_id);
7217
+ if (!visibleWorkspaces.length) return /* @__PURE__ */ new Map();
7218
+ const workspaceIds = Array.from(new Set(visibleWorkspaces.map((workspace) => String(workspace.workspace_id))));
7219
+ const configs = /* @__PURE__ */ new Map();
7220
+ if (process.env.NEXT_PUBLIC_BACKEND_URL) {
7221
+ try {
7222
+ const backendConfigs = await workspaceService.getWorkspaceLightConfigs({ workspaceIds });
7223
+ for (const [workspaceId, config] of Object.entries(backendConfigs)) {
7224
+ if (!config?.bulb_ip) continue;
7225
+ configs.set(workspaceId, {
7226
+ healthWorkspaceId: workspaceId,
7227
+ lineId: config.line_id,
7228
+ workspaceDbId: config.workspace_id || workspaceId,
7229
+ bulbIp: String(config.bulb_ip)
7230
+ });
7231
+ }
7232
+ if (configs.size === workspaceIds.length) {
7233
+ return configs;
7234
+ }
7235
+ } catch (error) {
7236
+ console.error("[workspaceHealthService] Backend light config lookup failed, falling back to direct Supabase:", error);
6875
7237
  }
6876
7238
  }
6877
- if (minuteOffset < outputArray.length) {
6878
- return this.interpretUptimeValue(outputArray[minuteOffset]);
7239
+ const unresolvedVisibleWorkspaces = visibleWorkspaces.filter(
7240
+ (workspace) => !configs.has(String(workspace.workspace_id))
7241
+ );
7242
+ if (!unresolvedVisibleWorkspaces.length) return configs;
7243
+ const fallbackWorkspaceIds = Array.from(new Set(unresolvedVisibleWorkspaces.map((workspace) => String(workspace.workspace_id))));
7244
+ const fallbackLineIds = Array.from(new Set(unresolvedVisibleWorkspaces.map((workspace) => workspace.line_id).filter(Boolean).map(String)));
7245
+ const fallbackVisiblePairs = new Set(unresolvedVisibleWorkspaces.map((workspace) => `${workspace.line_id || ""}::${workspace.workspace_id}`));
7246
+ let workspaceRows = [];
7247
+ try {
7248
+ let query = supabase.from("workspaces").select("id, workspace_id, line_id, enable_bulb_light").in("workspace_id", fallbackWorkspaceIds);
7249
+ if (fallbackLineIds.length) {
7250
+ query = query.in("line_id", fallbackLineIds);
7251
+ }
7252
+ const { data, error } = await query;
7253
+ if (error) throw error;
7254
+ workspaceRows = Array.isArray(data) ? data : [];
7255
+ } catch (error) {
7256
+ console.error("[workspaceHealthService] Error resolving light workspace rows:", error);
7257
+ return configs;
7258
+ }
7259
+ const dbIdToVisible = /* @__PURE__ */ new Map();
7260
+ for (const row of workspaceRows) {
7261
+ const healthWorkspaceId = row?.workspace_id ? String(row.workspace_id) : "";
7262
+ const lineId = row?.line_id ? String(row.line_id) : "";
7263
+ const dbId = row?.id ? String(row.id) : "";
7264
+ if (!healthWorkspaceId || !dbId) continue;
7265
+ if (fallbackLineIds.length && !fallbackVisiblePairs.has(`${lineId}::${healthWorkspaceId}`)) continue;
7266
+ if (row?.enable_bulb_light === false) continue;
7267
+ dbIdToVisible.set(dbId, { healthWorkspaceId, lineId });
7268
+ }
7269
+ const addMapping = (mapping, visible, workspaceDbId) => {
7270
+ const bulbIp = mapping?.bulb_ip ? String(mapping.bulb_ip).trim() : "";
7271
+ if (!bulbIp) return;
7272
+ configs.set(visible.healthWorkspaceId, {
7273
+ healthWorkspaceId: visible.healthWorkspaceId,
7274
+ lineId: visible.lineId,
7275
+ workspaceDbId,
7276
+ bulbIp
7277
+ });
7278
+ };
7279
+ const dbIds = Array.from(dbIdToVisible.keys());
7280
+ if (dbIds.length) {
7281
+ const { data, error } = await supabase.from("workspace_ip_mapping").select("workspace_id, bulb_ip").in("workspace_id", dbIds);
7282
+ if (!error && Array.isArray(data)) {
7283
+ for (const mapping of data) {
7284
+ const dbId = mapping?.workspace_id ? String(mapping.workspace_id) : "";
7285
+ const visible = dbIdToVisible.get(dbId);
7286
+ if (visible) addMapping(mapping, visible, dbId);
7287
+ }
7288
+ } else if (error) {
7289
+ console.error("[workspaceHealthService] Error resolving light IP mappings:", error);
7290
+ }
7291
+ }
7292
+ const unresolvedVisibleIds = fallbackWorkspaceIds.filter((workspaceId) => !configs.has(workspaceId));
7293
+ if (unresolvedVisibleIds.length) {
7294
+ const visibleById = new Map(unresolvedVisibleWorkspaces.map((workspace) => [
7295
+ String(workspace.workspace_id),
7296
+ { healthWorkspaceId: String(workspace.workspace_id), lineId: workspace.line_id }
7297
+ ]));
7298
+ const { data, error } = await supabase.from("workspace_ip_mapping").select("workspace_id, bulb_ip").in("workspace_id", unresolvedVisibleIds);
7299
+ if (!error && Array.isArray(data)) {
7300
+ for (const mapping of data) {
7301
+ const workspaceId = mapping?.workspace_id ? String(mapping.workspace_id) : "";
7302
+ const visible = visibleById.get(workspaceId);
7303
+ if (visible) addMapping(mapping, visible, null);
7304
+ }
7305
+ }
7306
+ }
7307
+ return configs;
7308
+ }
7309
+ async fetchLightIntervals(bulbIps, windowStart, windowEnd) {
7310
+ if (!bulbIps.length || windowEnd <= windowStart) return [];
7311
+ const supabase = _getSupabaseInstance();
7312
+ if (!supabase) throw new Error("Supabase client not initialized");
7313
+ const { data, error } = await supabase.from("factory_light_status_intervals").select("workspace_id, bulb_ip, status, started_at, ended_at, last_seen_at, last_error").in("bulb_ip", bulbIps).lt("started_at", windowEnd.toISOString()).or(`ended_at.is.null,ended_at.gt.${windowStart.toISOString()}`);
7314
+ if (error) {
7315
+ console.error("[workspaceHealthService] Error fetching light status intervals:", error);
7316
+ throw error;
6879
7317
  }
6880
- return "down";
7318
+ return Array.isArray(data) ? data : [];
6881
7319
  }
6882
- hasCompletedTimelineData(completedMinutes, shiftStartDate, outputHourly, outputArray, timezone) {
6883
- if (completedMinutes <= 0) {
6884
- return false;
7320
+ getLightIntervalEffectiveRange(interval, windowStart, windowEnd) {
7321
+ const startedAt = new Date(interval.started_at);
7322
+ const endedAt = interval.ended_at ? new Date(interval.ended_at) : windowEnd;
7323
+ const effectiveStart = new Date(Math.max(startedAt.getTime(), windowStart.getTime()));
7324
+ const effectiveEnd = new Date(Math.min(endedAt.getTime(), windowEnd.getTime()));
7325
+ if (effectiveEnd <= effectiveStart) return null;
7326
+ return {
7327
+ start: effectiveStart,
7328
+ end: effectiveEnd,
7329
+ seconds: this.secondsBetween(effectiveStart, effectiveEnd)
7330
+ };
7331
+ }
7332
+ summarizeLightIntervals(intervals, windowStart, windowEnd, now4) {
7333
+ let upSeconds = 0;
7334
+ let downSeconds = 0;
7335
+ let unknownSeconds = 0;
7336
+ let currentInterval = null;
7337
+ for (const interval of intervals) {
7338
+ if (!this.isLightStatus(interval.status)) continue;
7339
+ const range = this.getLightIntervalEffectiveRange(interval, windowStart, windowEnd);
7340
+ if (!range) continue;
7341
+ if (interval.status === "up") upSeconds += range.seconds;
7342
+ if (interval.status === "down") downSeconds += range.seconds;
7343
+ if (interval.status === "unknown") unknownSeconds += range.seconds;
7344
+ if (interval.ended_at == null) {
7345
+ currentInterval = interval;
7346
+ }
7347
+ }
7348
+ const totalObservedSeconds = upSeconds + downSeconds + unknownSeconds;
7349
+ return {
7350
+ hasLightConfig: true,
7351
+ bulbIp: null,
7352
+ currentStatus: this.isLightStatus(currentInterval?.status) ? currentInterval.status : null,
7353
+ currentStartedAt: currentInterval?.started_at || null,
7354
+ lastSeenAt: currentInterval?.last_seen_at || null,
7355
+ currentDurationSeconds: currentInterval?.started_at ? this.secondsBetween(new Date(currentInterval.started_at), now4) : null,
7356
+ lastError: currentInterval?.last_error || null,
7357
+ uptimePercent: totalObservedSeconds > 0 ? this.roundPercent(upSeconds / totalObservedSeconds * 100) : null,
7358
+ upSeconds,
7359
+ downSeconds,
7360
+ unknownSeconds,
7361
+ totalObservedSeconds
7362
+ };
7363
+ }
7364
+ async summarizeResolvedLightConfigs(configs, shiftWindow, now4) {
7365
+ if (!configs.size) return /* @__PURE__ */ new Map();
7366
+ const bulbIps = Array.from(new Set(Array.from(configs.values()).map((config) => config.bulbIp)));
7367
+ const intervals = await this.fetchLightIntervals(bulbIps, shiftWindow.shiftStartDate, shiftWindow.windowEndDate);
7368
+ const intervalsByBulb = /* @__PURE__ */ new Map();
7369
+ for (const interval of intervals) {
7370
+ const bulbIp = interval.bulb_ip ? String(interval.bulb_ip) : "";
7371
+ if (!bulbIp) continue;
7372
+ if (!intervalsByBulb.has(bulbIp)) intervalsByBulb.set(bulbIp, []);
7373
+ intervalsByBulb.get(bulbIp).push(interval);
7374
+ }
7375
+ const summaries = /* @__PURE__ */ new Map();
7376
+ for (const config of configs.values()) {
7377
+ const summary = this.summarizeLightIntervals(
7378
+ intervalsByBulb.get(config.bulbIp) || [],
7379
+ shiftWindow.shiftStartDate,
7380
+ shiftWindow.windowEndDate,
7381
+ now4
7382
+ );
7383
+ summaries.set(config.healthWorkspaceId, {
7384
+ ...summary,
7385
+ hasLightConfig: true,
7386
+ bulbIp: config.bulbIp
7387
+ });
6885
7388
  }
6886
- for (let minuteIndex = 0; minuteIndex < completedMinutes; minuteIndex++) {
6887
- const minuteDate = dateFns.addMinutes(shiftStartDate, minuteIndex);
6888
- const hourKey = dateFnsTz.formatInTimeZone(minuteDate, timezone, "H");
6889
- const minuteKey = Number.parseInt(dateFnsTz.formatInTimeZone(minuteDate, timezone, "m"), 10);
6890
- const hourBucket = outputHourly[hourKey];
6891
- if (Array.isArray(hourBucket) && hourBucket[minuteKey] !== void 0) {
6892
- return true;
7389
+ return summaries;
7390
+ }
7391
+ async getWorkspaceLightSummaries(workspaces, passedShiftConfig, passedTimezone, overrideDate, overrideShiftId, now4 = /* @__PURE__ */ new Date(), lineShiftConfigs) {
7392
+ const dashboardConfig = _getDashboardConfigInstance();
7393
+ const shiftConfig = passedShiftConfig || dashboardConfig?.shiftConfig;
7394
+ const timezone = passedTimezone || shiftConfig?.timezone || dashboardConfig?.dateTimeConfig?.defaultTimezone || "UTC";
7395
+ if (lineShiftConfigs && lineShiftConfigs.size > 0) {
7396
+ const resolvedConfigs = await this.resolveWorkspaceLightConfigs(workspaces);
7397
+ if (!resolvedConfigs.size) return /* @__PURE__ */ new Map();
7398
+ const groupedByLine = /* @__PURE__ */ new Map();
7399
+ const fallbackConfigs = /* @__PURE__ */ new Map();
7400
+ for (const workspace of workspaces) {
7401
+ const workspaceId = workspace.workspace_id ? String(workspace.workspace_id) : "";
7402
+ const config = workspaceId ? resolvedConfigs.get(workspaceId) : null;
7403
+ if (!config) continue;
7404
+ const lineId = workspace.line_id ? String(workspace.line_id) : "";
7405
+ const lineShiftConfig = lineId ? lineShiftConfigs.get(lineId) : null;
7406
+ if (!lineId || !lineShiftConfig) {
7407
+ fallbackConfigs.set(config.healthWorkspaceId, config);
7408
+ continue;
7409
+ }
7410
+ if (!groupedByLine.has(lineId)) groupedByLine.set(lineId, /* @__PURE__ */ new Map());
7411
+ groupedByLine.get(lineId).set(config.healthWorkspaceId, config);
6893
7412
  }
6894
- if (minuteIndex < outputArray.length) {
6895
- return true;
7413
+ const summaries = /* @__PURE__ */ new Map();
7414
+ const summaryPromises = Array.from(groupedByLine.entries()).map(async ([lineId, lineConfigs]) => {
7415
+ const lineShiftWindow = this.resolveLightShiftWindow(
7416
+ lineShiftConfigs.get(lineId),
7417
+ timezone,
7418
+ overrideDate,
7419
+ overrideShiftId,
7420
+ now4
7421
+ );
7422
+ return this.summarizeResolvedLightConfigs(lineConfigs, lineShiftWindow, now4);
7423
+ });
7424
+ if (fallbackConfigs.size > 0) {
7425
+ const fallbackShiftWindow = this.resolveLightShiftWindow(
7426
+ shiftConfig,
7427
+ timezone,
7428
+ overrideDate,
7429
+ overrideShiftId,
7430
+ now4
7431
+ );
7432
+ summaryPromises.push(this.summarizeResolvedLightConfigs(fallbackConfigs, fallbackShiftWindow, now4));
6896
7433
  }
7434
+ const resolvedSummaries = await Promise.all(summaryPromises);
7435
+ for (const groupSummaries of resolvedSummaries) {
7436
+ groupSummaries.forEach((summary, workspaceId) => summaries.set(workspaceId, summary));
7437
+ }
7438
+ return summaries;
6897
7439
  }
6898
- return false;
7440
+ const shiftWindow = this.resolveLightShiftWindow(shiftConfig, timezone, overrideDate, overrideShiftId, now4);
7441
+ const configs = await this.resolveWorkspaceLightConfigs(workspaces);
7442
+ return this.summarizeResolvedLightConfigs(configs, shiftWindow, now4);
7443
+ }
7444
+ async getWorkspaceLightTimeline(workspaceId, lineId, passedShiftConfig, passedTimezone, overrideDate, overrideShiftId, now4 = /* @__PURE__ */ new Date()) {
7445
+ if (!workspaceId) {
7446
+ throw new Error("workspaceId is required to fetch light timeline");
7447
+ }
7448
+ const dashboardConfig = _getDashboardConfigInstance();
7449
+ const shiftConfig = passedShiftConfig || dashboardConfig?.shiftConfig;
7450
+ const timezone = passedTimezone || shiftConfig?.timezone || dashboardConfig?.dateTimeConfig?.defaultTimezone || "UTC";
7451
+ const shiftWindow = this.resolveLightShiftWindow(shiftConfig, timezone, overrideDate, overrideShiftId, now4);
7452
+ const configs = await this.resolveWorkspaceLightConfigs([{ workspace_id: workspaceId, line_id: lineId }]);
7453
+ const config = configs.get(workspaceId);
7454
+ if (!config) {
7455
+ return this.emptyLightTimeline(shiftWindow, timezone);
7456
+ }
7457
+ const intervals = await this.fetchLightIntervals([config.bulbIp], shiftWindow.shiftStartDate, shiftWindow.windowEndDate);
7458
+ const summary = this.summarizeLightIntervals(intervals, shiftWindow.shiftStartDate, shiftWindow.windowEndDate, now4);
7459
+ const hasData = summary.totalObservedSeconds > 0;
7460
+ const points = [];
7461
+ if (hasData) {
7462
+ for (let minuteIndex = 0; minuteIndex < shiftWindow.totalMinutes; minuteIndex++) {
7463
+ const minuteStart = dateFns.addMinutes(shiftWindow.shiftStartDate, minuteIndex);
7464
+ const minuteEnd = dateFns.addMinutes(minuteStart, 1);
7465
+ const timestamp = dateFnsTz.formatInTimeZone(minuteStart, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX");
7466
+ let status = "pending";
7467
+ if (minuteIndex < shiftWindow.completedMinutes) {
7468
+ const matchingInterval = intervals.find((interval) => {
7469
+ if (!this.isLightStatus(interval.status)) return false;
7470
+ const start = new Date(interval.started_at);
7471
+ const end = interval.ended_at ? new Date(interval.ended_at) : shiftWindow.windowEndDate;
7472
+ return start < minuteEnd && end > minuteStart;
7473
+ });
7474
+ status = this.isLightStatus(matchingInterval?.status) ? matchingInterval.status : "pending";
7475
+ }
7476
+ points.push({ minuteIndex, timestamp, status });
7477
+ }
7478
+ }
7479
+ const statusSegments = intervals.filter((interval) => interval.status === "down" || interval.status === "unknown").reduce((segments, interval) => {
7480
+ const range = this.getLightIntervalEffectiveRange(interval, shiftWindow.shiftStartDate, shiftWindow.windowEndDate);
7481
+ if (!range || !this.isLightStatus(interval.status)) return segments;
7482
+ const startMinuteIndex = Math.max(0, Math.floor((range.start.getTime() - shiftWindow.shiftStartDate.getTime()) / 6e4));
7483
+ const endMinuteIndex = Math.min(
7484
+ shiftWindow.totalMinutes,
7485
+ Math.ceil((range.end.getTime() - shiftWindow.shiftStartDate.getTime()) / 6e4)
7486
+ );
7487
+ segments.push({
7488
+ status: interval.status,
7489
+ startMinuteIndex,
7490
+ endMinuteIndex,
7491
+ startTime: dateFnsTz.formatInTimeZone(range.start, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX"),
7492
+ endTime: dateFnsTz.formatInTimeZone(range.end, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX"),
7493
+ durationSeconds: range.seconds,
7494
+ durationMinutes: Math.ceil(range.seconds / 60),
7495
+ isCurrent: interval.ended_at == null,
7496
+ lastError: interval.last_error || null
7497
+ });
7498
+ return segments;
7499
+ }, []).sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime());
7500
+ return {
7501
+ shiftId: shiftWindow.shiftId,
7502
+ shiftLabel: shiftWindow.shiftLabel,
7503
+ shiftStart: dateFnsTz.formatInTimeZone(shiftWindow.shiftStartDate, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX"),
7504
+ shiftEnd: dateFnsTz.formatInTimeZone(shiftWindow.shiftEndDate, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX"),
7505
+ totalMinutes: shiftWindow.totalMinutes,
7506
+ completedMinutes: shiftWindow.completedMinutes,
7507
+ upSeconds: summary.upSeconds,
7508
+ downSeconds: summary.downSeconds,
7509
+ unknownSeconds: summary.unknownSeconds,
7510
+ totalObservedSeconds: summary.totalObservedSeconds,
7511
+ downtimeSeconds: summary.downSeconds,
7512
+ uptimePercentage: summary.uptimePercent,
7513
+ hasData,
7514
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
7515
+ bulbIp: config.bulbIp,
7516
+ currentStatus: summary.currentStatus,
7517
+ currentStartedAt: summary.currentStartedAt,
7518
+ currentDurationSeconds: summary.currentDurationSeconds,
7519
+ lastSeenAt: summary.lastSeenAt,
7520
+ lastError: summary.lastError,
7521
+ points,
7522
+ statusSegments
7523
+ };
6899
7524
  }
6900
7525
  async getWorkspaceHealthStatus(options = {}) {
6901
7526
  const supabase = _getSupabaseInstance();
@@ -6930,7 +7555,8 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
6930
7555
  timezone,
6931
7556
  options.lineShiftConfigs,
6932
7557
  options.date,
6933
- options.shiftId
7558
+ options.shiftId,
7559
+ options.lineId
6934
7560
  );
6935
7561
  } catch (error2) {
6936
7562
  console.error("Error calculating uptime:", error2);
@@ -6995,6 +7621,27 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
6995
7621
  } catch (e) {
6996
7622
  console.error("Error filtering workspaces:", e);
6997
7623
  }
7624
+ if (filteredData.length > 0) {
7625
+ try {
7626
+ const lightSummaries = await this.getWorkspaceLightSummaries(
7627
+ filteredData,
7628
+ shiftConfig,
7629
+ timezone,
7630
+ options.date,
7631
+ options.shiftId,
7632
+ /* @__PURE__ */ new Date(),
7633
+ options.lineShiftConfigs
7634
+ );
7635
+ if (lightSummaries.size > 0) {
7636
+ filteredData = filteredData.map((workspace) => {
7637
+ const lightSummary = lightSummaries.get(workspace.workspace_id);
7638
+ return lightSummary ? { ...workspace, lightSummary } : workspace;
7639
+ });
7640
+ }
7641
+ } catch (error2) {
7642
+ console.error("Error calculating light uptime summaries:", error2);
7643
+ }
7644
+ }
6998
7645
  if (options.status) {
6999
7646
  filteredData = filteredData.filter((item) => item.status === options.status);
7000
7647
  }
@@ -7023,7 +7670,7 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
7023
7670
  }
7024
7671
  return filteredData;
7025
7672
  }
7026
- async getWorkspaceUptimeTimeline(workspaceId, companyId, passedShiftConfig, passedTimezone, overrideDate, overrideShiftId) {
7673
+ async getWorkspaceUptimeTimeline(workspaceId, companyId, passedShiftConfig, passedTimezone, overrideDate, overrideShiftId, lineId) {
7027
7674
  if (!workspaceId) {
7028
7675
  throw new Error("workspaceId is required to fetch uptime timeline");
7029
7676
  }
@@ -7042,6 +7689,19 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
7042
7689
  const currentTiming = this.getShiftTiming(timezone, shiftConfig);
7043
7690
  const queryDate = overrideDate ?? currentTiming.date;
7044
7691
  const queryShiftId = overrideShiftId ?? currentTiming.shiftId;
7692
+ if (lineId) {
7693
+ const backendTimeline = await this.fetchBackendWorkspaceUptimeTimeline(
7694
+ workspaceId,
7695
+ companyId,
7696
+ lineId,
7697
+ queryDate,
7698
+ queryShiftId,
7699
+ timezone
7700
+ );
7701
+ if (backendTimeline) {
7702
+ return backendTimeline;
7703
+ }
7704
+ }
7045
7705
  let shiftStartDate;
7046
7706
  let shiftEndDate;
7047
7707
  let totalMinutes;
@@ -7090,18 +7750,17 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
7090
7750
  }
7091
7751
  const mergedRecords = this.mergeRecordsByWorkspace(Array.isArray(data) ? data : []);
7092
7752
  const record = mergedRecords.length > 0 ? mergedRecords[0] : null;
7093
- const outputHourly = this.normalizeOutputHourly(record?.output_hourly || {});
7753
+ const outputHourly = normalizeOutputHourly(record?.output_hourly || {});
7094
7754
  const outputArray = Array.isArray(record?.output_array) ? record.output_array : [];
7095
- const hasData = Boolean(
7096
- record && this.hasCompletedTimelineData(
7097
- completedMinutes,
7098
- shiftStartDate,
7099
- outputHourly,
7100
- outputArray,
7101
- timezone
7102
- )
7103
- );
7104
- if (!hasData) {
7755
+ const uptime = record ? computeWorkspaceUptime({
7756
+ totalMinutes,
7757
+ completedMinutes,
7758
+ shiftStartDate,
7759
+ outputHourly,
7760
+ outputArray,
7761
+ timezone
7762
+ }) : null;
7763
+ if (!uptime?.hasData) {
7105
7764
  return {
7106
7765
  shiftId: queryShiftId,
7107
7766
  shiftLabel,
@@ -7119,80 +7778,6 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
7119
7778
  downtimeSegments: []
7120
7779
  };
7121
7780
  }
7122
- const points = [];
7123
- let uptimeMinutes = 0;
7124
- let downtimeMinutes = 0;
7125
- const MIN_DOWNTIME_MINUTES = 2;
7126
- for (let minuteIndex = 0; minuteIndex < totalMinutes; minuteIndex++) {
7127
- const minuteDate = dateFns.addMinutes(shiftStartDate, minuteIndex);
7128
- const timestamp = dateFnsTz.formatInTimeZone(minuteDate, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX");
7129
- let status;
7130
- if (minuteIndex < completedMinutes) {
7131
- status = this.deriveStatusForMinute(
7132
- minuteIndex,
7133
- minuteDate,
7134
- outputHourly,
7135
- outputArray,
7136
- timezone
7137
- );
7138
- if (status === "up") {
7139
- uptimeMinutes += 1;
7140
- } else {
7141
- downtimeMinutes += 1;
7142
- }
7143
- } else {
7144
- status = "pending";
7145
- }
7146
- points.push({
7147
- minuteIndex,
7148
- timestamp,
7149
- status
7150
- });
7151
- }
7152
- const downtimeSegments = [];
7153
- let currentSegmentStart = null;
7154
- const pushSegment = (startIndex, endIndex) => {
7155
- if (endIndex <= startIndex) return;
7156
- const segmentStartDate = dateFns.addMinutes(shiftStartDate, startIndex);
7157
- const segmentEndDate = dateFns.addMinutes(shiftStartDate, endIndex);
7158
- downtimeSegments.push({
7159
- startMinuteIndex: startIndex,
7160
- endMinuteIndex: endIndex,
7161
- startTime: dateFnsTz.formatInTimeZone(segmentStartDate, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX"),
7162
- endTime: dateFnsTz.formatInTimeZone(segmentEndDate, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX"),
7163
- durationMinutes: endIndex - startIndex
7164
- });
7165
- };
7166
- for (let i = 0; i < completedMinutes; i++) {
7167
- const point = points[i];
7168
- if (point.status === "down") {
7169
- if (currentSegmentStart === null) {
7170
- currentSegmentStart = i;
7171
- }
7172
- } else if (currentSegmentStart !== null) {
7173
- pushSegment(currentSegmentStart, i);
7174
- currentSegmentStart = null;
7175
- }
7176
- }
7177
- if (currentSegmentStart !== null) {
7178
- pushSegment(currentSegmentStart, completedMinutes);
7179
- }
7180
- const filteredSegments = [];
7181
- downtimeSegments.forEach((segment) => {
7182
- if (segment.durationMinutes >= MIN_DOWNTIME_MINUTES) {
7183
- filteredSegments.push(segment);
7184
- } else {
7185
- for (let i = segment.startMinuteIndex; i < segment.endMinuteIndex; i++) {
7186
- if (points[i] && points[i].status === "down") {
7187
- points[i].status = "up";
7188
- downtimeMinutes = Math.max(0, downtimeMinutes - 1);
7189
- uptimeMinutes += 1;
7190
- }
7191
- }
7192
- }
7193
- });
7194
- const completedWindow = Math.max(1, uptimeMinutes + downtimeMinutes);
7195
- const uptimePercentage = completedMinutes > 0 ? Number((uptimeMinutes / completedWindow * 100).toFixed(1)) : 100;
7196
7781
  return {
7197
7782
  shiftId: queryShiftId,
7198
7783
  shiftLabel,
@@ -7200,14 +7785,14 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
7200
7785
  shiftEnd: dateFnsTz.formatInTimeZone(shiftEndDate, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX"),
7201
7786
  totalMinutes,
7202
7787
  completedMinutes,
7203
- uptimeMinutes,
7204
- downtimeMinutes,
7788
+ uptimeMinutes: uptime.uptimeMinutes,
7789
+ downtimeMinutes: uptime.downtimeMinutes,
7205
7790
  pendingMinutes,
7206
- uptimePercentage,
7791
+ uptimePercentage: uptime.uptimePercentage,
7207
7792
  hasData: true,
7208
7793
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
7209
- points,
7210
- downtimeSegments: filteredSegments
7794
+ points: uptime.points,
7795
+ downtimeSegments: uptime.downtimeSegments
7211
7796
  };
7212
7797
  }
7213
7798
  async getWorkspaceHealthById(workspaceId) {
@@ -7319,7 +7904,7 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
7319
7904
  getUptimeMapKey(lineId, workspaceId) {
7320
7905
  return lineId ? `${lineId}::${workspaceId}` : workspaceId;
7321
7906
  }
7322
- async calculateWorkspaceUptime(companyId, passedShiftConfig, timezone, lineShiftConfigs, overrideDate, overrideShiftId) {
7907
+ async calculateWorkspaceUptime(companyId, passedShiftConfig, timezone, lineShiftConfigs, overrideDate, overrideShiftId, lineId) {
7323
7908
  const supabase = _getSupabaseInstance();
7324
7909
  if (!supabase) throw new Error("Supabase client not initialized");
7325
7910
  const dashboardConfig = _getDashboardConfigInstance();
@@ -7341,6 +7926,16 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
7341
7926
  const currentTiming = this.getShiftTiming(effectiveTimezone, shiftConfig);
7342
7927
  const queryDate = overrideDate ?? currentTiming.date;
7343
7928
  const queryShiftId = overrideShiftId ?? currentTiming.shiftId;
7929
+ const backendUptime = await this.fetchBackendWorkspaceUptimeSummaries(
7930
+ companyId,
7931
+ lineId ? [lineId] : void 0,
7932
+ queryDate,
7933
+ queryShiftId,
7934
+ effectiveTimezone
7935
+ );
7936
+ if (backendUptime) {
7937
+ return backendUptime;
7938
+ }
7344
7939
  let shiftStartDate = currentTiming.shiftStartDate;
7345
7940
  let completedMinutes = currentTiming.completedMinutes;
7346
7941
  const isHistoricalDate = overrideDate && overrideDate !== currentTiming.date;
@@ -7364,50 +7959,21 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
7364
7959
  const uptimeMap = /* @__PURE__ */ new Map();
7365
7960
  const mergedRecords = this.mergeRecordsByWorkspace(queryData || []);
7366
7961
  for (const record of mergedRecords) {
7367
- const outputHourly = this.normalizeOutputHourly(record.output_hourly || {});
7962
+ const outputHourly = normalizeOutputHourly(record.output_hourly || {});
7368
7963
  const outputArray = Array.isArray(record.output_array) ? record.output_array : [];
7369
- let uptimeMinutes = 0;
7370
- let downtimeMinutes = 0;
7371
- const MIN_DOWNTIME_MINUTES = 2;
7372
- let currentDownRun = 0;
7373
- for (let minuteIndex = 0; minuteIndex < completedMinutes; minuteIndex++) {
7374
- const minuteDate = dateFns.addMinutes(shiftStartDate, minuteIndex);
7375
- const status = this.deriveStatusForMinute(
7376
- minuteIndex,
7377
- minuteDate,
7378
- outputHourly,
7379
- outputArray,
7380
- effectiveTimezone
7381
- );
7382
- if (status === "down") {
7383
- currentDownRun += 1;
7384
- } else {
7385
- if (currentDownRun > 0) {
7386
- if (currentDownRun >= MIN_DOWNTIME_MINUTES) {
7387
- downtimeMinutes += currentDownRun;
7388
- } else {
7389
- uptimeMinutes += currentDownRun;
7390
- }
7391
- currentDownRun = 0;
7392
- }
7393
- uptimeMinutes += 1;
7394
- }
7395
- }
7396
- if (currentDownRun > 0) {
7397
- if (currentDownRun >= MIN_DOWNTIME_MINUTES) {
7398
- downtimeMinutes += currentDownRun;
7399
- } else {
7400
- uptimeMinutes += currentDownRun;
7401
- }
7402
- currentDownRun = 0;
7403
- }
7404
- const completedWindow = uptimeMinutes + downtimeMinutes;
7405
- const percentage = completedWindow > 0 ? Number((uptimeMinutes / completedWindow * 100).toFixed(1)) : 100;
7964
+ const uptime = computeWorkspaceUptime({
7965
+ totalMinutes: completedMinutes,
7966
+ completedMinutes,
7967
+ shiftStartDate,
7968
+ outputHourly,
7969
+ outputArray,
7970
+ timezone: effectiveTimezone
7971
+ });
7406
7972
  const mapKey = this.getUptimeMapKey(record.line_id, record.workspace_id);
7407
7973
  uptimeMap.set(mapKey, {
7408
7974
  expectedMinutes: completedMinutes,
7409
- actualMinutes: uptimeMinutes,
7410
- percentage,
7975
+ actualMinutes: uptime.uptimeMinutes,
7976
+ percentage: completedMinutes > 0 ? uptime.uptimePercentage : 100,
7411
7977
  lastCalculated: (/* @__PURE__ */ new Date()).toISOString()
7412
7978
  });
7413
7979
  }
@@ -7460,6 +8026,26 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
7460
8026
  }
7461
8027
  uniqueQueries.get(key).lineConfigs.push({ lineId, shiftStartDate, completedMinutes });
7462
8028
  });
8029
+ const backendUptimeMap = /* @__PURE__ */ new Map();
8030
+ let backendAvailable = true;
8031
+ for (const { date, shiftId, lineConfigs } of uniqueQueries.values()) {
8032
+ const lineIds = Array.from(new Set(lineConfigs.map((lc) => lc.lineId).filter(Boolean)));
8033
+ const result = await this.fetchBackendWorkspaceUptimeSummaries(
8034
+ companyId,
8035
+ lineIds,
8036
+ date,
8037
+ shiftId,
8038
+ timezone
8039
+ );
8040
+ if (result === null) {
8041
+ backendAvailable = false;
8042
+ break;
8043
+ }
8044
+ result.forEach((value, key) => backendUptimeMap.set(key, value));
8045
+ }
8046
+ if (backendAvailable) {
8047
+ return backendUptimeMap;
8048
+ }
7463
8049
  console.log(`[calculateWorkspaceUptimeMultiLine] Querying ${uniqueQueries.size} unique date/shift combinations for ${lineShiftConfigs.size} lines`);
7464
8050
  uniqueQueries.forEach((queryConfig, key) => {
7465
8051
  console.log(`[calculateWorkspaceUptimeMultiLine] Query batch ${key}:`, {
@@ -7511,49 +8097,21 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
7511
8097
  continue;
7512
8098
  }
7513
8099
  const { shiftStartDate, completedMinutes } = lineConfig;
7514
- const outputHourly = this.normalizeOutputHourly(record.output_hourly || {});
8100
+ const outputHourly = normalizeOutputHourly(record.output_hourly || {});
7515
8101
  const outputArray = Array.isArray(record.output_array) ? record.output_array : [];
7516
- let uptimeMinutes = 0;
7517
- let downtimeMinutes = 0;
7518
- const MIN_DOWNTIME_MINUTES = 2;
7519
- let currentDownRun = 0;
7520
- for (let minuteIndex = 0; minuteIndex < completedMinutes; minuteIndex++) {
7521
- const minuteDate = dateFns.addMinutes(shiftStartDate, minuteIndex);
7522
- const status = this.deriveStatusForMinute(
7523
- minuteIndex,
7524
- minuteDate,
7525
- outputHourly,
7526
- outputArray,
7527
- timezone
7528
- );
7529
- if (status === "down") {
7530
- currentDownRun += 1;
7531
- } else {
7532
- if (currentDownRun > 0) {
7533
- if (currentDownRun >= MIN_DOWNTIME_MINUTES) {
7534
- downtimeMinutes += currentDownRun;
7535
- } else {
7536
- uptimeMinutes += currentDownRun;
7537
- }
7538
- currentDownRun = 0;
7539
- }
7540
- uptimeMinutes += 1;
7541
- }
7542
- }
7543
- if (currentDownRun > 0) {
7544
- if (currentDownRun >= MIN_DOWNTIME_MINUTES) {
7545
- downtimeMinutes += currentDownRun;
7546
- } else {
7547
- uptimeMinutes += currentDownRun;
7548
- }
7549
- }
7550
- const completedWindow = uptimeMinutes + downtimeMinutes;
7551
- const percentage = completedWindow > 0 ? Number((uptimeMinutes / completedWindow * 100).toFixed(1)) : 100;
8102
+ const uptime = computeWorkspaceUptime({
8103
+ totalMinutes: completedMinutes,
8104
+ completedMinutes,
8105
+ shiftStartDate,
8106
+ outputHourly,
8107
+ outputArray,
8108
+ timezone
8109
+ });
7552
8110
  const mapKey = this.getUptimeMapKey(record.line_id, record.workspace_id);
7553
8111
  uptimeMap.set(mapKey, {
7554
8112
  expectedMinutes: completedMinutes,
7555
- actualMinutes: uptimeMinutes,
7556
- percentage,
8113
+ actualMinutes: uptime.uptimeMinutes,
8114
+ percentage: completedMinutes > 0 ? uptime.uptimePercentage : 100,
7557
8115
  lastCalculated: (/* @__PURE__ */ new Date()).toISOString()
7558
8116
  });
7559
8117
  console.log(`[calculateWorkspaceUptimeMultiLine] Storing uptime:`, {
@@ -7562,8 +8120,8 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
7562
8120
  workspaceId: record.workspace_id,
7563
8121
  workspaceDisplayName: record.workspace_display_name,
7564
8122
  expectedMinutes: completedMinutes,
7565
- actualMinutes: uptimeMinutes,
7566
- percentage
8123
+ actualMinutes: uptime.uptimeMinutes,
8124
+ percentage: completedMinutes > 0 ? uptime.uptimePercentage : 100
7567
8125
  });
7568
8126
  }
7569
8127
  }
@@ -20804,7 +21362,8 @@ var useWorkspaceUptimeTimeline = (options) => {
20804
21362
  effectiveShiftConfig,
20805
21363
  effectiveTimezone,
20806
21364
  overrideDate,
20807
- overrideShiftId
21365
+ overrideShiftId,
21366
+ lineId
20808
21367
  );
20809
21368
  setTimeline(data);
20810
21369
  } catch (err) {
@@ -20814,7 +21373,89 @@ var useWorkspaceUptimeTimeline = (options) => {
20814
21373
  setLoading(false);
20815
21374
  isFetchingRef.current = false;
20816
21375
  }
20817
- }, [enabled, workspaceId, companyId, effectiveShiftConfig, effectiveTimezone, shiftConfigPending, overrideDate, overrideShiftId]);
21376
+ }, [enabled, workspaceId, companyId, effectiveShiftConfig, effectiveTimezone, shiftConfigPending, overrideDate, overrideShiftId, lineId]);
21377
+ React125.useEffect(() => {
21378
+ fetchTimeline();
21379
+ }, [fetchTimeline]);
21380
+ React125.useEffect(() => {
21381
+ if (!refreshInterval || refreshInterval <= 0 || !enabled) {
21382
+ return;
21383
+ }
21384
+ intervalRef.current = setInterval(() => {
21385
+ fetchTimeline();
21386
+ }, refreshInterval);
21387
+ return () => {
21388
+ if (intervalRef.current) {
21389
+ clearInterval(intervalRef.current);
21390
+ }
21391
+ };
21392
+ }, [refreshInterval, enabled, fetchTimeline]);
21393
+ return {
21394
+ timeline,
21395
+ loading: loading || shiftConfigPending,
21396
+ error,
21397
+ refetch: fetchTimeline
21398
+ };
21399
+ };
21400
+ var useWorkspaceLightTimeline = (options) => {
21401
+ const {
21402
+ workspaceId,
21403
+ lineId,
21404
+ enabled = true,
21405
+ refreshInterval,
21406
+ shiftConfig: passedShiftConfig,
21407
+ timezone: passedTimezone,
21408
+ date: overrideDate,
21409
+ shiftId: overrideShiftId
21410
+ } = options;
21411
+ const { shiftConfig: dynamicShiftConfig, isLoading: isShiftConfigLoading } = useDynamicShiftConfig(lineId);
21412
+ const appTimezone = useAppTimezone();
21413
+ const shiftConfigPending = !passedShiftConfig && (!lineId || isShiftConfigLoading || !dynamicShiftConfig);
21414
+ const effectiveShiftConfig = passedShiftConfig ?? (shiftConfigPending ? void 0 : dynamicShiftConfig);
21415
+ const effectiveTimezone = passedTimezone || appTimezone || effectiveShiftConfig?.timezone || "UTC";
21416
+ const [timeline, setTimeline] = React125.useState(null);
21417
+ const [loading, setLoading] = React125.useState(false);
21418
+ const [error, setError] = React125.useState(null);
21419
+ const isFetchingRef = React125.useRef(false);
21420
+ const intervalRef = React125.useRef(null);
21421
+ React125.useEffect(() => {
21422
+ setTimeline(null);
21423
+ setError(null);
21424
+ setLoading(enabled && Boolean(workspaceId));
21425
+ }, [workspaceId, lineId, enabled]);
21426
+ const fetchTimeline = React125.useCallback(async () => {
21427
+ if (!enabled) return;
21428
+ if (shiftConfigPending) {
21429
+ setLoading(true);
21430
+ return;
21431
+ }
21432
+ if (!effectiveShiftConfig || !workspaceId) {
21433
+ setLoading(false);
21434
+ setTimeline(null);
21435
+ return;
21436
+ }
21437
+ if (isFetchingRef.current) return;
21438
+ try {
21439
+ isFetchingRef.current = true;
21440
+ setLoading(true);
21441
+ setError(null);
21442
+ const data = await workspaceHealthService.getWorkspaceLightTimeline(
21443
+ workspaceId,
21444
+ lineId,
21445
+ effectiveShiftConfig,
21446
+ effectiveTimezone,
21447
+ overrideDate,
21448
+ overrideShiftId
21449
+ );
21450
+ setTimeline(data);
21451
+ } catch (err) {
21452
+ console.error("[useWorkspaceLightTimeline] Failed to fetch light timeline:", err);
21453
+ setError({ message: err?.message || "Failed to load light timeline", code: err?.code || "FETCH_ERROR" });
21454
+ } finally {
21455
+ setLoading(false);
21456
+ isFetchingRef.current = false;
21457
+ }
21458
+ }, [enabled, workspaceId, lineId, effectiveShiftConfig, effectiveTimezone, shiftConfigPending, overrideDate, overrideShiftId]);
20818
21459
  React125.useEffect(() => {
20819
21460
  fetchTimeline();
20820
21461
  }, [fetchTimeline]);
@@ -58241,7 +58882,14 @@ var WorkspaceHealthCard = ({
58241
58882
  event.stopPropagation();
58242
58883
  event.preventDefault();
58243
58884
  if (onViewDetails) {
58244
- onViewDetails(workspace);
58885
+ onViewDetails(workspace, "camera", "card_button");
58886
+ }
58887
+ };
58888
+ const handleLightChipClick = (event) => {
58889
+ event.stopPropagation();
58890
+ event.preventDefault();
58891
+ if (onViewDetails) {
58892
+ onViewDetails(workspace, "light", "light_chip");
58245
58893
  }
58246
58894
  };
58247
58895
  const handleKeyDown = (event) => {
@@ -58319,6 +58967,22 @@ var WorkspaceHealthCard = ({
58319
58967
  };
58320
58968
  };
58321
58969
  const downtimeConfig = getDowntimeConfig(workspace.uptimeDetails);
58970
+ const hasLightConfig = Boolean(workspace.lightSummary?.hasLightConfig && workspace.lightSummary.bulbIp);
58971
+ const lightStatus = workspace.lightSummary?.currentStatus || null;
58972
+ const lightChipLabel = (() => {
58973
+ if (!workspace.lightSummary) return "Light --";
58974
+ if (lightStatus === "down") return "Light offline";
58975
+ if (lightStatus === "unknown") return "Light unknown";
58976
+ if (typeof workspace.lightSummary.uptimePercent === "number") {
58977
+ return `Light ${workspace.lightSummary.uptimePercent.toFixed(1)}%`;
58978
+ }
58979
+ if (lightStatus === "up") return "Light operational";
58980
+ return "Light --";
58981
+ })();
58982
+ const lightChipClassName = clsx(
58983
+ "inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-semibold transition-all",
58984
+ lightStatus === "down" ? "border-rose-200 bg-rose-50 text-rose-700 hover:bg-rose-100" : lightStatus === "unknown" ? "border-amber-200 bg-amber-50 text-amber-700 hover:bg-amber-100" : "border-emerald-200 bg-emerald-50 text-emerald-700 hover:bg-emerald-100"
58985
+ );
58322
58986
  return /* @__PURE__ */ jsxRuntime.jsx(
58323
58987
  Card2,
58324
58988
  {
@@ -58388,6 +59052,20 @@ var WorkspaceHealthCard = ({
58388
59052
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium uppercase tracking-wide text-slate-500", children: "Camera IP:" }),
58389
59053
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-700 dark:text-gray-300", children: "N/A" })
58390
59054
  ] }),
59055
+ hasLightConfig && /* @__PURE__ */ jsxRuntime.jsxs(
59056
+ "button",
59057
+ {
59058
+ type: "button",
59059
+ onClick: handleLightChipClick,
59060
+ className: lightChipClassName,
59061
+ "aria-label": `Open light timeline for ${workspace.workspace_display_name || workspace.workspace_id}`,
59062
+ title: workspace.lightSummary?.bulbIp ? `Bulb IP ${workspace.lightSummary.bulbIp}` : "Light timeline",
59063
+ children: [
59064
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Lightbulb, { className: "h-3.5 w-3.5" }),
59065
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: lightChipLabel })
59066
+ ]
59067
+ }
59068
+ ),
58391
59069
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-3 pt-3 border-t border-slate-100 dark:border-slate-800", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
58392
59070
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-slate-500 uppercase tracking-wide", children: downtimeConfig.label }),
58393
59071
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: clsx("text-sm font-semibold", downtimeConfig.className), children: downtimeConfig.text })
@@ -58457,7 +59135,7 @@ var CompactWorkspaceHealthCard = ({
58457
59135
  event.stopPropagation();
58458
59136
  event.preventDefault();
58459
59137
  if (onViewDetails) {
58460
- onViewDetails(workspace);
59138
+ onViewDetails(workspace, "camera", "card_button");
58461
59139
  }
58462
59140
  };
58463
59141
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -84186,11 +84864,13 @@ var useWorkspaceHealth = (options) => {
84186
84864
  var STATUS_COLORS = {
84187
84865
  up: "bg-emerald-500",
84188
84866
  down: "bg-rose-500",
84867
+ unknown: "bg-amber-400",
84189
84868
  pending: "bg-gray-200"
84190
84869
  };
84191
84870
  var STATUS_TITLES = {
84192
84871
  up: "Uptime",
84193
84872
  down: "Downtime",
84873
+ unknown: "Unknown",
84194
84874
  pending: "Pending"
84195
84875
  };
84196
84876
  var formatTime4 = (date, timezone) => new Intl.DateTimeFormat("en-IN", {
@@ -84233,7 +84913,9 @@ var UptimeTimelineStrip = ({
84233
84913
  timezone,
84234
84914
  className = "",
84235
84915
  uptimePercentage = null,
84236
- downtimeMinutes = 0
84916
+ downtimeMinutes = 0,
84917
+ metricLabel = "uptime",
84918
+ summaryText
84237
84919
  }) => {
84238
84920
  const segments = React125.useMemo(() => {
84239
84921
  if (!points.length || totalMinutes <= 0) return [];
@@ -84297,9 +84979,11 @@ var UptimeTimelineStrip = ({
84297
84979
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full rounded-xl border border-dashed border-gray-200 bg-gray-50/50 p-6 text-center text-sm text-gray-600", children: "No uptime data available for this shift yet." });
84298
84980
  }
84299
84981
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `relative w-full ${className}`, children: [
84300
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center mb-3 text-sm", children: typeof uptimePercentage === "number" ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-gray-900 font-semibold", children: [
84982
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center mb-3 text-sm", children: summaryText ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-900 font-semibold", children: summaryText }) : typeof uptimePercentage === "number" ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-gray-900 font-semibold", children: [
84301
84983
  uptimePercentage.toFixed(1),
84302
- " % uptime ",
84984
+ " % ",
84985
+ metricLabel,
84986
+ " ",
84303
84987
  downtimeMinutes > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-gray-600 font-normal", children: [
84304
84988
  "(",
84305
84989
  formatDowntimeLabel(downtimeMinutes),
@@ -84367,6 +85051,27 @@ var formatDowntimeLabel2 = (minutes, includeSuffix = true) => {
84367
85051
  const label = formatDuration4(minutes);
84368
85052
  return includeSuffix ? `${label} down` : label;
84369
85053
  };
85054
+ var formatSecondsDuration = (seconds) => {
85055
+ if (!seconds || seconds <= 0) return "0 min";
85056
+ return formatDuration4(Math.max(1, Math.ceil(seconds / 60)));
85057
+ };
85058
+ var getLightStatusLabel = (status) => {
85059
+ if (status === "up") return "operational";
85060
+ if (status === "down") return "offline";
85061
+ if (status === "unknown") return "unknown";
85062
+ return "status unavailable";
85063
+ };
85064
+ var getLightDurationPrefix = (status) => {
85065
+ if (status === "up") return "Operational";
85066
+ if (status === "down") return "Offline";
85067
+ if (status === "unknown") return "Unknown";
85068
+ return "Current";
85069
+ };
85070
+ var formatLightError = (error) => {
85071
+ if (!error) return null;
85072
+ const cleaned = error.replace(/\s*\(after retry\)\s*$/i, "").trim();
85073
+ return cleaned || null;
85074
+ };
84370
85075
  var formatTimeRange = (start, end, timezone) => {
84371
85076
  const formatter = new Intl.DateTimeFormat("en-IN", {
84372
85077
  hour: "numeric",
@@ -84382,12 +85087,15 @@ var WorkspaceUptimeDetailModal = ({
84382
85087
  onClose,
84383
85088
  shiftConfig: passedShiftConfig,
84384
85089
  date,
84385
- shiftId
85090
+ shiftId,
85091
+ initialMode = "camera"
84386
85092
  }) => {
84387
85093
  const timezone = useAppTimezone() || "UTC";
84388
85094
  const logsContainerRef = React125.useRef(null);
84389
85095
  const [showScrollIndicator, setShowScrollIndicator] = React125.useState(false);
85096
+ const [activeMode, setActiveMode] = React125.useState(initialMode);
84390
85097
  const isHistorical = Boolean(date);
85098
+ const hasLightTimeline = Boolean(workspace?.lightSummary?.hasLightConfig && workspace?.lightSummary?.bulbIp);
84391
85099
  const {
84392
85100
  timeline,
84393
85101
  loading,
@@ -84396,7 +85104,7 @@ var WorkspaceUptimeDetailModal = ({
84396
85104
  } = useWorkspaceUptimeTimeline({
84397
85105
  workspaceId: workspace?.workspace_id,
84398
85106
  companyId: workspace?.company_id,
84399
- enabled: isOpen && Boolean(workspace?.workspace_id && workspace?.company_id),
85107
+ enabled: isOpen && activeMode === "camera" && Boolean(workspace?.workspace_id && workspace?.company_id),
84400
85108
  refreshInterval: isHistorical ? void 0 : 6e4,
84401
85109
  // Disable auto-refresh for historical
84402
85110
  lineId: workspace?.line_id,
@@ -84409,6 +85117,25 @@ var WorkspaceUptimeDetailModal = ({
84409
85117
  shiftId
84410
85118
  // Pass override shift ID for historical queries
84411
85119
  });
85120
+ const {
85121
+ timeline: lightTimeline,
85122
+ loading: lightLoading,
85123
+ error: lightError,
85124
+ refetch: refetchLight
85125
+ } = useWorkspaceLightTimeline({
85126
+ workspaceId: workspace?.workspace_id,
85127
+ enabled: isOpen && activeMode === "light" && hasLightTimeline && Boolean(workspace?.workspace_id),
85128
+ refreshInterval: isHistorical ? void 0 : 6e4,
85129
+ lineId: workspace?.line_id,
85130
+ shiftConfig: passedShiftConfig || void 0,
85131
+ timezone,
85132
+ date,
85133
+ shiftId
85134
+ });
85135
+ React125.useEffect(() => {
85136
+ if (!isOpen) return;
85137
+ setActiveMode(initialMode === "light" && hasLightTimeline ? "light" : "camera");
85138
+ }, [hasLightTimeline, initialMode, isOpen]);
84412
85139
  React125.useEffect(() => {
84413
85140
  if (!isOpen || !workspace) return;
84414
85141
  const handleKeyDown = (event) => {
@@ -84421,13 +85148,21 @@ var WorkspaceUptimeDetailModal = ({
84421
85148
  window.removeEventListener("keydown", handleKeyDown);
84422
85149
  };
84423
85150
  }, [isOpen, onClose, workspace]);
84424
- const shiftStart = timeline ? new Date(timeline.shiftStart) : null;
84425
- const shiftEnd = timeline ? new Date(timeline.shiftEnd) : null;
85151
+ const activeTimeline = activeMode === "light" ? lightTimeline : timeline;
85152
+ const activeLoading = activeMode === "light" ? lightLoading : loading;
85153
+ const activeError = activeMode === "light" ? lightError : error;
85154
+ const activeRefetch = activeMode === "light" ? refetchLight : refetch;
85155
+ const shiftStart = activeTimeline ? new Date(activeTimeline.shiftStart) : null;
85156
+ const shiftEnd = activeTimeline ? new Date(activeTimeline.shiftEnd) : null;
84426
85157
  const downtimeSegments = timeline?.downtimeSegments || [];
84427
85158
  downtimeSegments.length;
84428
85159
  const downtimeMinutes = timeline?.downtimeMinutes ?? 0;
84429
- const hasTimelineData = Boolean(timeline?.hasData);
84430
- const uptimePercentage = hasTimelineData ? timeline?.uptimePercentage ?? workspace?.uptimePercentage ?? null : null;
85160
+ const hasTimelineData = activeMode === "light" ? Boolean(lightTimeline?.hasData) : Boolean(timeline?.hasData);
85161
+ const uptimePercentage = activeMode === "light" ? lightTimeline?.uptimePercentage ?? null : hasTimelineData ? timeline?.uptimePercentage ?? workspace?.uptimePercentage ?? null : null;
85162
+ const lightStatus = lightTimeline?.currentStatus || workspace?.lightSummary?.currentStatus || null;
85163
+ const lightStatusText = `Light ${getLightStatusLabel(lightStatus)}`;
85164
+ const lightDurationText = lightTimeline?.currentDurationSeconds ? `${getLightDurationPrefix(lightStatus)} for ${formatSecondsDuration(lightTimeline.currentDurationSeconds)}` : null;
85165
+ const lightLastError = formatLightError(lightTimeline?.lastError);
84431
85166
  const allInterruptionsSorted = React125.useMemo(
84432
85167
  () => [...downtimeSegments].sort(
84433
85168
  (a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime()
@@ -84449,7 +85184,7 @@ var WorkspaceUptimeDetailModal = ({
84449
85184
  container.addEventListener("scroll", checkScroll);
84450
85185
  return () => container.removeEventListener("scroll", checkScroll);
84451
85186
  }
84452
- }, [downtimeSegments]);
85187
+ }, [downtimeSegments, lightTimeline?.statusSegments, activeMode]);
84453
85188
  const renderSegment = (segment) => {
84454
85189
  const start = new Date(segment.startTime);
84455
85190
  const end = new Date(segment.endTime);
@@ -84470,6 +85205,63 @@ var WorkspaceUptimeDetailModal = ({
84470
85205
  `${segment.startMinuteIndex}-${segment.endMinuteIndex}`
84471
85206
  );
84472
85207
  };
85208
+ const renderLightSegment = (segment) => {
85209
+ const start = new Date(segment.startTime);
85210
+ const end = new Date(segment.endTime);
85211
+ const startLabel = new Intl.DateTimeFormat("en-IN", {
85212
+ hour: "numeric",
85213
+ minute: "2-digit",
85214
+ hour12: true,
85215
+ timeZone: timezone
85216
+ }).format(start);
85217
+ const endLabel = segment.isCurrent ? "Now" : new Intl.DateTimeFormat("en-IN", {
85218
+ hour: "numeric",
85219
+ minute: "2-digit",
85220
+ hour12: true,
85221
+ timeZone: timezone
85222
+ }).format(end);
85223
+ const containerClasses = segment.status === "down" ? "border-rose-200 bg-rose-50" : "border-amber-200 bg-amber-50";
85224
+ return /* @__PURE__ */ jsxRuntime.jsxs(
85225
+ "div",
85226
+ {
85227
+ className: `rounded-lg border px-5 py-3 ${containerClasses}`,
85228
+ children: [
85229
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm font-semibold text-gray-900", children: [
85230
+ startLabel,
85231
+ " - ",
85232
+ endLabel
85233
+ ] }),
85234
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-gray-600 mt-1", children: [
85235
+ getLightDurationPrefix(segment.status),
85236
+ " for ",
85237
+ formatSecondsDuration(segment.durationSeconds)
85238
+ ] }),
85239
+ formatLightError(segment.lastError) && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-xs font-medium text-rose-700", children: formatLightError(segment.lastError) })
85240
+ ]
85241
+ },
85242
+ `${segment.status}-${segment.startMinuteIndex}-${segment.endMinuteIndex}`
85243
+ );
85244
+ };
85245
+ const handleModeChange = (mode) => {
85246
+ if (mode === activeMode) return;
85247
+ const previousMode = activeMode;
85248
+ setActiveMode(mode);
85249
+ trackCoreEvent("Health Timeline Mode Changed", {
85250
+ previous_mode: previousMode,
85251
+ selected_mode: mode,
85252
+ source: "segmented_control",
85253
+ workspace_id: workspace?.workspace_id,
85254
+ line_id: workspace?.line_id
85255
+ });
85256
+ };
85257
+ const handleRefresh = () => {
85258
+ trackCoreEvent("Health Timeline Refreshed", {
85259
+ mode: activeMode,
85260
+ workspace_id: workspace?.workspace_id,
85261
+ line_id: workspace?.line_id
85262
+ });
85263
+ activeRefetch();
85264
+ };
84473
85265
  if (!isOpen || !workspace) {
84474
85266
  return null;
84475
85267
  }
@@ -84493,13 +85285,35 @@ var WorkspaceUptimeDetailModal = ({
84493
85285
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0 mr-4", children: [
84494
85286
  /* @__PURE__ */ jsxRuntime.jsx("h2", { id: "uptime-detail-title", className: "text-2xl font-semibold text-gray-900 truncate mb-3", children: workspace.workspace_display_name || `Workspace ${workspace.workspace_id.slice(0, 6)}` }),
84495
85287
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2.5 text-sm text-gray-600", children: [
84496
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-gray-900", children: timeline?.shiftLabel || "Current Shift" }),
85288
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-gray-900", children: activeTimeline?.shiftLabel || "Current Shift" }),
84497
85289
  shiftStart && shiftEnd && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
84498
85290
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-300", children: "\u2022" }),
84499
85291
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-600", children: formatTimeRange(shiftStart, shiftEnd, timezone) })
84500
85292
  ] })
84501
85293
  ] }),
84502
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-2 flex flex-wrap items-center gap-2", children: [
85294
+ hasLightTimeline && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-3 inline-flex rounded-lg border border-gray-200 bg-gray-50 p-0.5", children: [
85295
+ /* @__PURE__ */ jsxRuntime.jsx(
85296
+ "button",
85297
+ {
85298
+ type: "button",
85299
+ onClick: () => handleModeChange("camera"),
85300
+ "aria-pressed": activeMode === "camera",
85301
+ className: `rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${activeMode === "camera" ? "bg-white text-gray-900 shadow-sm" : "text-gray-600 hover:text-gray-900"}`,
85302
+ children: "Camera"
85303
+ }
85304
+ ),
85305
+ /* @__PURE__ */ jsxRuntime.jsx(
85306
+ "button",
85307
+ {
85308
+ type: "button",
85309
+ onClick: () => handleModeChange("light"),
85310
+ "aria-pressed": activeMode === "light",
85311
+ className: `rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${activeMode === "light" ? "bg-white text-gray-900 shadow-sm" : "text-gray-600 hover:text-gray-900"}`,
85312
+ children: "Light"
85313
+ }
85314
+ )
85315
+ ] }),
85316
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2 flex flex-wrap items-center gap-2", children: activeMode === "camera" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
84503
85317
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
84504
85318
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: `w-2 h-2 rounded-full ${workspace.status === "healthy" ? "bg-emerald-500 animate-pulse" : workspace.status === "warning" ? "bg-amber-500" : "bg-rose-500"}` }),
84505
85319
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: `text-xs font-medium ${workspace.status === "healthy" ? "text-emerald-700" : workspace.status === "warning" ? "text-amber-700" : "text-rose-700"}`, children: workspace.status === "healthy" ? "Operational" : workspace.status === "warning" ? "Intermittent" : "Down" })
@@ -84516,17 +85330,37 @@ var WorkspaceUptimeDetailModal = ({
84516
85330
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-slate-600 dark:text-slate-300 select-all", children: workspace.cameraIp })
84517
85331
  ] })
84518
85332
  ] })
84519
- ] })
85333
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
85334
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
85335
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: `w-2 h-2 rounded-full ${lightStatus === "up" ? "bg-emerald-500 animate-pulse" : lightStatus === "unknown" ? "bg-amber-500" : "bg-rose-500"}` }),
85336
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: `text-xs font-medium ${lightStatus === "up" ? "text-emerald-700" : lightStatus === "unknown" ? "text-amber-700" : "text-rose-700"}`, children: lightStatusText })
85337
+ ] }),
85338
+ lightTimeline?.bulbIp && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
85339
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-300", children: "\u2022" }),
85340
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 bg-slate-50 px-2 py-0.5 rounded border border-slate-100", title: "Bulb IP", children: [
85341
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Lightbulb, { className: "h-3 w-3 text-slate-400" }),
85342
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-slate-600 select-all", children: lightTimeline.bulbIp })
85343
+ ] })
85344
+ ] }),
85345
+ lightDurationText && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
85346
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-300", children: "\u2022" }),
85347
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500", children: lightDurationText })
85348
+ ] }),
85349
+ lightLastError && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
85350
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-300", children: "\u2022" }),
85351
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs font-medium text-rose-700", children: lightLastError })
85352
+ ] })
85353
+ ] }) })
84520
85354
  ] }),
84521
85355
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 flex-shrink-0", children: [
84522
85356
  /* @__PURE__ */ jsxRuntime.jsxs(
84523
85357
  "button",
84524
85358
  {
84525
- onClick: refetch,
84526
- disabled: loading,
85359
+ onClick: handleRefresh,
85360
+ disabled: activeLoading,
84527
85361
  className: "inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50 transition-all duration-200 shadow-sm",
84528
85362
  children: [
84529
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { className: `h-4 w-4 ${loading ? "animate-spin" : ""}` }),
85363
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { className: `h-4 w-4 ${activeLoading ? "animate-spin" : ""}` }),
84530
85364
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: "Refresh" })
84531
85365
  ]
84532
85366
  }
@@ -84542,31 +85376,63 @@ var WorkspaceUptimeDetailModal = ({
84542
85376
  )
84543
85377
  ] })
84544
85378
  ] }),
84545
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 overflow-y-auto", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-8 py-6 space-y-6", children: error ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-xl border border-rose-100 bg-rose-50 p-5 text-sm text-rose-700", children: [
85379
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 overflow-y-auto", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-8 py-6 space-y-6", children: activeError ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-xl border border-rose-100 bg-rose-50 p-5 text-sm text-rose-700", children: [
84546
85380
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-semibold mb-1", children: "Unable to load uptime details" }),
84547
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-rose-600/90", children: error.message })
85381
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-rose-600/90", children: activeError.message })
84548
85382
  ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
84549
85383
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative pb-4 border-b border-gray-200", children: [
84550
85384
  /* @__PURE__ */ jsxRuntime.jsx(
84551
85385
  UptimeTimelineStrip_default,
84552
85386
  {
84553
- points: timeline?.points || [],
84554
- totalMinutes: timeline?.totalMinutes || 0,
84555
- shiftStart: timeline?.shiftStart || (/* @__PURE__ */ new Date()).toISOString(),
84556
- shiftEnd: timeline?.shiftEnd || (/* @__PURE__ */ new Date()).toISOString(),
85387
+ points: activeTimeline?.points || [],
85388
+ totalMinutes: activeTimeline?.totalMinutes || 0,
85389
+ shiftStart: activeTimeline?.shiftStart || (/* @__PURE__ */ new Date()).toISOString(),
85390
+ shiftEnd: activeTimeline?.shiftEnd || (/* @__PURE__ */ new Date()).toISOString(),
84557
85391
  timezone,
84558
85392
  uptimePercentage,
84559
- downtimeMinutes: hasTimelineData ? downtimeMinutes : 0
85393
+ downtimeMinutes: activeMode === "light" ? 0 : hasTimelineData ? downtimeMinutes : 0,
85394
+ metricLabel: activeMode === "light" ? "light uptime" : "uptime",
85395
+ summaryText: activeMode === "light" && typeof uptimePercentage === "number" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
85396
+ uptimePercentage.toFixed(1),
85397
+ " % light uptime",
85398
+ (lightTimeline?.downSeconds || 0) > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-gray-600 font-normal", children: [
85399
+ " (",
85400
+ formatSecondsDuration(lightTimeline?.downSeconds || 0),
85401
+ " offline)"
85402
+ ] }),
85403
+ (lightTimeline?.unknownSeconds || 0) > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-gray-600 font-normal", children: [
85404
+ " (",
85405
+ formatSecondsDuration(lightTimeline?.unknownSeconds || 0),
85406
+ " unknown)"
85407
+ ] })
85408
+ ] }) : void 0
84560
85409
  }
84561
85410
  ),
84562
- loading && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm text-gray-600 mt-4", children: [
85411
+ activeLoading && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm text-gray-600 mt-4", children: [
84563
85412
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { className: "h-4 w-4 animate-spin text-gray-500" }),
84564
85413
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Updating timeline\u2026" })
84565
85414
  ] })
84566
85415
  ] }),
84567
85416
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pt-4", children: [
84568
- /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold text-gray-900 uppercase tracking-wider mb-3", children: "Downtime Logs" }),
84569
- !hasTimelineData ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-lg border border-gray-100 bg-gray-50/50 px-5 py-4 text-center", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-600", children: "No uptime data available for this shift." }) }) : downtimeSegments.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-lg border border-gray-100 bg-gray-50/50 px-5 py-4 text-center", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-600", children: "No downtime events recorded for this shift." }) }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
85417
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold text-gray-900 uppercase tracking-wider mb-3", children: activeMode === "light" ? "Light Status Logs" : "Downtime Logs" }),
85418
+ activeMode === "light" ? !hasTimelineData ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-lg border border-gray-100 bg-gray-50/50 px-5 py-4 text-center", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-600", children: "No light status data available for this shift." }) }) : !lightTimeline?.statusSegments.length ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-lg border border-gray-100 bg-gray-50/50 px-5 py-4 text-center", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-600", children: "No light down or unknown events recorded for this shift." }) }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
85419
+ /* @__PURE__ */ jsxRuntime.jsx(
85420
+ "div",
85421
+ {
85422
+ ref: logsContainerRef,
85423
+ className: "max-h-[400px] overflow-y-auto space-y-2 pr-2",
85424
+ style: {
85425
+ scrollbarWidth: "thin",
85426
+ scrollbarColor: "#CBD5E0 #F7FAFC"
85427
+ },
85428
+ children: lightTimeline.statusSegments.map((segment) => renderLightSegment(segment))
85429
+ }
85430
+ ),
85431
+ showScrollIndicator && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute bottom-0 left-0 right-0 h-12 bg-gradient-to-t from-white via-white/80 to-transparent pointer-events-none flex items-end justify-center pb-2", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 text-xs text-gray-500 animate-bounce", children: [
85432
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: "Scroll for more" }),
85433
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { className: "h-4 w-4" })
85434
+ ] }) })
85435
+ ] }) : !hasTimelineData ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-lg border border-gray-100 bg-gray-50/50 px-5 py-4 text-center", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-600", children: "No uptime data available for this shift." }) }) : downtimeSegments.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-lg border border-gray-100 bg-gray-50/50 px-5 py-4 text-center", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-600", children: "No downtime events recorded for this shift." }) }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
84570
85436
  /* @__PURE__ */ jsxRuntime.jsx(
84571
85437
  "div",
84572
85438
  {
@@ -84613,6 +85479,7 @@ var WorkspaceHealthView = ({
84613
85479
  const timezone = useAppTimezone();
84614
85480
  const { shiftConfig, isLoading: isShiftConfigLoading } = useDynamicShiftConfig(effectiveLineIdForShiftConfig);
84615
85481
  const [selectedWorkspace, setSelectedWorkspace] = React125.useState(null);
85482
+ const [selectedTimelineMode, setSelectedTimelineMode] = React125.useState("camera");
84616
85483
  const [selectedDate, setSelectedDate] = React125.useState(void 0);
84617
85484
  const [selectedShiftId, setSelectedShiftId] = React125.useState(void 0);
84618
85485
  const [showDatePicker, setShowDatePicker] = React125.useState(false);
@@ -84672,11 +85539,33 @@ var WorkspaceHealthView = ({
84672
85539
  },
84673
85540
  [router$1, onNavigate]
84674
85541
  );
84675
- const handleViewDetails = React125.useCallback((workspace) => {
85542
+ const handleViewDetails = React125.useCallback((workspace, mode = "camera", source = "card_button") => {
85543
+ const hasLightConfig = Boolean(workspace.lightSummary?.hasLightConfig && workspace.lightSummary?.bulbIp);
85544
+ if (source === "light_chip") {
85545
+ trackCoreEvent("Health Light Chip Clicked", {
85546
+ workspace_id: workspace.workspace_id,
85547
+ line_id: workspace.line_id,
85548
+ light_status: workspace.lightSummary?.currentStatus || null,
85549
+ uptime_percent: workspace.lightSummary?.uptimePercent ?? null,
85550
+ bulb_ip_present: Boolean(workspace.lightSummary?.bulbIp)
85551
+ });
85552
+ }
85553
+ trackCoreEvent("Health Timeline Opened", {
85554
+ source,
85555
+ initial_mode: mode,
85556
+ workspace_id: workspace.workspace_id,
85557
+ line_id: workspace.line_id,
85558
+ has_light_config: hasLightConfig,
85559
+ light_status: workspace.lightSummary?.currentStatus || null,
85560
+ selected_date: selectedDate || operationalDate,
85561
+ selected_shift_id: selectedShiftId ?? currentShiftDetails.shiftId
85562
+ });
85563
+ setSelectedTimelineMode(mode);
84676
85564
  setSelectedWorkspace(workspace);
84677
- }, []);
85565
+ }, [currentShiftDetails.shiftId, operationalDate, selectedDate, selectedShiftId]);
84678
85566
  const handleCloseDetails = React125.useCallback(() => {
84679
85567
  setSelectedWorkspace(null);
85568
+ setSelectedTimelineMode("camera");
84680
85569
  }, []);
84681
85570
  const getStatusIcon = (status) => {
84682
85571
  switch (status) {
@@ -84934,7 +85823,8 @@ var WorkspaceHealthView = ({
84934
85823
  onClose: handleCloseDetails,
84935
85824
  shiftConfig: modalShiftConfig,
84936
85825
  date: selectedDate,
84937
- shiftId: selectedShiftId
85826
+ shiftId: selectedShiftId,
85827
+ initialMode: selectedTimelineMode
84938
85828
  }
84939
85829
  )
84940
85830
  ] });
@@ -93750,6 +94640,7 @@ exports.useWorkspaceHealthById = useWorkspaceHealthById;
93750
94640
  exports.useWorkspaceHealthLastSeen = useWorkspaceHealthLastSeen;
93751
94641
  exports.useWorkspaceHealthStatus = useWorkspaceHealthStatus;
93752
94642
  exports.useWorkspaceHourSummary = useWorkspaceHourSummary;
94643
+ exports.useWorkspaceLightTimeline = useWorkspaceLightTimeline;
93753
94644
  exports.useWorkspaceMetrics = useWorkspaceMetrics;
93754
94645
  exports.useWorkspaceNavigation = useWorkspaceNavigation;
93755
94646
  exports.useWorkspaceOperators = useWorkspaceOperators;