@optifye/dashboard-core 6.9.11 → 6.9.13

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
@@ -2163,6 +2163,88 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
2163
2163
  setCache(key, data) {
2164
2164
  this.cache.set(key, { data, timestamp: Date.now() });
2165
2165
  }
2166
+ getShiftTiming(timezone, shiftConfig) {
2167
+ const { shiftId, date } = getCurrentShift(timezone, shiftConfig);
2168
+ const dayShiftId = shiftConfig?.dayShift?.id ?? 0;
2169
+ const isDayShift = shiftId === dayShiftId;
2170
+ const defaultDayStart = "06:00";
2171
+ const defaultDayEnd = "18:00";
2172
+ const defaultNightStart = "18:00";
2173
+ const defaultNightEnd = "06:00";
2174
+ const shiftStartStr = isDayShift ? shiftConfig?.dayShift?.startTime || defaultDayStart : shiftConfig?.nightShift?.startTime || defaultNightStart;
2175
+ const shiftEndStr = isDayShift ? shiftConfig?.dayShift?.endTime || defaultDayEnd : shiftConfig?.nightShift?.endTime || defaultNightEnd;
2176
+ const shiftLabel = isDayShift ? "Day Shift" : "Night Shift";
2177
+ const parseTime = (value) => {
2178
+ const [hourPart = "0", minutePart = "0"] = value.split(":");
2179
+ const hour = Number.parseInt(hourPart, 10);
2180
+ const minute = Number.parseInt(minutePart, 10);
2181
+ return {
2182
+ hour: Number.isFinite(hour) ? hour : 0,
2183
+ minute: Number.isFinite(minute) ? minute : 0
2184
+ };
2185
+ };
2186
+ const getShiftDurationMinutes = (start, end) => {
2187
+ const startParsed = parseTime(start);
2188
+ const endParsed = parseTime(end);
2189
+ const startTotal = startParsed.hour * 60 + startParsed.minute;
2190
+ const endTotal = endParsed.hour * 60 + endParsed.minute;
2191
+ let duration = endTotal - startTotal;
2192
+ if (duration <= 0) {
2193
+ duration += 24 * 60;
2194
+ }
2195
+ return duration;
2196
+ };
2197
+ const shiftStartDate = dateFnsTz.fromZonedTime(`${date}T${shiftStartStr}:00`, timezone);
2198
+ const totalMinutes = getShiftDurationMinutes(shiftStartStr, shiftEndStr);
2199
+ const shiftEndDate = dateFns.addMinutes(shiftStartDate, totalMinutes);
2200
+ const now2 = /* @__PURE__ */ new Date();
2201
+ let completedMinutes = dateFns.differenceInMinutes(now2 < shiftEndDate ? now2 : shiftEndDate, shiftStartDate);
2202
+ if (completedMinutes < 0) completedMinutes = 0;
2203
+ if (completedMinutes > totalMinutes) completedMinutes = totalMinutes;
2204
+ const pendingMinutes = Math.max(0, totalMinutes - completedMinutes);
2205
+ return {
2206
+ shiftId,
2207
+ date,
2208
+ shiftLabel,
2209
+ shiftStartDate,
2210
+ shiftEndDate,
2211
+ totalMinutes,
2212
+ completedMinutes,
2213
+ pendingMinutes
2214
+ };
2215
+ }
2216
+ normalizeHourBucket(bucket) {
2217
+ if (Array.isArray(bucket)) return bucket;
2218
+ if (bucket && Array.isArray(bucket.values)) return bucket.values;
2219
+ return void 0;
2220
+ }
2221
+ normalizeOutputHourly(outputHourlyRaw) {
2222
+ return outputHourlyRaw && typeof outputHourlyRaw === "object" ? Object.fromEntries(
2223
+ Object.entries(outputHourlyRaw).map(([key, value]) => [key, this.normalizeHourBucket(value) || []])
2224
+ ) : {};
2225
+ }
2226
+ interpretUptimeValue(value) {
2227
+ if (value === null || value === void 0) return "down";
2228
+ if (typeof value === "string") {
2229
+ return value.trim().toLowerCase() === "x" ? "down" : "up";
2230
+ }
2231
+ return "up";
2232
+ }
2233
+ deriveStatusForMinute(minuteOffset, minuteDate, outputHourly, outputArray, timezone) {
2234
+ const hourKey = dateFnsTz.formatInTimeZone(minuteDate, timezone, "H");
2235
+ const minuteKey = Number.parseInt(dateFnsTz.formatInTimeZone(minuteDate, timezone, "m"), 10);
2236
+ const hourBucket = outputHourly[hourKey];
2237
+ if (Array.isArray(hourBucket)) {
2238
+ const value = hourBucket[minuteKey];
2239
+ if (value !== void 0) {
2240
+ return this.interpretUptimeValue(value);
2241
+ }
2242
+ }
2243
+ if (minuteOffset < outputArray.length) {
2244
+ return this.interpretUptimeValue(outputArray[minuteOffset]);
2245
+ }
2246
+ return "down";
2247
+ }
2166
2248
  async getWorkspaceHealthStatus(options = {}) {
2167
2249
  const supabase = _getSupabaseInstance();
2168
2250
  if (!supabase) throw new Error("Supabase client not initialized");
@@ -2179,15 +2261,13 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
2179
2261
  throw error;
2180
2262
  }
2181
2263
  const processedData = (data || []).map((item) => this.processHealthStatus(item));
2264
+ const companyId = options.companyId || data?.[0]?.company_id;
2182
2265
  let uptimeMap = /* @__PURE__ */ new Map();
2183
- if (options.companyId || data && data.length > 0) {
2184
- const companyId = options.companyId || data[0]?.company_id;
2185
- if (companyId) {
2186
- try {
2187
- uptimeMap = await this.calculateWorkspaceUptime(companyId);
2188
- } catch (error2) {
2189
- console.error("Error calculating uptime:", error2);
2190
- }
2266
+ if (companyId) {
2267
+ try {
2268
+ uptimeMap = await this.calculateWorkspaceUptime(companyId);
2269
+ } catch (error2) {
2270
+ console.error("Error calculating uptime:", error2);
2191
2271
  }
2192
2272
  }
2193
2273
  const dataWithUptime = processedData.map((workspace) => {
@@ -2203,16 +2283,29 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
2203
2283
  });
2204
2284
  let filteredData = dataWithUptime;
2205
2285
  try {
2206
- const { data: enabledWorkspaces, error: workspaceError } = await supabase.from("workspaces").select("workspace_id, display_name").eq("enable", true);
2286
+ const enabledQuery = supabase.from("workspaces").select("workspace_id, display_name, line_id").eq("enable", true);
2287
+ if (options.lineId) {
2288
+ enabledQuery.eq("line_id", options.lineId);
2289
+ }
2290
+ const { data: enabledWorkspaces, error: workspaceError } = await enabledQuery;
2207
2291
  if (!workspaceError && enabledWorkspaces && enabledWorkspaces.length > 0) {
2208
- const enabledWorkspaceNames = /* @__PURE__ */ new Set();
2292
+ const enabledByLineAndId = /* @__PURE__ */ new Set();
2293
+ const enabledByLineAndName = /* @__PURE__ */ new Set();
2209
2294
  enabledWorkspaces.forEach((w) => {
2210
- if (w.workspace_id) enabledWorkspaceNames.add(w.workspace_id);
2211
- if (w.display_name) enabledWorkspaceNames.add(w.display_name);
2295
+ const lineKey = w.line_id ? String(w.line_id) : "";
2296
+ const idKey = w.workspace_id ? String(w.workspace_id) : "";
2297
+ const nameKey = w.display_name ? String(w.display_name) : "";
2298
+ if (lineKey && idKey) enabledByLineAndId.add(`${lineKey}::${idKey}`);
2299
+ if (lineKey && nameKey) enabledByLineAndName.add(`${lineKey}::${nameKey}`);
2212
2300
  });
2213
2301
  filteredData = filteredData.filter((item) => {
2214
- const displayName = item.workspace_display_name || "";
2215
- return enabledWorkspaceNames.has(displayName);
2302
+ const lineKey = item.line_id ? String(item.line_id) : "";
2303
+ if (!lineKey) return false;
2304
+ const idKey = item.workspace_id ? `${lineKey}::${item.workspace_id}` : "";
2305
+ const nameKey = item.workspace_display_name ? `${lineKey}::${item.workspace_display_name}` : "";
2306
+ if (idKey && enabledByLineAndId.has(idKey)) return true;
2307
+ if (nameKey && enabledByLineAndName.has(nameKey)) return true;
2308
+ return false;
2216
2309
  });
2217
2310
  } else if (!workspaceError && enabledWorkspaces && enabledWorkspaces.length === 0) {
2218
2311
  return [];
@@ -2250,6 +2343,127 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
2250
2343
  }
2251
2344
  return filteredData;
2252
2345
  }
2346
+ async getWorkspaceUptimeTimeline(workspaceId, companyId) {
2347
+ if (!workspaceId) {
2348
+ throw new Error("workspaceId is required to fetch uptime timeline");
2349
+ }
2350
+ if (!companyId) {
2351
+ throw new Error("companyId is required to fetch uptime timeline");
2352
+ }
2353
+ const supabase = _getSupabaseInstance();
2354
+ if (!supabase) throw new Error("Supabase client not initialized");
2355
+ const dashboardConfig = _getDashboardConfigInstance();
2356
+ const timezone = dashboardConfig?.dateTimeConfig?.defaultTimezone || "UTC";
2357
+ const shiftConfig = dashboardConfig?.shiftConfig;
2358
+ const {
2359
+ shiftId,
2360
+ shiftLabel,
2361
+ shiftStartDate,
2362
+ shiftEndDate,
2363
+ totalMinutes,
2364
+ completedMinutes,
2365
+ pendingMinutes,
2366
+ date
2367
+ } = this.getShiftTiming(timezone, shiftConfig);
2368
+ const tableName = `performance_metrics_${companyId.replace(/-/g, "_")}`;
2369
+ const { data, error } = await supabase.from(tableName).select("workspace_id, output_hourly, output_array").eq("workspace_id", workspaceId).eq("date", date).eq("shift_id", shiftId).limit(1);
2370
+ if (error) {
2371
+ console.error("Error fetching uptime timeline metrics:", error);
2372
+ throw error;
2373
+ }
2374
+ const record = Array.isArray(data) && data.length > 0 ? data[0] : null;
2375
+ const outputHourly = this.normalizeOutputHourly(record?.output_hourly || {});
2376
+ const outputArray = Array.isArray(record?.output_array) ? record.output_array : [];
2377
+ const points = [];
2378
+ let uptimeMinutes = 0;
2379
+ let downtimeMinutes = 0;
2380
+ const MIN_DOWNTIME_MINUTES = 2;
2381
+ for (let minuteIndex = 0; minuteIndex < totalMinutes; minuteIndex++) {
2382
+ const minuteDate = dateFns.addMinutes(shiftStartDate, minuteIndex);
2383
+ const timestamp = dateFnsTz.formatInTimeZone(minuteDate, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX");
2384
+ let status;
2385
+ if (minuteIndex < completedMinutes) {
2386
+ status = this.deriveStatusForMinute(
2387
+ minuteIndex,
2388
+ minuteDate,
2389
+ outputHourly,
2390
+ outputArray,
2391
+ timezone
2392
+ );
2393
+ if (status === "up") {
2394
+ uptimeMinutes += 1;
2395
+ } else {
2396
+ downtimeMinutes += 1;
2397
+ }
2398
+ } else {
2399
+ status = "pending";
2400
+ }
2401
+ points.push({
2402
+ minuteIndex,
2403
+ timestamp,
2404
+ status
2405
+ });
2406
+ }
2407
+ const downtimeSegments = [];
2408
+ let currentSegmentStart = null;
2409
+ const pushSegment = (startIndex, endIndex) => {
2410
+ if (endIndex <= startIndex) return;
2411
+ const segmentStartDate = dateFns.addMinutes(shiftStartDate, startIndex);
2412
+ const segmentEndDate = dateFns.addMinutes(shiftStartDate, endIndex);
2413
+ downtimeSegments.push({
2414
+ startMinuteIndex: startIndex,
2415
+ endMinuteIndex: endIndex,
2416
+ startTime: dateFnsTz.formatInTimeZone(segmentStartDate, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX"),
2417
+ endTime: dateFnsTz.formatInTimeZone(segmentEndDate, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX"),
2418
+ durationMinutes: endIndex - startIndex
2419
+ });
2420
+ };
2421
+ for (let i = 0; i < completedMinutes; i++) {
2422
+ const point = points[i];
2423
+ if (point.status === "down") {
2424
+ if (currentSegmentStart === null) {
2425
+ currentSegmentStart = i;
2426
+ }
2427
+ } else if (currentSegmentStart !== null) {
2428
+ pushSegment(currentSegmentStart, i);
2429
+ currentSegmentStart = null;
2430
+ }
2431
+ }
2432
+ if (currentSegmentStart !== null) {
2433
+ pushSegment(currentSegmentStart, completedMinutes);
2434
+ }
2435
+ const filteredSegments = [];
2436
+ downtimeSegments.forEach((segment) => {
2437
+ if (segment.durationMinutes >= MIN_DOWNTIME_MINUTES) {
2438
+ filteredSegments.push(segment);
2439
+ } else {
2440
+ for (let i = segment.startMinuteIndex; i < segment.endMinuteIndex; i++) {
2441
+ if (points[i] && points[i].status === "down") {
2442
+ points[i].status = "up";
2443
+ downtimeMinutes = Math.max(0, downtimeMinutes - 1);
2444
+ uptimeMinutes += 1;
2445
+ }
2446
+ }
2447
+ }
2448
+ });
2449
+ const completedWindow = Math.max(1, uptimeMinutes + downtimeMinutes);
2450
+ const uptimePercentage = completedMinutes > 0 ? Number((uptimeMinutes / completedWindow * 100).toFixed(1)) : 100;
2451
+ return {
2452
+ shiftId,
2453
+ shiftLabel,
2454
+ shiftStart: dateFnsTz.formatInTimeZone(shiftStartDate, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX"),
2455
+ shiftEnd: dateFnsTz.formatInTimeZone(shiftEndDate, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX"),
2456
+ totalMinutes,
2457
+ completedMinutes,
2458
+ uptimeMinutes,
2459
+ downtimeMinutes,
2460
+ pendingMinutes,
2461
+ uptimePercentage,
2462
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2463
+ points,
2464
+ downtimeSegments: filteredSegments
2465
+ };
2466
+ }
2253
2467
  async getWorkspaceHealthById(workspaceId) {
2254
2468
  const cacheKey = `health-${workspaceId}`;
2255
2469
  const cached = this.getFromCache(cacheKey);
@@ -2387,145 +2601,63 @@ var WorkspaceHealthService = class _WorkspaceHealthService {
2387
2601
  const dashboardConfig = _getDashboardConfigInstance();
2388
2602
  const timezone = dashboardConfig?.dateTimeConfig?.defaultTimezone || "UTC";
2389
2603
  const shiftConfig = dashboardConfig?.shiftConfig;
2390
- const currentShiftInfo = getCurrentShift(timezone, shiftConfig);
2391
- const currentDate = currentShiftInfo.date;
2392
- const currentShiftId = currentShiftInfo.shiftId;
2393
- const now2 = /* @__PURE__ */ new Date();
2394
- const currentHour = now2.getHours();
2395
- const currentMinute = now2.getMinutes();
2396
- const dayShiftStart = shiftConfig?.dayShift?.startTime || "08:00";
2397
- const dayShiftEnd = shiftConfig?.dayShift?.endTime || "19:30";
2398
- const nightShiftStart = shiftConfig?.nightShift?.startTime || "19:30";
2399
- const nightShiftEnd = shiftConfig?.nightShift?.endTime || "06:00";
2400
- const parseShiftTime = (timeStr) => {
2401
- const [hour, minute] = timeStr.split(":").map(Number);
2402
- return { hour, minute };
2403
- };
2404
- let shiftStart, shiftEnd;
2405
- if (currentShiftId === 0) {
2406
- shiftStart = parseShiftTime(dayShiftStart);
2407
- shiftEnd = parseShiftTime(dayShiftEnd);
2408
- } else {
2409
- shiftStart = parseShiftTime(nightShiftStart);
2410
- shiftEnd = parseShiftTime(nightShiftEnd);
2411
- }
2412
- let elapsedMinutes = 0;
2413
- if (currentShiftId === 0) {
2414
- const shiftStartTotalMinutes = shiftStart.hour * 60 + shiftStart.minute;
2415
- const currentTotalMinutes = currentHour * 60 + currentMinute;
2416
- const shiftEndTotalMinutes = shiftEnd.hour * 60 + shiftEnd.minute;
2417
- if (currentTotalMinutes >= shiftStartTotalMinutes && currentTotalMinutes < shiftEndTotalMinutes) {
2418
- elapsedMinutes = currentTotalMinutes - shiftStartTotalMinutes;
2419
- } else if (currentTotalMinutes >= shiftEndTotalMinutes) {
2420
- elapsedMinutes = shiftEndTotalMinutes - shiftStartTotalMinutes;
2421
- }
2422
- } else {
2423
- const shiftStartTotalMinutes = shiftStart.hour * 60 + shiftStart.minute;
2424
- const currentTotalMinutes = currentHour * 60 + currentMinute;
2425
- const shiftEndTotalMinutes = shiftEnd.hour * 60 + shiftEnd.minute;
2426
- if (currentHour >= shiftStart.hour || currentHour < shiftEnd.hour) {
2427
- if (currentHour >= shiftStart.hour) {
2428
- elapsedMinutes = currentTotalMinutes - shiftStartTotalMinutes;
2429
- } else {
2430
- elapsedMinutes = 24 * 60 - shiftStartTotalMinutes + currentTotalMinutes;
2431
- }
2432
- if (currentHour >= shiftEnd.hour && currentTotalMinutes >= shiftEndTotalMinutes) {
2433
- const totalShiftMinutes = 24 * 60 - shiftStartTotalMinutes + shiftEndTotalMinutes;
2434
- elapsedMinutes = Math.min(elapsedMinutes, totalShiftMinutes);
2435
- }
2436
- }
2437
- }
2604
+ const {
2605
+ shiftId,
2606
+ date,
2607
+ shiftStartDate,
2608
+ completedMinutes
2609
+ } = this.getShiftTiming(timezone, shiftConfig);
2438
2610
  const tableName = `performance_metrics_${companyId.replace(/-/g, "_")}`;
2439
2611
  try {
2440
- const { data: queryData, error } = await supabase.from(tableName).select("workspace_id, workspace_display_name, output_hourly").eq("date", currentDate).eq("shift_id", currentShiftId);
2612
+ const { data: queryData, error } = await supabase.from(tableName).select("workspace_id, workspace_display_name, output_hourly, output_array").eq("date", date).eq("shift_id", shiftId);
2441
2613
  if (error) {
2442
2614
  console.error("Error fetching performance metrics:", error);
2443
2615
  return /* @__PURE__ */ new Map();
2444
2616
  }
2445
2617
  const uptimeMap = /* @__PURE__ */ new Map();
2446
2618
  for (const record of queryData || []) {
2447
- let actualMinutes = 0;
2448
- let totalPossibleMinutes = 0;
2449
- const outputHourly = record.output_hourly || {};
2450
- const hoursElapsed = Math.ceil(elapsedMinutes / 60);
2451
- for (let hourIndex = 0; hourIndex < hoursElapsed; hourIndex++) {
2452
- const actualHour = (shiftStart.hour + hourIndex) % 24;
2453
- let hourData = [];
2454
- let minutesInThisHour = 60;
2455
- if (shiftStart.minute > 0) {
2456
- if (hourIndex === 0) {
2457
- const firstHourData = outputHourly[actualHour.toString()] || [];
2458
- const nextHour = (actualHour + 1) % 24;
2459
- const nextHourData = outputHourly[nextHour.toString()] || [];
2460
- hourData = [
2461
- ...Array.isArray(firstHourData) ? firstHourData.slice(shiftStart.minute) : [],
2462
- ...Array.isArray(nextHourData) ? nextHourData.slice(0, shiftStart.minute) : []
2463
- ];
2464
- } else if (hourIndex < hoursElapsed - 1) {
2465
- const currentHourData = outputHourly[actualHour.toString()] || [];
2466
- const nextHour = (actualHour + 1) % 24;
2467
- const nextHourData = outputHourly[nextHour.toString()] || [];
2468
- hourData = [
2469
- ...Array.isArray(currentHourData) ? currentHourData.slice(shiftStart.minute) : [],
2470
- ...Array.isArray(nextHourData) ? nextHourData.slice(0, shiftStart.minute) : []
2471
- ];
2472
- } else {
2473
- const isLastHourPartial = hourIndex === hoursElapsed - 1 && elapsedMinutes % 60 > 0;
2474
- if (isLastHourPartial) {
2475
- minutesInThisHour = elapsedMinutes % 60;
2476
- const currentHourData = outputHourly[actualHour.toString()] || [];
2477
- if (shiftStart.minute > 0) {
2478
- const nextHour = (actualHour + 1) % 24;
2479
- const nextHourData = outputHourly[nextHour.toString()] || [];
2480
- const firstPart = Array.isArray(currentHourData) ? currentHourData.slice(shiftStart.minute) : [];
2481
- const secondPart = Array.isArray(nextHourData) ? nextHourData.slice(0, Math.min(shiftStart.minute, minutesInThisHour)) : [];
2482
- hourData = [...firstPart, ...secondPart].slice(0, minutesInThisHour);
2483
- } else {
2484
- hourData = Array.isArray(currentHourData) ? currentHourData.slice(0, minutesInThisHour) : [];
2485
- }
2486
- } else {
2487
- const currentHourData = outputHourly[actualHour.toString()] || [];
2488
- const nextHour = (actualHour + 1) % 24;
2489
- const nextHourData = outputHourly[nextHour.toString()] || [];
2490
- hourData = [
2491
- ...Array.isArray(currentHourData) ? currentHourData.slice(shiftStart.minute) : [],
2492
- ...Array.isArray(nextHourData) ? nextHourData.slice(0, shiftStart.minute) : []
2493
- ];
2494
- }
2495
- }
2619
+ const outputHourly = this.normalizeOutputHourly(record.output_hourly || {});
2620
+ const outputArray = Array.isArray(record.output_array) ? record.output_array : [];
2621
+ let uptimeMinutes = 0;
2622
+ let downtimeMinutes = 0;
2623
+ const MIN_DOWNTIME_MINUTES = 2;
2624
+ let currentDownRun = 0;
2625
+ for (let minuteIndex = 0; minuteIndex < completedMinutes; minuteIndex++) {
2626
+ const minuteDate = dateFns.addMinutes(shiftStartDate, minuteIndex);
2627
+ const status = this.deriveStatusForMinute(
2628
+ minuteIndex,
2629
+ minuteDate,
2630
+ outputHourly,
2631
+ outputArray,
2632
+ timezone
2633
+ );
2634
+ if (status === "down") {
2635
+ currentDownRun += 1;
2496
2636
  } else {
2497
- hourData = outputHourly[actualHour.toString()] || [];
2498
- if (hourIndex === hoursElapsed - 1) {
2499
- const remainingMinutes = elapsedMinutes % 60;
2500
- if (remainingMinutes > 0) {
2501
- minutesInThisHour = remainingMinutes;
2502
- hourData = Array.isArray(hourData) ? hourData.slice(0, minutesInThisHour) : [];
2637
+ if (currentDownRun > 0) {
2638
+ if (currentDownRun >= MIN_DOWNTIME_MINUTES) {
2639
+ downtimeMinutes += currentDownRun;
2640
+ } else {
2641
+ uptimeMinutes += currentDownRun;
2503
2642
  }
2643
+ currentDownRun = 0;
2504
2644
  }
2645
+ uptimeMinutes += 1;
2505
2646
  }
2506
- const hasXArchitecture = Array.isArray(hourData) && hourData.some((val) => val === "x");
2507
- if (hasXArchitecture) {
2508
- const xCount = hourData.filter((val) => val === "x").length;
2509
- if (xCount <= 2) {
2510
- actualMinutes += minutesInThisHour;
2511
- } else {
2512
- const uptimeCount = hourData.filter((val) => val !== "x").length;
2513
- actualMinutes += Math.min(uptimeCount, minutesInThisHour);
2514
- }
2647
+ }
2648
+ if (currentDownRun > 0) {
2649
+ if (currentDownRun >= MIN_DOWNTIME_MINUTES) {
2650
+ downtimeMinutes += currentDownRun;
2515
2651
  } else {
2516
- const arrayLength = Array.isArray(hourData) ? hourData.length : 0;
2517
- if (arrayLength >= Math.min(58, minutesInThisHour - 2)) {
2518
- actualMinutes += minutesInThisHour;
2519
- } else {
2520
- actualMinutes += Math.min(arrayLength, minutesInThisHour);
2521
- }
2652
+ uptimeMinutes += currentDownRun;
2522
2653
  }
2523
- totalPossibleMinutes += minutesInThisHour;
2654
+ currentDownRun = 0;
2524
2655
  }
2525
- const percentage = totalPossibleMinutes > 0 ? Math.round(actualMinutes / totalPossibleMinutes * 1e3) / 10 : 100;
2656
+ const completedWindow = uptimeMinutes + downtimeMinutes;
2657
+ const percentage = completedWindow > 0 ? Number((uptimeMinutes / completedWindow * 100).toFixed(1)) : 100;
2526
2658
  uptimeMap.set(record.workspace_id, {
2527
- expectedMinutes: totalPossibleMinutes,
2528
- actualMinutes,
2659
+ expectedMinutes: completedMinutes,
2660
+ actualMinutes: uptimeMinutes,
2529
2661
  percentage,
2530
2662
  lastCalculated: (/* @__PURE__ */ new Date()).toISOString()
2531
2663
  });
@@ -2925,8 +3057,8 @@ var AuthService = class {
2925
3057
  "Authorization": `Bearer ${accessToken}`,
2926
3058
  "Content-Type": "application/json"
2927
3059
  },
2928
- timeout: 3e5,
2929
- // 5 minutes
3060
+ timeout: 1e4,
3061
+ // 10 seconds
2930
3062
  retries: 1,
2931
3063
  silentErrors: false
2932
3064
  // We want to know about auth errors
@@ -2963,8 +3095,8 @@ var AuthService = class {
2963
3095
  "Authorization": `Bearer ${accessToken}`,
2964
3096
  "Content-Type": "application/json"
2965
3097
  },
2966
- timeout: 3e5,
2967
- // 5 minutes
3098
+ timeout: 1e4,
3099
+ // 10 seconds
2968
3100
  retries: 2,
2969
3101
  // More retries for validation
2970
3102
  silentErrors: true,
@@ -2996,8 +3128,8 @@ var AuthService = class {
2996
3128
  "Authorization": `Bearer ${accessToken}`,
2997
3129
  "Content-Type": "application/json"
2998
3130
  },
2999
- timeout: 3e5,
3000
- // 5 minutes
3131
+ timeout: 1e4,
3132
+ // 10 seconds
3001
3133
  retries: 1,
3002
3134
  silentErrors: false
3003
3135
  }
@@ -10897,6 +11029,65 @@ var useWorkspaceHealthStatus = (workspaceId) => {
10897
11029
  refetch: fetchHealthStatus
10898
11030
  };
10899
11031
  };
11032
+ var useWorkspaceUptimeTimeline = (options) => {
11033
+ const {
11034
+ workspaceId,
11035
+ companyId,
11036
+ enabled = true,
11037
+ refreshInterval
11038
+ } = options;
11039
+ const [timeline, setTimeline] = React23.useState(null);
11040
+ const [loading, setLoading] = React23.useState(false);
11041
+ const [error, setError] = React23.useState(null);
11042
+ const isFetchingRef = React23.useRef(false);
11043
+ const intervalRef = React23.useRef(null);
11044
+ const fetchTimeline = React23.useCallback(async () => {
11045
+ if (!enabled) return;
11046
+ if (!workspaceId || !companyId) {
11047
+ setTimeline(null);
11048
+ return;
11049
+ }
11050
+ if (isFetchingRef.current) return;
11051
+ try {
11052
+ isFetchingRef.current = true;
11053
+ setLoading(true);
11054
+ setError(null);
11055
+ const data = await workspaceHealthService.getWorkspaceUptimeTimeline(
11056
+ workspaceId,
11057
+ companyId
11058
+ );
11059
+ setTimeline(data);
11060
+ } catch (err) {
11061
+ console.error("[useWorkspaceUptimeTimeline] Failed to fetch timeline:", err);
11062
+ setError({ message: err?.message || "Failed to load uptime timeline", code: err?.code || "FETCH_ERROR" });
11063
+ } finally {
11064
+ setLoading(false);
11065
+ isFetchingRef.current = false;
11066
+ }
11067
+ }, [enabled, workspaceId, companyId]);
11068
+ React23.useEffect(() => {
11069
+ fetchTimeline();
11070
+ }, [fetchTimeline]);
11071
+ React23.useEffect(() => {
11072
+ if (!refreshInterval || refreshInterval <= 0 || !enabled) {
11073
+ return;
11074
+ }
11075
+ intervalRef.current = setInterval(() => {
11076
+ fetchTimeline();
11077
+ }, refreshInterval);
11078
+ return () => {
11079
+ if (intervalRef.current) {
11080
+ clearInterval(intervalRef.current);
11081
+ }
11082
+ };
11083
+ }, [refreshInterval, enabled, fetchTimeline]);
11084
+ return {
11085
+ timeline,
11086
+ loading,
11087
+ error,
11088
+ refetch: fetchTimeline
11089
+ };
11090
+ };
10900
11091
  function useDateFormatter() {
10901
11092
  const { defaultTimezone, defaultLocale, dateFormatOptions, timeFormatOptions, dateTimeFormatOptions } = useDateTimeConfig();
10902
11093
  const formatDate = React23.useCallback(
@@ -10912,7 +11103,7 @@ function useDateFormatter() {
10912
11103
  },
10913
11104
  [defaultTimezone, defaultLocale, dateFormatOptions]
10914
11105
  );
10915
- const formatTime3 = React23.useCallback(
11106
+ const formatTime5 = React23.useCallback(
10916
11107
  (date, formatString) => {
10917
11108
  const dateObj = typeof date === "string" ? dateFns.parseISO(date) : date;
10918
11109
  if (!dateFns.isValid(dateObj)) return "Invalid Time";
@@ -10943,7 +11134,7 @@ function useDateFormatter() {
10943
11134
  }, []);
10944
11135
  return {
10945
11136
  formatDate,
10946
- formatTime: formatTime3,
11137
+ formatTime: formatTime5,
10947
11138
  formatDateTime,
10948
11139
  getNow,
10949
11140
  timezone: defaultTimezone || "UTC",
@@ -23929,7 +24120,7 @@ var HourlyOutputChartComponent = ({
23929
24120
  endHour = Math.floor(endDecimalHour) % 24;
23930
24121
  endMinute = Math.round(endDecimalHour % 1 * 60);
23931
24122
  }
23932
- const formatTime3 = (h, m) => {
24123
+ const formatTime5 = (h, m) => {
23933
24124
  const period = h >= 12 ? "PM" : "AM";
23934
24125
  const hour12 = h === 0 ? 12 : h > 12 ? h - 12 : h;
23935
24126
  if (m === 0) {
@@ -23937,9 +24128,9 @@ var HourlyOutputChartComponent = ({
23937
24128
  }
23938
24129
  return `${hour12}:${m.toString().padStart(2, "0")}${period}`;
23939
24130
  };
23940
- return `${formatTime3(startHour, startMinute)}-${formatTime3(endHour, endMinute)}`;
24131
+ return `${formatTime5(startHour, startMinute)}-${formatTime5(endHour, endMinute)}`;
23941
24132
  }, [shiftStartTime.decimalHour, SHIFT_DURATION, shiftEndTime]);
23942
- const formatTimeRange = React23__namespace.default.useCallback((hourIndex) => {
24133
+ const formatTimeRange2 = React23__namespace.default.useCallback((hourIndex) => {
23943
24134
  const isLastHour = hourIndex === SHIFT_DURATION - 1;
23944
24135
  const startDecimalHour = shiftStartTime.decimalHour + hourIndex;
23945
24136
  const startHour = Math.floor(startDecimalHour) % 24;
@@ -23953,12 +24144,12 @@ var HourlyOutputChartComponent = ({
23953
24144
  endHour = Math.floor(endDecimalHour) % 24;
23954
24145
  endMinute = Math.round(endDecimalHour % 1 * 60);
23955
24146
  }
23956
- const formatTime3 = (h, m) => {
24147
+ const formatTime5 = (h, m) => {
23957
24148
  const period = h >= 12 ? "PM" : "AM";
23958
24149
  const hour12 = h === 0 ? 12 : h > 12 ? h - 12 : h;
23959
24150
  return `${hour12}:${m.toString().padStart(2, "0")} ${period}`;
23960
24151
  };
23961
- return `${formatTime3(startHour, startMinute)} - ${formatTime3(endHour, endMinute)}`;
24152
+ return `${formatTime5(startHour, startMinute)} - ${formatTime5(endHour, endMinute)}`;
23962
24153
  }, [shiftStartTime.decimalHour, SHIFT_DURATION, shiftEndTime]);
23963
24154
  const chartData = React23__namespace.default.useMemo(() => {
23964
24155
  return Array.from({ length: SHIFT_DURATION }, (_, i) => {
@@ -24019,7 +24210,7 @@ var HourlyOutputChartComponent = ({
24019
24210
  idleMinutes = idleArray.filter((val) => val === "1" || val === 1).length;
24020
24211
  return {
24021
24212
  hour: formatHour(i),
24022
- timeRange: formatTimeRange(i),
24213
+ timeRange: formatTimeRange2(i),
24023
24214
  output: animatedData[i] || 0,
24024
24215
  originalOutput: data[i] || 0,
24025
24216
  // Keep original data for labels
@@ -24028,7 +24219,7 @@ var HourlyOutputChartComponent = ({
24028
24219
  idleArray
24029
24220
  };
24030
24221
  });
24031
- }, [animatedData, data, pphThreshold, idleTimeHourly, shiftStartTime.hour, shiftStartTime.minute, shiftEndTime, formatHour, formatTimeRange, SHIFT_DURATION]);
24222
+ }, [animatedData, data, pphThreshold, idleTimeHourly, shiftStartTime.hour, shiftStartTime.minute, shiftEndTime, formatHour, formatTimeRange2, SHIFT_DURATION]);
24032
24223
  const IdleBar = React23__namespace.default.useMemo(() => {
24033
24224
  if (!idleBarState.visible) return null;
24034
24225
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -25129,7 +25320,7 @@ var SOPComplianceChart = ({
25129
25320
  }
25130
25321
  };
25131
25322
  }, [data, animateToNewData, mockData]);
25132
- const formatTime3 = (minuteIndex) => {
25323
+ const formatTime5 = (minuteIndex) => {
25133
25324
  const totalMinutes = shiftStartHour * 60 + minuteIndex;
25134
25325
  const hours = Math.floor(totalMinutes / 60) % 24;
25135
25326
  const minutes = totalMinutes % 60;
@@ -25141,7 +25332,7 @@ var SOPComplianceChart = ({
25141
25332
  const hasDataForMinute = index < animatedData.length - 10;
25142
25333
  return {
25143
25334
  minute: index,
25144
- time: formatTime3(index),
25335
+ time: formatTime5(index),
25145
25336
  compliance: hasDataForMinute ? animatedData[index] : null
25146
25337
  };
25147
25338
  });
@@ -25575,7 +25766,7 @@ var DateTimeDisplay = ({
25575
25766
  const {
25576
25767
  defaultTimezone
25577
25768
  } = useDateTimeConfig();
25578
- const { formatDate, formatTime: formatTime3 } = useDateFormatter();
25769
+ const { formatDate, formatTime: formatTime5 } = useDateFormatter();
25579
25770
  const [now2, setNow] = React23.useState(() => getCurrentTimeInZone(defaultTimezone || "UTC"));
25580
25771
  React23.useEffect(() => {
25581
25772
  const timerId = setInterval(() => {
@@ -25587,7 +25778,7 @@ var DateTimeDisplay = ({
25587
25778
  return null;
25588
25779
  }
25589
25780
  const formattedDate = showDate ? formatDate(now2) : "";
25590
- const formattedTime = showTime ? formatTime3(now2) : "";
25781
+ const formattedTime = showTime ? formatTime5(now2) : "";
25591
25782
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: clsx_default("flex items-center space-x-2 text-sm text-gray-700 dark:text-gray-300", className), children: [
25592
25783
  showDate && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "date-display", "aria-label": `Current date: ${formattedDate}`, children: formattedDate }),
25593
25784
  showDate && showTime && formattedDate && formattedTime && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "separator", "aria-hidden": "true", children: "|" }),
@@ -25751,7 +25942,7 @@ var BreakNotificationPopup = ({
25751
25942
  const handlePrevious = () => {
25752
25943
  setCurrentIndex((prev) => (prev - 1 + visibleBreaks.length) % visibleBreaks.length);
25753
25944
  };
25754
- const formatTime3 = (minutes) => {
25945
+ const formatTime5 = (minutes) => {
25755
25946
  const hours = Math.floor(minutes / 60);
25756
25947
  const mins = minutes % 60;
25757
25948
  if (hours > 0) {
@@ -25825,9 +26016,9 @@ var BreakNotificationPopup = ({
25825
26016
  formatTo12Hour(currentBreak.endTime)
25826
26017
  ] }),
25827
26018
  /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-gray-500 mb-2", children: [
25828
- formatTime3(currentBreak.elapsedMinutes),
26019
+ formatTime5(currentBreak.elapsedMinutes),
25829
26020
  " elapsed of ",
25830
- formatTime3(currentBreak.duration),
26021
+ formatTime5(currentBreak.duration),
25831
26022
  " total"
25832
26023
  ] }),
25833
26024
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full bg-gray-200 rounded-full h-1.5", children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -26131,64 +26322,208 @@ var getSeverityColor = (severity) => {
26131
26322
  return "bg-gray-500";
26132
26323
  }
26133
26324
  };
26134
- var PlayPauseIndicator = ({
26135
- show,
26325
+ var formatTime2 = (seconds) => {
26326
+ if (!seconds || isNaN(seconds)) return "0:00";
26327
+ const h = Math.floor(seconds / 3600);
26328
+ const m = Math.floor(seconds % 3600 / 60);
26329
+ const s = Math.floor(seconds % 60);
26330
+ if (h > 0) {
26331
+ return `${h}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
26332
+ }
26333
+ return `${m}:${s.toString().padStart(2, "0")}`;
26334
+ };
26335
+ var VideoControls = ({
26136
26336
  isPlaying,
26137
- duration = 600
26337
+ currentTime,
26338
+ duration,
26339
+ buffered,
26340
+ showControls,
26341
+ controlsPinned = false,
26342
+ onTogglePinControls,
26343
+ playbackRate = 1,
26344
+ onPlayPause,
26345
+ onSeek,
26346
+ onSeekStart,
26347
+ onSeekEnd,
26348
+ onToggleFullscreen,
26349
+ onPlaybackRateChange,
26350
+ className = ""
26138
26351
  }) => {
26139
- const [isVisible, setIsVisible] = React23.useState(false);
26140
- const [isFading, setIsFading] = React23.useState(false);
26352
+ const [isDragging, setIsDragging] = React23.useState(false);
26353
+ const [dragTime, setDragTime] = React23.useState(0);
26354
+ const [isHoveringProgressBar, setIsHoveringProgressBar] = React23.useState(false);
26355
+ const [showSpeedMenu, setShowSpeedMenu] = React23.useState(false);
26356
+ const speedMenuRef = React23.useRef(null);
26357
+ const progressColor = "#4b5563";
26358
+ const controlsVisible = showControls || controlsPinned;
26359
+ const getPercentage = (current, total) => {
26360
+ if (!total || total === 0) return 0;
26361
+ return Math.min(Math.max(current / total * 100, 0), 100);
26362
+ };
26363
+ const handleSeekChange = (e) => {
26364
+ const newTime = parseFloat(e.target.value);
26365
+ setDragTime(newTime);
26366
+ onSeek(newTime);
26367
+ };
26368
+ const handleSeekStart = () => {
26369
+ setIsDragging(true);
26370
+ setDragTime(currentTime);
26371
+ onSeekStart?.();
26372
+ };
26373
+ const handleSeekEnd = () => {
26374
+ setIsDragging(false);
26375
+ onSeekEnd?.();
26376
+ };
26141
26377
  React23.useEffect(() => {
26142
- if (show) {
26143
- setIsVisible(true);
26144
- setIsFading(false);
26145
- const fadeTimer = setTimeout(() => {
26146
- setIsFading(true);
26147
- }, 100);
26148
- const hideTimer = setTimeout(() => {
26149
- setIsVisible(false);
26150
- setIsFading(false);
26151
- }, duration);
26152
- return () => {
26153
- clearTimeout(fadeTimer);
26154
- clearTimeout(hideTimer);
26155
- };
26156
- }
26157
- }, [show, duration]);
26158
- if (!isVisible) return null;
26159
- return /* @__PURE__ */ jsxRuntime.jsx(
26378
+ const handleClickOutside = (event) => {
26379
+ if (speedMenuRef.current && !speedMenuRef.current.contains(event.target)) {
26380
+ setShowSpeedMenu(false);
26381
+ }
26382
+ };
26383
+ document.addEventListener("mousedown", handleClickOutside);
26384
+ return () => {
26385
+ document.removeEventListener("mousedown", handleClickOutside);
26386
+ };
26387
+ }, []);
26388
+ const displayTime = isDragging ? dragTime : currentTime;
26389
+ const progressPercent = getPercentage(displayTime, duration);
26390
+ const bufferedPercent = getPercentage(buffered, duration);
26391
+ return /* @__PURE__ */ jsxRuntime.jsxs(
26160
26392
  "div",
26161
26393
  {
26162
- className: "absolute inset-0 flex items-center justify-center pointer-events-none z-10",
26163
- style: {
26164
- opacity: isFading ? 0 : 1,
26165
- transition: `opacity ${duration - 100}ms ease-out`
26166
- },
26167
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-black/70 rounded-full p-6", children: isPlaying ? (
26168
- // Play icon (triangle)
26169
- /* @__PURE__ */ jsxRuntime.jsx(
26170
- "svg",
26171
- {
26172
- xmlns: "http://www.w3.org/2000/svg",
26173
- viewBox: "0 0 24 24",
26174
- fill: "white",
26175
- className: "w-16 h-16",
26176
- children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8 5v14l11-7z" })
26177
- }
26178
- )
26179
- ) : (
26180
- // Pause icon (two bars)
26181
- /* @__PURE__ */ jsxRuntime.jsx(
26182
- "svg",
26394
+ className: `absolute bottom-0 left-0 right-0 px-3 pb-3 pt-12 bg-gradient-to-t from-black/80 via-black/40 to-transparent transition-opacity duration-300 ${controlsVisible ? "opacity-100" : "opacity-0 pointer-events-none"} ${className}`,
26395
+ style: { touchAction: "none" },
26396
+ children: [
26397
+ /* @__PURE__ */ jsxRuntime.jsxs(
26398
+ "div",
26183
26399
  {
26184
- xmlns: "http://www.w3.org/2000/svg",
26185
- viewBox: "0 0 24 24",
26186
- fill: "white",
26187
- className: "w-16 h-16",
26188
- children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 4h4v16H6V4zm8 0h4v16h-4V4z" })
26400
+ className: "relative h-1 mb-4 group cursor-pointer",
26401
+ onMouseEnter: () => setIsHoveringProgressBar(true),
26402
+ onMouseLeave: () => setIsHoveringProgressBar(false),
26403
+ children: [
26404
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute -top-2 -bottom-2 left-0 right-0 z-20" }),
26405
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute top-0 left-0 right-0 bottom-0 bg-white/20 rounded-full overflow-hidden z-0", children: /* @__PURE__ */ jsxRuntime.jsx(
26406
+ "div",
26407
+ {
26408
+ className: "absolute top-0 left-0 bottom-0 bg-white/40 transition-all duration-200",
26409
+ style: { width: `${bufferedPercent}%` }
26410
+ }
26411
+ ) }),
26412
+ /* @__PURE__ */ jsxRuntime.jsx(
26413
+ "div",
26414
+ {
26415
+ className: "absolute top-0 left-0 bottom-0 bg-[#007bff] transition-all duration-75 z-10",
26416
+ style: { width: `${progressPercent}%`, backgroundColor: progressColor },
26417
+ children: /* @__PURE__ */ jsxRuntime.jsx(
26418
+ "div",
26419
+ {
26420
+ className: `absolute right-0 top-1/2 -translate-y-1/2 translate-x-1/2 w-3 h-3 rounded-full shadow transform transition-transform duration-200 ${isHoveringProgressBar || isDragging ? "scale-100" : "scale-0"}`,
26421
+ style: { backgroundColor: progressColor }
26422
+ }
26423
+ )
26424
+ }
26425
+ ),
26426
+ /* @__PURE__ */ jsxRuntime.jsx(
26427
+ "input",
26428
+ {
26429
+ type: "range",
26430
+ min: "0",
26431
+ max: duration || 100,
26432
+ step: "0.1",
26433
+ value: displayTime,
26434
+ onChange: handleSeekChange,
26435
+ onMouseDown: handleSeekStart,
26436
+ onMouseUp: handleSeekEnd,
26437
+ onTouchStart: handleSeekStart,
26438
+ onTouchEnd: handleSeekEnd,
26439
+ className: "absolute inset-0 w-full h-full opacity-0 cursor-pointer z-30 margin-0 padding-0"
26440
+ }
26441
+ )
26442
+ ]
26189
26443
  }
26190
- )
26191
- ) })
26444
+ ),
26445
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-white", children: [
26446
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4", children: [
26447
+ /* @__PURE__ */ jsxRuntime.jsx(
26448
+ "button",
26449
+ {
26450
+ onClick: (e) => {
26451
+ e.stopPropagation();
26452
+ onPlayPause();
26453
+ },
26454
+ className: "hover:text-[#007bff] transition-colors focus:outline-none",
26455
+ "aria-label": isPlaying ? "Pause" : "Play",
26456
+ children: isPlaying ? /* @__PURE__ */ jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 4h4v16H6V4zm8 0h4v16h-4V4z" }) }) : /* @__PURE__ */ jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8 5v14l11-7z" }) })
26457
+ }
26458
+ ),
26459
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-xs font-medium font-sans", children: [
26460
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: formatTime2(displayTime) }),
26461
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mx-1 text-white/70", children: "/" }),
26462
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-white/70", children: formatTime2(duration) })
26463
+ ] })
26464
+ ] }),
26465
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4", children: [
26466
+ onTogglePinControls && /* @__PURE__ */ jsxRuntime.jsx(
26467
+ "button",
26468
+ {
26469
+ onClick: (e) => {
26470
+ e.stopPropagation();
26471
+ onTogglePinControls();
26472
+ },
26473
+ className: `transition-colors focus:outline-none ${controlsPinned ? "text-[#007bff]" : "hover:text-[#007bff]"}`,
26474
+ "aria-label": controlsPinned ? "Unpin controls" : "Pin controls",
26475
+ title: controlsPinned ? "Unpin controls" : "Pin controls",
26476
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "currentColor", children: controlsPinned ? /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 3h6l-1 7h3v2h-4.5l-.5 4.5-2 1L10 12H6v-2h3z" }) : /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 3h6l-1 7h3v2h-4v5l-2 1-1-6H6v-2h3z" }) })
26477
+ }
26478
+ ),
26479
+ onPlaybackRateChange && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", ref: speedMenuRef, children: [
26480
+ /* @__PURE__ */ jsxRuntime.jsxs(
26481
+ "button",
26482
+ {
26483
+ onClick: (e) => {
26484
+ e.stopPropagation();
26485
+ setShowSpeedMenu(!showSpeedMenu);
26486
+ },
26487
+ className: "text-xs font-medium hover:text-[#007bff] transition-colors focus:outline-none min-w-[32px]",
26488
+ "aria-label": "Playback Speed",
26489
+ children: [
26490
+ playbackRate,
26491
+ "x"
26492
+ ]
26493
+ }
26494
+ ),
26495
+ showSpeedMenu && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute bottom-full right-0 mb-2 bg-black/90 text-white rounded shadow-lg overflow-hidden z-50 min-w-[80px]", children: [0.5, 1, 1.5, 2, 2.5, 3, 4, 5].map((rate) => /* @__PURE__ */ jsxRuntime.jsxs(
26496
+ "button",
26497
+ {
26498
+ onClick: (e) => {
26499
+ e.stopPropagation();
26500
+ onPlaybackRateChange(rate);
26501
+ setShowSpeedMenu(false);
26502
+ },
26503
+ className: `block w-full text-left px-4 py-2 text-xs hover:bg-white/20 transition-colors ${playbackRate === rate ? "text-[#007bff] font-bold" : ""}`,
26504
+ children: [
26505
+ rate,
26506
+ "x"
26507
+ ]
26508
+ },
26509
+ rate
26510
+ )) })
26511
+ ] }),
26512
+ onToggleFullscreen && /* @__PURE__ */ jsxRuntime.jsx(
26513
+ "button",
26514
+ {
26515
+ onClick: (e) => {
26516
+ e.stopPropagation();
26517
+ onToggleFullscreen();
26518
+ },
26519
+ className: "hover:text-[#007bff] transition-colors focus:outline-none",
26520
+ "aria-label": "Toggle Fullscreen",
26521
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" }) })
26522
+ }
26523
+ )
26524
+ ] })
26525
+ ] })
26526
+ ]
26192
26527
  }
26193
26528
  );
26194
26529
  };
@@ -26316,9 +26651,15 @@ var HlsVideoPlayer = React23.forwardRef(({
26316
26651
  const blobUrlRef = React23.useRef(null);
26317
26652
  const [isReady, setIsReady] = React23.useState(false);
26318
26653
  const [isLoading, setIsLoading] = React23.useState(true);
26319
- const [showIndicator, setShowIndicator] = React23.useState(false);
26320
- const [indicatorIsPlaying, setIndicatorIsPlaying] = React23.useState(false);
26321
- const indicatorKeyRef = React23.useRef(0);
26654
+ const [showControls, setShowControls] = React23.useState(true);
26655
+ const [controlsPinned, setControlsPinned] = React23.useState(false);
26656
+ const [isPlaying, setIsPlaying] = React23.useState(false);
26657
+ const [currentTime, setCurrentTime] = React23.useState(0);
26658
+ const [duration, setDuration] = React23.useState(0);
26659
+ const [buffered, setBuffered] = React23.useState(0);
26660
+ const [playbackRate, setPlaybackRate] = React23.useState(1);
26661
+ const userSeekingRef = React23.useRef(false);
26662
+ const controlsTimeoutRef = React23.useRef(null);
26322
26663
  const eventCallbacksRef = React23.useRef({
26323
26664
  onReady,
26324
26665
  onPlay,
@@ -26497,8 +26838,6 @@ var HlsVideoPlayer = React23.forwardRef(({
26497
26838
  }
26498
26839
  });
26499
26840
  hls.on(Hls3.Events.FRAG_LOADING, () => {
26500
- setIsLoading(true);
26501
- eventCallbacksRef.current.onLoadingChange?.(true);
26502
26841
  });
26503
26842
  hls.on(Hls3.Events.FRAG_LOADED, () => {
26504
26843
  setIsLoading(false);
@@ -26552,25 +26891,52 @@ var HlsVideoPlayer = React23.forwardRef(({
26552
26891
  const handleCanPlay = () => {
26553
26892
  if (!hlsRef.current) {
26554
26893
  setIsReady(true);
26555
- onReady?.(player);
26556
26894
  }
26895
+ setIsLoading(false);
26896
+ eventCallbacksRef.current.onLoadingChange?.(false);
26897
+ onReady?.(player);
26898
+ };
26899
+ const handlePlay = () => {
26900
+ setIsPlaying(true);
26901
+ eventCallbacksRef.current.onPlay?.(player);
26902
+ };
26903
+ const handlePause = () => {
26904
+ if (userSeekingRef.current && videoRef.current) {
26905
+ videoRef.current.play().catch((err) => console.warn("Auto-resume after seek pause failed:", err));
26906
+ return;
26907
+ }
26908
+ setIsPlaying(false);
26909
+ eventCallbacksRef.current.onPause?.(player);
26557
26910
  };
26558
- const handlePlay = () => eventCallbacksRef.current.onPlay?.(player);
26559
- const handlePause = () => eventCallbacksRef.current.onPause?.(player);
26560
26911
  const handlePlaying = () => {
26561
26912
  setIsLoading(false);
26913
+ setIsPlaying(true);
26562
26914
  eventCallbacksRef.current.onLoadingChange?.(false);
26563
26915
  eventCallbacksRef.current.onPlaying?.(player);
26564
26916
  };
26565
26917
  const handleTimeUpdate = () => {
26566
26918
  const currentTime2 = video.currentTime || 0;
26919
+ setCurrentTime(currentTime2);
26920
+ if (video.buffered.length > 0) {
26921
+ for (let i = 0; i < video.buffered.length; i++) {
26922
+ if (video.buffered.start(i) <= currentTime2 && video.buffered.end(i) >= currentTime2) {
26923
+ setBuffered(video.buffered.end(i));
26924
+ break;
26925
+ }
26926
+ }
26927
+ }
26567
26928
  eventCallbacksRef.current.onTimeUpdate?.(player, currentTime2);
26568
26929
  };
26569
26930
  const handleDurationChange = () => {
26570
26931
  const duration2 = video.duration || 0;
26932
+ setDuration(duration2);
26571
26933
  eventCallbacksRef.current.onDurationChange?.(player, duration2);
26572
26934
  };
26573
- const handleEnded = () => eventCallbacksRef.current.onEnded?.(player);
26935
+ const handleEnded = () => {
26936
+ setIsPlaying(false);
26937
+ userSeekingRef.current = false;
26938
+ eventCallbacksRef.current.onEnded?.(player);
26939
+ };
26574
26940
  const handleLoadStart = () => {
26575
26941
  setIsLoading(true);
26576
26942
  eventCallbacksRef.current.onLoadingChange?.(true);
@@ -26586,8 +26952,19 @@ var HlsVideoPlayer = React23.forwardRef(({
26586
26952
  setIsLoading(true);
26587
26953
  eventCallbacksRef.current.onLoadingChange?.(true);
26588
26954
  };
26589
- const handleSeeking = () => eventCallbacksRef.current.onSeeking?.(player);
26590
- const handleSeeked = () => eventCallbacksRef.current.onSeeked?.(player);
26955
+ const handleSeeking = () => {
26956
+ userSeekingRef.current = true;
26957
+ eventCallbacksRef.current.onSeeking?.(player);
26958
+ };
26959
+ const handleSeeked = () => {
26960
+ setIsLoading(false);
26961
+ eventCallbacksRef.current.onLoadingChange?.(false);
26962
+ if (videoRef.current) {
26963
+ videoRef.current.play().catch((err) => console.warn("Resume playback after seek failed:", err));
26964
+ }
26965
+ userSeekingRef.current = false;
26966
+ eventCallbacksRef.current.onSeeked?.(player);
26967
+ };
26591
26968
  const handleError = () => {
26592
26969
  const error = video.error;
26593
26970
  if (error) {
@@ -26600,6 +26977,10 @@ var HlsVideoPlayer = React23.forwardRef(({
26600
26977
  eventCallbacksRef.current.onError?.(player, errorInfo);
26601
26978
  }
26602
26979
  };
26980
+ const handlePlaybackRateChange2 = (e) => {
26981
+ const target = e.target;
26982
+ setPlaybackRate(target.playbackRate);
26983
+ };
26603
26984
  video.addEventListener("canplay", handleCanPlay);
26604
26985
  video.addEventListener("play", handlePlay);
26605
26986
  video.addEventListener("pause", handlePause);
@@ -26614,6 +26995,7 @@ var HlsVideoPlayer = React23.forwardRef(({
26614
26995
  video.addEventListener("seeking", handleSeeking);
26615
26996
  video.addEventListener("seeked", handleSeeked);
26616
26997
  video.addEventListener("error", handleError);
26998
+ video.addEventListener("ratechange", handlePlaybackRateChange2);
26617
26999
  return () => {
26618
27000
  video.removeEventListener("canplay", handleCanPlay);
26619
27001
  video.removeEventListener("play", handlePlay);
@@ -26629,6 +27011,7 @@ var HlsVideoPlayer = React23.forwardRef(({
26629
27011
  video.removeEventListener("seeking", handleSeeking);
26630
27012
  video.removeEventListener("seeked", handleSeeked);
26631
27013
  video.removeEventListener("error", handleError);
27014
+ video.removeEventListener("ratechange", handlePlaybackRateChange2);
26632
27015
  };
26633
27016
  }, [
26634
27017
  src,
@@ -26657,20 +27040,46 @@ var HlsVideoPlayer = React23.forwardRef(({
26657
27040
  }
26658
27041
  }
26659
27042
  }, [autoplay]);
27043
+ const resetControlsTimeout = React23.useCallback(() => {
27044
+ if (controlsPinned) {
27045
+ setShowControls(true);
27046
+ return;
27047
+ }
27048
+ setShowControls(true);
27049
+ if (controlsTimeoutRef.current) {
27050
+ clearTimeout(controlsTimeoutRef.current);
27051
+ }
27052
+ if (isPlaying) {
27053
+ controlsTimeoutRef.current = setTimeout(() => {
27054
+ setShowControls(false);
27055
+ }, 3e3);
27056
+ }
27057
+ }, [isPlaying, controlsPinned]);
27058
+ const handleMouseMove = React23.useCallback(() => {
27059
+ resetControlsTimeout();
27060
+ }, [resetControlsTimeout]);
27061
+ React23.useEffect(() => {
27062
+ resetControlsTimeout();
27063
+ return () => {
27064
+ if (controlsTimeoutRef.current) {
27065
+ clearTimeout(controlsTimeoutRef.current);
27066
+ }
27067
+ };
27068
+ }, [isPlaying, resetControlsTimeout]);
26660
27069
  const play = React23.useCallback(() => {
26661
27070
  return videoRef.current?.play();
26662
27071
  }, []);
26663
27072
  const pause = React23.useCallback(() => {
26664
27073
  videoRef.current?.pause();
26665
27074
  }, []);
26666
- const currentTime = React23.useCallback((time2) => {
27075
+ const currentTimeProp = React23.useCallback((time2) => {
26667
27076
  if (time2 !== void 0 && videoRef.current) {
26668
27077
  videoRef.current.currentTime = time2;
26669
27078
  return time2;
26670
27079
  }
26671
27080
  return videoRef.current?.currentTime || 0;
26672
27081
  }, []);
26673
- const duration = React23.useCallback(() => {
27082
+ const durationProp = React23.useCallback(() => {
26674
27083
  return videoRef.current?.duration || 0;
26675
27084
  }, []);
26676
27085
  const paused = React23.useCallback(() => {
@@ -26683,95 +27092,144 @@ var HlsVideoPlayer = React23.forwardRef(({
26683
27092
  }
26684
27093
  return videoRef.current?.muted ?? false;
26685
27094
  }, []);
26686
- const volume = React23.useCallback((level) => {
27095
+ const volumeProp = React23.useCallback((level) => {
26687
27096
  if (level !== void 0 && videoRef.current) {
26688
27097
  videoRef.current.volume = level;
26689
27098
  return level;
26690
27099
  }
26691
27100
  return videoRef.current?.volume ?? 1;
26692
27101
  }, []);
26693
- const playbackRate = React23.useCallback((rate) => {
27102
+ const playbackRateProp = React23.useCallback((rate) => {
26694
27103
  if (rate !== void 0 && videoRef.current) {
26695
27104
  videoRef.current.playbackRate = rate;
26696
27105
  return rate;
26697
27106
  }
26698
27107
  return videoRef.current?.playbackRate ?? 1;
26699
27108
  }, []);
27109
+ const handleTogglePlay = React23.useCallback(() => {
27110
+ if (videoRef.current) {
27111
+ if (videoRef.current.paused) {
27112
+ videoRef.current.play();
27113
+ } else {
27114
+ videoRef.current.pause();
27115
+ }
27116
+ }
27117
+ }, []);
27118
+ const handleSeek = React23.useCallback((time2) => {
27119
+ if (videoRef.current) {
27120
+ videoRef.current.currentTime = time2;
27121
+ videoRef.current.play().catch((err) => console.warn("Resume playback failed during seek:", err));
27122
+ }
27123
+ }, []);
27124
+ const handleSeekStart = React23.useCallback(() => {
27125
+ userSeekingRef.current = true;
27126
+ }, []);
27127
+ const handleSeekEnd = React23.useCallback(() => {
27128
+ if (videoRef.current) {
27129
+ videoRef.current.play().catch((err) => console.warn("Resume playback failed after seek:", err));
27130
+ }
27131
+ }, []);
27132
+ const handlePlaybackRateChange = React23.useCallback((rate) => {
27133
+ if (videoRef.current) {
27134
+ videoRef.current.playbackRate = rate;
27135
+ }
27136
+ }, []);
27137
+ const handleToggleFullscreen = React23.useCallback(() => {
27138
+ if (videoContainerRef.current) {
27139
+ if (!document.fullscreenElement) {
27140
+ videoContainerRef.current.requestFullscreen();
27141
+ } else {
27142
+ document.exitFullscreen();
27143
+ }
27144
+ }
27145
+ }, []);
26700
27146
  React23.useImperativeHandle(ref, () => ({
26701
27147
  hls: hlsRef.current,
26702
27148
  video: videoRef.current,
26703
27149
  play,
26704
27150
  pause,
26705
- currentTime,
26706
- duration,
27151
+ currentTime: currentTimeProp,
27152
+ duration: durationProp,
26707
27153
  paused,
26708
27154
  mute,
26709
- volume,
26710
- playbackRate,
27155
+ volume: volumeProp,
27156
+ playbackRate: playbackRateProp,
26711
27157
  dispose,
26712
27158
  isReady,
26713
27159
  // For backward compatibility with Video.js API
26714
27160
  player: playerLikeObject()
26715
- }), [play, pause, currentTime, duration, paused, mute, volume, playbackRate, dispose, isReady, playerLikeObject]);
26716
- const handleClickWithIndicator = React23.useCallback(() => {
26717
- if (!onClick || !videoRef.current) return;
26718
- const willBePlaying = videoRef.current.paused;
26719
- setIndicatorIsPlaying(willBePlaying);
26720
- setShowIndicator(false);
26721
- setTimeout(() => {
26722
- indicatorKeyRef.current += 1;
26723
- setShowIndicator(true);
26724
- }, 0);
26725
- onClick();
26726
- }, [onClick]);
26727
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `hls-video-player-wrapper ${className}`, style: { position: "relative", width: "100%", height: "100%" }, children: [
26728
- /* @__PURE__ */ jsxRuntime.jsx(
26729
- "div",
26730
- {
26731
- className: "hls-video-player-container",
26732
- ref: videoContainerRef,
26733
- children: /* @__PURE__ */ jsxRuntime.jsx(
26734
- "video",
27161
+ }), [play, pause, currentTimeProp, durationProp, paused, mute, volumeProp, playbackRateProp, dispose, isReady, playerLikeObject]);
27162
+ const handleContainerClick = React23.useCallback(() => {
27163
+ if (!onClick && !controls) {
27164
+ handleTogglePlay();
27165
+ }
27166
+ if (onClick) {
27167
+ onClick();
27168
+ }
27169
+ }, [onClick, controls, handleTogglePlay]);
27170
+ return /* @__PURE__ */ jsxRuntime.jsxs(
27171
+ "div",
27172
+ {
27173
+ className: `hls-video-player-wrapper ${className} group`,
27174
+ style: { position: "relative", width: "100%", height: "100%" },
27175
+ onMouseMove: handleMouseMove,
27176
+ onMouseLeave: () => isPlaying && !controlsPinned && setShowControls(false),
27177
+ children: [
27178
+ /* @__PURE__ */ jsxRuntime.jsxs(
27179
+ "div",
26735
27180
  {
26736
- ref: videoRef,
26737
- className: "hls-video-element",
26738
- poster,
26739
- controls,
26740
- loop,
26741
- muted,
26742
- playsInline,
26743
- autoPlay: autoplay,
26744
- preload: "metadata"
27181
+ className: "hls-video-player-container",
27182
+ ref: videoContainerRef,
27183
+ onClick: handleContainerClick,
27184
+ children: [
27185
+ /* @__PURE__ */ jsxRuntime.jsx(
27186
+ "video",
27187
+ {
27188
+ ref: videoRef,
27189
+ className: "hls-video-element",
27190
+ poster,
27191
+ controls: false,
27192
+ loop,
27193
+ muted,
27194
+ playsInline,
27195
+ autoPlay: autoplay,
27196
+ preload: "metadata"
27197
+ }
27198
+ ),
27199
+ controls && /* @__PURE__ */ jsxRuntime.jsx(
27200
+ VideoControls,
27201
+ {
27202
+ isPlaying,
27203
+ currentTime,
27204
+ duration,
27205
+ buffered,
27206
+ showControls: controlsPinned || showControls || !isPlaying,
27207
+ controlsPinned,
27208
+ playbackRate,
27209
+ onPlayPause: handleTogglePlay,
27210
+ onSeek: handleSeek,
27211
+ onSeekStart: handleSeekStart,
27212
+ onSeekEnd: handleSeekEnd,
27213
+ onPlaybackRateChange: handlePlaybackRateChange,
27214
+ onTogglePinControls: () => setControlsPinned((prev) => {
27215
+ const next = !prev;
27216
+ if (next) {
27217
+ setShowControls(true);
27218
+ } else {
27219
+ resetControlsTimeout();
27220
+ }
27221
+ return next;
27222
+ }),
27223
+ onToggleFullscreen: handleToggleFullscreen
27224
+ }
27225
+ )
27226
+ ]
26745
27227
  }
26746
- )
26747
- }
26748
- ),
26749
- isLoading && !externalLoadingControl && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "hls-video-player-loading", children: /* @__PURE__ */ jsxRuntime.jsx(OptifyeLogoLoader_default, { size: "md", message: "Loading video..." }) }),
26750
- onClick && !controls && /* @__PURE__ */ jsxRuntime.jsx(
26751
- "div",
26752
- {
26753
- onClick: handleClickWithIndicator,
26754
- style: {
26755
- position: "absolute",
26756
- top: 0,
26757
- left: 0,
26758
- right: 0,
26759
- bottom: 0,
26760
- zIndex: 1,
26761
- cursor: "pointer"
26762
- },
26763
- "aria-label": "Click to play/pause"
26764
- }
26765
- ),
26766
- onClick && !controls && /* @__PURE__ */ jsxRuntime.jsx(
26767
- PlayPauseIndicator,
26768
- {
26769
- show: showIndicator,
26770
- isPlaying: indicatorIsPlaying
26771
- },
26772
- indicatorKeyRef.current
26773
- )
26774
- ] });
27228
+ ),
27229
+ isLoading && !externalLoadingControl && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "hls-video-player-loading", children: /* @__PURE__ */ jsxRuntime.jsx(OptifyeLogoLoader_default, { size: "md", message: "Loading video..." }) })
27230
+ ]
27231
+ }
27232
+ );
26775
27233
  });
26776
27234
  HlsVideoPlayer.displayName = "HlsVideoPlayer";
26777
27235
  var VideoPlayer = HlsVideoPlayer;
@@ -26779,6 +27237,7 @@ var CroppedHlsVideoPlayer = React23.forwardRef(({
26779
27237
  crop,
26780
27238
  debug = false,
26781
27239
  onClick,
27240
+ controls = true,
26782
27241
  ...videoProps
26783
27242
  }, ref) => {
26784
27243
  const {
@@ -26800,9 +27259,15 @@ var CroppedHlsVideoPlayer = React23.forwardRef(({
26800
27259
  const [isVideoReady, setIsVideoReady] = React23.useState(false);
26801
27260
  const [canvasDimensions, setCanvasDimensions] = React23.useState({ width: 0, height: 0 });
26802
27261
  const [isProcessing, setIsProcessing] = React23.useState(false);
26803
- const [showIndicator, setShowIndicator] = React23.useState(false);
26804
- const [indicatorIsPlaying, setIndicatorIsPlaying] = React23.useState(false);
26805
- const indicatorKeyRef = React23.useRef(0);
27262
+ const [showControls, setShowControls] = React23.useState(true);
27263
+ const [isPlaying, setIsPlaying] = React23.useState(false);
27264
+ const [currentTime, setCurrentTime] = React23.useState(0);
27265
+ const [duration, setDuration] = React23.useState(0);
27266
+ const [buffered, setBuffered] = React23.useState(0);
27267
+ const [playbackRate, setPlaybackRate] = React23.useState(1);
27268
+ const controlsTimeoutRef = React23.useRef(null);
27269
+ const userSeekingRef = React23.useRef(false);
27270
+ const [controlsPinned, setControlsPinned] = React23.useState(false);
26806
27271
  const stopCanvasRendering = React23.useCallback(() => {
26807
27272
  if (animationFrameRef.current) {
26808
27273
  cancelAnimationFrame(animationFrameRef.current);
@@ -26887,7 +27352,7 @@ var CroppedHlsVideoPlayer = React23.forwardRef(({
26887
27352
  const canvas = canvasRef.current;
26888
27353
  const video = videoElementRef.current;
26889
27354
  const ctx = canvas.getContext("2d");
26890
- if (!ctx || video.paused || video.ended) {
27355
+ if (!ctx || video.readyState < 2) {
26891
27356
  return;
26892
27357
  }
26893
27358
  const videoWidth = video.videoWidth;
@@ -26914,7 +27379,9 @@ var CroppedHlsVideoPlayer = React23.forwardRef(({
26914
27379
  canvas.height
26915
27380
  // Destination (full canvas)
26916
27381
  );
26917
- animationFrameRef.current = requestAnimationFrame(renderFrameToCanvas);
27382
+ if (!video.paused && !video.ended) {
27383
+ animationFrameRef.current = requestAnimationFrame(renderFrameToCanvas);
27384
+ }
26918
27385
  }, [crop]);
26919
27386
  const handleVideoReady = React23.useCallback((player) => {
26920
27387
  console.log("[CroppedHlsVideoPlayer] Video player ready");
@@ -26922,11 +27389,15 @@ var CroppedHlsVideoPlayer = React23.forwardRef(({
26922
27389
  if (videoEl) {
26923
27390
  videoElementRef.current = videoEl;
26924
27391
  setIsVideoReady(true);
27392
+ if (videoEl.readyState >= 2) {
27393
+ renderFrameToCanvas();
27394
+ }
26925
27395
  }
26926
27396
  onReadyProp?.(player);
26927
- }, [onReadyProp]);
27397
+ }, [onReadyProp, renderFrameToCanvas]);
26928
27398
  const handleVideoPlay = React23.useCallback((player) => {
26929
27399
  console.log("[CroppedHlsVideoPlayer] Video playing, starting canvas rendering");
27400
+ setIsPlaying(true);
26930
27401
  if (crop && canvasRef.current) {
26931
27402
  setIsProcessing(true);
26932
27403
  renderFrameToCanvas();
@@ -26934,43 +27405,40 @@ var CroppedHlsVideoPlayer = React23.forwardRef(({
26934
27405
  onPlayProp?.(player);
26935
27406
  }, [crop, renderFrameToCanvas, onPlayProp]);
26936
27407
  const handleVideoPause = React23.useCallback((player) => {
26937
- console.log("[CroppedHlsVideoPlayer] Video paused, stopping canvas rendering and CLEARING canvas");
27408
+ console.log("[CroppedHlsVideoPlayer] Video paused, stopping canvas rendering (keeping last frame)");
27409
+ if (userSeekingRef.current && hiddenVideoRef.current) {
27410
+ hiddenVideoRef.current.play()?.catch(() => {
27411
+ });
27412
+ return;
27413
+ }
26938
27414
  stopCanvasRendering();
26939
27415
  setIsProcessing(false);
26940
- if (canvasRef.current) {
26941
- const ctx = canvasRef.current.getContext("2d");
26942
- if (ctx) {
26943
- ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
26944
- ctx.fillStyle = "black";
26945
- ctx.fillRect(0, 0, canvasRef.current.width, canvasRef.current.height);
26946
- }
26947
- }
27416
+ setIsPlaying(false);
27417
+ renderFrameToCanvas();
26948
27418
  onPauseProp?.(player);
26949
- }, [stopCanvasRendering, onPauseProp]);
27419
+ }, [stopCanvasRendering, onPauseProp, renderFrameToCanvas]);
26950
27420
  const handleVideoEnded = React23.useCallback((player) => {
26951
- console.log("[CroppedHlsVideoPlayer] Video ended, CLEARING canvas");
27421
+ console.log("[CroppedHlsVideoPlayer] Video ended, stopping canvas rendering (keeping last frame)");
26952
27422
  stopCanvasRendering();
26953
27423
  setIsProcessing(false);
26954
- if (canvasRef.current) {
26955
- const ctx = canvasRef.current.getContext("2d");
26956
- if (ctx) {
26957
- ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
26958
- ctx.fillStyle = "black";
26959
- ctx.fillRect(0, 0, canvasRef.current.width, canvasRef.current.height);
26960
- }
26961
- }
27424
+ setIsPlaying(false);
27425
+ userSeekingRef.current = false;
26962
27426
  onEndedProp?.(player);
26963
27427
  }, [stopCanvasRendering, onEndedProp]);
26964
27428
  const handleSeeking = React23.useCallback((player) => {
26965
27429
  console.log("[CroppedHlsVideoPlayer] Video seeking");
26966
- if (crop && !videoElementRef.current?.paused) {
27430
+ userSeekingRef.current = true;
27431
+ if (crop) {
26967
27432
  renderFrameToCanvas();
26968
27433
  }
26969
27434
  onSeekingProp?.(player);
26970
27435
  }, [crop, renderFrameToCanvas, onSeekingProp]);
26971
27436
  const handleSeeked = React23.useCallback((player) => {
26972
27437
  console.log("[CroppedHlsVideoPlayer] Video seeked");
26973
- if (crop && !videoElementRef.current?.paused) {
27438
+ hiddenVideoRef.current?.play()?.catch(() => {
27439
+ });
27440
+ userSeekingRef.current = false;
27441
+ if (crop) {
26974
27442
  renderFrameToCanvas();
26975
27443
  }
26976
27444
  onSeekedProp?.(player);
@@ -26978,8 +27446,29 @@ var CroppedHlsVideoPlayer = React23.forwardRef(({
26978
27446
  const handleLoadedMetadata = React23.useCallback((player) => {
26979
27447
  console.log("[CroppedHlsVideoPlayer] Video metadata loaded");
26980
27448
  calculateCanvasDimensions();
27449
+ if (hiddenVideoRef.current?.video) {
27450
+ setDuration(hiddenVideoRef.current.video.duration || 0);
27451
+ }
27452
+ requestAnimationFrame(() => renderFrameToCanvas());
26981
27453
  onLoadedMetadataProp?.(player);
26982
- }, [calculateCanvasDimensions, onLoadedMetadataProp]);
27454
+ }, [calculateCanvasDimensions, onLoadedMetadataProp, renderFrameToCanvas]);
27455
+ const handleTimeUpdate = React23.useCallback((player, time2) => {
27456
+ setCurrentTime(time2);
27457
+ if (hiddenVideoRef.current?.video && hiddenVideoRef.current.video.buffered.length > 0) {
27458
+ const video = hiddenVideoRef.current.video;
27459
+ for (let i = 0; i < video.buffered.length; i++) {
27460
+ if (video.buffered.start(i) <= time2 && video.buffered.end(i) >= time2) {
27461
+ setBuffered(video.buffered.end(i));
27462
+ break;
27463
+ }
27464
+ }
27465
+ }
27466
+ videoProps.onTimeUpdate?.(player, time2);
27467
+ }, [videoProps.onTimeUpdate]);
27468
+ const handleDurationChange = React23.useCallback((player, dur) => {
27469
+ setDuration(dur);
27470
+ videoProps.onDurationChange?.(player, dur);
27471
+ }, [videoProps.onDurationChange]);
26983
27472
  React23.useEffect(() => {
26984
27473
  calculateCanvasDimensions();
26985
27474
  const handleResize = () => {
@@ -27007,33 +27496,97 @@ var CroppedHlsVideoPlayer = React23.forwardRef(({
27007
27496
  stopCanvasRendering();
27008
27497
  };
27009
27498
  }, [stopCanvasRendering]);
27499
+ const resetControlsTimeout = React23.useCallback(() => {
27500
+ if (controlsPinned) {
27501
+ setShowControls(true);
27502
+ return;
27503
+ }
27504
+ setShowControls(true);
27505
+ if (controlsTimeoutRef.current) {
27506
+ clearTimeout(controlsTimeoutRef.current);
27507
+ }
27508
+ if (isPlaying) {
27509
+ controlsTimeoutRef.current = setTimeout(() => {
27510
+ setShowControls(false);
27511
+ }, 3e3);
27512
+ }
27513
+ }, [isPlaying, controlsPinned]);
27514
+ const handleMouseMove = React23.useCallback(() => {
27515
+ resetControlsTimeout();
27516
+ }, [resetControlsTimeout]);
27517
+ React23.useEffect(() => {
27518
+ resetControlsTimeout();
27519
+ return () => {
27520
+ if (controlsTimeoutRef.current) {
27521
+ clearTimeout(controlsTimeoutRef.current);
27522
+ }
27523
+ };
27524
+ }, [isPlaying, resetControlsTimeout]);
27525
+ const handleTogglePlay = React23.useCallback(() => {
27526
+ if (hiddenVideoRef.current?.video) {
27527
+ if (hiddenVideoRef.current.video.paused) {
27528
+ hiddenVideoRef.current.play();
27529
+ } else {
27530
+ hiddenVideoRef.current.pause();
27531
+ }
27532
+ }
27533
+ }, []);
27534
+ const handleSeek = React23.useCallback((time2) => {
27535
+ if (hiddenVideoRef.current) {
27536
+ hiddenVideoRef.current.currentTime(time2);
27537
+ hiddenVideoRef.current.play()?.catch(() => {
27538
+ });
27539
+ setTimeout(() => renderFrameToCanvas(), 50);
27540
+ }
27541
+ }, [renderFrameToCanvas]);
27542
+ const handleSeekStart = React23.useCallback(() => {
27543
+ userSeekingRef.current = true;
27544
+ }, []);
27545
+ const handleSeekEnd = React23.useCallback(() => {
27546
+ if (hiddenVideoRef.current) {
27547
+ hiddenVideoRef.current.play()?.catch(() => {
27548
+ });
27549
+ }
27550
+ }, []);
27551
+ const handlePlaybackRateChange = React23.useCallback((rate) => {
27552
+ if (hiddenVideoRef.current) {
27553
+ hiddenVideoRef.current.playbackRate(rate);
27554
+ setPlaybackRate(rate);
27555
+ }
27556
+ }, []);
27557
+ const handleToggleFullscreen = React23.useCallback(() => {
27558
+ if (videoContainerRef.current) {
27559
+ if (!document.fullscreenElement) {
27560
+ videoContainerRef.current.requestFullscreen();
27561
+ } else {
27562
+ document.exitFullscreen();
27563
+ }
27564
+ }
27565
+ }, []);
27010
27566
  if (!crop) {
27011
- return /* @__PURE__ */ jsxRuntime.jsx(HlsVideoPlayer, { ref, ...videoProps, onClick });
27012
- }
27013
- const handleClickWithIndicator = () => {
27014
- if (!onClick || !hiddenVideoRef.current?.video) return;
27015
- const video = hiddenVideoRef.current.video;
27016
- const willBePlaying = video.paused;
27017
- setIndicatorIsPlaying(willBePlaying);
27018
- setShowIndicator(false);
27019
- setTimeout(() => {
27020
- indicatorKeyRef.current += 1;
27021
- setShowIndicator(true);
27022
- }, 0);
27023
- onClick();
27567
+ return /* @__PURE__ */ jsxRuntime.jsx(HlsVideoPlayer, { ref, ...videoProps, onClick, controls });
27568
+ }
27569
+ const handleClick = () => {
27570
+ if (!onClick && !controls) {
27571
+ handleTogglePlay();
27572
+ }
27573
+ if (onClick) onClick();
27024
27574
  };
27025
27575
  return /* @__PURE__ */ jsxRuntime.jsxs(
27026
27576
  "div",
27027
27577
  {
27028
27578
  ref: videoContainerRef,
27029
- className: `relative w-full h-full flex items-center justify-center bg-black ${onClick ? "cursor-pointer" : ""} ${inheritedClassName}`,
27030
- onClick: handleClickWithIndicator,
27579
+ className: `relative w-full h-full flex items-center justify-center bg-black group ${inheritedClassName} ${onClick || controls ? "cursor-pointer" : ""}`,
27580
+ onClick: handleClick,
27581
+ onMouseMove: handleMouseMove,
27582
+ onMouseLeave: () => isPlaying && !controlsPinned && setShowControls(false),
27031
27583
  children: [
27032
27584
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "hidden", children: /* @__PURE__ */ jsxRuntime.jsx(
27033
27585
  HlsVideoPlayer,
27034
27586
  {
27035
27587
  ref: hiddenVideoRef,
27036
27588
  ...videoProps,
27589
+ controls: false,
27037
27590
  onReady: handleVideoReady,
27038
27591
  onPlay: handleVideoPlay,
27039
27592
  onPause: handleVideoPause,
@@ -27043,7 +27596,9 @@ var CroppedHlsVideoPlayer = React23.forwardRef(({
27043
27596
  onLoadedMetadata: handleLoadedMetadata,
27044
27597
  onLoadedData: videoProps.onLoadedData,
27045
27598
  onPlaying: videoProps.onPlaying,
27046
- onLoadingChange: videoProps.onLoadingChange
27599
+ onLoadingChange: videoProps.onLoadingChange,
27600
+ onTimeUpdate: handleTimeUpdate,
27601
+ onDurationChange: handleDurationChange
27047
27602
  }
27048
27603
  ) }),
27049
27604
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -27060,8 +27615,8 @@ var CroppedHlsVideoPlayer = React23.forwardRef(({
27060
27615
  }
27061
27616
  }
27062
27617
  ),
27063
- !isVideoReady && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(OptifyeLogoLoader_default, { size: "md", message: "Loading video..." }) }),
27064
- debug && isVideoReady && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute top-2 left-2 bg-black/80 text-white text-xs p-2 rounded font-mono", children: [
27618
+ !isVideoReady && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 flex items-center justify-center pointer-events-none", children: /* @__PURE__ */ jsxRuntime.jsx(OptifyeLogoLoader_default, { size: "md", message: "Loading video..." }) }),
27619
+ debug && isVideoReady && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute top-2 left-2 bg-black/80 text-white text-xs p-2 rounded font-mono pointer-events-none z-20", children: [
27065
27620
  /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
27066
27621
  "Crop: ",
27067
27622
  crop.x,
@@ -27085,13 +27640,32 @@ var CroppedHlsVideoPlayer = React23.forwardRef(({
27085
27640
  isProcessing ? "Yes" : "No"
27086
27641
  ] })
27087
27642
  ] }),
27088
- onClick && /* @__PURE__ */ jsxRuntime.jsx(
27089
- PlayPauseIndicator,
27643
+ controls && isVideoReady && /* @__PURE__ */ jsxRuntime.jsx(
27644
+ VideoControls,
27090
27645
  {
27091
- show: showIndicator,
27092
- isPlaying: indicatorIsPlaying
27093
- },
27094
- indicatorKeyRef.current
27646
+ isPlaying,
27647
+ currentTime,
27648
+ duration,
27649
+ buffered,
27650
+ showControls: controlsPinned || showControls || !isPlaying,
27651
+ controlsPinned,
27652
+ playbackRate,
27653
+ onPlayPause: handleTogglePlay,
27654
+ onSeek: handleSeek,
27655
+ onSeekStart: handleSeekStart,
27656
+ onSeekEnd: handleSeekEnd,
27657
+ onPlaybackRateChange: handlePlaybackRateChange,
27658
+ onTogglePinControls: () => setControlsPinned((prev) => {
27659
+ const next = !prev;
27660
+ if (next) {
27661
+ setShowControls(true);
27662
+ } else {
27663
+ resetControlsTimeout();
27664
+ }
27665
+ return next;
27666
+ }),
27667
+ onToggleFullscreen: handleToggleFullscreen
27668
+ }
27095
27669
  )
27096
27670
  ]
27097
27671
  }
@@ -27630,6 +28204,67 @@ var SilentErrorBoundary = class extends React23__namespace.default.Component {
27630
28204
  ] }) });
27631
28205
  }
27632
28206
  };
28207
+ var PlayPauseIndicator = ({
28208
+ show,
28209
+ isPlaying,
28210
+ duration = 600
28211
+ }) => {
28212
+ const [isVisible, setIsVisible] = React23.useState(false);
28213
+ const [isFading, setIsFading] = React23.useState(false);
28214
+ React23.useEffect(() => {
28215
+ if (show) {
28216
+ setIsVisible(true);
28217
+ setIsFading(false);
28218
+ const fadeTimer = setTimeout(() => {
28219
+ setIsFading(true);
28220
+ }, 100);
28221
+ const hideTimer = setTimeout(() => {
28222
+ setIsVisible(false);
28223
+ setIsFading(false);
28224
+ }, duration);
28225
+ return () => {
28226
+ clearTimeout(fadeTimer);
28227
+ clearTimeout(hideTimer);
28228
+ };
28229
+ }
28230
+ }, [show, duration]);
28231
+ if (!isVisible) return null;
28232
+ return /* @__PURE__ */ jsxRuntime.jsx(
28233
+ "div",
28234
+ {
28235
+ className: "absolute inset-0 flex items-center justify-center pointer-events-none z-10",
28236
+ style: {
28237
+ opacity: isFading ? 0 : 1,
28238
+ transition: `opacity ${duration - 100}ms ease-out`
28239
+ },
28240
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-black/70 rounded-full p-6", children: isPlaying ? (
28241
+ // Play icon (triangle)
28242
+ /* @__PURE__ */ jsxRuntime.jsx(
28243
+ "svg",
28244
+ {
28245
+ xmlns: "http://www.w3.org/2000/svg",
28246
+ viewBox: "0 0 24 24",
28247
+ fill: "white",
28248
+ className: "w-16 h-16",
28249
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8 5v14l11-7z" })
28250
+ }
28251
+ )
28252
+ ) : (
28253
+ // Pause icon (two bars)
28254
+ /* @__PURE__ */ jsxRuntime.jsx(
28255
+ "svg",
28256
+ {
28257
+ xmlns: "http://www.w3.org/2000/svg",
28258
+ viewBox: "0 0 24 24",
28259
+ fill: "white",
28260
+ className: "w-16 h-16",
28261
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 4h4v16H6V4zm8 0h4v16h-4V4z" })
28262
+ }
28263
+ )
28264
+ ) })
28265
+ }
28266
+ );
28267
+ };
27633
28268
  var BackButton = ({
27634
28269
  onClick,
27635
28270
  text = "Back",
@@ -31049,7 +31684,7 @@ function DiagnosisVideoModal({
31049
31684
  }
31050
31685
  loadClip();
31051
31686
  }, [clipId, supabase, transformPlaylistUrls]);
31052
- const formatTime3 = (seconds) => {
31687
+ const formatTime5 = (seconds) => {
31053
31688
  const mins = Math.floor(seconds / 60);
31054
31689
  const secs = Math.floor(seconds % 60);
31055
31690
  return `${mins}:${secs.toString().padStart(2, "0")}`;
@@ -31241,9 +31876,9 @@ function DiagnosisVideoModal({
31241
31876
  }
31242
31877
  ),
31243
31878
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm font-medium", children: [
31244
- formatTime3(currentTime),
31879
+ formatTime5(currentTime),
31245
31880
  " / ",
31246
- formatTime3(duration)
31881
+ formatTime5(duration)
31247
31882
  ] }),
31248
31883
  /* @__PURE__ */ jsxRuntime.jsx(
31249
31884
  "input",
@@ -33063,45 +33698,35 @@ var LinePdfGenerator = ({
33063
33698
  const doc = new jsPDF.jsPDF();
33064
33699
  const pageHeight = doc.internal.pageSize.height;
33065
33700
  const addHeaderPage1 = () => {
33066
- doc.setFontSize(12);
33701
+ doc.setFontSize(14);
33067
33702
  doc.setFont("helvetica", "bold");
33068
- doc.setTextColor(70, 70, 70);
33703
+ doc.setTextColor(50, 50, 50);
33069
33704
  doc.text("OPTIFYE.AI", 20, 15);
33070
- doc.setFontSize(10);
33705
+ doc.setFontSize(9);
33071
33706
  doc.setFont("helvetica", "normal");
33072
33707
  doc.setTextColor(100, 100, 100);
33073
- const reportText = "REAL-TIME PERFORMANCE REPORT";
33074
- const reportTextWidth = doc.getStringUnitWidth(reportText) * 10 / doc.internal.scaleFactor;
33075
- doc.text(reportText, doc.internal.pageSize.width - 20 - reportTextWidth, 15);
33076
- doc.setDrawColor(220, 220, 220);
33077
- doc.setLineWidth(0.3);
33708
+ const generatedText = `Generated on ${(/* @__PURE__ */ new Date()).toLocaleString("en-IN", { timeZone: "Asia/Kolkata" })}`;
33709
+ const generatedTextWidth = doc.getStringUnitWidth(generatedText) * 9 / doc.internal.scaleFactor;
33710
+ doc.text(generatedText, doc.internal.pageSize.width - 20 - generatedTextWidth, 15);
33711
+ doc.setDrawColor(200, 200, 200);
33712
+ doc.setLineWidth(0.5);
33078
33713
  doc.line(20, 20, 190, 20);
33079
33714
  };
33080
33715
  const addHeaderPage2 = () => {
33081
- doc.setFontSize(12);
33716
+ doc.setFontSize(14);
33082
33717
  doc.setFont("helvetica", "bold");
33083
- doc.setTextColor(70, 70, 70);
33718
+ doc.setTextColor(50, 50, 50);
33084
33719
  doc.text("OPTIFYE.AI", 20, 15);
33085
- doc.setFontSize(10);
33720
+ doc.setFontSize(9);
33086
33721
  doc.setFont("helvetica", "normal");
33087
33722
  doc.setTextColor(100, 100, 100);
33088
- const reportText = "REAL-TIME PERFORMANCE REPORT";
33089
- const reportTextWidth = doc.getStringUnitWidth(reportText) * 10 / doc.internal.scaleFactor;
33090
- doc.text(reportText, doc.internal.pageSize.width - 20 - reportTextWidth, 15);
33091
- doc.text("Page 2", doc.internal.pageSize.width - 30, pageHeight - 15);
33092
- doc.setDrawColor(220, 220, 220);
33093
- doc.setLineWidth(0.3);
33094
- doc.line(20, 20, 190, 20);
33095
- return 35;
33096
- };
33097
- const addFooter = (pageNum) => {
33098
- doc.setFontSize(9);
33099
- doc.setTextColor(130, 130, 130);
33100
33723
  const generatedText = `Generated on ${(/* @__PURE__ */ new Date()).toLocaleString("en-IN", { timeZone: "Asia/Kolkata" })}`;
33101
- doc.text(generatedText, 20, pageHeight - 15);
33102
- if (pageNum === 1) {
33103
- doc.text("Page 1", doc.internal.pageSize.width - 30, pageHeight - 15);
33104
- }
33724
+ const generatedTextWidth = doc.getStringUnitWidth(generatedText) * 9 / doc.internal.scaleFactor;
33725
+ doc.text(generatedText, doc.internal.pageSize.width - 20 - generatedTextWidth, 15);
33726
+ doc.setDrawColor(200, 200, 200);
33727
+ doc.setLineWidth(0.5);
33728
+ doc.line(20, 20, 190, 20);
33729
+ return 30;
33105
33730
  };
33106
33731
  addHeaderPage1();
33107
33732
  doc.setFontSize(26);
@@ -33147,37 +33772,47 @@ var LinePdfGenerator = ({
33147
33772
  doc.setDrawColor(200, 200, 200);
33148
33773
  doc.setLineWidth(0.5);
33149
33774
  doc.line(20, 53, 190, 53);
33150
- doc.setFontSize(16);
33775
+ const perfOverviewStartY = 58;
33776
+ doc.setFillColor(245, 245, 245);
33777
+ doc.roundedRect(15, perfOverviewStartY, 180, 50, 3, 3, "F");
33778
+ doc.setFontSize(18);
33151
33779
  doc.setFont("helvetica", "bold");
33152
- doc.setTextColor(70, 70, 70);
33153
- doc.text("Line Performance Overview", 20, 65);
33780
+ doc.setTextColor(40, 40, 40);
33781
+ doc.text("Line Performance Overview", 20, 68);
33154
33782
  doc.setTextColor(0, 0, 0);
33155
33783
  const createKPIBox = (y) => {
33156
- doc.setFillColor(250, 250, 250);
33157
- doc.roundedRect(22, y - 7, 165, 12, 1, 1, "F");
33784
+ doc.setFillColor(255, 255, 255);
33785
+ doc.roundedRect(22, y - 7, 165, 12, 2, 2, "F");
33786
+ doc.setDrawColor(230, 230, 230);
33787
+ doc.setLineWidth(0.2);
33788
+ doc.roundedRect(22, y - 7, 165, 12, 2, 2, "S");
33158
33789
  };
33159
- createKPIBox(77);
33160
- doc.setFontSize(12);
33790
+ const kpiStartY = 80;
33791
+ const kpiSpacing = 10;
33792
+ createKPIBox(kpiStartY);
33793
+ doc.setFontSize(11);
33161
33794
  doc.setFont("helvetica", "normal");
33162
- doc.text("Output:", 25, 77);
33795
+ doc.text("Output:", 25, kpiStartY);
33163
33796
  doc.setFont("helvetica", "bold");
33164
- doc.text(`${lineInfo.metrics.current_output} / ${lineInfo.metrics.line_threshold}`, 120, 77);
33165
- createKPIBox(87);
33797
+ doc.text(`${lineInfo.metrics.current_output} / ${lineInfo.metrics.line_threshold}`, 120, kpiStartY);
33798
+ createKPIBox(kpiStartY + kpiSpacing);
33166
33799
  doc.setFont("helvetica", "normal");
33167
- doc.text("Underperforming Workspaces:", 25, 87);
33800
+ doc.text("Underperforming Workspaces:", 25, kpiStartY + kpiSpacing);
33168
33801
  doc.setFont("helvetica", "bold");
33169
- doc.text(`${lineInfo.metrics.underperforming_workspaces} / ${lineInfo.metrics.total_workspaces}`, 120, 87);
33170
- createKPIBox(97);
33802
+ doc.text(`${lineInfo.metrics.underperforming_workspaces} / ${lineInfo.metrics.total_workspaces}`, 120, kpiStartY + kpiSpacing);
33803
+ createKPIBox(kpiStartY + kpiSpacing * 2);
33171
33804
  doc.setFont("helvetica", "normal");
33172
- doc.text("Average Efficiency:", 25, 97);
33805
+ doc.text("Average Efficiency:", 25, kpiStartY + kpiSpacing * 2);
33173
33806
  doc.setFont("helvetica", "bold");
33174
- doc.text(`${lineInfo.metrics.avg_efficiency.toFixed(1)}%`, 120, 97);
33175
- doc.setDrawColor(200, 200, 200);
33176
- doc.line(20, 110, 190, 110);
33177
- doc.setFontSize(16);
33807
+ doc.text(`${lineInfo.metrics.avg_efficiency.toFixed(1)}%`, 120, kpiStartY + kpiSpacing * 2);
33808
+ doc.setDrawColor(180, 180, 180);
33809
+ doc.setLineWidth(0.8);
33810
+ doc.line(20, 118, 190, 118);
33811
+ const hourlyOverviewStartY = 123;
33812
+ doc.setFontSize(18);
33178
33813
  doc.setFont("helvetica", "bold");
33179
- doc.setTextColor(70, 70, 70);
33180
- doc.text("Hourly Output Overview", 20, 135);
33814
+ doc.setTextColor(40, 40, 40);
33815
+ doc.text("Hourly Output Overview", 20, 133);
33181
33816
  doc.setTextColor(0, 0, 0);
33182
33817
  const getHourlyTimeRanges = (startTimeStr, endTimeStr) => {
33183
33818
  const [hours, minutes] = startTimeStr.split(":");
@@ -33214,7 +33849,7 @@ var LinePdfGenerator = ({
33214
33849
  }
33215
33850
  hourEndTime.setSeconds(0);
33216
33851
  hourEndTime.setMilliseconds(0);
33217
- const formatTime3 = (date2) => {
33852
+ const formatTime5 = (date2) => {
33218
33853
  return date2.toLocaleTimeString("en-IN", {
33219
33854
  hour: "2-digit",
33220
33855
  minute: "2-digit",
@@ -33222,7 +33857,7 @@ var LinePdfGenerator = ({
33222
33857
  timeZone: "Asia/Kolkata"
33223
33858
  });
33224
33859
  };
33225
- return `${formatTime3(hourStartTime)} - ${formatTime3(hourEndTime)}`;
33860
+ return `${formatTime5(hourStartTime)} - ${formatTime5(hourEndTime)}`;
33226
33861
  });
33227
33862
  };
33228
33863
  const hourlyTimeRanges = lineInfo.metrics.shift_start ? getHourlyTimeRanges(lineInfo.metrics.shift_start, lineInfo.metrics.shift_end) : [];
@@ -33369,86 +34004,94 @@ var LinePdfGenerator = ({
33369
34004
  return Math.round(lineInfo.metrics.current_output / shiftDuration);
33370
34005
  });
33371
34006
  }
34007
+ const tableHeaderY = 143;
34008
+ const tableStartY = 151;
34009
+ const rowSpacing = 8;
34010
+ const bottomPadding = 8;
34011
+ const hourlyTableHeight = hourlyTimeRanges.length * rowSpacing;
34012
+ const backgroundHeight = tableStartY - hourlyOverviewStartY + hourlyTableHeight + bottomPadding;
34013
+ doc.setFillColor(245, 245, 245);
34014
+ doc.roundedRect(15, hourlyOverviewStartY, 180, backgroundHeight, 3, 3, "F");
33372
34015
  doc.setFontSize(11);
33373
34016
  doc.setFont("helvetica", "bold");
33374
- doc.setFillColor(245, 245, 245);
33375
- doc.roundedRect(20, 140, 170, 8, 1, 1, "F");
33376
- doc.text("Time Range", 25, 145);
33377
- doc.text("Actual Output", 80, 145);
33378
- doc.text("Target Output", 125, 145);
33379
- doc.text("Status", 170, 145);
33380
- doc.setLineWidth(0.2);
33381
- doc.setDrawColor(220, 220, 220);
33382
- doc.line(20, 148, 190, 148);
34017
+ doc.text("Time Range", 25, tableHeaderY);
34018
+ doc.text("Output", 80, tableHeaderY);
34019
+ doc.text("Target", 125, tableHeaderY);
34020
+ doc.text("Status", 170, tableHeaderY);
34021
+ doc.setLineWidth(0.3);
34022
+ doc.setDrawColor(200, 200, 200);
34023
+ doc.line(20, 146, 190, 146);
33383
34024
  doc.setFont("helvetica", "normal");
33384
- let yPos = 155;
33385
- const rowSpacing = 7;
34025
+ let yPos = tableStartY;
34026
+ const lineDateForTable = new Date(lineInfo.date);
34027
+ const todayForTable = /* @__PURE__ */ new Date();
34028
+ todayForTable.setHours(0, 0, 0, 0);
34029
+ lineDateForTable.setHours(0, 0, 0, 0);
34030
+ const isTodayForTable = lineDateForTable.getTime() === todayForTable.getTime();
34031
+ let currentHour = 24;
34032
+ if (isTodayForTable) {
34033
+ const now2 = /* @__PURE__ */ new Date();
34034
+ const currentTimeIST = new Date(now2.toLocaleString("en-US", { timeZone: "Asia/Kolkata" }));
34035
+ currentHour = currentTimeIST.getHours();
34036
+ }
33386
34037
  hourlyTimeRanges.forEach((timeRange, index) => {
33387
- if (index % 2 === 0) {
33388
- doc.setFillColor(252, 252, 252);
33389
- doc.roundedRect(20, yPos - 5, 170, 8, 1, 1, "F");
33390
- }
33391
- const actualOutput = hourlyActualOutput[index];
33392
- const isOverTarget = actualOutput >= targetOutputPerHour;
34038
+ const actualOutput = hourlyActualOutput[index] || 0;
34039
+ const [startHourStr] = (lineInfo.metrics.shift_start || "6:00").split(":");
34040
+ const startHour = parseInt(startHourStr);
34041
+ const hourNumber = (startHour + index) % 24;
34042
+ const dataCollected = !isTodayForTable || hourNumber < currentHour;
34043
+ const outputStr = dataCollected ? actualOutput.toString() : "TBD";
34044
+ const targetStr = targetOutputPerHour.toString();
33393
34045
  doc.text(timeRange, 25, yPos);
33394
- doc.text(actualOutput.toString(), 80, yPos);
33395
- doc.text(targetOutputPerHour.toString(), 125, yPos);
33396
- if (isOverTarget) {
33397
- doc.setDrawColor(0, 171, 69);
33398
- doc.setLineWidth(0.5);
33399
- const tickX = 170;
33400
- const tickY = yPos - 1;
33401
- doc.line(tickX - 2, tickY, tickX, tickY + 1.5);
33402
- doc.line(tickX, tickY + 1.5, tickX + 2, tickY - 2);
34046
+ doc.text(outputStr, 80, yPos);
34047
+ doc.text(targetStr, 125, yPos);
34048
+ if (!dataCollected) {
34049
+ doc.setTextColor(100, 100, 100);
34050
+ doc.text("-", 170, yPos);
34051
+ } else if (actualOutput >= targetOutputPerHour) {
34052
+ doc.setTextColor(0, 171, 69);
34053
+ doc.setFont("ZapfDingbats", "normal");
34054
+ doc.text("4", 170, yPos);
34055
+ doc.setFont("helvetica", "normal");
33403
34056
  } else {
33404
- doc.setDrawColor(227, 67, 41);
33405
- doc.setLineWidth(0.5);
33406
- const crossX = 170;
33407
- const crossY = yPos - 1;
33408
- doc.line(crossX - 2, crossY - 2, crossX + 2, crossY + 2);
33409
- doc.line(crossX - 2, crossY + 2, crossX + 2, crossY - 2);
34057
+ doc.setTextColor(227, 67, 41);
34058
+ doc.text("\xD7", 170, yPos);
33410
34059
  }
33411
- doc.setDrawColor(220, 220, 220);
33412
- doc.setLineWidth(0.2);
34060
+ doc.setTextColor(0, 0, 0);
33413
34061
  yPos += rowSpacing;
33414
34062
  });
33415
- doc.roundedRect(20, 140, 170, yPos - 140 - 3, 1, 1, "S");
33416
- addFooter(1);
33417
34063
  doc.addPage();
33418
34064
  yPos = addHeaderPage2();
33419
- doc.setFontSize(16);
34065
+ const workspaceSectionStartY = yPos;
34066
+ const sortedWorkspaces = [...workspaceData].sort((a, b) => (a.efficiency || 0) - (b.efficiency || 0)).slice(0, 10);
34067
+ const wsRowCount = sortedWorkspaces.length > 0 ? sortedWorkspaces.length : 1;
34068
+ const wsTableHeight = 10 + 8 + 7 + wsRowCount * 8 + 8;
34069
+ doc.setFillColor(245, 245, 245);
34070
+ doc.roundedRect(15, workspaceSectionStartY, 180, wsTableHeight, 3, 3, "F");
34071
+ doc.setFontSize(18);
33420
34072
  doc.setFont("helvetica", "bold");
33421
- doc.setTextColor(70, 70, 70);
34073
+ doc.setTextColor(40, 40, 40);
33422
34074
  doc.text("Poorest Performing Workspaces", 20, yPos);
33423
34075
  doc.setTextColor(0, 0, 0);
33424
34076
  yPos += 10;
33425
34077
  doc.setFontSize(11);
33426
34078
  doc.setFont("helvetica", "bold");
33427
- doc.setFillColor(245, 245, 245);
33428
- doc.roundedRect(20, yPos, 170, 8, 1, 1, "F");
33429
34079
  yPos += 5;
33430
34080
  doc.text("Workspace", 25, yPos);
33431
- doc.text("Current/Ideal", 85, yPos);
34081
+ doc.text("Current/Target", 85, yPos);
33432
34082
  doc.text("Efficiency", 145, yPos);
33433
34083
  yPos += 3;
33434
- doc.setLineWidth(0.2);
33435
- doc.setDrawColor(220, 220, 220);
34084
+ doc.setLineWidth(0.3);
34085
+ doc.setDrawColor(200, 200, 200);
33436
34086
  doc.line(20, yPos, 190, yPos);
33437
34087
  doc.setFont("helvetica", "normal");
33438
- const tableStartY = yPos;
33439
34088
  yPos += 7;
33440
- const sortedWorkspaces = [...workspaceData].sort((a, b) => (a.efficiency || 0) - (b.efficiency || 0)).slice(0, 10);
33441
34089
  if (sortedWorkspaces.length === 0) {
33442
34090
  doc.text("No workspace data available", 25, yPos);
33443
34091
  yPos += 10;
33444
34092
  } else {
33445
34093
  sortedWorkspaces.forEach((ws, index) => {
33446
- if (index % 2 === 0) {
33447
- doc.setFillColor(252, 252, 252);
33448
- doc.roundedRect(20, yPos - 5, 170, 8, 1, 1, "F");
33449
- }
33450
34094
  const workspaceName = getWorkspaceDisplayName(ws.workspace_name, lineInfo.line_id);
33451
- const maxWidth = 55;
33452
34095
  const truncatedName = workspaceName.length > 25 ? workspaceName.substring(0, 22) + "..." : workspaceName;
33453
34096
  doc.text(truncatedName, 25, yPos);
33454
34097
  doc.text(`${ws.action_count || 0} / ${ws.action_threshold || 0}`, 85, yPos);
@@ -33456,9 +34099,6 @@ var LinePdfGenerator = ({
33456
34099
  yPos += 8;
33457
34100
  });
33458
34101
  }
33459
- const wsTableHeight = yPos - tableStartY - 3;
33460
- doc.roundedRect(20, tableStartY, 170, wsTableHeight, 1, 1, "S");
33461
- addFooter(2);
33462
34102
  doc.save(`${lineInfo.line_name}_${date.split(",")[0]}.pdf`);
33463
34103
  } catch (error) {
33464
34104
  console.error("PDF generation failed:", error);
@@ -34632,25 +35272,25 @@ var WorkspacePdfGenerator = ({ workspace, className }) => {
34632
35272
  doc.setFont("helvetica", "bold");
34633
35273
  doc.setTextColor(50, 50, 50);
34634
35274
  doc.text("OPTIFYE.AI", 20, 15);
34635
- doc.setFontSize(11);
35275
+ doc.setFontSize(9);
34636
35276
  doc.setFont("helvetica", "normal");
34637
- doc.setTextColor(80, 80, 80);
34638
- const reportText = "REAL-TIME PERFORMANCE REPORT";
34639
- const reportTextWidth = doc.getStringUnitWidth(reportText) * 11 / doc.internal.scaleFactor;
34640
- doc.text(reportText, doc.internal.pageSize.width - 20 - reportTextWidth, 15);
35277
+ doc.setTextColor(100, 100, 100);
35278
+ const generatedText = `Generated on ${(/* @__PURE__ */ new Date()).toLocaleString("en-IN", { timeZone: "Asia/Kolkata" })}`;
35279
+ const generatedTextWidth = doc.getStringUnitWidth(generatedText) * 9 / doc.internal.scaleFactor;
35280
+ doc.text(generatedText, doc.internal.pageSize.width - 20 - generatedTextWidth, 15);
34641
35281
  doc.setDrawColor(200, 200, 200);
34642
35282
  doc.setLineWidth(0.5);
34643
35283
  doc.line(20, 20, 190, 20);
34644
35284
  doc.setFillColor(250, 250, 250);
34645
- doc.roundedRect(15, 25, 180, 55, 3, 3, "F");
35285
+ doc.roundedRect(15, 25, 180, 53, 3, 3, "F");
34646
35286
  doc.setFontSize(32);
34647
35287
  doc.setFont("helvetica", "bold");
34648
35288
  doc.setTextColor(0, 0, 0);
34649
- doc.text(lineName, 20, 40);
35289
+ doc.text(lineName, 20, 39);
34650
35290
  doc.setFontSize(22);
34651
35291
  doc.setFont("helvetica", "normal");
34652
35292
  doc.setTextColor(40, 40, 40);
34653
- doc.text(getWorkspaceDisplayName(workspace.workspace_name, workspace.line_id), 20, 52);
35293
+ doc.text(getWorkspaceDisplayName(workspace.workspace_name, workspace.line_id), 20, 51);
34654
35294
  doc.setFontSize(13);
34655
35295
  doc.setFont("helvetica", "normal");
34656
35296
  doc.setTextColor(60, 60, 60);
@@ -34661,8 +35301,8 @@ var WorkspacePdfGenerator = ({ workspace, className }) => {
34661
35301
  timeZone: "Asia/Kolkata"
34662
35302
  });
34663
35303
  const shiftType = "Day Shift";
34664
- doc.text(`${date}`, 20, 65);
34665
- doc.text(`${shiftType}`, 20, 73);
35304
+ doc.text(`${date}`, 20, 63);
35305
+ doc.text(`${shiftType}`, 20, 71);
34666
35306
  const currentTime = (/* @__PURE__ */ new Date()).toLocaleTimeString("en-IN", {
34667
35307
  hour: "2-digit",
34668
35308
  minute: "2-digit",
@@ -34675,11 +35315,11 @@ var WorkspacePdfGenerator = ({ workspace, className }) => {
34675
35315
  });
34676
35316
  doc.setFontSize(12);
34677
35317
  doc.setTextColor(80, 80, 80);
34678
- doc.text(`Report Period: ${shiftStartTime} - ${currentTime}`, 20, 81);
35318
+ doc.text(`Report Period: ${shiftStartTime} - ${currentTime}`, 20, 79);
34679
35319
  doc.setTextColor(0, 0, 0);
34680
35320
  doc.setDrawColor(180, 180, 180);
34681
35321
  doc.setLineWidth(0.8);
34682
- doc.line(20, 90, 190, 90);
35322
+ doc.line(20, 88, 190, 88);
34683
35323
  const createKPIBox = (y) => {
34684
35324
  doc.setFillColor(255, 255, 255);
34685
35325
  doc.roundedRect(22, y - 7, 165, 12, 2, 2, "F");
@@ -34687,66 +35327,72 @@ var WorkspacePdfGenerator = ({ workspace, className }) => {
34687
35327
  doc.setLineWidth(0.2);
34688
35328
  doc.roundedRect(22, y - 7, 165, 12, 2, 2, "S");
34689
35329
  };
35330
+ const perfOverviewStartY = 93;
34690
35331
  doc.setFillColor(245, 245, 245);
34691
- doc.roundedRect(15, 95, 180, 60, 3, 3, "F");
35332
+ doc.roundedRect(15, perfOverviewStartY, 180, 60, 3, 3, "F");
34692
35333
  doc.setFontSize(18);
34693
35334
  doc.setFont("helvetica", "bold");
34694
35335
  doc.setTextColor(40, 40, 40);
34695
- doc.text("Performance Overview", 20, 105);
35336
+ doc.text("Performance Overview", 20, 103);
34696
35337
  doc.setTextColor(0, 0, 0);
34697
- const kpiStartY = 117;
35338
+ const kpiStartY = 115;
34698
35339
  const kpiSpacing = 10;
34699
35340
  createKPIBox(kpiStartY);
34700
35341
  doc.setFontSize(11);
34701
35342
  doc.setFont("helvetica", "normal");
34702
- doc.text("Progress:", 25, kpiStartY);
35343
+ doc.text("Current Output/Target Output:", 25, kpiStartY);
34703
35344
  doc.setFont("helvetica", "bold");
34704
- doc.text(`${(workspace.total_actions / workspace.target_output * 100).toFixed(0)}% of Today's target`, 120, kpiStartY);
35345
+ doc.text(`${workspace.total_actions} / ${workspace.target_output}`, 120, kpiStartY);
34705
35346
  createKPIBox(kpiStartY + kpiSpacing);
34706
35347
  doc.setFont("helvetica", "normal");
34707
- doc.text("Current Output/Ideal Output:", 25, kpiStartY + kpiSpacing);
35348
+ doc.text("Efficiency:", 25, kpiStartY + kpiSpacing);
34708
35349
  doc.setFont("helvetica", "bold");
34709
- doc.text(`${workspace.total_actions} / ${workspace.target_output}`, 120, kpiStartY + kpiSpacing);
35350
+ doc.text(`${(workspace.avg_efficiency || 0).toFixed(1)}% (Target: 80%)`, 120, kpiStartY + kpiSpacing);
34710
35351
  createKPIBox(kpiStartY + kpiSpacing * 2);
34711
35352
  doc.setFont("helvetica", "normal");
34712
- doc.text("Efficiency:", 25, kpiStartY + kpiSpacing * 2);
34713
- doc.setFont("helvetica", "bold");
34714
- doc.text(`${(workspace.avg_efficiency || 0).toFixed(1)}% (Target: 80%)`, 120, kpiStartY + kpiSpacing * 2);
34715
- createKPIBox(kpiStartY + kpiSpacing * 3);
34716
- doc.setFont("helvetica", "normal");
34717
- doc.text("PPH (Pieces Per Hour):", 25, kpiStartY + kpiSpacing * 3);
35353
+ doc.text("PPH (Pieces Per Hour):", 25, kpiStartY + kpiSpacing * 2);
34718
35354
  doc.setFont("helvetica", "bold");
34719
- doc.text(`${workspace.avg_pph.toFixed(1)} (Standard: ${workspace.pph_threshold.toFixed(1)})`, 120, kpiStartY + kpiSpacing * 3);
35355
+ doc.text(`${workspace.avg_pph.toFixed(1)} (Standard: ${workspace.pph_threshold.toFixed(1)})`, 120, kpiStartY + kpiSpacing * 2);
34720
35356
  doc.setDrawColor(180, 180, 180);
34721
35357
  doc.setLineWidth(0.8);
34722
- doc.line(20, 170, 190, 170);
35358
+ doc.line(20, 163, 190, 163);
35359
+ const hourlyPerfStartY = 168;
35360
+ const hourlyData = workspace.hourly_action_counts || [];
35361
+ const hourlyTarget = workspace.pph_threshold;
35362
+ const tableStartY = 199;
35363
+ const rowHeight = 8;
35364
+ const bottomPadding = 8;
35365
+ const backgroundHeight = tableStartY - hourlyPerfStartY + hourlyData.length * rowHeight + bottomPadding;
34723
35366
  doc.setFillColor(245, 245, 245);
34724
- doc.roundedRect(15, 175, 180, 85, 3, 3, "F");
35367
+ doc.roundedRect(15, hourlyPerfStartY, 180, backgroundHeight, 3, 3, "F");
34725
35368
  doc.setFontSize(18);
34726
35369
  doc.setFont("helvetica", "bold");
34727
35370
  doc.setTextColor(40, 40, 40);
34728
- doc.text("Hourly Performance", 20, 185);
35371
+ doc.text("Hourly Performance", 20, 178);
34729
35372
  doc.setTextColor(0, 0, 0);
34730
35373
  doc.setFontSize(11);
34731
35374
  doc.setFont("helvetica", "bold");
34732
- doc.setFillColor(245, 245, 245);
34733
- doc.roundedRect(20, 177, 170, 8, 1, 1, "F");
34734
- doc.text("Time Range", 25, 182);
34735
- doc.text("Output", 95, 182);
34736
- doc.text("Target", 135, 182);
34737
- doc.text("Status", 170, 182);
34738
- doc.setLineWidth(0.2);
34739
- doc.setDrawColor(220, 220, 220);
34740
- doc.line(20, 185, 190, 185);
35375
+ doc.text("Time Range", 25, 188);
35376
+ doc.text("Output", 95, 188);
35377
+ doc.text("Target", 135, 188);
35378
+ doc.text("Status", 170, 188);
35379
+ doc.setLineWidth(0.3);
35380
+ doc.setDrawColor(200, 200, 200);
35381
+ doc.line(20, 191, 190, 191);
34741
35382
  doc.setFont("helvetica", "normal");
34742
- let yPos = 191;
34743
- const hourlyData = workspace.hourly_action_counts || [];
34744
- const hourlyTarget = workspace.pph_threshold;
35383
+ let yPos = tableStartY;
35384
+ const workspaceDate = new Date(workspace.date);
35385
+ const today = /* @__PURE__ */ new Date();
35386
+ today.setHours(0, 0, 0, 0);
35387
+ workspaceDate.setHours(0, 0, 0, 0);
35388
+ const isToday = workspaceDate.getTime() === today.getTime();
35389
+ let currentHour = 24;
35390
+ if (isToday) {
35391
+ const now2 = /* @__PURE__ */ new Date();
35392
+ const currentTimeIST = new Date(now2.toLocaleString("en-US", { timeZone: "Asia/Kolkata" }));
35393
+ currentHour = currentTimeIST.getHours();
35394
+ }
34745
35395
  hourlyData.forEach((output, index) => {
34746
- if (index % 2 === 0) {
34747
- doc.setFillColor(252, 252, 252);
34748
- doc.roundedRect(20, yPos - 5, 170, 8, 1, 1, "F");
34749
- }
34750
35396
  const startTime = /* @__PURE__ */ new Date(`2000-01-01 ${workspace.shift_start}`);
34751
35397
  startTime.setHours(startTime.getHours() + index);
34752
35398
  const endTime = new Date(startTime);
@@ -34758,14 +35404,17 @@ var WorkspacePdfGenerator = ({ workspace, className }) => {
34758
35404
  hour: "numeric",
34759
35405
  hour12: true
34760
35406
  })}`;
34761
- const outputStr = output.toString();
35407
+ const hourNumber = startTime.getHours();
35408
+ const dataCollected = !isToday || hourNumber < currentHour;
35409
+ const outputStr = dataCollected ? output.toString() : "TBD";
34762
35410
  const targetStr = hourlyTarget.toString();
34763
- const outputX = 95 + doc.getStringUnitWidth(outputStr) * doc.getFontSize() / (2 * doc.internal.scaleFactor);
34764
- const targetX = 135 + doc.getStringUnitWidth(targetStr) * doc.getFontSize() / (2 * doc.internal.scaleFactor);
34765
35411
  doc.text(timeRange, 25, yPos);
34766
- doc.text(outputStr, outputX, yPos);
34767
- doc.text(targetStr, targetX, yPos);
34768
- if (output >= hourlyTarget) {
35412
+ doc.text(outputStr, 95, yPos);
35413
+ doc.text(targetStr, 135, yPos);
35414
+ if (!dataCollected) {
35415
+ doc.setTextColor(100, 100, 100);
35416
+ doc.text("-", 170, yPos);
35417
+ } else if (output >= hourlyTarget) {
34769
35418
  doc.setTextColor(0, 171, 69);
34770
35419
  doc.setFont("ZapfDingbats", "normal");
34771
35420
  doc.text("4", 170, yPos);
@@ -34775,15 +35424,8 @@ var WorkspacePdfGenerator = ({ workspace, className }) => {
34775
35424
  doc.text("\xD7", 170, yPos);
34776
35425
  }
34777
35426
  doc.setTextColor(0, 0, 0);
34778
- yPos += 8;
35427
+ yPos += rowHeight;
34779
35428
  });
34780
- doc.setLineWidth(0.2);
34781
- doc.setDrawColor(220, 220, 220);
34782
- doc.roundedRect(20, 187, 170, yPos - 187 - 3, 1, 1, "S");
34783
- doc.setFontSize(9);
34784
- doc.setTextColor(130, 130, 130);
34785
- const generatedText = `Generated on ${(/* @__PURE__ */ new Date()).toLocaleString("en-IN", { timeZone: "Asia/Kolkata" })}`;
34786
- doc.text(generatedText, 20, 280);
34787
35429
  doc.save(`${workspace.workspace_name}_${date.split(",")[0]}.pdf`);
34788
35430
  } catch (error) {
34789
35431
  console.error("PDF generation failed:", error);
@@ -35863,7 +36505,8 @@ var WorkspaceHealthCard = ({
35863
36505
  workspace,
35864
36506
  onClick,
35865
36507
  showDetails = true,
35866
- className = ""
36508
+ className = "",
36509
+ onViewDetails
35867
36510
  }) => {
35868
36511
  const getStatusConfig = () => {
35869
36512
  switch (workspace.status) {
@@ -35916,38 +36559,101 @@ var WorkspaceHealthCard = ({
35916
36559
  onClick(workspace);
35917
36560
  }
35918
36561
  };
36562
+ const handleViewDetails = (event) => {
36563
+ event.stopPropagation();
36564
+ event.preventDefault();
36565
+ if (onViewDetails) {
36566
+ onViewDetails(workspace);
36567
+ }
36568
+ };
35919
36569
  const handleKeyDown = (event) => {
35920
36570
  if (onClick && (event.key === "Enter" || event.key === " ")) {
35921
36571
  event.preventDefault();
35922
36572
  onClick(workspace);
35923
36573
  }
35924
36574
  };
36575
+ const formatDuration3 = (minutes) => {
36576
+ if (!minutes || minutes <= 0) return "0 min";
36577
+ const rounded = Math.max(Math.round(minutes), 0);
36578
+ const days = Math.floor(rounded / 1440);
36579
+ const hours = Math.floor(rounded % 1440 / 60);
36580
+ const mins = rounded % 60;
36581
+ const parts = [];
36582
+ if (days) parts.push(`${days} day${days === 1 ? "" : "s"}`);
36583
+ if (hours) parts.push(`${hours} hr${hours === 1 ? "" : "s"}`);
36584
+ if (mins) parts.push(`${mins} min${mins === 1 ? "" : "s"}`);
36585
+ if (!parts.length) {
36586
+ parts.push("1 min");
36587
+ }
36588
+ return parts.join(" ");
36589
+ };
35925
36590
  const formatTimeAgo = (timeString) => {
35926
- return timeString.replace("about ", "").replace(" ago", "");
36591
+ if (!timeString) return "Unknown";
36592
+ const cleaned = timeString.replace("about ", "").trim();
36593
+ if (cleaned.toLowerCase() === "never") return "Never";
36594
+ const minuteMatch = cleaned.match(/(\d+)\s*m/);
36595
+ if (!minuteMatch) {
36596
+ return cleaned;
36597
+ }
36598
+ const minutes = parseInt(minuteMatch[1], 10);
36599
+ if (!Number.isFinite(minutes)) return cleaned;
36600
+ if (minutes < 1) return "Just now";
36601
+ if (minutes < 60) {
36602
+ return `${minutes} min ago`;
36603
+ }
36604
+ const hours = Math.floor(minutes / 60);
36605
+ const remainingMinutes = minutes % 60;
36606
+ if (hours < 24) {
36607
+ const parts2 = [`${hours} hr${hours === 1 ? "" : "s"}`];
36608
+ if (remainingMinutes) {
36609
+ parts2.push(`${remainingMinutes} min`);
36610
+ }
36611
+ return `${parts2.join(" ")} ago`;
36612
+ }
36613
+ const days = Math.floor(hours / 24);
36614
+ const remainingHours = hours % 24;
36615
+ const parts = [`${days} day${days === 1 ? "" : "s"}`];
36616
+ if (remainingHours) {
36617
+ parts.push(`${remainingHours} hr${remainingHours === 1 ? "" : "s"}`);
36618
+ }
36619
+ return `${parts.join(" ")} ago`;
35927
36620
  };
35928
- const formatDowntime = (uptimeDetails) => {
35929
- if (!uptimeDetails) return "";
36621
+ const getDowntimeConfig = (uptimeDetails) => {
36622
+ if (!uptimeDetails) {
36623
+ if (workspace.status === "healthy") {
36624
+ return {
36625
+ text: "0m",
36626
+ className: "text-emerald-600 dark:text-emerald-400",
36627
+ label: "Total Downtime"
36628
+ };
36629
+ }
36630
+ return { text: "--", className: "text-slate-400", label: "Total Downtime" };
36631
+ }
35930
36632
  const downtimeMinutes = Math.max(0, uptimeDetails.expectedMinutes - uptimeDetails.actualMinutes);
35931
- if (downtimeMinutes === 0) return "No downtime";
35932
- if (downtimeMinutes < 1) return "< 1 min downtime";
35933
- if (downtimeMinutes < 60) return `${downtimeMinutes} min downtime`;
35934
- const hours = Math.floor(downtimeMinutes / 60);
35935
- const minutes = downtimeMinutes % 60;
35936
- if (minutes === 0) {
35937
- return `${hours} hr downtime`;
36633
+ if (downtimeMinutes === 0) {
36634
+ return {
36635
+ text: "0m",
36636
+ className: "text-emerald-600 dark:text-emerald-400",
36637
+ label: "Total Downtime"
36638
+ };
35938
36639
  }
35939
- return `${hours} hr ${minutes} min downtime`;
36640
+ return {
36641
+ text: `${formatDuration3(downtimeMinutes)}`,
36642
+ className: downtimeMinutes > 60 ? "text-rose-600 dark:text-rose-400" : "text-amber-600 dark:text-amber-400",
36643
+ label: "Total Downtime"
36644
+ };
35940
36645
  };
36646
+ const downtimeConfig = getDowntimeConfig(workspace.uptimeDetails);
35941
36647
  return /* @__PURE__ */ jsxRuntime.jsx(
35942
36648
  Card2,
35943
36649
  {
35944
36650
  className: clsx(
35945
- "relative overflow-hidden transition-all duration-300",
36651
+ "relative overflow-hidden transition-all duration-300 h-full flex flex-col",
35946
36652
  "bg-gradient-to-br",
35947
36653
  config.gradient,
35948
36654
  "border",
35949
36655
  config.border,
35950
- "shadow-sm hover:shadow-md",
36656
+ "shadow-sm hover:shadow-lg hover:border-blue-300 dark:hover:border-blue-700",
35951
36657
  onClick && "cursor-pointer hover:scale-[1.01]",
35952
36658
  workspace.isStale && "opacity-90",
35953
36659
  className
@@ -35957,7 +36663,7 @@ var WorkspaceHealthCard = ({
35957
36663
  tabIndex: onClick ? 0 : void 0,
35958
36664
  role: onClick ? "button" : void 0,
35959
36665
  "aria-label": `Workspace ${workspace.workspace_display_name} status: ${workspace.status}`,
35960
- children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4", children: [
36666
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 flex-grow flex flex-col", children: [
35961
36667
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-3", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-start gap-2 sm:gap-3", children: [
35962
36668
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [
35963
36669
  /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-semibold text-gray-900 dark:text-gray-100 mb-1 break-words", children: workspace.workspace_display_name || `Workspace ${workspace.workspace_id.slice(0, 8)}` }),
@@ -35979,21 +36685,20 @@ var WorkspaceHealthCard = ({
35979
36685
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: formatTimeAgo(workspace.timeSinceLastUpdate) })
35980
36686
  ] })
35981
36687
  ] }),
35982
- workspace.uptimePercentage !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
35983
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Activity, { className: "h-3.5 w-3.5 text-gray-400" }),
35984
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm text-gray-600 dark:text-gray-400 whitespace-nowrap", children: [
35985
- "Uptime today: ",
35986
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-medium", children: [
35987
- workspace.uptimePercentage.toFixed(1),
35988
- "%"
35989
- ] })
35990
- ] })
35991
- ] }),
35992
- workspace.uptimeDetails && workspace.uptimeDetails.expectedMinutes > workspace.uptimeDetails.actualMinutes && workspace.uptimePercentage !== void 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: clsx(
35993
- "inline-flex items-center px-2 py-0.5 rounded text-xs font-medium",
35994
- workspace.uptimePercentage >= 97 ? "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400" : workspace.uptimePercentage >= 90 ? "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400" : "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400"
35995
- ), children: formatDowntime(workspace.uptimeDetails) }) })
35996
- ] })
36688
+ /* @__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: [
36689
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-slate-500 uppercase tracking-wide", children: "Total Downtime" }),
36690
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: clsx("text-sm font-semibold", downtimeConfig.className), children: downtimeConfig.text })
36691
+ ] }) })
36692
+ ] }),
36693
+ onViewDetails && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pt-4 mt-auto", children: /* @__PURE__ */ jsxRuntime.jsx(
36694
+ "button",
36695
+ {
36696
+ onClick: handleViewDetails,
36697
+ className: "w-full inline-flex items-center justify-center rounded-lg border border-slate-200/60 bg-white/60 px-3 py-2 text-sm font-medium text-slate-700 shadow-sm backdrop-blur-sm transition-all hover:bg-white/90 hover:text-slate-900 dark:bg-white/10 dark:text-slate-200 dark:border-white/10 dark:hover:bg-white/20",
36698
+ type: "button",
36699
+ children: "View shift timeline"
36700
+ }
36701
+ ) })
35997
36702
  ] })
35998
36703
  }
35999
36704
  );
@@ -36001,8 +36706,11 @@ var WorkspaceHealthCard = ({
36001
36706
  var CompactWorkspaceHealthCard = ({
36002
36707
  workspace,
36003
36708
  onClick,
36004
- className = ""
36709
+ className = "",
36710
+ onViewDetails
36005
36711
  }) => {
36712
+ const downtimeMinutes = workspace.uptimeDetails ? Math.max(0, workspace.uptimeDetails.expectedMinutes - workspace.uptimeDetails.actualMinutes) : null;
36713
+ const downtimeLabel = downtimeMinutes === null ? "No downtime data" : downtimeMinutes === 0 ? "No downtime" : `${downtimeMinutes} min down`;
36006
36714
  const getStatusConfig = () => {
36007
36715
  switch (workspace.status) {
36008
36716
  case "healthy":
@@ -36042,6 +36750,13 @@ var CompactWorkspaceHealthCard = ({
36042
36750
  onClick(workspace);
36043
36751
  }
36044
36752
  };
36753
+ const handleViewDetails = (event) => {
36754
+ event.stopPropagation();
36755
+ event.preventDefault();
36756
+ if (onViewDetails) {
36757
+ onViewDetails(workspace);
36758
+ }
36759
+ };
36045
36760
  return /* @__PURE__ */ jsxRuntime.jsxs(
36046
36761
  "div",
36047
36762
  {
@@ -36069,14 +36784,20 @@ var CompactWorkspaceHealthCard = ({
36069
36784
  workspace.uptimePercentage.toFixed(1),
36070
36785
  "%"
36071
36786
  ] }),
36072
- workspace.uptimeDetails && workspace.uptimeDetails.expectedMinutes > workspace.uptimeDetails.actualMinutes && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400 dark:text-gray-400", children: "\u2022" }),
36073
- workspace.uptimeDetails && workspace.uptimeDetails.expectedMinutes > workspace.uptimeDetails.actualMinutes && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: [
36074
- Math.max(0, workspace.uptimeDetails.expectedMinutes - workspace.uptimeDetails.actualMinutes),
36075
- "m down"
36076
- ] })
36787
+ downtimeMinutes !== null && downtimeMinutes > 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400 dark:text-gray-400", children: "\u2022" }),
36788
+ downtimeLabel && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: downtimeLabel })
36077
36789
  ] }),
36078
36790
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: workspace.timeSinceLastUpdate }),
36079
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: clsx("h-2 w-2 rounded-full", config.dot) })
36791
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: clsx("h-2 w-2 rounded-full", config.dot) }),
36792
+ onViewDetails && /* @__PURE__ */ jsxRuntime.jsx(
36793
+ "button",
36794
+ {
36795
+ onClick: handleViewDetails,
36796
+ className: "rounded-full border border-gray-200 px-2 py-0.5 text-[11px] font-medium text-gray-600 hover:bg-gray-50",
36797
+ type: "button",
36798
+ children: "View"
36799
+ }
36800
+ )
36080
36801
  ] })
36081
36802
  ]
36082
36803
  }
@@ -36085,6 +36806,7 @@ var CompactWorkspaceHealthCard = ({
36085
36806
  var HealthStatusGrid = ({
36086
36807
  workspaces,
36087
36808
  onWorkspaceClick,
36809
+ onWorkspaceViewDetails,
36088
36810
  showFilters = true,
36089
36811
  groupBy: initialGroupBy = "none",
36090
36812
  className = ""
@@ -36272,7 +36994,8 @@ var HealthStatusGrid = ({
36272
36994
  {
36273
36995
  workspace,
36274
36996
  onClick: onWorkspaceClick,
36275
- showDetails: true
36997
+ showDetails: true,
36998
+ onViewDetails: onWorkspaceViewDetails
36276
36999
  },
36277
37000
  workspace.workspace_id
36278
37001
  )) })
@@ -39808,7 +40531,7 @@ var AIAgentView = () => {
39808
40531
  }
39809
40532
  return formattedLines.join("");
39810
40533
  };
39811
- const formatTime3 = (timestamp) => {
40534
+ const formatTime5 = (timestamp) => {
39812
40535
  const date = new Date(timestamp);
39813
40536
  return date.toLocaleTimeString([], {
39814
40537
  hour: "2-digit",
@@ -41072,7 +41795,7 @@ var AIAgentView = () => {
41072
41795
  }
41073
41796
  ),
41074
41797
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `mt-1.5 sm:mt-2 flex items-center gap-2 text-xs text-gray-400 ${message.role === "user" ? "justify-end" : "justify-start"}`, children: [
41075
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: formatTime3(message.created_at) }),
41798
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: formatTime5(message.created_at) }),
41076
41799
  message.role === "assistant" && message.id !== -1 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
41077
41800
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1 h-1 bg-gray-300 rounded-full" }),
41078
41801
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Axel" })
@@ -43903,8 +44626,7 @@ var KPIsOverviewView = ({
43903
44626
  " Shift"
43904
44627
  ] })
43905
44628
  ] })
43906
- ] }) }),
43907
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs sm:text-sm text-gray-600 text-center mt-2 sm:mt-3 px-2 sm:px-0", children: "Click on any line to view detailed performance metrics" })
44629
+ ] }) })
43908
44630
  ] }) }),
43909
44631
  /* @__PURE__ */ jsxRuntime.jsx("main", { className: "flex-1 p-3 sm:p-4 md:p-6 overflow-y-auto", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 sm:gap-4 md:gap-6", children: lines.map((line) => /* @__PURE__ */ jsxRuntime.jsx(
43910
44632
  LineCard,
@@ -48221,18 +48943,41 @@ var useWorkspaceHealth = (options) => {
48221
48943
  const isFetchingRef = React23.useRef(false);
48222
48944
  const refreshIntervalRef = React23.useRef(null);
48223
48945
  const healthTable = databaseConfig?.tables?.workspace_health || "workspace_health_status";
48224
- const calculateHealthStatus = (workspace) => {
48225
- if (!workspace.last_heartbeat) return "unknown";
48226
- const now2 = /* @__PURE__ */ new Date();
48227
- const lastHeartbeat = new Date(workspace.last_heartbeat);
48228
- const minutesSince = (now2.getTime() - lastHeartbeat.getTime()) / 6e4;
48229
- if (minutesSince < 5) return "healthy";
48230
- if (minutesSince < 15) return "warning";
48231
- return "unhealthy";
48232
- };
48946
+ const computeSummary = React23.useCallback((data) => {
48947
+ const total = data.length;
48948
+ const healthy = data.filter((w) => w.status === "healthy").length;
48949
+ const unhealthy = data.filter((w) => w.status === "unhealthy").length;
48950
+ const warning6 = data.filter((w) => w.status === "warning").length;
48951
+ let uptimePercentage = total > 0 ? healthy / total * 100 : 100;
48952
+ let totalDowntimeMinutes;
48953
+ const withUptime = data.filter(
48954
+ (w) => w.uptimePercentage !== void 0 && w.uptimeDetails !== void 0
48955
+ );
48956
+ if (withUptime.length > 0) {
48957
+ const totalUptime = withUptime.reduce((sum, w) => sum + (w.uptimePercentage || 0), 0);
48958
+ uptimePercentage = totalUptime / withUptime.length;
48959
+ totalDowntimeMinutes = withUptime.reduce((sum, w) => {
48960
+ if (w.uptimeDetails) {
48961
+ return sum + Math.max(0, w.uptimeDetails.expectedMinutes - w.uptimeDetails.actualMinutes);
48962
+ }
48963
+ return sum;
48964
+ }, 0);
48965
+ }
48966
+ return {
48967
+ totalWorkspaces: total,
48968
+ healthyWorkspaces: healthy,
48969
+ unhealthyWorkspaces: unhealthy,
48970
+ warningWorkspaces: warning6,
48971
+ uptimePercentage,
48972
+ totalDowntimeMinutes,
48973
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
48974
+ };
48975
+ }, []);
48233
48976
  const fetchWorkspacesHealth = React23.useCallback(async () => {
48234
- if (!supabase || isFetchingRef.current) return;
48977
+ if (isFetchingRef.current) return;
48235
48978
  if (!options.companyId) {
48979
+ setWorkspaces([]);
48980
+ setSummary(computeSummary([]));
48236
48981
  setLoading(false);
48237
48982
  return;
48238
48983
  }
@@ -48240,39 +48985,12 @@ var useWorkspaceHealth = (options) => {
48240
48985
  isFetchingRef.current = true;
48241
48986
  setLoading(true);
48242
48987
  setError(null);
48243
- let query = supabase.from(healthTable).select("*").eq("company_id", options.companyId);
48244
- if (options.lineId) {
48245
- query = query.eq("line_id", options.lineId);
48246
- }
48247
- query = query.order("workspace_display_name", { ascending: true });
48248
- const { data, error: fetchError } = await query;
48249
- if (fetchError) throw fetchError;
48250
- const healthData = data || [];
48251
- const workspacesWithStatus = healthData.map((ws) => {
48252
- const status = calculateHealthStatus(ws);
48253
- const now2 = /* @__PURE__ */ new Date();
48254
- const lastUpdate = ws.last_heartbeat ? new Date(ws.last_heartbeat) : null;
48255
- const timeSinceLastUpdate = lastUpdate ? `${Math.floor((now2.getTime() - lastUpdate.getTime()) / 6e4)}m ago` : "Never";
48256
- return {
48257
- ...ws,
48258
- status,
48259
- timeSinceLastUpdate,
48260
- isStale: !lastUpdate || now2.getTime() - lastUpdate.getTime() > 15 * 6e4
48261
- };
48988
+ const workspacesWithStatus = await workspaceHealthService.getWorkspaceHealthStatus({
48989
+ lineId: options.lineId,
48990
+ companyId: options.companyId
48262
48991
  });
48263
48992
  setWorkspaces(workspacesWithStatus);
48264
- const total = workspacesWithStatus.length;
48265
- const healthy = workspacesWithStatus.filter((w) => w.status === "healthy").length;
48266
- const unhealthy = workspacesWithStatus.filter((w) => w.status === "unhealthy").length;
48267
- const warning6 = workspacesWithStatus.filter((w) => w.status === "warning").length;
48268
- setSummary({
48269
- totalWorkspaces: total,
48270
- healthyWorkspaces: healthy,
48271
- unhealthyWorkspaces: unhealthy,
48272
- warningWorkspaces: warning6,
48273
- uptimePercentage: total > 0 ? healthy / total * 100 : 100,
48274
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
48275
- });
48993
+ setSummary(computeSummary(workspacesWithStatus));
48276
48994
  } catch (err) {
48277
48995
  console.error("[useWorkspaceHealth] Error fetching workspace health:", err);
48278
48996
  setError({ message: err.message, code: err.code || "FETCH_ERROR" });
@@ -48281,7 +48999,7 @@ var useWorkspaceHealth = (options) => {
48281
48999
  setLoading(false);
48282
49000
  isFetchingRef.current = false;
48283
49001
  }
48284
- }, [supabase, options.companyId, options.lineId, healthTable]);
49002
+ }, [options.companyId, options.lineId, computeSummary]);
48285
49003
  React23.useEffect(() => {
48286
49004
  fetchWorkspacesHealth();
48287
49005
  }, [fetchWorkspacesHealth]);
@@ -48326,6 +49044,395 @@ var useWorkspaceHealth = (options) => {
48326
49044
  refetch: fetchWorkspacesHealth
48327
49045
  };
48328
49046
  };
49047
+ var STATUS_COLORS = {
49048
+ up: "bg-emerald-500",
49049
+ down: "bg-rose-500",
49050
+ pending: "bg-gray-200"
49051
+ };
49052
+ var STATUS_TITLES = {
49053
+ up: "Uptime",
49054
+ down: "Downtime",
49055
+ pending: "Pending"
49056
+ };
49057
+ var formatTime4 = (date, timezone) => new Intl.DateTimeFormat("en-IN", {
49058
+ hour: "numeric",
49059
+ minute: "2-digit",
49060
+ hour12: true,
49061
+ timeZone: timezone
49062
+ }).format(date);
49063
+ var formatDuration = (minutes) => {
49064
+ if (minutes < 1) return "<1 min";
49065
+ if (minutes < 60) return `${minutes} min`;
49066
+ const hours = Math.floor(minutes / 60);
49067
+ const remainder = minutes % 60;
49068
+ if (remainder === 0) return `${hours} hr${hours > 1 ? "s" : ""}`;
49069
+ return `${hours} hr ${remainder} min`;
49070
+ };
49071
+ var formatDowntimeLabel = (minutes, includeSuffix = true) => {
49072
+ if (!minutes || minutes <= 0) {
49073
+ return includeSuffix ? "No downtime" : "0 min";
49074
+ }
49075
+ const rounded = Math.max(Math.round(minutes), 0);
49076
+ const days = Math.floor(rounded / 1440);
49077
+ const hours = Math.floor(rounded % 1440 / 60);
49078
+ const mins = rounded % 60;
49079
+ const parts = [];
49080
+ if (days) parts.push(`${days} day${days === 1 ? "" : "s"}`);
49081
+ if (hours) parts.push(`${hours} hr${hours === 1 ? "" : "s"}`);
49082
+ if (mins) parts.push(`${mins} min${mins === 1 ? "" : "s"}`);
49083
+ if (!parts.length) {
49084
+ parts.push("1 min");
49085
+ }
49086
+ const label = parts.join(" ");
49087
+ return includeSuffix ? `${label} down` : label;
49088
+ };
49089
+ var UptimeTimelineStrip = ({
49090
+ points,
49091
+ totalMinutes,
49092
+ shiftStart,
49093
+ shiftEnd,
49094
+ timezone,
49095
+ className = "",
49096
+ uptimePercentage = 0,
49097
+ downtimeMinutes = 0
49098
+ }) => {
49099
+ const segments = React23.useMemo(() => {
49100
+ if (!points.length || totalMinutes <= 0) return [];
49101
+ const result = [];
49102
+ let current = {
49103
+ status: points[0].status,
49104
+ length: 1,
49105
+ startMinuteIndex: points[0].minuteIndex,
49106
+ startTimestamp: points[0].timestamp,
49107
+ endTimestamp: points[0].timestamp
49108
+ };
49109
+ for (let i = 1; i < points.length; i++) {
49110
+ const point = points[i];
49111
+ if (point.status === current.status) {
49112
+ current.length += 1;
49113
+ current.endTimestamp = point.timestamp;
49114
+ } else {
49115
+ result.push(current);
49116
+ current = {
49117
+ status: point.status,
49118
+ length: 1,
49119
+ startMinuteIndex: point.minuteIndex,
49120
+ startTimestamp: point.timestamp,
49121
+ endTimestamp: point.timestamp
49122
+ };
49123
+ }
49124
+ }
49125
+ result.push(current);
49126
+ return result;
49127
+ }, [points, totalMinutes]);
49128
+ const markers = React23.useMemo(() => {
49129
+ if (totalMinutes <= 0) return [];
49130
+ const startDate = new Date(shiftStart);
49131
+ const endDate = new Date(shiftEnd);
49132
+ const roundedTotal = Math.max(totalMinutes, 1);
49133
+ const markerInterval = totalMinutes > 360 ? 120 : 60;
49134
+ const markerList = [];
49135
+ for (let minute = 0; minute <= roundedTotal; minute += markerInterval) {
49136
+ const markerMinute = Math.min(minute, roundedTotal);
49137
+ const markerDate = dateFns.addMinutes(startDate, markerMinute);
49138
+ markerList.push({
49139
+ minute: markerMinute,
49140
+ label: formatTime4(markerDate, timezone)
49141
+ });
49142
+ }
49143
+ const endLabel = formatTime4(endDate, timezone);
49144
+ if (!markerList.some((marker) => marker.minute === roundedTotal)) {
49145
+ markerList.push({
49146
+ minute: roundedTotal,
49147
+ label: endLabel
49148
+ });
49149
+ } else {
49150
+ markerList[markerList.length - 1] = {
49151
+ minute: roundedTotal,
49152
+ label: endLabel
49153
+ };
49154
+ }
49155
+ return markerList;
49156
+ }, [shiftStart, shiftEnd, timezone, totalMinutes]);
49157
+ if (!points.length || totalMinutes <= 0) {
49158
+ 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." });
49159
+ }
49160
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `relative w-full ${className}`, children: [
49161
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center mb-3 text-sm", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-gray-900 font-semibold", children: [
49162
+ uptimePercentage.toFixed(1),
49163
+ " % uptime ",
49164
+ downtimeMinutes > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-gray-600 font-normal", children: [
49165
+ "(",
49166
+ formatDowntimeLabel(downtimeMinutes),
49167
+ ")"
49168
+ ] })
49169
+ ] }) }),
49170
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative flex h-4 overflow-hidden rounded-lg border border-gray-200 shadow-sm bg-white", children: segments.map((segment, index) => {
49171
+ const startDate = new Date(segment.startTimestamp);
49172
+ const endDate = new Date(segment.endTimestamp);
49173
+ if (segment.length > 1) {
49174
+ endDate.setMinutes(endDate.getMinutes() + 1);
49175
+ }
49176
+ const tooltip = `${STATUS_TITLES[segment.status]} \u2022 ${formatDuration(segment.length)} (${formatTime4(
49177
+ startDate,
49178
+ timezone
49179
+ )} - ${formatTime4(endDate, timezone)})`;
49180
+ return /* @__PURE__ */ jsxRuntime.jsx(
49181
+ "div",
49182
+ {
49183
+ className: `${STATUS_COLORS[segment.status]} transition-all hover:opacity-80 cursor-pointer`,
49184
+ style: { flex: segment.length },
49185
+ title: tooltip
49186
+ },
49187
+ `${segment.status}-${segment.startMinuteIndex}-${index}`
49188
+ );
49189
+ }) }),
49190
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pointer-events-none relative w-full mt-4 min-h-6", children: markers.map((marker) => {
49191
+ const left = totalMinutes > 0 ? marker.minute / totalMinutes * 100 : 0;
49192
+ return /* @__PURE__ */ jsxRuntime.jsxs(
49193
+ "div",
49194
+ {
49195
+ className: "absolute flex -translate-x-1/2 flex-col items-center",
49196
+ style: { left: `${left}%` },
49197
+ children: [
49198
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mb-1.5 h-2 w-px bg-gray-300" }),
49199
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] font-medium text-gray-600 whitespace-nowrap", children: marker.label })
49200
+ ]
49201
+ },
49202
+ `${marker.label}-${marker.minute}`
49203
+ );
49204
+ }) })
49205
+ ] });
49206
+ };
49207
+ var UptimeTimelineStrip_default = UptimeTimelineStrip;
49208
+ var SHORT_INTERRUPT_THRESHOLD_MINUTES = 3;
49209
+ var formatDuration2 = (minutes) => {
49210
+ if (!minutes || minutes <= 0) return "0 min";
49211
+ const rounded = Math.max(Math.round(minutes), 0);
49212
+ const days = Math.floor(rounded / 1440);
49213
+ const hours = Math.floor(rounded % 1440 / 60);
49214
+ const mins = rounded % 60;
49215
+ const parts = [];
49216
+ if (days) parts.push(`${days} day${days === 1 ? "" : "s"}`);
49217
+ if (hours) parts.push(`${hours} hr${hours === 1 ? "" : "s"}`);
49218
+ if (mins) parts.push(`${mins} min${mins === 1 ? "" : "s"}`);
49219
+ if (!parts.length) {
49220
+ parts.push("1 min");
49221
+ }
49222
+ return parts.join(" ");
49223
+ };
49224
+ var formatDowntimeLabel2 = (minutes, includeSuffix = true) => {
49225
+ if (!minutes || minutes <= 0) {
49226
+ return includeSuffix ? "No downtime" : "0 min";
49227
+ }
49228
+ const label = formatDuration2(minutes);
49229
+ return includeSuffix ? `${label} down` : label;
49230
+ };
49231
+ var formatTimeRange = (start, end, timezone) => {
49232
+ const formatter = new Intl.DateTimeFormat("en-IN", {
49233
+ hour: "numeric",
49234
+ minute: "2-digit",
49235
+ hour12: true,
49236
+ timeZone: timezone
49237
+ });
49238
+ return `${formatter.format(start)} - ${formatter.format(end)}`;
49239
+ };
49240
+ var WorkspaceUptimeDetailModal = ({
49241
+ workspace,
49242
+ isOpen,
49243
+ onClose
49244
+ }) => {
49245
+ const timezone = useAppTimezone() || "UTC";
49246
+ const logsContainerRef = React23.useRef(null);
49247
+ const [showScrollIndicator, setShowScrollIndicator] = React23.useState(false);
49248
+ const {
49249
+ timeline,
49250
+ loading,
49251
+ error,
49252
+ refetch
49253
+ } = useWorkspaceUptimeTimeline({
49254
+ workspaceId: workspace?.workspace_id,
49255
+ companyId: workspace?.company_id,
49256
+ enabled: isOpen && Boolean(workspace?.workspace_id && workspace?.company_id),
49257
+ refreshInterval: 6e4
49258
+ });
49259
+ React23.useEffect(() => {
49260
+ if (!isOpen || !workspace) return;
49261
+ const handleKeyDown = (event) => {
49262
+ if (event.key === "Escape") {
49263
+ onClose();
49264
+ }
49265
+ };
49266
+ window.addEventListener("keydown", handleKeyDown);
49267
+ return () => {
49268
+ window.removeEventListener("keydown", handleKeyDown);
49269
+ };
49270
+ }, [isOpen, onClose, workspace]);
49271
+ const shiftStart = timeline ? new Date(timeline.shiftStart) : null;
49272
+ const shiftEnd = timeline ? new Date(timeline.shiftEnd) : null;
49273
+ const downtimeSegments = timeline?.downtimeSegments || [];
49274
+ downtimeSegments.length;
49275
+ const downtimeMinutes = timeline?.downtimeMinutes ?? 0;
49276
+ const uptimePercentage = timeline?.uptimePercentage ?? workspace?.uptimePercentage ?? 0;
49277
+ const allInterruptionsSorted = React23.useMemo(
49278
+ () => [...downtimeSegments].sort(
49279
+ (a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime()
49280
+ ),
49281
+ [downtimeSegments]
49282
+ );
49283
+ React23.useEffect(() => {
49284
+ const checkScroll = () => {
49285
+ const container2 = logsContainerRef.current;
49286
+ if (container2) {
49287
+ const hasScroll = container2.scrollHeight > container2.clientHeight;
49288
+ const isAtBottom = container2.scrollHeight - container2.scrollTop <= container2.clientHeight + 10;
49289
+ setShowScrollIndicator(hasScroll && !isAtBottom);
49290
+ }
49291
+ };
49292
+ checkScroll();
49293
+ const container = logsContainerRef.current;
49294
+ if (container) {
49295
+ container.addEventListener("scroll", checkScroll);
49296
+ return () => container.removeEventListener("scroll", checkScroll);
49297
+ }
49298
+ }, [downtimeSegments]);
49299
+ const renderSegment = (segment) => {
49300
+ const start = new Date(segment.startTime);
49301
+ const end = new Date(segment.endTime);
49302
+ const isMajor = segment.durationMinutes >= SHORT_INTERRUPT_THRESHOLD_MINUTES;
49303
+ const containerClasses = isMajor ? "border-rose-200 bg-rose-50" : "border-gray-200 bg-white";
49304
+ return /* @__PURE__ */ jsxRuntime.jsxs(
49305
+ "div",
49306
+ {
49307
+ className: `rounded-lg border px-5 py-3 ${containerClasses}`,
49308
+ children: [
49309
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-semibold text-gray-900", children: formatTimeRange(start, end, timezone) }),
49310
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-gray-600 mt-1", children: [
49311
+ "Duration: ",
49312
+ formatDowntimeLabel2(segment.durationMinutes, false)
49313
+ ] })
49314
+ ]
49315
+ },
49316
+ `${segment.startMinuteIndex}-${segment.endMinuteIndex}`
49317
+ );
49318
+ };
49319
+ if (!isOpen || !workspace) {
49320
+ return null;
49321
+ }
49322
+ return /* @__PURE__ */ jsxRuntime.jsx(
49323
+ "div",
49324
+ {
49325
+ className: "fixed inset-0 z-[60] flex items-center justify-center bg-black/40 backdrop-blur-sm p-4",
49326
+ onClick: onClose,
49327
+ "aria-modal": "true",
49328
+ role: "dialog",
49329
+ "aria-labelledby": "uptime-detail-title",
49330
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
49331
+ "div",
49332
+ {
49333
+ className: "relative flex w-full max-w-4xl max-h-[90vh] flex-col rounded-2xl bg-white shadow-2xl border border-gray-100",
49334
+ onClick: (event) => event.stopPropagation(),
49335
+ role: "document",
49336
+ "aria-labelledby": "uptime-detail-title",
49337
+ children: [
49338
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex items-start justify-between border-b border-gray-100 px-8 py-6 sticky top-0 z-10 bg-white rounded-t-2xl", children: [
49339
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0 mr-4", children: [
49340
+ /* @__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)}` }),
49341
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2.5 text-sm text-gray-600", children: [
49342
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-gray-900", children: timeline?.shiftLabel || "Current Shift" }),
49343
+ shiftStart && shiftEnd && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
49344
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-300", children: "\u2022" }),
49345
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-600", children: formatTimeRange(shiftStart, shiftEnd, timezone) })
49346
+ ] })
49347
+ ] }),
49348
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-2 flex items-center gap-2", children: [
49349
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
49350
+ /* @__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"}` }),
49351
+ /* @__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" })
49352
+ ] }),
49353
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-300", children: "\u2022" }),
49354
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-gray-500", children: [
49355
+ "Last heartbeat ",
49356
+ workspace.timeSinceLastUpdate
49357
+ ] })
49358
+ ] })
49359
+ ] }),
49360
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 flex-shrink-0", children: [
49361
+ /* @__PURE__ */ jsxRuntime.jsxs(
49362
+ "button",
49363
+ {
49364
+ onClick: refetch,
49365
+ disabled: loading,
49366
+ 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",
49367
+ children: [
49368
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { className: `h-4 w-4 ${loading ? "animate-spin" : ""}` }),
49369
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: "Refresh" })
49370
+ ]
49371
+ }
49372
+ ),
49373
+ /* @__PURE__ */ jsxRuntime.jsx(
49374
+ "button",
49375
+ {
49376
+ onClick: onClose,
49377
+ className: "rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600 transition-colors duration-200",
49378
+ "aria-label": "Close uptime details",
49379
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "h-5 w-5" })
49380
+ }
49381
+ )
49382
+ ] })
49383
+ ] }),
49384
+ /* @__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: [
49385
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-semibold mb-1", children: "Unable to load uptime details" }),
49386
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-rose-600/90", children: error.message })
49387
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
49388
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative pb-4 border-b border-gray-200", children: [
49389
+ /* @__PURE__ */ jsxRuntime.jsx(
49390
+ UptimeTimelineStrip_default,
49391
+ {
49392
+ points: timeline?.points || [],
49393
+ totalMinutes: timeline?.totalMinutes || 0,
49394
+ shiftStart: timeline?.shiftStart || (/* @__PURE__ */ new Date()).toISOString(),
49395
+ shiftEnd: timeline?.shiftEnd || (/* @__PURE__ */ new Date()).toISOString(),
49396
+ timezone,
49397
+ uptimePercentage,
49398
+ downtimeMinutes
49399
+ }
49400
+ ),
49401
+ loading && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm text-gray-600 mt-4", children: [
49402
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { className: "h-4 w-4 animate-spin text-gray-500" }),
49403
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Updating timeline\u2026" })
49404
+ ] })
49405
+ ] }),
49406
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pt-4", children: [
49407
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold text-gray-900 uppercase tracking-wider mb-3", children: "Downtime Logs" }),
49408
+ 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: [
49409
+ /* @__PURE__ */ jsxRuntime.jsx(
49410
+ "div",
49411
+ {
49412
+ ref: logsContainerRef,
49413
+ className: "max-h-[400px] overflow-y-auto space-y-2 pr-2",
49414
+ style: {
49415
+ scrollbarWidth: "thin",
49416
+ scrollbarColor: "#CBD5E0 #F7FAFC"
49417
+ },
49418
+ children: allInterruptionsSorted.map((segment) => renderSegment(segment))
49419
+ }
49420
+ ),
49421
+ 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: [
49422
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { className: "h-4 w-4" }),
49423
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: "Scroll for more" }),
49424
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { className: "h-4 w-4" })
49425
+ ] }) })
49426
+ ] })
49427
+ ] })
49428
+ ] }) }) })
49429
+ ]
49430
+ }
49431
+ )
49432
+ }
49433
+ );
49434
+ };
49435
+ var WorkspaceUptimeDetailModal_default = WorkspaceUptimeDetailModal;
48329
49436
  var WorkspaceHealthView = ({
48330
49437
  lineId,
48331
49438
  companyId,
@@ -48335,6 +49442,7 @@ var WorkspaceHealthView = ({
48335
49442
  const router$1 = router.useRouter();
48336
49443
  const [groupBy, setGroupBy] = React23.useState("line");
48337
49444
  const timezone = useAppTimezone();
49445
+ const [selectedWorkspace, setSelectedWorkspace] = React23.useState(null);
48338
49446
  const operationalDate = getOperationalDate(timezone || "UTC");
48339
49447
  const currentHour = (/* @__PURE__ */ new Date()).getHours();
48340
49448
  const isNightShift = currentHour >= 18 || currentHour < 6;
@@ -48375,6 +49483,12 @@ var WorkspaceHealthView = ({
48375
49483
  },
48376
49484
  [router$1, onNavigate]
48377
49485
  );
49486
+ const handleViewDetails = React23.useCallback((workspace) => {
49487
+ setSelectedWorkspace(workspace);
49488
+ }, []);
49489
+ const handleCloseDetails = React23.useCallback(() => {
49490
+ setSelectedWorkspace(null);
49491
+ }, []);
48378
49492
  const handleExport = React23.useCallback(() => {
48379
49493
  const csv = [
48380
49494
  ["Workspace", "Line", "Company", "Status", "Last Heartbeat", "Consecutive Misses"],
@@ -48432,178 +49546,189 @@ var WorkspaceHealthView = ({
48432
49546
  )
48433
49547
  ] }) }) }) });
48434
49548
  }
48435
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: clsx("min-h-screen bg-slate-50", className), children: [
48436
- /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "sticky top-0 z-10 px-3 sm:px-4 md:px-5 lg:px-6 py-2 sm:py-2.5 lg:py-3 flex flex-col shadow-sm bg-white", children: [
48437
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sm:hidden", children: [
48438
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-2", children: [
48439
- /* @__PURE__ */ jsxRuntime.jsx(
49549
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
49550
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: clsx("min-h-screen bg-slate-50", className), children: [
49551
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "sticky top-0 z-10 px-3 sm:px-4 md:px-5 lg:px-6 py-2 sm:py-2.5 lg:py-3 flex flex-col shadow-sm bg-white", children: [
49552
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sm:hidden", children: [
49553
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-1", children: [
49554
+ /* @__PURE__ */ jsxRuntime.jsx(
49555
+ BackButtonMinimal,
49556
+ {
49557
+ onClick: () => router$1.push("/"),
49558
+ text: "Back",
49559
+ size: "sm",
49560
+ "aria-label": "Navigate back to dashboard"
49561
+ }
49562
+ ),
49563
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
49564
+ /* @__PURE__ */ jsxRuntime.jsx(
49565
+ "button",
49566
+ {
49567
+ onClick: () => {
49568
+ refetch();
49569
+ },
49570
+ className: "p-1.5 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors",
49571
+ "aria-label": "Refresh",
49572
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { className: "h-4 w-4" })
49573
+ }
49574
+ ),
49575
+ /* @__PURE__ */ jsxRuntime.jsx(
49576
+ "button",
49577
+ {
49578
+ onClick: handleExport,
49579
+ className: "p-1.5 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors",
49580
+ "aria-label": "Export CSV",
49581
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { className: "h-4 w-4" })
49582
+ }
49583
+ )
49584
+ ] })
49585
+ ] }),
49586
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-center", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-2", children: [
49587
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-base font-semibold text-gray-900", children: "System Health" }),
49588
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex h-2 w-2", children: [
49589
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" }),
49590
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "relative inline-flex rounded-full h-2 w-2 bg-emerald-500" })
49591
+ ] })
49592
+ ] }) })
49593
+ ] }),
49594
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "hidden sm:block", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex items-center", children: [
49595
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute left-0 z-10", children: /* @__PURE__ */ jsxRuntime.jsx(
48440
49596
  BackButtonMinimal,
48441
49597
  {
48442
49598
  onClick: () => router$1.push("/"),
48443
49599
  text: "Back",
48444
- size: "sm",
49600
+ size: "default",
48445
49601
  "aria-label": "Navigate back to dashboard"
48446
49602
  }
48447
- ),
48448
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
49603
+ ) }),
49604
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute left-1/2 transform -translate-x-1/2 max-w-[calc(100%-200px)]", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
49605
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-lg md:text-xl lg:text-2xl xl:text-3xl font-semibold text-gray-900 truncate", children: "System Health" }),
49606
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex h-2.5 w-2.5", children: [
49607
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" }),
49608
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500" })
49609
+ ] })
49610
+ ] }) }),
49611
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute right-0 flex gap-2", children: [
48449
49612
  /* @__PURE__ */ jsxRuntime.jsx(
48450
49613
  "button",
48451
49614
  {
48452
49615
  onClick: () => {
48453
49616
  refetch();
48454
49617
  },
48455
- className: "p-1.5 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors",
49618
+ className: "p-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors",
48456
49619
  "aria-label": "Refresh",
48457
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { className: "h-4 w-4" })
49620
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { className: "h-5 w-5" })
48458
49621
  }
48459
49622
  ),
48460
49623
  /* @__PURE__ */ jsxRuntime.jsx(
48461
49624
  "button",
48462
49625
  {
48463
49626
  onClick: handleExport,
48464
- className: "p-1.5 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors",
49627
+ className: "p-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors",
48465
49628
  "aria-label": "Export CSV",
48466
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { className: "h-4 w-4" })
49629
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { className: "h-5 w-5" })
48467
49630
  }
48468
49631
  )
48469
- ] })
48470
- ] }),
48471
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-center", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-2", children: [
48472
- /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-base font-semibold text-gray-900", children: "System Health" }),
48473
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex h-2 w-2", children: [
48474
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" }),
48475
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "relative inline-flex rounded-full h-2 w-2 bg-emerald-500" })
49632
+ ] }),
49633
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full h-8" })
49634
+ ] }) }),
49635
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-1 sm:mt-2 bg-blue-50 px-2 sm:px-3 py-1.5 sm:py-2 rounded-lg", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-center gap-2 sm:gap-4", children: [
49636
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm sm:text-base md:text-lg font-medium text-blue-600", children: /* @__PURE__ */ jsxRuntime.jsx(LiveTimer, {}) }),
49637
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "hidden sm:block w-px h-4 bg-blue-300" }),
49638
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs sm:text-sm md:text-base font-medium text-blue-600", children: formatDate(operationalDate) }),
49639
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "hidden sm:block w-px h-4 bg-blue-300" }),
49640
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 sm:gap-2", children: [
49641
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-blue-600", children: getShiftIcon(shiftType) }),
49642
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs sm:text-sm md:text-base font-medium text-blue-600", children: [
49643
+ shiftType,
49644
+ " Shift"
49645
+ ] })
48476
49646
  ] })
48477
49647
  ] }) })
48478
49648
  ] }),
48479
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "hidden sm:block", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex items-center", children: [
48480
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute left-0 z-10", children: /* @__PURE__ */ jsxRuntime.jsx(
48481
- BackButtonMinimal,
49649
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-w-7xl mx-auto p-4 space-y-6", children: [
49650
+ summary && /* @__PURE__ */ jsxRuntime.jsxs(
49651
+ motion.div,
48482
49652
  {
48483
- onClick: () => router$1.push("/"),
48484
- text: "Back",
48485
- size: "default",
48486
- "aria-label": "Navigate back to dashboard"
48487
- }
48488
- ) }),
48489
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute left-1/2 transform -translate-x-1/2 max-w-[calc(100%-200px)]", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
48490
- /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-lg md:text-xl lg:text-2xl xl:text-3xl font-semibold text-gray-900 truncate", children: "System Health" }),
48491
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex h-2.5 w-2.5", children: [
48492
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" }),
48493
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500" })
48494
- ] })
48495
- ] }) }),
48496
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute right-0 flex gap-2", children: [
48497
- /* @__PURE__ */ jsxRuntime.jsx(
48498
- "button",
48499
- {
48500
- onClick: () => {
48501
- refetch();
48502
- },
48503
- className: "p-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors",
48504
- "aria-label": "Refresh",
48505
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { className: "h-5 w-5" })
48506
- }
48507
- ),
48508
- /* @__PURE__ */ jsxRuntime.jsx(
48509
- "button",
48510
- {
48511
- onClick: handleExport,
48512
- className: "p-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors",
48513
- "aria-label": "Export CSV",
48514
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { className: "h-5 w-5" })
48515
- }
48516
- )
48517
- ] }),
48518
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full h-8" })
48519
- ] }) }),
48520
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2 sm:mt-3 bg-blue-50 px-2 sm:px-3 py-1.5 sm:py-2 rounded-lg", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-center gap-2 sm:gap-4", children: [
48521
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm sm:text-base md:text-lg font-medium text-blue-600", children: /* @__PURE__ */ jsxRuntime.jsx(LiveTimer, {}) }),
48522
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "hidden sm:block w-px h-4 bg-blue-300" }),
48523
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs sm:text-sm md:text-base font-medium text-blue-600", children: formatDate(operationalDate) }),
48524
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "hidden sm:block w-px h-4 bg-blue-300" }),
48525
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 sm:gap-2", children: [
48526
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-blue-600", children: getShiftIcon(shiftType) }),
48527
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs sm:text-sm md:text-base font-medium text-blue-600", children: [
48528
- shiftType,
48529
- " Shift"
48530
- ] })
48531
- ] })
48532
- ] }) })
48533
- ] }),
48534
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-w-7xl mx-auto p-4 space-y-6", children: [
48535
- summary && /* @__PURE__ */ jsxRuntime.jsxs(
48536
- motion.div,
48537
- {
48538
- initial: { opacity: 0, y: 20 },
48539
- animate: { opacity: 1, y: 0 },
48540
- transition: { duration: 0.3, delay: 0.1 },
48541
- className: "grid grid-cols-2 sm:grid-cols-2 md:grid-cols-5 gap-2 sm:gap-3 lg:gap-4",
48542
- children: [
48543
- /* @__PURE__ */ jsxRuntime.jsxs(Card2, { className: "col-span-2 sm:col-span-2 md:col-span-2 bg-white", children: [
48544
- /* @__PURE__ */ jsxRuntime.jsx(CardHeader2, { className: "pb-3", children: /* @__PURE__ */ jsxRuntime.jsx(CardTitle2, { className: "text-sm font-medium text-gray-500 dark:text-gray-400", children: "System Availability" }) }),
48545
- /* @__PURE__ */ jsxRuntime.jsxs(CardContent2, { children: [
48546
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-baseline gap-2", children: [
48547
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: clsx("text-3xl font-bold", getUptimeColor(summary.uptimePercentage)), children: [
48548
- summary.uptimePercentage.toFixed(1),
48549
- "%"
49653
+ initial: { opacity: 0, y: 20 },
49654
+ animate: { opacity: 1, y: 0 },
49655
+ transition: { duration: 0.3, delay: 0.1 },
49656
+ className: "grid grid-cols-2 sm:grid-cols-2 md:grid-cols-5 gap-2 sm:gap-3 lg:gap-4",
49657
+ children: [
49658
+ /* @__PURE__ */ jsxRuntime.jsxs(Card2, { className: "col-span-2 sm:col-span-2 md:col-span-2 bg-white", children: [
49659
+ /* @__PURE__ */ jsxRuntime.jsx(CardHeader2, { className: "pb-3", children: /* @__PURE__ */ jsxRuntime.jsx(CardTitle2, { className: "text-sm font-medium text-gray-500 dark:text-gray-400", children: "System Availability" }) }),
49660
+ /* @__PURE__ */ jsxRuntime.jsxs(CardContent2, { children: [
49661
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-baseline gap-2", children: [
49662
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: clsx("text-3xl font-bold", getUptimeColor(summary.uptimePercentage)), children: [
49663
+ summary.uptimePercentage.toFixed(1),
49664
+ "%"
49665
+ ] }),
49666
+ summary.uptimePercentage >= 97 ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.TrendingUp, { className: "h-5 w-5 text-green-500" }) : summary.uptimePercentage >= 90 ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Activity, { className: "h-5 w-5 text-yellow-500" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.TrendingDown, { className: "h-5 w-5 text-red-500" })
48550
49667
  ] }),
48551
- summary.uptimePercentage >= 97 ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.TrendingUp, { className: "h-5 w-5 text-green-500" }) : summary.uptimePercentage >= 90 ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Activity, { className: "h-5 w-5 text-yellow-500" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.TrendingDown, { className: "h-5 w-5 text-red-500" })
48552
- ] }),
48553
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: "Overall system uptime today" })
48554
- ] })
48555
- ] }),
48556
- /* @__PURE__ */ jsxRuntime.jsxs(Card2, { className: "bg-white", children: [
48557
- /* @__PURE__ */ jsxRuntime.jsx(CardHeader2, { className: "pb-3", children: /* @__PURE__ */ jsxRuntime.jsxs(CardTitle2, { className: "text-sm font-medium text-gray-500 dark:text-gray-400 flex items-center gap-2", children: [
48558
- getStatusIcon("healthy"),
48559
- "Healthy"
48560
- ] }) }),
48561
- /* @__PURE__ */ jsxRuntime.jsxs(CardContent2, { children: [
48562
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-2xl font-bold text-gray-900 dark:text-gray-50", children: summary.healthyWorkspaces }),
48563
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: "Operating normally" })
48564
- ] })
48565
- ] }),
48566
- /* @__PURE__ */ jsxRuntime.jsxs(Card2, { className: "bg-white", children: [
48567
- /* @__PURE__ */ jsxRuntime.jsx(CardHeader2, { className: "pb-3", children: /* @__PURE__ */ jsxRuntime.jsxs(CardTitle2, { className: "text-sm font-medium text-gray-500 dark:text-gray-400 flex items-center gap-2", children: [
48568
- getStatusIcon("warning"),
48569
- "Warning"
48570
- ] }) }),
48571
- /* @__PURE__ */ jsxRuntime.jsxs(CardContent2, { children: [
48572
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-2xl font-bold text-gray-900 dark:text-gray-50", children: summary.warningWorkspaces }),
48573
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: "Delayed updates" })
48574
- ] })
48575
- ] }),
48576
- /* @__PURE__ */ jsxRuntime.jsxs(Card2, { className: "bg-white", children: [
48577
- /* @__PURE__ */ jsxRuntime.jsx(CardHeader2, { className: "pb-3", children: /* @__PURE__ */ jsxRuntime.jsxs(CardTitle2, { className: "text-sm font-medium text-gray-500 dark:text-gray-400 flex items-center gap-2", children: [
48578
- getStatusIcon("unhealthy"),
48579
- "Unhealthy"
48580
- ] }) }),
48581
- /* @__PURE__ */ jsxRuntime.jsxs(CardContent2, { children: [
48582
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-2xl font-bold text-gray-900 dark:text-gray-50", children: summary.unhealthyWorkspaces }),
48583
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: "Requires attention" })
49668
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: "Overall system uptime today" })
49669
+ ] })
49670
+ ] }),
49671
+ /* @__PURE__ */ jsxRuntime.jsxs(Card2, { className: "bg-white", children: [
49672
+ /* @__PURE__ */ jsxRuntime.jsx(CardHeader2, { className: "pb-3", children: /* @__PURE__ */ jsxRuntime.jsxs(CardTitle2, { className: "text-sm font-medium text-gray-500 dark:text-gray-400 flex items-center gap-2", children: [
49673
+ getStatusIcon("healthy"),
49674
+ "Healthy"
49675
+ ] }) }),
49676
+ /* @__PURE__ */ jsxRuntime.jsxs(CardContent2, { children: [
49677
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-2xl font-bold text-gray-900 dark:text-gray-50", children: summary.healthyWorkspaces }),
49678
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: "Operating normally" })
49679
+ ] })
49680
+ ] }),
49681
+ /* @__PURE__ */ jsxRuntime.jsxs(Card2, { className: "bg-white", children: [
49682
+ /* @__PURE__ */ jsxRuntime.jsx(CardHeader2, { className: "pb-3", children: /* @__PURE__ */ jsxRuntime.jsxs(CardTitle2, { className: "text-sm font-medium text-gray-500 dark:text-gray-400 flex items-center gap-2", children: [
49683
+ getStatusIcon("warning"),
49684
+ "Warning"
49685
+ ] }) }),
49686
+ /* @__PURE__ */ jsxRuntime.jsxs(CardContent2, { children: [
49687
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-2xl font-bold text-gray-900 dark:text-gray-50", children: summary.warningWorkspaces }),
49688
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: "Delayed updates" })
49689
+ ] })
49690
+ ] }),
49691
+ /* @__PURE__ */ jsxRuntime.jsxs(Card2, { className: "bg-white", children: [
49692
+ /* @__PURE__ */ jsxRuntime.jsx(CardHeader2, { className: "pb-3", children: /* @__PURE__ */ jsxRuntime.jsxs(CardTitle2, { className: "text-sm font-medium text-gray-500 dark:text-gray-400 flex items-center gap-2", children: [
49693
+ getStatusIcon("unhealthy"),
49694
+ "Unhealthy"
49695
+ ] }) }),
49696
+ /* @__PURE__ */ jsxRuntime.jsxs(CardContent2, { children: [
49697
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-2xl font-bold text-gray-900 dark:text-gray-50", children: summary.unhealthyWorkspaces }),
49698
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: "Requires attention" })
49699
+ ] })
48584
49700
  ] })
48585
- ] })
48586
- ]
48587
- }
48588
- ),
48589
- /* @__PURE__ */ jsxRuntime.jsx(
48590
- motion.div,
48591
- {
48592
- initial: { opacity: 0, y: 20 },
48593
- animate: { opacity: 1, y: 0 },
48594
- transition: { duration: 0.3, delay: 0.2 },
48595
- children: /* @__PURE__ */ jsxRuntime.jsx(
48596
- HealthStatusGrid,
48597
- {
48598
- workspaces,
48599
- onWorkspaceClick: handleWorkspaceClick,
48600
- showFilters: true,
48601
- groupBy
48602
- }
48603
- )
48604
- }
48605
- )
48606
- ] })
49701
+ ]
49702
+ }
49703
+ ),
49704
+ /* @__PURE__ */ jsxRuntime.jsx(
49705
+ motion.div,
49706
+ {
49707
+ initial: { opacity: 0, y: 20 },
49708
+ animate: { opacity: 1, y: 0 },
49709
+ transition: { duration: 0.3, delay: 0.2 },
49710
+ children: /* @__PURE__ */ jsxRuntime.jsx(
49711
+ HealthStatusGrid,
49712
+ {
49713
+ workspaces,
49714
+ onWorkspaceClick: handleWorkspaceClick,
49715
+ onWorkspaceViewDetails: handleViewDetails,
49716
+ showFilters: true,
49717
+ groupBy
49718
+ }
49719
+ )
49720
+ }
49721
+ )
49722
+ ] })
49723
+ ] }),
49724
+ /* @__PURE__ */ jsxRuntime.jsx(
49725
+ WorkspaceUptimeDetailModal_default,
49726
+ {
49727
+ workspace: selectedWorkspace,
49728
+ isOpen: Boolean(selectedWorkspace),
49729
+ onClose: handleCloseDetails
49730
+ }
49731
+ )
48607
49732
  ] });
48608
49733
  };
48609
49734
  var WorkspaceHealthView_default = withAuth(WorkspaceHealthView, {
@@ -52563,6 +53688,7 @@ exports.useWorkspaceHealthStatus = useWorkspaceHealthStatus;
52563
53688
  exports.useWorkspaceMetrics = useWorkspaceMetrics;
52564
53689
  exports.useWorkspaceNavigation = useWorkspaceNavigation;
52565
53690
  exports.useWorkspaceOperators = useWorkspaceOperators;
53691
+ exports.useWorkspaceUptimeTimeline = useWorkspaceUptimeTimeline;
52566
53692
  exports.userService = userService;
52567
53693
  exports.videoPrefetchManager = videoPrefetchManager;
52568
53694
  exports.videoPreloader = videoPreloader;