@optifye/dashboard-core 6.12.49 → 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
  }
@@ -13820,12 +14378,22 @@ var stripSeconds = (timeStr) => {
13820
14378
  if (!timeStr) return timeStr;
13821
14379
  return timeStr.substring(0, 5);
13822
14380
  };
14381
+ var normalizeOffDays = (value) => {
14382
+ if (!value) return [];
14383
+ if (Array.isArray(value)) {
14384
+ return value.map((day) => String(day || "").trim().toLowerCase()).filter(Boolean);
14385
+ }
14386
+ if (Array.isArray(value.off_days)) return normalizeOffDays(value.off_days);
14387
+ if (Array.isArray(value.offDays)) return normalizeOffDays(value.offDays);
14388
+ return [];
14389
+ };
13823
14390
  var buildShiftConfigFromOperatingHoursRows = (rows, fallback) => {
13824
14391
  const mapped = (rows || []).map((row) => ({
13825
14392
  shiftId: row.shift_id,
13826
14393
  shiftName: row.shift_name || `Shift ${row.shift_id}`,
13827
14394
  startTime: stripSeconds(row.start_time),
13828
14395
  endTime: stripSeconds(row.end_time),
14396
+ offDays: normalizeOffDays(row.off_days),
13829
14397
  breaks: (() => {
13830
14398
  const raw = Array.isArray(row.breaks) ? row.breaks : Array.isArray(row.breaks?.breaks) ? row.breaks.breaks : [];
13831
14399
  return raw.map((b) => ({
@@ -13885,7 +14453,7 @@ var fetchAndStoreShiftConfig = async (supabase, lineId, fallback) => {
13885
14453
  if (existing) return existing;
13886
14454
  const promise = (async () => {
13887
14455
  try {
13888
- const { data, error } = await supabase.from("line_operating_hours").select("line_id, shift_id, shift_name, start_time, end_time, breaks, timezone").eq("line_id", lineId);
14456
+ const { data, error } = await supabase.from("line_operating_hours").select("line_id, shift_id, shift_name, start_time, end_time, breaks, timezone, off_days").eq("line_id", lineId);
13889
14457
  if (error) {
13890
14458
  throw new Error(`Failed to fetch shift config: ${error.message}`);
13891
14459
  }
@@ -14578,7 +15146,7 @@ var useMultiLineShiftConfigs = (lineIds, fallbackConfig) => {
14578
15146
  setError(null);
14579
15147
  }
14580
15148
  console.log(`[useMultiLineShiftConfigs] Fetching shift configs for ${missingLineIds.length} lines`);
14581
- const { data, error: fetchError } = await supabase.from("line_operating_hours").select("line_id, shift_id, shift_name, start_time, end_time, breaks, timezone").in("line_id", missingLineIds);
15149
+ const { data, error: fetchError } = await supabase.from("line_operating_hours").select("line_id, shift_id, shift_name, start_time, end_time, breaks, timezone, off_days").in("line_id", missingLineIds);
14582
15150
  if (fetchError) {
14583
15151
  console.error("[useMultiLineShiftConfigs] Error fetching shift configs:", fetchError);
14584
15152
  throw new Error(`Failed to fetch shift configs: ${fetchError.message}`);
@@ -20794,7 +21362,8 @@ var useWorkspaceUptimeTimeline = (options) => {
20794
21362
  effectiveShiftConfig,
20795
21363
  effectiveTimezone,
20796
21364
  overrideDate,
20797
- overrideShiftId
21365
+ overrideShiftId,
21366
+ lineId
20798
21367
  );
20799
21368
  setTimeline(data);
20800
21369
  } catch (err) {
@@ -20804,7 +21373,89 @@ var useWorkspaceUptimeTimeline = (options) => {
20804
21373
  setLoading(false);
20805
21374
  isFetchingRef.current = false;
20806
21375
  }
20807
- }, [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]);
20808
21459
  React125.useEffect(() => {
20809
21460
  fetchTimeline();
20810
21461
  }, [fetchTimeline]);
@@ -37808,7 +38459,12 @@ var HourlyOutputChartComponent = ({
37808
38459
  }, [idleBarState.visible, idleBarState.key, idleBarState.shouldAnimate]);
37809
38460
  const maxDataValue = Math.max(...data, 0);
37810
38461
  const numericChartTargets = chartData.map((d) => d.target).filter((target) => target !== null && Number.isFinite(target));
37811
- const maxTargetValue = Math.max(...numericChartTargets, pphThreshold, 0);
38462
+ const hasAuthoritativeNumericTargets = hasHourlyTargetOutputProp && numericChartTargets.length > 0;
38463
+ const maxTargetValue = Math.max(
38464
+ ...numericChartTargets,
38465
+ hasAuthoritativeNumericTargets ? 0 : pphThreshold,
38466
+ 0
38467
+ );
37812
38468
  const maxYValue = Math.max(
37813
38469
  Math.ceil(maxTargetValue * 1.5),
37814
38470
  Math.ceil(maxDataValue * 1.15)
@@ -37963,14 +38619,9 @@ var HourlyOutputChartComponent = ({
37963
38619
  return /* @__PURE__ */ jsxRuntime.jsx("g", { children: lines });
37964
38620
  }, [hourlyTargetSegments, targetTimelineSegments, SHIFT_DURATION, pphThreshold, targetLineEndOffset, hasHourlyTargetOutputProp]);
37965
38621
  const renderLegend = () => {
37966
- const uniqueTargets = [...new Set(
37967
- chartData.map((d) => d.target).filter((target) => target !== null && Number.isFinite(target)).map((target) => Math.round(target))
37968
- )].sort((a, b) => a - b);
37969
- const unitLabel = hasHourlyTargetOutputProp ? "units" : "units/hr";
37970
- const targetText = uniqueTargets.length === 0 ? `Target` : uniqueTargets.length === 1 ? `Target: ${uniqueTargets[0]} ${unitLabel}` : `Target: ${uniqueTargets[0]} - ${uniqueTargets[uniqueTargets.length - 1]} ${unitLabel}`;
37971
38622
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center text-sm text-gray-600 bg-white py-1", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 border border-gray-100 rounded-full px-3 py-1", children: [
37972
38623
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-8 flex items-center", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full border-t-2 border-[#E34329] border-dashed" }) }),
37973
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: targetText })
38624
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Target" })
37974
38625
  ] }) });
37975
38626
  };
37976
38627
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -52488,6 +53139,106 @@ var getOrdinal = (n) => {
52488
53139
  const v = n % 100;
52489
53140
  return n + (suffix[(v - 20) % 10] || suffix[v] || suffix[0]);
52490
53141
  };
53142
+ var resolveDailyTargetOutput = (shiftData) => {
53143
+ if (!shiftData || shiftData.hasData === false) return 0;
53144
+ const target = Number(shiftData?.targetOutput || shiftData?.idealOutput || 0);
53145
+ return Number.isFinite(target) && target > 0 ? target : 0;
53146
+ };
53147
+ var getUniqueRoundedTargets = (data) => Array.from(new Set(
53148
+ data.map((entry) => Number(entry.targetOutput || 0)).filter((target) => Number.isFinite(target) && target > 0).map((target) => Math.round(target))
53149
+ )).sort((a, b) => a - b);
53150
+ var formatDailyTargetLegend = (targets) => {
53151
+ if (targets.length === 0) return "";
53152
+ if (targets.length === 1) return `Target: ${targets[0].toLocaleString()} units/day`;
53153
+ return `Target: ${targets[0].toLocaleString()} - ${targets[targets.length - 1].toLocaleString()} units/day`;
53154
+ };
53155
+ var WEEKDAY_NAMES = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
53156
+ var getShiftOffDays = (shiftConfig, selectedShiftId) => {
53157
+ const shift = shiftConfig?.shifts?.find((candidate) => candidate.shiftId === selectedShiftId);
53158
+ const raw = shift?.offDays || shift?.off_days || [];
53159
+ return Array.isArray(raw) ? raw.map((day) => String(day || "").trim().toLowerCase()).filter(Boolean) : [];
53160
+ };
53161
+ var isScheduledOffDay = (dateKey, shiftConfig, selectedShiftId) => {
53162
+ const offDays = getShiftOffDays(shiftConfig, selectedShiftId);
53163
+ if (offDays.length === 0) return false;
53164
+ const date = parseDateKeyToDate(dateKey);
53165
+ return offDays.includes(WEEKDAY_NAMES[date.getDay()]);
53166
+ };
53167
+ var renderDailyOutputCapsules = (props, data) => {
53168
+ const { offset, yAxisMap } = props;
53169
+ if (!offset || !yAxisMap || data.length === 0) return null;
53170
+ const { left, width } = offset;
53171
+ const yAxis = yAxisMap.default || yAxisMap[0];
53172
+ if (!Number.isFinite(left) || !Number.isFinite(width) || width <= 0 || !yAxis?.scale) {
53173
+ return null;
53174
+ }
53175
+ const slotWidth = width / data.length;
53176
+ const capsuleWidth = Math.max(10, Math.min(28, slotWidth * 0.44));
53177
+ const radius = capsuleWidth / 2;
53178
+ const baseY = yAxis.scale(0);
53179
+ const capsules = [];
53180
+ data.forEach((entry, index) => {
53181
+ const target = Number(entry.targetOutput || 0);
53182
+ const output = Number(entry.output || 0);
53183
+ if ((!Number.isFinite(target) || target <= 0) && (!Number.isFinite(output) || output <= 0)) return;
53184
+ const x = left + index * slotWidth + (slotWidth - capsuleWidth) / 2;
53185
+ const targetTop = target > 0 ? yAxis.scale(target) : yAxis.scale(output);
53186
+ const capsuleTop = Math.min(targetTop, baseY - 4);
53187
+ const capsuleHeight = Math.max(baseY - capsuleTop, 4);
53188
+ const fillValue = target > 0 ? Math.min(Math.max(output, 0), target) : Math.max(output, 0);
53189
+ const fillTop = fillValue > 0 ? Math.max(yAxis.scale(fillValue), capsuleTop) : baseY;
53190
+ const fillHeight = Math.max(baseY - fillTop, 0);
53191
+ const fillColor = target > 0 ? output >= target ? "#00AB45" : "#E34329" : entry.color || "#6b7280";
53192
+ const trackFill = output >= target ? "#f0fdf4" : "#fff5f3";
53193
+ const trackStroke = output >= target ? "#00AB45" : "#E34329";
53194
+ const clipId = `line-daily-output-capsule-${index}`;
53195
+ capsules.push(
53196
+ /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
53197
+ /* @__PURE__ */ jsxRuntime.jsx("defs", { children: /* @__PURE__ */ jsxRuntime.jsx("clipPath", { id: clipId, children: /* @__PURE__ */ jsxRuntime.jsx(
53198
+ "rect",
53199
+ {
53200
+ x,
53201
+ y: capsuleTop,
53202
+ width: capsuleWidth,
53203
+ height: capsuleHeight,
53204
+ rx: radius,
53205
+ ry: radius
53206
+ }
53207
+ ) }) }),
53208
+ target > 0 && /* @__PURE__ */ jsxRuntime.jsx(
53209
+ "rect",
53210
+ {
53211
+ "data-testid": "daily-target-capsule-track",
53212
+ x,
53213
+ y: capsuleTop,
53214
+ width: capsuleWidth,
53215
+ height: capsuleHeight,
53216
+ rx: radius,
53217
+ ry: radius,
53218
+ fill: trackFill,
53219
+ stroke: trackStroke,
53220
+ strokeWidth: 1.5
53221
+ }
53222
+ ),
53223
+ /* @__PURE__ */ jsxRuntime.jsx(
53224
+ "rect",
53225
+ {
53226
+ "data-testid": "daily-output-capsule-fill",
53227
+ x,
53228
+ y: fillTop,
53229
+ width: capsuleWidth,
53230
+ height: fillHeight,
53231
+ rx: fillHeight >= capsuleHeight ? radius : 0,
53232
+ ry: fillHeight >= capsuleHeight ? radius : 0,
53233
+ fill: fillColor,
53234
+ clipPath: target > 0 ? `url(#${clipId})` : void 0
53235
+ }
53236
+ )
53237
+ ] }, `daily-output-capsule-${index}`)
53238
+ );
53239
+ });
53240
+ return capsules.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("g", { children: capsules }) : null;
53241
+ };
52491
53242
  var CustomTooltip2 = ({ active, payload, label, isUptimeMode }) => {
52492
53243
  if (!active || !payload || payload.length === 0) return null;
52493
53244
  if (isUptimeMode) {
@@ -52571,6 +53322,7 @@ var LineMonthlyHistory = ({
52571
53322
  legend,
52572
53323
  monitoringMode,
52573
53324
  lineAssembly = false,
53325
+ shiftConfig,
52574
53326
  underperformingWorkspaces = {},
52575
53327
  lineId,
52576
53328
  selectedShiftId = 0,
@@ -52588,6 +53340,13 @@ var LineMonthlyHistory = ({
52588
53340
  const { isIdleTimeVlmEnabled } = useIdleTimeVlmConfig();
52589
53341
  const idleTimeVlmEnabled = isIdleTimeVlmEnabled(lineId);
52590
53342
  const isUptimeMode = monitoringMode === "uptime";
53343
+ const [isMobile, setIsMobile] = React125.useState(false);
53344
+ React125.useEffect(() => {
53345
+ const checkMobile = () => setIsMobile(window.innerWidth < 640);
53346
+ checkMobile();
53347
+ window.addEventListener("resize", checkMobile);
53348
+ return () => window.removeEventListener("resize", checkMobile);
53349
+ }, []);
52591
53350
  const chartKey = React125.useMemo(() => `${lineId}-${month}-${year}-${selectedShiftId}-${rangeStart}-${rangeEnd}`, [lineId, month, year, selectedShiftId, rangeStart, rangeEnd]);
52592
53351
  const monthBounds = React125.useMemo(() => getMonthKeyBounds(year, month), [year, month]);
52593
53352
  const normalizedRange = React125.useMemo(() => {
@@ -52749,29 +53508,21 @@ var LineMonthlyHistory = ({
52749
53508
  });
52750
53509
  }
52751
53510
  const yAxisMax2 = maxHours > 0 ? 100 : 1;
52752
- return { data: dailyData2, maxOutput: 0, lastSetTarget: 0, yAxisMax: yAxisMax2 };
53511
+ return { data: dailyData2, maxOutput: 0, targetValues: [], yAxisMax: yAxisMax2, targetLegend: "" };
52753
53512
  }
52754
53513
  const dailyData = [];
52755
53514
  let maxOutput = 0;
52756
- let lastSetTarget = 0;
52757
- for (let i = rangeDateKeys.length - 1; i >= 0; i--) {
52758
- const dayKey = rangeDateKeys[i];
52759
- const dayData = analysisMonthlyDataByKey.get(dayKey);
52760
- const shiftData = dayData ? getShiftData2(dayData, selectedShiftId) : null;
52761
- const idealOutput = shiftData ? shiftData.idealOutput || 0 : 0;
52762
- if (idealOutput > 0) {
52763
- lastSetTarget = idealOutput;
52764
- break;
52765
- }
52766
- }
53515
+ let maxTarget = 0;
52767
53516
  for (const dayKey of rangeDateKeys) {
52768
53517
  const day = Number(dayKey.slice(-2));
52769
53518
  const dayData = analysisMonthlyDataByKey.get(dayKey);
53519
+ const isOffDay = isScheduledOffDay(dayKey, shiftConfig, selectedShiftId);
52770
53520
  const shiftData = dayData ? getShiftData2(dayData, selectedShiftId) : null;
52771
- const output = shiftData && hasRealData(shiftData) ? shiftData.output || 0 : 0;
52772
- const idealOutput = shiftData ? shiftData.idealOutput || 0 : 0;
53521
+ const output = !isOffDay && shiftData && hasRealData(shiftData) ? shiftData.output || 0 : 0;
53522
+ const targetOutput = isOffDay ? 0 : resolveDailyTargetOutput(shiftData);
52773
53523
  if (output > maxOutput) maxOutput = output;
52774
- const color2 = output >= lastSetTarget ? "#00AB45" : "#E34329";
53524
+ if (targetOutput > maxTarget) maxTarget = targetOutput;
53525
+ const color2 = targetOutput > 0 && output >= targetOutput ? "#00AB45" : "#E34329";
52775
53526
  dailyData.push({
52776
53527
  hour: getOrdinal(day),
52777
53528
  // Using ordinal format (1st, 2nd, 3rd, etc.)
@@ -52779,17 +53530,25 @@ var LineMonthlyHistory = ({
52779
53530
  output,
52780
53531
  originalOutput: output,
52781
53532
  // For label display
52782
- idealOutput,
53533
+ idealOutput: targetOutput,
53534
+ targetOutput,
52783
53535
  color: color2
52784
53536
  });
52785
53537
  }
52786
- const calculatedMax = Math.max(maxOutput, lastSetTarget);
53538
+ const calculatedMax = Math.max(maxOutput, maxTarget);
52787
53539
  const yAxisMax = calculatedMax > 0 ? calculatedMax * 1.1 : 100;
52788
- return { data: dailyData, maxOutput, lastSetTarget, yAxisMax };
52789
- }, [analysisMonthlyDataByKey, normalizedRange.endKey, normalizedRange.startKey, selectedShiftId, isUptimeMode, timezone]);
53540
+ const targetValues = getUniqueRoundedTargets(dailyData);
53541
+ return {
53542
+ data: dailyData,
53543
+ maxOutput,
53544
+ targetValues,
53545
+ yAxisMax,
53546
+ targetLegend: formatDailyTargetLegend(targetValues)
53547
+ };
53548
+ }, [analysisMonthlyDataByKey, normalizedRange.endKey, normalizedRange.startKey, selectedShiftId, isUptimeMode, timezone, shiftConfig]);
52790
53549
  const yAxisTicks = React125.useMemo(() => {
52791
53550
  const max = chartData.yAxisMax;
52792
- const target = chartData.lastSetTarget;
53551
+ const targets = chartData.targetValues || [];
52793
53552
  if (!max || max <= 0) return void 0;
52794
53553
  const desiredIntervals = 4;
52795
53554
  const roughStep = max / desiredIntervals;
@@ -52803,11 +53562,18 @@ var LineMonthlyHistory = ({
52803
53562
  for (let v = 0; v <= max; v += step) {
52804
53563
  ticks.push(Math.round(v));
52805
53564
  }
52806
- if (target > 0) {
52807
- ticks.push(Math.round(target));
52808
- }
53565
+ targets.forEach((target) => ticks.push(Math.round(target)));
52809
53566
  return Array.from(new Set(ticks)).filter((v) => v >= 0 && v <= max).sort((a, b) => a - b);
52810
- }, [chartData.yAxisMax, chartData.lastSetTarget]);
53567
+ }, [chartData.yAxisMax, chartData.targetValues]);
53568
+ const visibleYAxisTicks = React125.useMemo(() => {
53569
+ if (!isMobile || isUptimeMode || !yAxisTicks || yAxisTicks.length <= 3) return yAxisTicks;
53570
+ const importantTicks = /* @__PURE__ */ new Set([
53571
+ 0,
53572
+ Math.round(chartData.yAxisMax),
53573
+ ...chartData.targetValues || []
53574
+ ]);
53575
+ return yAxisTicks.filter((tick) => importantTicks.has(Math.round(tick))).sort((a, b) => a - b).slice(-3);
53576
+ }, [chartData.targetValues, chartData.yAxisMax, isMobile, isUptimeMode, yAxisTicks]);
52811
53577
  const pieChartData = React125.useMemo(() => {
52812
53578
  if (!isUptimeMode) return [];
52813
53579
  const validShifts = (analysisMonthlyData || []).map((day) => getShiftData2(day, selectedShiftId)).filter(
@@ -53126,36 +53892,35 @@ var LineMonthlyHistory = ({
53126
53892
  ] }),
53127
53893
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-white rounded-lg sm:rounded-xl shadow-sm border border-gray-100 p-2 sm:p-3 lg:p-4", children: [
53128
53894
  /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-xs sm:text-sm font-bold text-gray-700 mb-1 sm:mb-2 text-left", children: isUptimeMode ? "Daily Utilization" : "Daily Output" }),
53129
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-[160px] sm:h-[180px] lg:h-[220px]", children: /* @__PURE__ */ jsxRuntime.jsx(recharts.ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxRuntime.jsxs(
53895
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-[140px] sm:h-[180px] lg:h-[220px]", children: /* @__PURE__ */ jsxRuntime.jsx(recharts.ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxRuntime.jsxs(
53130
53896
  recharts.BarChart,
53131
53897
  {
53132
53898
  data: chartData.data,
53133
- margin: { top: 20, right: 10, bottom: 40, left: 10 },
53899
+ margin: isMobile ? { top: 8, right: 4, bottom: 28, left: 0 } : { top: 20, right: 10, bottom: 40, left: 10 },
53134
53900
  children: [
53135
53901
  /* @__PURE__ */ jsxRuntime.jsx(recharts.CartesianGrid, { strokeDasharray: "3 3", vertical: false, stroke: "#f3f4f6" }),
53136
53902
  /* @__PURE__ */ jsxRuntime.jsx(
53137
53903
  recharts.XAxis,
53138
53904
  {
53139
53905
  dataKey: "hour",
53140
- tick: { fontSize: 10, fill: "#6b7280" },
53141
- interval: 0,
53906
+ tick: { fontSize: isMobile ? 9 : 10, fill: "#6b7280" },
53907
+ interval: isMobile ? 3 : 0,
53142
53908
  angle: -45,
53143
53909
  textAnchor: "end",
53144
- height: 60
53910
+ height: isMobile ? 42 : 60
53145
53911
  }
53146
53912
  ),
53147
53913
  /* @__PURE__ */ jsxRuntime.jsx(
53148
53914
  recharts.YAxis,
53149
53915
  {
53150
53916
  domain: [0, chartData.yAxisMax],
53151
- width: 40,
53152
- ticks: isUptimeMode ? [0, 25, 50, 75, 100] : yAxisTicks,
53917
+ width: isMobile ? 34 : 40,
53918
+ ticks: isUptimeMode ? [0, 25, 50, 75, 100] : visibleYAxisTicks,
53153
53919
  tickFormatter: isUptimeMode ? (value) => `${value}%` : void 0,
53154
53920
  tick: isUptimeMode ? void 0 : (props) => {
53155
53921
  const { x, y, payload } = props;
53156
53922
  const value = Math.round(payload.value);
53157
- const targetValue = Math.round(chartData.lastSetTarget);
53158
- const isTarget = value === targetValue && targetValue > 0;
53923
+ const isTarget = (chartData.targetValues || []).includes(value);
53159
53924
  return /* @__PURE__ */ jsxRuntime.jsx(
53160
53925
  "text",
53161
53926
  {
@@ -53178,15 +53943,7 @@ var LineMonthlyHistory = ({
53178
53943
  content: (props) => /* @__PURE__ */ jsxRuntime.jsx(CustomTooltip2, { ...props, isUptimeMode })
53179
53944
  }
53180
53945
  ),
53181
- !isUptimeMode && chartData.lastSetTarget > 0 && /* @__PURE__ */ jsxRuntime.jsx(
53182
- recharts.ReferenceLine,
53183
- {
53184
- y: chartData.lastSetTarget,
53185
- stroke: "#E34329",
53186
- strokeDasharray: "5 5",
53187
- strokeWidth: 2
53188
- }
53189
- ),
53946
+ !isUptimeMode && /* @__PURE__ */ jsxRuntime.jsx(recharts.Customized, { component: (props) => renderDailyOutputCapsules(props, chartData.data) }),
53190
53947
  isUptimeMode ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
53191
53948
  /* @__PURE__ */ jsxRuntime.jsx(
53192
53949
  recharts.Bar,
@@ -53219,6 +53976,8 @@ var LineMonthlyHistory = ({
53219
53976
  {
53220
53977
  dataKey: "output",
53221
53978
  radius: [4, 4, 0, 0],
53979
+ fill: "transparent",
53980
+ opacity: 0,
53222
53981
  isAnimationActive: true,
53223
53982
  animationBegin: 0,
53224
53983
  animationDuration: 1e3,
@@ -53237,13 +53996,9 @@ var LineMonthlyHistory = ({
53237
53996
  },
53238
53997
  chartKey
53239
53998
  ) }) }),
53240
- !isUptimeMode && chartData.lastSetTarget > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center text-sm text-gray-600 bg-white py-1 pt-2", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 border border-gray-100 rounded-full px-3 py-1", children: [
53241
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-8 flex items-center", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full border-t-2 border-[#E34329] border-dashed" }) }),
53242
- /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
53243
- "Target: ",
53244
- Math.round(chartData.lastSetTarget),
53245
- " units/day"
53246
- ] })
53999
+ !isUptimeMode && chartData.targetValues.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center text-sm text-gray-600 bg-white py-1 pt-2", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 border border-gray-100 rounded-full px-3 py-1", children: [
54000
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative h-5 w-3 rounded-full border border-[#E34329] bg-[#fff5f3] overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-x-0 bottom-0 h-1/2 bg-[#E34329]" }) }),
54001
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: chartData.targetLegend })
53247
54002
  ] }) })
53248
54003
  ] })
53249
54004
  ] })
@@ -55166,6 +55921,106 @@ var formatCycleSeconds = (value) => {
55166
55921
  if (!Number.isFinite(value)) return "0.0s";
55167
55922
  return `${value.toFixed(1)}s`;
55168
55923
  };
55924
+ var resolveDailyTargetOutput2 = (shiftData) => {
55925
+ if (!shiftData || shiftData.hasData === false) return 0;
55926
+ const target = Number(shiftData?.targetOutput || shiftData?.idealOutput || 0);
55927
+ return Number.isFinite(target) && target > 0 ? target : 0;
55928
+ };
55929
+ var getUniqueRoundedTargets2 = (data) => Array.from(new Set(
55930
+ data.map((entry) => Number(entry.targetOutput || 0)).filter((target) => Number.isFinite(target) && target > 0).map((target) => Math.round(target))
55931
+ )).sort((a, b) => a - b);
55932
+ var formatDailyTargetLegend2 = (targets) => {
55933
+ if (targets.length === 0) return "";
55934
+ if (targets.length === 1) return `Target: ${targets[0].toLocaleString()} units/day`;
55935
+ return `Target: ${targets[0].toLocaleString()} - ${targets[targets.length - 1].toLocaleString()} units/day`;
55936
+ };
55937
+ var WEEKDAY_NAMES2 = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
55938
+ var getShiftOffDays2 = (shiftConfig, selectedShiftId) => {
55939
+ const shift = shiftConfig?.shifts?.find((candidate) => candidate.shiftId === selectedShiftId);
55940
+ const raw = shift?.offDays || shift?.off_days || [];
55941
+ return Array.isArray(raw) ? raw.map((day) => String(day || "").trim().toLowerCase()).filter(Boolean) : [];
55942
+ };
55943
+ var isScheduledOffDay2 = (dateKey, shiftConfig, selectedShiftId) => {
55944
+ const offDays = getShiftOffDays2(shiftConfig, selectedShiftId);
55945
+ if (offDays.length === 0) return false;
55946
+ const date = parseDateKeyToDate(dateKey);
55947
+ return offDays.includes(WEEKDAY_NAMES2[date.getDay()]);
55948
+ };
55949
+ var renderDailyOutputCapsules2 = (props, data) => {
55950
+ const { offset, yAxisMap } = props;
55951
+ if (!offset || !yAxisMap || data.length === 0) return null;
55952
+ const { left, width } = offset;
55953
+ const yAxis = yAxisMap.default || yAxisMap[0];
55954
+ if (!Number.isFinite(left) || !Number.isFinite(width) || width <= 0 || !yAxis?.scale) {
55955
+ return null;
55956
+ }
55957
+ const slotWidth = width / data.length;
55958
+ const capsuleWidth = Math.max(10, Math.min(28, slotWidth * 0.44));
55959
+ const radius = capsuleWidth / 2;
55960
+ const baseY = yAxis.scale(0);
55961
+ const capsules = [];
55962
+ data.forEach((entry, index) => {
55963
+ const target = Number(entry.targetOutput || 0);
55964
+ const output = Number(entry.output || 0);
55965
+ if ((!Number.isFinite(target) || target <= 0) && (!Number.isFinite(output) || output <= 0)) return;
55966
+ const x = left + index * slotWidth + (slotWidth - capsuleWidth) / 2;
55967
+ const targetTop = target > 0 ? yAxis.scale(target) : yAxis.scale(output);
55968
+ const capsuleTop = Math.min(targetTop, baseY - 4);
55969
+ const capsuleHeight = Math.max(baseY - capsuleTop, 4);
55970
+ const fillValue = target > 0 ? Math.min(Math.max(output, 0), target) : Math.max(output, 0);
55971
+ const fillTop = fillValue > 0 ? Math.max(yAxis.scale(fillValue), capsuleTop) : baseY;
55972
+ const fillHeight = Math.max(baseY - fillTop, 0);
55973
+ const fillColor = target > 0 ? output >= target ? "#00AB45" : "#E34329" : entry.color || "#6b7280";
55974
+ const trackFill = output >= target ? "#f0fdf4" : "#fff5f3";
55975
+ const trackStroke = output >= target ? "#00AB45" : "#E34329";
55976
+ const clipId = `workspace-daily-output-capsule-${index}`;
55977
+ capsules.push(
55978
+ /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
55979
+ /* @__PURE__ */ jsxRuntime.jsx("defs", { children: /* @__PURE__ */ jsxRuntime.jsx("clipPath", { id: clipId, children: /* @__PURE__ */ jsxRuntime.jsx(
55980
+ "rect",
55981
+ {
55982
+ x,
55983
+ y: capsuleTop,
55984
+ width: capsuleWidth,
55985
+ height: capsuleHeight,
55986
+ rx: radius,
55987
+ ry: radius
55988
+ }
55989
+ ) }) }),
55990
+ target > 0 && /* @__PURE__ */ jsxRuntime.jsx(
55991
+ "rect",
55992
+ {
55993
+ "data-testid": "daily-target-capsule-track",
55994
+ x,
55995
+ y: capsuleTop,
55996
+ width: capsuleWidth,
55997
+ height: capsuleHeight,
55998
+ rx: radius,
55999
+ ry: radius,
56000
+ fill: trackFill,
56001
+ stroke: trackStroke,
56002
+ strokeWidth: 1.5
56003
+ }
56004
+ ),
56005
+ /* @__PURE__ */ jsxRuntime.jsx(
56006
+ "rect",
56007
+ {
56008
+ "data-testid": "daily-output-capsule-fill",
56009
+ x,
56010
+ y: fillTop,
56011
+ width: capsuleWidth,
56012
+ height: fillHeight,
56013
+ rx: fillHeight >= capsuleHeight ? radius : 0,
56014
+ ry: fillHeight >= capsuleHeight ? radius : 0,
56015
+ fill: fillColor,
56016
+ clipPath: target > 0 ? `url(#${clipId})` : void 0
56017
+ }
56018
+ )
56019
+ ] }, `daily-output-capsule-${index}`)
56020
+ );
56021
+ });
56022
+ return capsules.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("g", { children: capsules }) : null;
56023
+ };
55169
56024
  var CustomTooltip3 = ({ active, payload, label, isUptimeMode }) => {
55170
56025
  if (!active || !payload || payload.length === 0) return null;
55171
56026
  if (isUptimeMode) {
@@ -55345,28 +56200,20 @@ var WorkspaceMonthlyHistory = ({
55345
56200
  });
55346
56201
  }
55347
56202
  const yAxisMax2 = maxHours > 0 ? maxHours * 1.1 : 1;
55348
- return { data: dailyData, yAxisMax: yAxisMax2, lastSetTarget: 0 };
56203
+ return { data: dailyData, yAxisMax: yAxisMax2, targetValues: [], targetLegend: "" };
55349
56204
  }
55350
56205
  let maxOutput = 0;
55351
- let lastSetTarget = 0;
55352
- for (let i = rangeDateKeys.length - 1; i >= 0; i--) {
55353
- const dateKey = rangeDateKeys[i];
55354
- const dayData = analysisMonthlyDataByKey.get(dateKey);
55355
- const shiftData = dayData ? getShiftData(dayData, selectedShiftId) : null;
55356
- const idealOutput = shiftData ? shiftData.idealOutput : 0;
55357
- if (idealOutput > 0) {
55358
- lastSetTarget = idealOutput;
55359
- break;
55360
- }
55361
- }
56206
+ let maxTarget = 0;
55362
56207
  for (const dateKey of rangeDateKeys) {
55363
56208
  const dayData = analysisMonthlyDataByKey.get(dateKey);
55364
56209
  const dayNumber = Number(dateKey.slice(-2));
56210
+ const isOffDay = isScheduledOffDay2(dateKey, shiftConfig, selectedShiftId);
55365
56211
  const shiftData = dayData ? getShiftData(dayData, selectedShiftId) : null;
55366
- const output = shiftData && hasRealData(shiftData) ? shiftData.output : 0;
55367
- const idealOutput = shiftData ? shiftData.idealOutput : 0;
56212
+ const output = !isOffDay && shiftData && hasRealData(shiftData) ? shiftData.output : 0;
56213
+ const targetOutput = isOffDay ? 0 : resolveDailyTargetOutput2(shiftData);
55368
56214
  if (output > maxOutput) maxOutput = output;
55369
- const color2 = output >= lastSetTarget ? "#00AB45" : "#E34329";
56215
+ if (targetOutput > maxTarget) maxTarget = targetOutput;
56216
+ const color2 = targetOutput > 0 && output >= targetOutput ? "#00AB45" : "#E34329";
55370
56217
  dailyData.push({
55371
56218
  hour: getOrdinal2(dayNumber),
55372
56219
  // Using ordinal format (1st, 2nd, 3rd, etc.)
@@ -55374,21 +56221,29 @@ var WorkspaceMonthlyHistory = ({
55374
56221
  output,
55375
56222
  originalOutput: output,
55376
56223
  // For label display
55377
- idealOutput,
56224
+ idealOutput: targetOutput,
56225
+ targetOutput,
55378
56226
  efficiency: shiftData && hasRealData(shiftData) ? shiftData.efficiency : 0,
55379
56227
  color: color2,
55380
56228
  idleMinutes: 0
55381
56229
  // Not used but keeps structure consistent
55382
56230
  });
55383
56231
  }
55384
- const calculatedMax = Math.max(maxOutput, lastSetTarget);
56232
+ const calculatedMax = Math.max(maxOutput, maxTarget);
55385
56233
  const yAxisMax = calculatedMax > 0 ? calculatedMax * 1.1 : 100;
55386
- return { data: dailyData, maxOutput, lastSetTarget, yAxisMax };
55387
- }, [analysisMonthlyDataByKey, rangeDateKeys, selectedShiftId, isUptimeMode, shiftWorkSeconds]);
56234
+ const targetValues = getUniqueRoundedTargets2(dailyData);
56235
+ return {
56236
+ data: dailyData,
56237
+ maxOutput,
56238
+ targetValues,
56239
+ yAxisMax,
56240
+ targetLegend: formatDailyTargetLegend2(targetValues)
56241
+ };
56242
+ }, [analysisMonthlyDataByKey, rangeDateKeys, selectedShiftId, isUptimeMode, shiftConfig, shiftWorkSeconds]);
55388
56243
  const yAxisTicks = React125.useMemo(() => {
55389
56244
  if (isUptimeMode) return void 0;
55390
56245
  const max = chartData.yAxisMax;
55391
- const target = chartData.lastSetTarget;
56246
+ const targets = chartData.targetValues || [];
55392
56247
  if (!max || max <= 0) return void 0;
55393
56248
  const desiredIntervals = 4;
55394
56249
  const roughStep = max / desiredIntervals;
@@ -55402,7 +56257,7 @@ var WorkspaceMonthlyHistory = ({
55402
56257
  for (let v = 0; v <= max; v += step) {
55403
56258
  ticks.push(Math.round(v));
55404
56259
  }
55405
- if (target > 0) {
56260
+ targets.forEach((target) => {
55406
56261
  const roundedTarget = Math.round(target);
55407
56262
  if (!ticks.includes(roundedTarget)) {
55408
56263
  let nearestIndex = -1;
@@ -55421,9 +56276,18 @@ var WorkspaceMonthlyHistory = ({
55421
56276
  ticks.push(roundedTarget);
55422
56277
  }
55423
56278
  }
55424
- }
56279
+ });
55425
56280
  return ticks.filter((v) => v >= 0 && v <= max * 1.05).sort((a, b) => a - b);
55426
- }, [chartData.yAxisMax, chartData.lastSetTarget, isUptimeMode]);
56281
+ }, [chartData.yAxisMax, chartData.targetValues, isUptimeMode]);
56282
+ const visibleYAxisTicks = React125.useMemo(() => {
56283
+ if (!isMobile || isUptimeMode || !yAxisTicks || yAxisTicks.length <= 3) return yAxisTicks;
56284
+ const importantTicks = /* @__PURE__ */ new Set([
56285
+ 0,
56286
+ Math.round(chartData.yAxisMax),
56287
+ ...chartData.targetValues || []
56288
+ ]);
56289
+ return yAxisTicks.filter((tick) => importantTicks.has(Math.round(tick))).sort((a, b) => a - b).slice(-3);
56290
+ }, [chartData.targetValues, chartData.yAxisMax, isMobile, isUptimeMode, yAxisTicks]);
55427
56291
  const pieChartData = React125.useMemo(() => {
55428
56292
  const aggregateMode = isUptimeMode ? "uptime" : "output";
55429
56293
  const validShifts = analysisMonthlyData.map((d) => getShiftData(d, selectedShiftId)).filter(
@@ -55813,22 +56677,22 @@ var WorkspaceMonthlyHistory = ({
55813
56677
  ] }),
55814
56678
  (!isAssemblyWorkspace || isUptimeMode) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-white rounded-lg shadow-sm border border-gray-100 p-4 flex-1", children: [
55815
56679
  /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-bold text-gray-700 mb-3 text-left", children: isUptimeMode ? "Daily Utilization" : "Daily Output" }),
55816
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: "220px" }, children: /* @__PURE__ */ jsxRuntime.jsx(recharts.ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxRuntime.jsxs(
56680
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: isMobile ? "150px" : "220px" }, children: /* @__PURE__ */ jsxRuntime.jsx(recharts.ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxRuntime.jsxs(
55817
56681
  recharts.BarChart,
55818
56682
  {
55819
56683
  data: chartData.data,
55820
- margin: { top: 20, right: 10, bottom: 40, left: 10 },
56684
+ margin: isMobile ? { top: 8, right: 4, bottom: 28, left: 0 } : { top: 20, right: 10, bottom: 40, left: 10 },
55821
56685
  children: [
55822
56686
  /* @__PURE__ */ jsxRuntime.jsx(recharts.CartesianGrid, { strokeDasharray: "3 3", vertical: false, stroke: "#f3f4f6" }),
55823
56687
  /* @__PURE__ */ jsxRuntime.jsx(
55824
56688
  recharts.XAxis,
55825
56689
  {
55826
56690
  dataKey: "hour",
55827
- tick: { fontSize: 10, fill: "#6b7280" },
55828
- interval: isMobile ? "preserveStartEnd" : 0,
56691
+ tick: { fontSize: isMobile ? 9 : 10, fill: "#6b7280" },
56692
+ interval: isMobile ? 3 : 0,
55829
56693
  angle: -45,
55830
56694
  textAnchor: "end",
55831
- height: 60
56695
+ height: isMobile ? 42 : 60
55832
56696
  }
55833
56697
  ),
55834
56698
  isUptimeMode ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -55843,13 +56707,12 @@ var WorkspaceMonthlyHistory = ({
55843
56707
  recharts.YAxis,
55844
56708
  {
55845
56709
  domain: [0, chartData.yAxisMax],
55846
- width: 40,
55847
- ticks: yAxisTicks,
56710
+ width: isMobile ? 34 : 40,
56711
+ ticks: visibleYAxisTicks,
55848
56712
  tick: (props) => {
55849
56713
  const { x, y, payload } = props;
55850
56714
  const value = Math.round(payload.value);
55851
- const targetValue = Math.round(chartData.lastSetTarget);
55852
- const isTarget = value === targetValue && targetValue > 0;
56715
+ const isTarget = (chartData.targetValues || []).includes(value);
55853
56716
  return /* @__PURE__ */ jsxRuntime.jsx(
55854
56717
  "text",
55855
56718
  {
@@ -55872,15 +56735,7 @@ var WorkspaceMonthlyHistory = ({
55872
56735
  content: (props) => /* @__PURE__ */ jsxRuntime.jsx(CustomTooltip3, { ...props, isUptimeMode })
55873
56736
  }
55874
56737
  ),
55875
- !isUptimeMode && chartData.lastSetTarget > 0 && /* @__PURE__ */ jsxRuntime.jsx(
55876
- recharts.ReferenceLine,
55877
- {
55878
- y: chartData.lastSetTarget,
55879
- stroke: "#E34329",
55880
- strokeDasharray: "5 5",
55881
- strokeWidth: 2
55882
- }
55883
- ),
56738
+ !isUptimeMode && /* @__PURE__ */ jsxRuntime.jsx(recharts.Customized, { component: (props) => renderDailyOutputCapsules2(props, chartData.data) }),
55884
56739
  isUptimeMode ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
55885
56740
  /* @__PURE__ */ jsxRuntime.jsx(
55886
56741
  recharts.Bar,
@@ -55913,6 +56768,8 @@ var WorkspaceMonthlyHistory = ({
55913
56768
  {
55914
56769
  dataKey: "output",
55915
56770
  radius: [4, 4, 0, 0],
56771
+ fill: "transparent",
56772
+ opacity: 0,
55916
56773
  isAnimationActive: true,
55917
56774
  animationBegin: 0,
55918
56775
  animationDuration: 1e3,
@@ -55956,13 +56813,9 @@ var WorkspaceMonthlyHistory = ({
55956
56813
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-2.5 h-2.5 rounded-full", style: { backgroundColor: "#e5e7eb" } }),
55957
56814
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-600", children: "Idle" })
55958
56815
  ] })
55959
- ] }) : chartData.lastSetTarget > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
55960
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-12 h-0.5 border-t-2 border-dashed", style: { borderColor: "#E34329" } }),
55961
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-gray-600", children: [
55962
- "Target: ",
55963
- Math.round(chartData.lastSetTarget),
55964
- " units/day"
55965
- ] })
56816
+ ] }) : chartData.targetValues.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
56817
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative h-5 w-3 rounded-full border border-[#E34329] bg-[#fff5f3] overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-x-0 bottom-0 h-1/2 bg-[#E34329]" }) }),
56818
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-600", children: chartData.targetLegend })
55966
56819
  ] }) })
55967
56820
  ] })
55968
56821
  ] })
@@ -58029,7 +58882,14 @@ var WorkspaceHealthCard = ({
58029
58882
  event.stopPropagation();
58030
58883
  event.preventDefault();
58031
58884
  if (onViewDetails) {
58032
- 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");
58033
58893
  }
58034
58894
  };
58035
58895
  const handleKeyDown = (event) => {
@@ -58107,6 +58967,22 @@ var WorkspaceHealthCard = ({
58107
58967
  };
58108
58968
  };
58109
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
+ );
58110
58986
  return /* @__PURE__ */ jsxRuntime.jsx(
58111
58987
  Card2,
58112
58988
  {
@@ -58176,6 +59052,20 @@ var WorkspaceHealthCard = ({
58176
59052
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium uppercase tracking-wide text-slate-500", children: "Camera IP:" }),
58177
59053
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-700 dark:text-gray-300", children: "N/A" })
58178
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
+ ),
58179
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: [
58180
59070
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-slate-500 uppercase tracking-wide", children: downtimeConfig.label }),
58181
59071
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: clsx("text-sm font-semibold", downtimeConfig.className), children: downtimeConfig.text })
@@ -58245,7 +59135,7 @@ var CompactWorkspaceHealthCard = ({
58245
59135
  event.stopPropagation();
58246
59136
  event.preventDefault();
58247
59137
  if (onViewDetails) {
58248
- onViewDetails(workspace);
59138
+ onViewDetails(workspace, "camera", "card_button");
58249
59139
  }
58250
59140
  };
58251
59141
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -67592,6 +68482,17 @@ var setSessionSeenValue = (key) => {
67592
68482
  };
67593
68483
  var buildAllGreenCelebrationSeenKey = (identity) => `${ALL_GREEN_CELEBRATION_SEEN_PREFIX}${identity}`;
67594
68484
  var buildAllGreenMilestoneSeenKey = (identity, milestoneSeconds) => `${ALL_GREEN_MILESTONE_SEEN_PREFIX}${identity}:${milestoneSeconds}`;
68485
+ var LINE_SELECTOR_INDICATOR_VERSION = "incident_exclamation_v1";
68486
+ var LineSelectorIncidentIcon = ({ lineId }) => /* @__PURE__ */ jsxRuntime.jsx(
68487
+ "span",
68488
+ {
68489
+ "data-testid": `line-selector-incident-icon-${lineId}`,
68490
+ "aria-label": "Line needs attention",
68491
+ role: "img",
68492
+ className: "inline-flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full border border-rose-200 bg-white text-[13px] font-semibold leading-none text-rose-600 shadow-[0_1px_2px_rgba(15,23,42,0.06)]",
68493
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", className: "-mt-px", children: "!" })
68494
+ }
68495
+ );
67595
68496
  var LoadingPageCmp = LoadingPage_default;
67596
68497
  var LoadingOverlayCmp = LoadingOverlay_default;
67597
68498
  function HomeView({
@@ -67931,25 +68832,49 @@ function HomeView({
67931
68832
  const currentIsCurrentScopeResolved = isBootstrapMonitorMode ? bootstrapMonitor.isCurrentScopeResolved : legacyIsCurrentScopeResolved;
67932
68833
  const currentMetricsError = isBootstrapMonitorMode ? bootstrapMonitor.error : legacyMetricsError;
67933
68834
  const currentRefetchMetrics = isBootstrapMonitorMode ? bootstrapMonitor.refetch : refetchLegacyMetrics;
67934
- const lineSelectorSignalStatusByLine = React125.useMemo(() => {
67935
- const statusByLine = /* @__PURE__ */ new Map();
68835
+ const lineSelectorIndicatorByLine = React125.useMemo(() => {
68836
+ const indicatorByLine = /* @__PURE__ */ new Map();
67936
68837
  const legend = currentEfficiencyLegend || DEFAULT_EFFICIENCY_LEGEND;
67937
68838
  const addRows = (rows) => {
67938
68839
  (rows || []).forEach((row) => {
67939
68840
  const lineId = typeof row?.line_id === "string" ? row.line_id : "";
67940
- if (!lineId || statusByLine.has(lineId)) {
68841
+ if (!lineId) {
68842
+ return;
68843
+ }
68844
+ if (row?.red_flow_incident?.active === true) {
68845
+ indicatorByLine.set(lineId, "incident");
68846
+ return;
68847
+ }
68848
+ if (indicatorByLine.get(lineId) === "incident") {
67941
68849
  return;
67942
68850
  }
67943
68851
  const status = getKpiSignalStatus(row?.line_signal, legend);
67944
- if (status) {
67945
- statusByLine.set(lineId, status);
68852
+ if (status === "attention") {
68853
+ indicatorByLine.set(lineId, "red");
67946
68854
  }
67947
68855
  });
67948
68856
  };
67949
68857
  addRows(currentSelectorLineMetrics);
67950
68858
  addRows(currentLineMetrics);
67951
- return statusByLine;
68859
+ return indicatorByLine;
67952
68860
  }, [currentEfficiencyLegend, currentLineMetrics, currentSelectorLineMetrics]);
68861
+ const lineSelectorIndicatorStats = React125.useMemo(() => {
68862
+ let incidentLineCount = 0;
68863
+ let redFlowLineCount = 0;
68864
+ visibleLineIds.forEach((lineId) => {
68865
+ const indicator = lineSelectorIndicatorByLine.get(lineId);
68866
+ if (indicator === "incident") {
68867
+ incidentLineCount += 1;
68868
+ } else if (indicator === "red") {
68869
+ redFlowLineCount += 1;
68870
+ }
68871
+ });
68872
+ return {
68873
+ incidentLineCount,
68874
+ redFlowLineCount,
68875
+ hasAnyIncident: incidentLineCount > 0
68876
+ };
68877
+ }, [lineSelectorIndicatorByLine, visibleLineIds]);
67953
68878
  const metricsDisplayNames = React125.useMemo(() => {
67954
68879
  const nextDisplayNames = {};
67955
68880
  currentWorkspaceMetrics.forEach((workspace) => {
@@ -68806,9 +69731,12 @@ function HomeView({
68806
69731
  new_line_ids: normalizedLineIds,
68807
69732
  selected_line_count: normalizedLineIds.length,
68808
69733
  selection_mode: isAllLinesSelection(normalizedLineIds) ? "all" : normalizedLineIds.length === 1 ? "single" : "custom",
69734
+ incident_line_count: lineSelectorIndicatorStats.incidentLineCount,
69735
+ red_flow_line_count: lineSelectorIndicatorStats.redFlowLineCount,
69736
+ selector_indicator_version: LINE_SELECTOR_INDICATOR_VERSION,
68809
69737
  line_name: getLineSelectionLabel(normalizedLineIds)
68810
69738
  });
68811
- }, [factoryViewId, getLineSelectionLabel, getTrackedLineScope, selectedLineIds, selectedLineIdsKey, visibleLineIds]);
69739
+ }, [factoryViewId, getLineSelectionLabel, getTrackedLineScope, lineSelectorIndicatorStats, selectedLineIds, selectedLineIdsKey, visibleLineIds]);
68812
69740
  React125.useCallback(() => {
68813
69741
  updateSelectedLineIds(visibleLineIds);
68814
69742
  }, [updateSelectedLineIds, visibleLineIds]);
@@ -68866,7 +69794,10 @@ function HomeView({
68866
69794
  current_display_mode_label: getHomeDisplayModeLabel(displayMode),
68867
69795
  selected_line_ids: selectedLineIds,
68868
69796
  selected_line_count: selectedLineIds.length,
68869
- is_all_lines: isAllLinesSelection(selectedLineIds)
69797
+ is_all_lines: isAllLinesSelection(selectedLineIds),
69798
+ incident_line_count: lineSelectorIndicatorStats.incidentLineCount,
69799
+ red_flow_line_count: lineSelectorIndicatorStats.redFlowLineCount,
69800
+ selector_indicator_version: LINE_SELECTOR_INDICATOR_VERSION
68870
69801
  });
68871
69802
  }
68872
69803
  },
@@ -68907,8 +69838,8 @@ function HomeView({
68907
69838
  ] }),
68908
69839
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "max-h-56 space-y-0.5 overflow-y-auto pr-1", children: visibleLineIds.map((lineId) => {
68909
69840
  const isChecked = pendingSelectedLineIds.includes(lineId);
68910
- const signalStatus = lineSelectorSignalStatusByLine.get(lineId);
68911
- const signalDotClass = signalStatus === "stable" ? "bg-green-500" : signalStatus === "warning" ? "bg-yellow-400" : signalStatus === "attention" ? "bg-red-500" : "";
69841
+ const selectorIndicator = lineSelectorIndicatorByLine.get(lineId);
69842
+ const lineLabel = mergedLineNames[lineId] || `Line ${lineId.substring(0, 4)}`;
68912
69843
  return /* @__PURE__ */ jsxRuntime.jsxs(
68913
69844
  "label",
68914
69845
  {
@@ -68918,6 +69849,7 @@ function HomeView({
68918
69849
  "input",
68919
69850
  {
68920
69851
  type: "checkbox",
69852
+ "aria-label": lineLabel,
68921
69853
  className: "h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500",
68922
69854
  checked: isChecked,
68923
69855
  onChange: () => {
@@ -68933,12 +69865,12 @@ function HomeView({
68933
69865
  }
68934
69866
  }
68935
69867
  ),
68936
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 truncate", children: mergedLineNames[lineId] || `Line ${lineId.substring(0, 4)}` }),
68937
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-2.5 w-2.5 flex-shrink-0 items-center justify-center", children: signalStatus ? /* @__PURE__ */ jsxRuntime.jsx(
69868
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 truncate", children: lineLabel }),
69869
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-5 w-5 flex-shrink-0 items-center justify-center", children: selectorIndicator === "incident" ? /* @__PURE__ */ jsxRuntime.jsx(LineSelectorIncidentIcon, { lineId }) : selectorIndicator === "red" && !lineSelectorIndicatorStats.hasAnyIncident ? /* @__PURE__ */ jsxRuntime.jsx(
68938
69870
  "span",
68939
69871
  {
68940
69872
  "data-testid": `line-selector-signal-dot-${lineId}`,
68941
- className: `h-2 w-2 rounded-full ${signalDotClass}`,
69873
+ className: "h-2 w-2 rounded-full bg-red-500",
68942
69874
  "aria-hidden": "true"
68943
69875
  }
68944
69876
  ) : null })
@@ -68970,7 +69902,8 @@ function HomeView({
68970
69902
  mergedLineNames,
68971
69903
  selectedLineIds,
68972
69904
  pendingSelectedLineIds,
68973
- lineSelectorSignalStatusByLine,
69905
+ lineSelectorIndicatorByLine,
69906
+ lineSelectorIndicatorStats,
68974
69907
  displayMode,
68975
69908
  slideshowActiveLineId,
68976
69909
  visibleLineIds,
@@ -83931,11 +84864,13 @@ var useWorkspaceHealth = (options) => {
83931
84864
  var STATUS_COLORS = {
83932
84865
  up: "bg-emerald-500",
83933
84866
  down: "bg-rose-500",
84867
+ unknown: "bg-amber-400",
83934
84868
  pending: "bg-gray-200"
83935
84869
  };
83936
84870
  var STATUS_TITLES = {
83937
84871
  up: "Uptime",
83938
84872
  down: "Downtime",
84873
+ unknown: "Unknown",
83939
84874
  pending: "Pending"
83940
84875
  };
83941
84876
  var formatTime4 = (date, timezone) => new Intl.DateTimeFormat("en-IN", {
@@ -83978,7 +84913,9 @@ var UptimeTimelineStrip = ({
83978
84913
  timezone,
83979
84914
  className = "",
83980
84915
  uptimePercentage = null,
83981
- downtimeMinutes = 0
84916
+ downtimeMinutes = 0,
84917
+ metricLabel = "uptime",
84918
+ summaryText
83982
84919
  }) => {
83983
84920
  const segments = React125.useMemo(() => {
83984
84921
  if (!points.length || totalMinutes <= 0) return [];
@@ -84042,9 +84979,11 @@ var UptimeTimelineStrip = ({
84042
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." });
84043
84980
  }
84044
84981
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `relative w-full ${className}`, children: [
84045
- /* @__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: [
84046
84983
  uptimePercentage.toFixed(1),
84047
- " % uptime ",
84984
+ " % ",
84985
+ metricLabel,
84986
+ " ",
84048
84987
  downtimeMinutes > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-gray-600 font-normal", children: [
84049
84988
  "(",
84050
84989
  formatDowntimeLabel(downtimeMinutes),
@@ -84112,6 +85051,27 @@ var formatDowntimeLabel2 = (minutes, includeSuffix = true) => {
84112
85051
  const label = formatDuration4(minutes);
84113
85052
  return includeSuffix ? `${label} down` : label;
84114
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
+ };
84115
85075
  var formatTimeRange = (start, end, timezone) => {
84116
85076
  const formatter = new Intl.DateTimeFormat("en-IN", {
84117
85077
  hour: "numeric",
@@ -84127,12 +85087,15 @@ var WorkspaceUptimeDetailModal = ({
84127
85087
  onClose,
84128
85088
  shiftConfig: passedShiftConfig,
84129
85089
  date,
84130
- shiftId
85090
+ shiftId,
85091
+ initialMode = "camera"
84131
85092
  }) => {
84132
85093
  const timezone = useAppTimezone() || "UTC";
84133
85094
  const logsContainerRef = React125.useRef(null);
84134
85095
  const [showScrollIndicator, setShowScrollIndicator] = React125.useState(false);
85096
+ const [activeMode, setActiveMode] = React125.useState(initialMode);
84135
85097
  const isHistorical = Boolean(date);
85098
+ const hasLightTimeline = Boolean(workspace?.lightSummary?.hasLightConfig && workspace?.lightSummary?.bulbIp);
84136
85099
  const {
84137
85100
  timeline,
84138
85101
  loading,
@@ -84141,7 +85104,7 @@ var WorkspaceUptimeDetailModal = ({
84141
85104
  } = useWorkspaceUptimeTimeline({
84142
85105
  workspaceId: workspace?.workspace_id,
84143
85106
  companyId: workspace?.company_id,
84144
- enabled: isOpen && Boolean(workspace?.workspace_id && workspace?.company_id),
85107
+ enabled: isOpen && activeMode === "camera" && Boolean(workspace?.workspace_id && workspace?.company_id),
84145
85108
  refreshInterval: isHistorical ? void 0 : 6e4,
84146
85109
  // Disable auto-refresh for historical
84147
85110
  lineId: workspace?.line_id,
@@ -84154,6 +85117,25 @@ var WorkspaceUptimeDetailModal = ({
84154
85117
  shiftId
84155
85118
  // Pass override shift ID for historical queries
84156
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]);
84157
85139
  React125.useEffect(() => {
84158
85140
  if (!isOpen || !workspace) return;
84159
85141
  const handleKeyDown = (event) => {
@@ -84166,13 +85148,21 @@ var WorkspaceUptimeDetailModal = ({
84166
85148
  window.removeEventListener("keydown", handleKeyDown);
84167
85149
  };
84168
85150
  }, [isOpen, onClose, workspace]);
84169
- const shiftStart = timeline ? new Date(timeline.shiftStart) : null;
84170
- 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;
84171
85157
  const downtimeSegments = timeline?.downtimeSegments || [];
84172
85158
  downtimeSegments.length;
84173
85159
  const downtimeMinutes = timeline?.downtimeMinutes ?? 0;
84174
- const hasTimelineData = Boolean(timeline?.hasData);
84175
- 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);
84176
85166
  const allInterruptionsSorted = React125.useMemo(
84177
85167
  () => [...downtimeSegments].sort(
84178
85168
  (a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime()
@@ -84194,7 +85184,7 @@ var WorkspaceUptimeDetailModal = ({
84194
85184
  container.addEventListener("scroll", checkScroll);
84195
85185
  return () => container.removeEventListener("scroll", checkScroll);
84196
85186
  }
84197
- }, [downtimeSegments]);
85187
+ }, [downtimeSegments, lightTimeline?.statusSegments, activeMode]);
84198
85188
  const renderSegment = (segment) => {
84199
85189
  const start = new Date(segment.startTime);
84200
85190
  const end = new Date(segment.endTime);
@@ -84215,6 +85205,63 @@ var WorkspaceUptimeDetailModal = ({
84215
85205
  `${segment.startMinuteIndex}-${segment.endMinuteIndex}`
84216
85206
  );
84217
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
+ };
84218
85265
  if (!isOpen || !workspace) {
84219
85266
  return null;
84220
85267
  }
@@ -84238,13 +85285,35 @@ var WorkspaceUptimeDetailModal = ({
84238
85285
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0 mr-4", children: [
84239
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)}` }),
84240
85287
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2.5 text-sm text-gray-600", children: [
84241
- /* @__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" }),
84242
85289
  shiftStart && shiftEnd && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
84243
85290
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-300", children: "\u2022" }),
84244
85291
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-600", children: formatTimeRange(shiftStart, shiftEnd, timezone) })
84245
85292
  ] })
84246
85293
  ] }),
84247
- /* @__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: [
84248
85317
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
84249
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"}` }),
84250
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" })
@@ -84261,17 +85330,37 @@ var WorkspaceUptimeDetailModal = ({
84261
85330
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-slate-600 dark:text-slate-300 select-all", children: workspace.cameraIp })
84262
85331
  ] })
84263
85332
  ] })
84264
- ] })
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
+ ] }) })
84265
85354
  ] }),
84266
85355
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 flex-shrink-0", children: [
84267
85356
  /* @__PURE__ */ jsxRuntime.jsxs(
84268
85357
  "button",
84269
85358
  {
84270
- onClick: refetch,
84271
- disabled: loading,
85359
+ onClick: handleRefresh,
85360
+ disabled: activeLoading,
84272
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",
84273
85362
  children: [
84274
- /* @__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" : ""}` }),
84275
85364
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: "Refresh" })
84276
85365
  ]
84277
85366
  }
@@ -84287,31 +85376,63 @@ var WorkspaceUptimeDetailModal = ({
84287
85376
  )
84288
85377
  ] })
84289
85378
  ] }),
84290
- /* @__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: [
84291
85380
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-semibold mb-1", children: "Unable to load uptime details" }),
84292
- /* @__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 })
84293
85382
  ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
84294
85383
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative pb-4 border-b border-gray-200", children: [
84295
85384
  /* @__PURE__ */ jsxRuntime.jsx(
84296
85385
  UptimeTimelineStrip_default,
84297
85386
  {
84298
- points: timeline?.points || [],
84299
- totalMinutes: timeline?.totalMinutes || 0,
84300
- shiftStart: timeline?.shiftStart || (/* @__PURE__ */ new Date()).toISOString(),
84301
- 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(),
84302
85391
  timezone,
84303
85392
  uptimePercentage,
84304
- 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
84305
85409
  }
84306
85410
  ),
84307
- 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: [
84308
85412
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { className: "h-4 w-4 animate-spin text-gray-500" }),
84309
85413
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Updating timeline\u2026" })
84310
85414
  ] })
84311
85415
  ] }),
84312
85416
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pt-4", children: [
84313
- /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold text-gray-900 uppercase tracking-wider mb-3", children: "Downtime Logs" }),
84314
- !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: [
84315
85436
  /* @__PURE__ */ jsxRuntime.jsx(
84316
85437
  "div",
84317
85438
  {
@@ -84358,6 +85479,7 @@ var WorkspaceHealthView = ({
84358
85479
  const timezone = useAppTimezone();
84359
85480
  const { shiftConfig, isLoading: isShiftConfigLoading } = useDynamicShiftConfig(effectiveLineIdForShiftConfig);
84360
85481
  const [selectedWorkspace, setSelectedWorkspace] = React125.useState(null);
85482
+ const [selectedTimelineMode, setSelectedTimelineMode] = React125.useState("camera");
84361
85483
  const [selectedDate, setSelectedDate] = React125.useState(void 0);
84362
85484
  const [selectedShiftId, setSelectedShiftId] = React125.useState(void 0);
84363
85485
  const [showDatePicker, setShowDatePicker] = React125.useState(false);
@@ -84417,11 +85539,33 @@ var WorkspaceHealthView = ({
84417
85539
  },
84418
85540
  [router$1, onNavigate]
84419
85541
  );
84420
- 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);
84421
85564
  setSelectedWorkspace(workspace);
84422
- }, []);
85565
+ }, [currentShiftDetails.shiftId, operationalDate, selectedDate, selectedShiftId]);
84423
85566
  const handleCloseDetails = React125.useCallback(() => {
84424
85567
  setSelectedWorkspace(null);
85568
+ setSelectedTimelineMode("camera");
84425
85569
  }, []);
84426
85570
  const getStatusIcon = (status) => {
84427
85571
  switch (status) {
@@ -84679,7 +85823,8 @@ var WorkspaceHealthView = ({
84679
85823
  onClose: handleCloseDetails,
84680
85824
  shiftConfig: modalShiftConfig,
84681
85825
  date: selectedDate,
84682
- shiftId: selectedShiftId
85826
+ shiftId: selectedShiftId,
85827
+ initialMode: selectedTimelineMode
84683
85828
  }
84684
85829
  )
84685
85830
  ] });
@@ -93495,6 +94640,7 @@ exports.useWorkspaceHealthById = useWorkspaceHealthById;
93495
94640
  exports.useWorkspaceHealthLastSeen = useWorkspaceHealthLastSeen;
93496
94641
  exports.useWorkspaceHealthStatus = useWorkspaceHealthStatus;
93497
94642
  exports.useWorkspaceHourSummary = useWorkspaceHourSummary;
94643
+ exports.useWorkspaceLightTimeline = useWorkspaceLightTimeline;
93498
94644
  exports.useWorkspaceMetrics = useWorkspaceMetrics;
93499
94645
  exports.useWorkspaceNavigation = useWorkspaceNavigation;
93500
94646
  exports.useWorkspaceOperators = useWorkspaceOperators;