@optifye/dashboard-core 6.12.1 → 6.12.3

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
@@ -2628,19 +2628,206 @@ function getSentry() {
2628
2628
  return null;
2629
2629
  }
2630
2630
  }
2631
+ var SENTRY_HANDLED_EVENT_WINDOW_MS = 10 * 60 * 1e3;
2632
+ var SENTRY_HANDLED_EVENT_SESSION_LIMIT = 20;
2633
+ var SENTRY_QUOTA_STORAGE_KEY = "optifye:sentry-handled-quota:v1";
2634
+ var sentryFingerprintSentAt = /* @__PURE__ */ new Map();
2635
+ var handledSentryEventCount = 0;
2636
+ var UUID_PATTERN = /[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi;
2637
+ var LONG_HEX_PATTERN = /\b[0-9a-f]{24,}\b/gi;
2638
+ var EMAIL_PATTERN = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
2639
+ var BEARER_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]+/gi;
2640
+ var URI_PATTERN = /\b[a-z][a-z0-9+.-]{1,31}:\/\/[^\s)]+/gi;
2641
+ var MEDIA_PATH_PATTERN = /\b[^\s)]+\.m3u8(?:\?[^\s)]*)?/gi;
2642
+ var SENSITIVE_ASSIGNMENT_PATTERN = /\b([A-Za-z0-9_.-]*(?:authorization|cookie|password|secret|token|session|email|api[_-]?key)[A-Za-z0-9_.-]*)\b\s*[:=]\s*["']?[^"'\s,;)}\]]+/gi;
2643
+ var SENSITIVE_KEY_PATTERN = /(authorization|cookie|password|secret|token|session|email|api[_-]?key|url|stream|playlist|media)/i;
2644
+ function resetSentryQuotaForTests() {
2645
+ sentryFingerprintSentAt.clear();
2646
+ handledSentryEventCount = 0;
2647
+ const storage = getBrowserSessionStorage();
2648
+ storage?.removeItem(SENTRY_QUOTA_STORAGE_KEY);
2649
+ }
2650
+ var sanitizeString = (value) => {
2651
+ return value.replace(BEARER_PATTERN, "Bearer [redacted]").replace(SENSITIVE_ASSIGNMENT_PATTERN, (_match, key) => `${key}=[redacted]`).replace(URI_PATTERN, "[url]").replace(MEDIA_PATH_PATTERN, "[media]").replace(EMAIL_PATTERN, "[email]").replace(UUID_PATTERN, ":uuid").replace(LONG_HEX_PATTERN, ":id").replace(/\?.*$/, "").replace(/\s+/g, " ").trim();
2652
+ };
2653
+ var sanitizeRoute = (value) => {
2654
+ if (typeof value !== "string" || value.trim().length === 0) return void 0;
2655
+ try {
2656
+ const parsed = new URL(value, "https://optifye.local");
2657
+ return sanitizeString(parsed.pathname);
2658
+ } catch {
2659
+ return sanitizeString(value.split("?")[0] || value);
2660
+ }
2661
+ };
2662
+ var sanitizeValue = (value, depth = 0) => {
2663
+ if (depth > 3) return "[truncated]";
2664
+ if (typeof value === "string") return sanitizeString(value);
2665
+ if (typeof value === "number" || typeof value === "boolean" || value === null || value === void 0) return value;
2666
+ if (Array.isArray(value)) {
2667
+ return value.slice(0, 20).map((item) => sanitizeValue(item, depth + 1));
2668
+ }
2669
+ if (typeof value === "object") {
2670
+ const sanitized = {};
2671
+ Object.entries(value).forEach(([key, item]) => {
2672
+ sanitized[key] = SENSITIVE_KEY_PATTERN.test(key) ? "[redacted]" : sanitizeValue(item, depth + 1);
2673
+ });
2674
+ return sanitized;
2675
+ }
2676
+ return String(value);
2677
+ };
2678
+ var sanitizeStack = (stack) => {
2679
+ return stack.split("\n").map((line) => sanitizeString(line)).join("\n");
2680
+ };
2681
+ var sanitizeExtras = (extras) => {
2682
+ if (!extras) return void 0;
2683
+ return sanitizeValue(extras);
2684
+ };
2685
+ var getBrowserSessionStorage = () => {
2686
+ if (typeof window === "undefined") return null;
2687
+ try {
2688
+ return window.sessionStorage;
2689
+ } catch {
2690
+ return null;
2691
+ }
2692
+ };
2693
+ var loadSentryQuotaFromStorage = (now4) => {
2694
+ const storage = getBrowserSessionStorage();
2695
+ if (!storage) return;
2696
+ const rawState = storage.getItem(SENTRY_QUOTA_STORAGE_KEY);
2697
+ if (!rawState) return;
2698
+ try {
2699
+ const parsed = JSON.parse(rawState);
2700
+ const persistedCount = Number(parsed.handledEventCount);
2701
+ handledSentryEventCount = Number.isFinite(persistedCount) && persistedCount > 0 ? Math.floor(persistedCount) : 0;
2702
+ sentryFingerprintSentAt.clear();
2703
+ if (parsed.fingerprintSentAt && typeof parsed.fingerprintSentAt === "object") {
2704
+ Object.entries(parsed.fingerprintSentAt).forEach(([fingerprint, sentAt]) => {
2705
+ const sentAtMs = Number(sentAt);
2706
+ if (Number.isFinite(sentAtMs) && now4 - sentAtMs < SENTRY_HANDLED_EVENT_WINDOW_MS) {
2707
+ sentryFingerprintSentAt.set(fingerprint, sentAtMs);
2708
+ }
2709
+ });
2710
+ }
2711
+ } catch {
2712
+ storage.removeItem(SENTRY_QUOTA_STORAGE_KEY);
2713
+ }
2714
+ };
2715
+ var persistSentryQuotaToStorage = () => {
2716
+ const storage = getBrowserSessionStorage();
2717
+ if (!storage) return;
2718
+ try {
2719
+ storage.setItem(
2720
+ SENTRY_QUOTA_STORAGE_KEY,
2721
+ JSON.stringify({
2722
+ handledEventCount: handledSentryEventCount,
2723
+ fingerprintSentAt: Object.fromEntries(sentryFingerprintSentAt)
2724
+ })
2725
+ );
2726
+ } catch {
2727
+ }
2728
+ };
2729
+ var normalizeMessage = (error) => {
2730
+ const message = error instanceof Error ? error.message : String(error ?? "unknown");
2731
+ return sanitizeString(message.toLowerCase()).slice(0, 180) || "unknown";
2732
+ };
2733
+ var getErrorName = (error) => {
2734
+ if (error && typeof error === "object" && "name" in error) {
2735
+ const name = error.name;
2736
+ if (typeof name === "string" && name.trim()) return sanitizeString(name);
2737
+ }
2738
+ return error instanceof Error ? error.constructor.name : typeof error;
2739
+ };
2740
+ var getStatusFromError = (error) => {
2741
+ const message = error instanceof Error ? error.message : String(error ?? "");
2742
+ const statusMatch = message.match(/\((\d{3})\)/) || message.match(/http\s+(\d{3})/i) || message.match(/status:\s*(\d{3})/i);
2743
+ if (!statusMatch) return null;
2744
+ const status = Number.parseInt(statusMatch[1], 10);
2745
+ return Number.isFinite(status) ? status : null;
2746
+ };
2747
+ var hasCaptureOptionShape = (value) => {
2748
+ return "surface" in value || "route" in value || "status" in value || "severity" in value || "quotaKey" in value || "extras" in value;
2749
+ };
2750
+ var normalizeCaptureOptions = (options) => {
2751
+ if (!options) return {};
2752
+ const optionRecord = options;
2753
+ if (!hasCaptureOptionShape(optionRecord)) {
2754
+ return { extras: optionRecord };
2755
+ }
2756
+ const {
2757
+ surface,
2758
+ route,
2759
+ status,
2760
+ severity,
2761
+ quotaKey,
2762
+ extras,
2763
+ ...rest
2764
+ } = options;
2765
+ return {
2766
+ surface: typeof surface === "string" ? surface : void 0,
2767
+ route: typeof route === "string" ? route : void 0,
2768
+ status: typeof status === "number" ? status : null,
2769
+ severity: severity === "info" || severity === "warning" || severity === "error" ? severity : void 0,
2770
+ quotaKey: typeof quotaKey === "string" ? quotaKey : void 0,
2771
+ extras: {
2772
+ ...rest,
2773
+ ...extras && typeof extras === "object" && !Array.isArray(extras) ? extras : {}
2774
+ }
2775
+ };
2776
+ };
2777
+ var buildSentryFingerprint = (error, options) => {
2778
+ if (options.quotaKey) return sanitizeString(options.quotaKey);
2779
+ const surface = sanitizeString(options.surface || "frontend");
2780
+ const route = sanitizeRoute(options.route || options.extras?.route || options.extras?.endpoint || options.extras?.url) || "unknown-route";
2781
+ const status = options.status ?? getStatusFromError(error) ?? "unknown-status";
2782
+ return [
2783
+ surface,
2784
+ route,
2785
+ String(status),
2786
+ getErrorName(error),
2787
+ normalizeMessage(error)
2788
+ ].join("|");
2789
+ };
2790
+ var shouldSendHandledSentryEvent = (fingerprint) => {
2791
+ const now4 = Date.now();
2792
+ loadSentryQuotaFromStorage(now4);
2793
+ const lastSentAt = sentryFingerprintSentAt.get(fingerprint);
2794
+ if (lastSentAt !== void 0 && now4 - lastSentAt < SENTRY_HANDLED_EVENT_WINDOW_MS) {
2795
+ return false;
2796
+ }
2797
+ if (handledSentryEventCount >= SENTRY_HANDLED_EVENT_SESSION_LIMIT) {
2798
+ return false;
2799
+ }
2800
+ sentryFingerprintSentAt.set(fingerprint, now4);
2801
+ handledSentryEventCount += 1;
2802
+ persistSentryQuotaToStorage();
2803
+ return true;
2804
+ };
2805
+ var createSanitizedSentryException = (error) => {
2806
+ if (error instanceof Error) {
2807
+ const sanitizedError = new Error(sanitizeString(error.message) || getErrorName(error) || "Error");
2808
+ sanitizedError.name = getErrorName(error) || "Error";
2809
+ if (typeof error.stack === "string") {
2810
+ sanitizedError.stack = sanitizeStack(error.stack);
2811
+ }
2812
+ return sanitizedError;
2813
+ }
2814
+ if (typeof error === "string") {
2815
+ return sanitizeString(error) || "unknown";
2816
+ }
2817
+ return sanitizeValue(error);
2818
+ };
2631
2819
  function isIgnorableFrontendError(error) {
2632
2820
  const name = error && typeof error === "object" ? error.name || "" : "";
2633
2821
  const message = error instanceof Error ? error.message : String(error ?? "");
2634
2822
  const lowerMessage = message.toLowerCase();
2635
- return name === "AbortError" || lowerMessage.includes("the operation was aborted") || lowerMessage.includes("signal is aborted") || lowerMessage.includes("resizeobserver loop");
2823
+ return name === "AbortError" || name === "NotAllowedError" || lowerMessage.includes("the operation was aborted") || lowerMessage.includes("signal is aborted") || lowerMessage.includes("resizeobserver loop") || lowerMessage.includes("play() failed") || lowerMessage.includes("play request was interrupted") || lowerMessage.includes("autoplay") || lowerMessage.includes("not allowed by the user agent") || lowerMessage.includes("recoverable hls") || lowerMessage.includes("non-fatal hls") || lowerMessage.includes("fragloaderror") || lowerMessage.includes("frag load error") || lowerMessage.includes("fragloadtimeout") || lowerMessage.includes("bufferstallederror") || lowerMessage.includes("buffer stalled") || lowerMessage.includes("media error recovered") || lowerMessage.includes("recovermediaerror");
2636
2824
  }
2637
2825
  function setSentryUserContext(user) {
2638
2826
  const sentry = getSentry();
2639
2827
  if (!sentry) return;
2640
2828
  if (user) {
2641
2829
  sentry.setUser({
2642
- id: user.id,
2643
- email: user.email
2830
+ id: user.id
2644
2831
  });
2645
2832
  sentry.setTags({
2646
2833
  company_id: user.company_id || "unknown",
@@ -2691,28 +2878,60 @@ function applyScopeExtras(scope, extras) {
2691
2878
  function captureSentryMessage(message, level = "warning", extras) {
2692
2879
  const sentry = getSentry();
2693
2880
  if (!sentry || !sentry.captureMessage) return;
2881
+ const options = normalizeCaptureOptions({ ...normalizeCaptureOptions(extras), severity: level });
2882
+ const fingerprint = buildSentryFingerprint(new Error(message), options);
2883
+ if (!shouldSendHandledSentryEvent(fingerprint)) return;
2884
+ const sanitizedExtras = sanitizeExtras({
2885
+ ...options.extras,
2886
+ sentry_quota_key: fingerprint,
2887
+ sentry_capture_policy: "quota_controlled"
2888
+ });
2889
+ const route = sanitizeRoute(options.route || options.extras?.route || options.extras?.endpoint || options.extras?.url);
2890
+ const sanitizedMessage = sanitizeString(message) || "Sentry message";
2694
2891
  if (sentry.withScope) {
2695
2892
  sentry.withScope((scope) => {
2696
2893
  scope.setLevel?.(level);
2697
- applyScopeExtras(scope, extras);
2698
- sentry.captureMessage?.(message);
2894
+ scope.setFingerprint?.([fingerprint]);
2895
+ if (options.surface) scope.setTag?.("surface", sanitizeString(options.surface));
2896
+ if (route) scope.setTag?.("route", route);
2897
+ if (options.status !== void 0 && options.status !== null) scope.setTag?.("status", options.status);
2898
+ applyScopeExtras(scope, sanitizedExtras);
2899
+ sentry.captureMessage?.(sanitizedMessage);
2699
2900
  });
2700
2901
  return;
2701
2902
  }
2702
- sentry.captureMessage(message, level);
2903
+ sentry.captureMessage(sanitizedMessage, level);
2703
2904
  }
2704
2905
  function captureSentryException(error, extras) {
2906
+ if (isIgnorableFrontendError(error)) {
2907
+ return;
2908
+ }
2705
2909
  const sentry = getSentry();
2706
2910
  if (!sentry || !sentry.captureException) return;
2911
+ const options = normalizeCaptureOptions(extras);
2912
+ const fingerprint = buildSentryFingerprint(error, options);
2913
+ if (!shouldSendHandledSentryEvent(fingerprint)) return;
2914
+ const sanitizedExtras = sanitizeExtras({
2915
+ ...options.extras,
2916
+ sentry_quota_key: fingerprint,
2917
+ sentry_capture_policy: "quota_controlled"
2918
+ });
2919
+ const route = sanitizeRoute(options.route || options.extras?.route || options.extras?.endpoint || options.extras?.url);
2920
+ const status = options.status ?? getStatusFromError(error);
2921
+ const sanitizedError = createSanitizedSentryException(error);
2707
2922
  if (sentry.withScope) {
2708
2923
  sentry.withScope((scope) => {
2709
- scope.setLevel?.("error");
2710
- applyScopeExtras(scope, extras);
2711
- sentry.captureException?.(error);
2924
+ scope.setLevel?.(options.severity || "error");
2925
+ scope.setFingerprint?.([fingerprint]);
2926
+ if (options.surface) scope.setTag?.("surface", sanitizeString(options.surface));
2927
+ if (route) scope.setTag?.("route", route);
2928
+ if (status !== null && status !== void 0) scope.setTag?.("status", status);
2929
+ applyScopeExtras(scope, sanitizedExtras);
2930
+ sentry.captureException?.(sanitizedError);
2712
2931
  });
2713
2932
  return;
2714
2933
  }
2715
- sentry.captureException(error);
2934
+ sentry.captureException(sanitizedError);
2716
2935
  }
2717
2936
  function captureHandledFrontendException(error, extras) {
2718
2937
  if (isIgnorableFrontendError(error)) {
@@ -2720,6 +2939,23 @@ function captureHandledFrontendException(error, extras) {
2720
2939
  }
2721
2940
  captureSentryException(error, extras);
2722
2941
  }
2942
+ function addSentryBreadcrumb(message, options = {}) {
2943
+ const sentry = getSentry();
2944
+ if (!sentry?.addBreadcrumb) return;
2945
+ const route = sanitizeRoute(options.route || options.extras?.route || options.extras?.endpoint || options.extras?.url);
2946
+ const data = sanitizeExtras({
2947
+ surface: options.surface,
2948
+ route,
2949
+ status: options.status,
2950
+ ...options.extras
2951
+ });
2952
+ sentry.addBreadcrumb({
2953
+ category: options.category || options.surface || "frontend",
2954
+ message: sanitizeString(message),
2955
+ level: options.severity || "info",
2956
+ data
2957
+ });
2958
+ }
2723
2959
 
2724
2960
  // src/lib/services/backendClient.ts
2725
2961
  var ACCESS_TOKEN_REFRESH_BUFFER_MS = 6e4;
@@ -2787,7 +3023,7 @@ var createAbortError = () => {
2787
3023
  return error;
2788
3024
  }
2789
3025
  };
2790
- var getStatusFromError = (error) => {
3026
+ var getStatusFromError2 = (error) => {
2791
3027
  const message = error instanceof Error ? error.message : String(error ?? "");
2792
3028
  const statusMatch = message.match(/\((\d{3})\)/) || message.match(/http\s+(\d{3})/i);
2793
3029
  if (!statusMatch) {
@@ -2803,7 +3039,7 @@ var isRetryableError = (error) => {
2803
3039
  if (isAbortError(error)) {
2804
3040
  return false;
2805
3041
  }
2806
- const status = getStatusFromError(error);
3042
+ const status = getStatusFromError2(error);
2807
3043
  if (status !== null) {
2808
3044
  return status >= 500 || status === 408 || status === 425 || status === 429;
2809
3045
  }
@@ -2827,6 +3063,7 @@ var fetchBackendJson = async (supabase, endpoint, options = {}) => {
2827
3063
  timeoutMs = DEFAULT_TIMEOUT_MS,
2828
3064
  retries = 0,
2829
3065
  retryDelayMs = DEFAULT_RETRY_DELAY_MS,
3066
+ sentry,
2830
3067
  signal: externalSignal,
2831
3068
  ...fetchOptions
2832
3069
  } = options;
@@ -2864,14 +3101,22 @@ var fetchBackendJson = async (supabase, endpoint, options = {}) => {
2864
3101
  } catch (error) {
2865
3102
  const wrappedError = externalSignal?.aborted || controller.signal.aborted && !isAbortError(error) ? createAbortError() : error;
2866
3103
  if (attempt >= retries || !isRetryableError(wrappedError)) {
2867
- captureHandledFrontendException(wrappedError, {
2868
- surface: "backend_client",
2869
- url,
2870
- method,
2871
- retry_attempt: attempt,
2872
- retries,
2873
- pathname: typeof window !== "undefined" ? window.location.pathname : "unknown"
2874
- });
3104
+ if (sentry && sentry.capture !== false) {
3105
+ captureHandledFrontendException(wrappedError, {
3106
+ surface: sentry.surface || "backend_client",
3107
+ route: sentry.route || endpoint,
3108
+ status: sentry.status ?? getStatusFromError2(wrappedError),
3109
+ severity: sentry.severity || "error",
3110
+ quotaKey: sentry.quotaKey,
3111
+ extras: {
3112
+ method,
3113
+ retry_attempt: attempt,
3114
+ retries,
3115
+ pathname: typeof window !== "undefined" ? window.location.pathname : "unknown",
3116
+ ...sentry.extras
3117
+ }
3118
+ });
3119
+ }
2875
3120
  throw wrappedError;
2876
3121
  }
2877
3122
  await new Promise((resolve) => globalThis.setTimeout(resolve, retryDelayMs));
@@ -2957,6 +3202,7 @@ var ApiClient = class {
2957
3202
  retryDelay = 1e3,
2958
3203
  silentErrors = true,
2959
3204
  fallbackData = null,
3205
+ sentry,
2960
3206
  ...fetchOptions
2961
3207
  } = options;
2962
3208
  let lastError = null;
@@ -2990,24 +3236,36 @@ var ApiClient = class {
2990
3236
  await new Promise((resolve) => setTimeout(resolve, retryDelay));
2991
3237
  continue;
2992
3238
  }
2993
- if (silentErrors) {
3239
+ if (sentry && sentry.capture !== false) {
2994
3240
  captureHandledFrontendException(lastError, {
3241
+ surface: sentry.surface || "api_client",
3242
+ route: sentry.route || url,
3243
+ status: sentry.status,
3244
+ severity: sentry.severity || "error",
3245
+ quotaKey: sentry.quotaKey,
3246
+ extras: {
3247
+ retries,
3248
+ silent_errors: silentErrors,
3249
+ pathname: typeof window !== "undefined" ? window.location.pathname : "unknown",
3250
+ ...sentry.extras
3251
+ }
3252
+ });
3253
+ } else {
3254
+ addSentryBreadcrumb("API client request failed", {
2995
3255
  surface: "api_client",
2996
- url,
2997
- retries,
2998
- silent_errors: true,
2999
- pathname: typeof window !== "undefined" ? window.location.pathname : "unknown"
3256
+ route: url,
3257
+ severity: silentErrors ? "info" : "warning",
3258
+ extras: {
3259
+ retries,
3260
+ silent_errors: silentErrors,
3261
+ pathname: typeof window !== "undefined" ? window.location.pathname : "unknown"
3262
+ }
3000
3263
  });
3264
+ }
3265
+ if (silentErrors) {
3001
3266
  console.warn("[ApiClient] All retries exhausted, returning fallback data");
3002
3267
  return fallbackData;
3003
3268
  } else {
3004
- captureHandledFrontendException(lastError, {
3005
- surface: "api_client",
3006
- url,
3007
- retries,
3008
- silent_errors: false,
3009
- pathname: typeof window !== "undefined" ? window.location.pathname : "unknown"
3010
- });
3011
3269
  throw lastError;
3012
3270
  }
3013
3271
  }
@@ -3259,8 +3517,12 @@ var AuthService = class {
3259
3517
  timeout: 1e4,
3260
3518
  // 10 seconds
3261
3519
  retries: 1,
3262
- silentErrors: false
3520
+ silentErrors: false,
3263
3521
  // We want to know about auth errors
3522
+ sentry: {
3523
+ surface: "auth_session_request",
3524
+ route: "/api/auth/session"
3525
+ }
3264
3526
  }
3265
3527
  );
3266
3528
  console.log("[AuthService] Session loaded:", {
@@ -3330,7 +3592,11 @@ var AuthService = class {
3330
3592
  timeout: 1e4,
3331
3593
  // 10 seconds
3332
3594
  retries: 1,
3333
- silentErrors: false
3595
+ silentErrors: false,
3596
+ sentry: {
3597
+ surface: "auth_permissions_request",
3598
+ route: "/api/auth/permissions"
3599
+ }
3334
3600
  }
3335
3601
  );
3336
3602
  console.log("[AuthService] Permissions loaded for role:", data.role);
@@ -5972,12 +6238,16 @@ var workspaceService = {
5972
6238
  return displayNamesMap;
5973
6239
  } catch (error) {
5974
6240
  console.error("Error fetching workspace display names:", error);
5975
- captureSentryException(error, {
6241
+ addSentryBreadcrumb("Workspace display-name fallback failed", {
5976
6242
  surface: "workspace_display_names",
5977
6243
  route: "/api/workspaces/display-names",
5978
- company_id: companyId || null,
5979
- line_id: lineId || null,
5980
- pathname: typeof window !== "undefined" ? window.location.pathname : "unknown"
6244
+ severity: "warning",
6245
+ extras: {
6246
+ company_id: companyId || null,
6247
+ line_id: lineId || null,
6248
+ pathname: typeof window !== "undefined" ? window.location.pathname : "unknown",
6249
+ error_message: error instanceof Error ? error.message : String(error)
6250
+ }
5981
6251
  });
5982
6252
  throw error;
5983
6253
  }
@@ -7581,11 +7851,28 @@ var shouldReportMixpanel = (key, isError) => {
7581
7851
  };
7582
7852
  var reportMixpanelWarning = (key, message, extras) => {
7583
7853
  if (!shouldReportMixpanel(key, false)) return;
7584
- captureSentryMessage(message, "warning", { ...baseMixpanelExtras(), ...extras });
7854
+ addSentryBreadcrumb(message, {
7855
+ surface: "mixpanel",
7856
+ severity: "warning",
7857
+ extras: {
7858
+ key,
7859
+ ...baseMixpanelExtras(),
7860
+ ...extras
7861
+ }
7862
+ });
7585
7863
  };
7586
7864
  var reportMixpanelError = (key, error, extras) => {
7587
7865
  if (!shouldReportMixpanel(key, true)) return;
7588
- captureSentryException(error, { ...baseMixpanelExtras(), ...extras });
7866
+ addSentryBreadcrumb("Mixpanel operation failed", {
7867
+ surface: "mixpanel",
7868
+ severity: "warning",
7869
+ extras: {
7870
+ key,
7871
+ error_message: error instanceof Error ? error.message : String(error ?? "unknown"),
7872
+ ...baseMixpanelExtras(),
7873
+ ...extras
7874
+ }
7875
+ });
7589
7876
  };
7590
7877
  var normalizeCoreEventName = (eventName) => {
7591
7878
  const canonicalName = MIXPANEL_EVENT_NAME_ALIASES[eventName] || eventName;
@@ -9602,11 +9889,15 @@ var LinesService = class {
9602
9889
  }));
9603
9890
  } catch (error) {
9604
9891
  console.error("Error fetching lines:", error);
9605
- captureHandledFrontendException(error, {
9892
+ addSentryBreadcrumb("Lines service request failed", {
9606
9893
  surface: "lines_service",
9607
- operation: "getLinesByCompanyId",
9608
- company_id: companyId,
9609
- pathname: typeof window !== "undefined" ? window.location.pathname : "unknown"
9894
+ route: "/api/lines",
9895
+ severity: "warning",
9896
+ extras: {
9897
+ operation: "getLinesByCompanyId",
9898
+ company_id: companyId,
9899
+ pathname: typeof window !== "undefined" ? window.location.pathname : "unknown"
9900
+ }
9610
9901
  });
9611
9902
  throw new Error(`Failed to fetch lines: ${error.message}`);
9612
9903
  }
@@ -9657,10 +9948,14 @@ var LinesService = class {
9657
9948
  }));
9658
9949
  } catch (error) {
9659
9950
  console.error("Error fetching all lines:", error);
9660
- captureHandledFrontendException(error, {
9951
+ addSentryBreadcrumb("Lines service request failed", {
9661
9952
  surface: "lines_service",
9662
- operation: "getAllLines",
9663
- pathname: typeof window !== "undefined" ? window.location.pathname : "unknown"
9953
+ route: "/api/lines",
9954
+ severity: "warning",
9955
+ extras: {
9956
+ operation: "getAllLines",
9957
+ pathname: typeof window !== "undefined" ? window.location.pathname : "unknown"
9958
+ }
9664
9959
  });
9665
9960
  throw new Error(`Failed to fetch lines: ${error.message}`);
9666
9961
  }
@@ -9720,11 +10015,15 @@ var LinesService = class {
9720
10015
  };
9721
10016
  } catch (error) {
9722
10017
  console.error("Error fetching line:", error);
9723
- captureHandledFrontendException(error, {
10018
+ addSentryBreadcrumb("Lines service request failed", {
9724
10019
  surface: "lines_service",
9725
- operation: "getLineById",
9726
- line_id: lineId,
9727
- pathname: typeof window !== "undefined" ? window.location.pathname : "unknown"
10020
+ route: "/api/lines/:id",
10021
+ severity: "warning",
10022
+ extras: {
10023
+ operation: "getLineById",
10024
+ line_id: lineId,
10025
+ pathname: typeof window !== "undefined" ? window.location.pathname : "unknown"
10026
+ }
9728
10027
  });
9729
10028
  throw new Error(`Failed to fetch line: ${error.message}`);
9730
10029
  }
@@ -13033,7 +13332,8 @@ var toWorkspaceDetailedMetrics = ({
13033
13332
  const targetOutput = coerceNumber(data.target_output ?? data.total_day_output, 0);
13034
13333
  const idealOutput = coerceNumber(data.ideal_output ?? data.ideal_output_until_now, 0);
13035
13334
  const outputDifference = totalActions - idealOutput;
13036
- const hourlyTargetOutput = Array.isArray(data.hourly_target_output) ? data.hourly_target_output.map((value) => value === null || value === void 0 ? null : coerceNumber(value, 0)) : null;
13335
+ const hasHourlyTargetOutputField = Object.prototype.hasOwnProperty.call(data, "hourly_target_output");
13336
+ const hourlyTargetOutput = Array.isArray(data.hourly_target_output) ? data.hourly_target_output.map((value) => value === null || value === void 0 ? null : coerceNumber(value, 0)) : hasHourlyTargetOutputField ? null : void 0;
13037
13337
  const hourlyCycleTimes = Array.isArray(data.hourly_cycle_times) ? data.hourly_cycle_times.map((value) => coerceNumber(value, 0)) : [];
13038
13338
  const cycleCompletionClipCount = data.cycle_completion_clip_count === null || data.cycle_completion_clip_count === void 0 ? null : coerceNumber(data.cycle_completion_clip_count, 0);
13039
13339
  const cycleTimeDataStatus = data.cycle_time_data_status === "missing_clips" ? "missing_clips" : data.cycle_time_data_status === "available" ? "available" : null;
@@ -13198,6 +13498,17 @@ var useWorkspaceDetailedMetrics = (workspaceId, date, shiftId, options) => {
13198
13498
  workspaceMetricsStore.setDetailed(transformedData);
13199
13499
  } catch (err) {
13200
13500
  console.error("Error fetching workspace metrics:", err);
13501
+ captureHandledFrontendException(err, {
13502
+ surface: "workspace_detail_metrics",
13503
+ route: "/api/dashboard/workspace/:workspaceId/metrics",
13504
+ extras: {
13505
+ workspace_id: workspaceId,
13506
+ company_id: companyId,
13507
+ date,
13508
+ shift_id: shiftId,
13509
+ pathname: typeof window !== "undefined" ? window.location.pathname : "unknown"
13510
+ }
13511
+ });
13201
13512
  setError({ message: err.message, code: err.code });
13202
13513
  } finally {
13203
13514
  isFetchingRef.current = false;
@@ -14338,6 +14649,7 @@ var transformMonitorWorkspaceMetrics = ({
14338
14649
  recent_flow_window_minutes: item.recent_flow_window_minutes ?? null,
14339
14650
  recent_flow_effective_end_at: item.recent_flow_effective_end_at ?? null,
14340
14651
  recent_flow_computed_at: item.recent_flow_computed_at ?? null,
14652
+ recent_flow_forced_zero_after_shift: item.recent_flow_forced_zero_after_shift ?? null,
14341
14653
  scheduled_break_active: item.scheduled_break_active ?? false,
14342
14654
  incoming_wip_current: item.incoming_wip_current ?? null,
14343
14655
  incoming_wip_effective_at: item.incoming_wip_effective_at ?? null,
@@ -21464,20 +21776,26 @@ var apiUtils = {
21464
21776
  const token = sessionResponse.data.session?.access_token;
21465
21777
  if (!token) {
21466
21778
  console.error("API Util: No authentication token available.");
21467
- captureHandledFrontendException(new Error("Authentication required."), {
21779
+ addSentryBreadcrumb("API utility request skipped without auth token", {
21468
21780
  surface: "api_utils",
21469
- endpoint: relativeEndpoint,
21470
- reason: "missing_auth_token"
21781
+ route: relativeEndpoint,
21782
+ severity: "warning",
21783
+ extras: {
21784
+ reason: "missing_auth_token"
21785
+ }
21471
21786
  });
21472
21787
  throw new Error("Authentication required.");
21473
21788
  }
21474
21789
  const baseUrl = config.apiBaseUrl;
21475
21790
  if (!baseUrl) {
21476
21791
  console.error("API Util: apiBaseUrl is not configured.");
21477
- captureHandledFrontendException(new Error("API base URL is not configured."), {
21792
+ addSentryBreadcrumb("API utility request skipped without base URL", {
21478
21793
  surface: "api_utils",
21479
- endpoint: relativeEndpoint,
21480
- reason: "missing_api_base_url"
21794
+ route: relativeEndpoint,
21795
+ severity: "warning",
21796
+ extras: {
21797
+ reason: "missing_api_base_url"
21798
+ }
21481
21799
  });
21482
21800
  throw new Error("API base URL is not configured.");
21483
21801
  }
@@ -21507,11 +21825,14 @@ var apiUtils = {
21507
21825
  return await response.json();
21508
21826
  } catch (error) {
21509
21827
  console.error(`Network or fetch error calling ${endpoint}:`, error);
21510
- captureHandledFrontendException(error, {
21828
+ addSentryBreadcrumb("API utility request failed", {
21511
21829
  surface: "api_utils",
21512
- endpoint,
21513
- method: options.method || "GET",
21514
- pathname: typeof window !== "undefined" ? window.location.pathname : "unknown"
21830
+ route: endpoint,
21831
+ severity: "warning",
21832
+ extras: {
21833
+ method: options.method || "GET",
21834
+ pathname: typeof window !== "undefined" ? window.location.pathname : "unknown"
21835
+ }
21515
21836
  });
21516
21837
  if (error instanceof Error) {
21517
21838
  throw error;
@@ -35214,6 +35535,261 @@ var Button = React144__namespace.forwardRef(
35214
35535
  }
35215
35536
  );
35216
35537
  Button.displayName = "Button";
35538
+
35539
+ // src/lib/utils/hourlyTargets.ts
35540
+ var stripSeconds2 = (timeStr) => timeStr ? timeStr.slice(0, 5) : timeStr;
35541
+ var MINUTES_PER_DAY = 24 * 60;
35542
+ var parseTimeToMinutes2 = (timeString) => {
35543
+ const normalized = stripSeconds2(timeString || "");
35544
+ if (!normalized || !/^[0-2]\d:[0-5]\d$/.test(normalized)) return Number.NaN;
35545
+ const [hours, minutes] = normalized.split(":").map(Number);
35546
+ return hours * 60 + minutes;
35547
+ };
35548
+ var normalizeBreaksOnShiftTimeline = (shiftStart, breaks) => {
35549
+ const shiftStartMinutes = parseTimeToMinutes2(shiftStart);
35550
+ if (!Number.isFinite(shiftStartMinutes)) return [];
35551
+ const normalizedBreaks = [];
35552
+ for (const entry of breaks) {
35553
+ const startRaw = parseTimeToMinutes2(entry.startTime);
35554
+ const endRaw = parseTimeToMinutes2(entry.endTime);
35555
+ if (!Number.isFinite(startRaw) || !Number.isFinite(endRaw)) continue;
35556
+ let start = startRaw;
35557
+ let end = endRaw;
35558
+ if (end <= start) {
35559
+ end += 24 * 60;
35560
+ }
35561
+ if (start < shiftStartMinutes) {
35562
+ start += 24 * 60;
35563
+ end += 24 * 60;
35564
+ }
35565
+ const label = entry.remarks?.trim() || "Break";
35566
+ normalizedBreaks.push({ start, end, label });
35567
+ }
35568
+ return normalizedBreaks;
35569
+ };
35570
+ var roundTarget = (value, mode) => {
35571
+ if (!Number.isFinite(value)) return 0;
35572
+ switch (mode) {
35573
+ case "floor":
35574
+ return Math.floor(value);
35575
+ case "ceil":
35576
+ return Math.ceil(value);
35577
+ case "round":
35578
+ default:
35579
+ return Math.round(value);
35580
+ }
35581
+ };
35582
+ var formatDateKey = (date) => {
35583
+ const year = date.getUTCFullYear();
35584
+ const month = `${date.getUTCMonth() + 1}`.padStart(2, "0");
35585
+ const day = `${date.getUTCDate()}`.padStart(2, "0");
35586
+ return `${year}-${month}-${day}`;
35587
+ };
35588
+ var shiftDateKey = (dateKey, deltaDays) => {
35589
+ const [yearPart, monthPart, dayPart] = dateKey.split("-").map(Number);
35590
+ const year = Number.isFinite(yearPart) ? yearPart : 1970;
35591
+ const month = Number.isFinite(monthPart) ? monthPart : 1;
35592
+ const day = Number.isFinite(dayPart) ? dayPart : 1;
35593
+ const date = new Date(Date.UTC(year, month - 1, day));
35594
+ date.setUTCDate(date.getUTCDate() + deltaDays);
35595
+ return formatDateKey(date);
35596
+ };
35597
+ var getZonedNowSnapshot = (timeZone, now4) => {
35598
+ const formatter = new Intl.DateTimeFormat("en-US", {
35599
+ timeZone,
35600
+ year: "numeric",
35601
+ month: "2-digit",
35602
+ day: "2-digit",
35603
+ hour: "2-digit",
35604
+ minute: "2-digit",
35605
+ hourCycle: "h23"
35606
+ });
35607
+ const parts = formatter.formatToParts(now4).reduce((acc, part) => {
35608
+ if (part.type !== "literal") {
35609
+ acc[part.type] = part.value;
35610
+ }
35611
+ return acc;
35612
+ }, {});
35613
+ const year = Number(parts.year);
35614
+ const month = Number(parts.month);
35615
+ const day = Number(parts.day);
35616
+ const hour = Number(parts.hour);
35617
+ const minute = Number(parts.minute);
35618
+ return {
35619
+ dateKey: `${String(year).padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`,
35620
+ minutesOfDay: (Number.isFinite(hour) ? hour : 0) * 60 + (Number.isFinite(minute) ? minute : 0)
35621
+ };
35622
+ };
35623
+ var getDateKeyInTimeZone = (timeZone, now4 = /* @__PURE__ */ new Date()) => getZonedNowSnapshot(timeZone, now4).dateKey;
35624
+ var buildHourlyIntervals = ({
35625
+ shiftStart,
35626
+ shiftEnd,
35627
+ bucketMinutes = 60,
35628
+ fallbackHours = 11
35629
+ }) => {
35630
+ const startMinutes = parseTimeToMinutes2(shiftStart);
35631
+ if (!Number.isFinite(startMinutes)) return [];
35632
+ const bucket = Number.isFinite(bucketMinutes) && bucketMinutes > 0 ? Math.floor(bucketMinutes) : 60;
35633
+ let totalMinutes;
35634
+ const endRaw = shiftEnd ? parseTimeToMinutes2(shiftEnd) : Number.NaN;
35635
+ if (!Number.isFinite(endRaw)) {
35636
+ totalMinutes = Math.max(0, Math.round(fallbackHours * 60));
35637
+ } else {
35638
+ let endMinutes = endRaw;
35639
+ if (endMinutes <= startMinutes) {
35640
+ endMinutes += 24 * 60;
35641
+ }
35642
+ totalMinutes = endMinutes - startMinutes;
35643
+ }
35644
+ if (!Number.isFinite(totalMinutes) || totalMinutes <= 0) return [];
35645
+ const count = Math.ceil(totalMinutes / bucket);
35646
+ const shiftEndMinutes = startMinutes + totalMinutes;
35647
+ const intervals = [];
35648
+ for (let i = 0; i < count; i += 1) {
35649
+ const start = startMinutes + i * bucket;
35650
+ const end = Math.min(start + bucket, shiftEndMinutes);
35651
+ const minutes = Math.max(0, end - start);
35652
+ if (minutes <= 0) continue;
35653
+ intervals.push({ start, end, minutes });
35654
+ }
35655
+ return intervals;
35656
+ };
35657
+ var computeBreakMinutesByInterval = ({
35658
+ intervals,
35659
+ shiftStart,
35660
+ breaks
35661
+ }) => {
35662
+ if (!intervals.length || !breaks.length) return intervals.map(() => 0);
35663
+ const normalizedBreaks = normalizeBreaksOnShiftTimeline(shiftStart, breaks);
35664
+ return intervals.map((interval) => {
35665
+ if (!normalizedBreaks.length) return 0;
35666
+ let total = 0;
35667
+ for (const brk of normalizedBreaks) {
35668
+ const overlap = Math.max(0, Math.min(interval.end, brk.end) - Math.max(interval.start, brk.start));
35669
+ total += overlap;
35670
+ if (total >= interval.minutes) return interval.minutes;
35671
+ }
35672
+ return Math.min(interval.minutes, total);
35673
+ });
35674
+ };
35675
+ var computeBreakRemarksByInterval = ({
35676
+ intervals,
35677
+ shiftStart,
35678
+ breaks
35679
+ }) => {
35680
+ if (!intervals.length || !breaks.length) return intervals.map(() => "");
35681
+ const normalizedBreaks = normalizeBreaksOnShiftTimeline(shiftStart, breaks);
35682
+ return intervals.map((interval) => {
35683
+ const labels = normalizedBreaks.filter((brk) => Math.max(0, Math.min(interval.end, brk.end) - Math.max(interval.start, brk.start)) > 0).map((brk) => brk.label).filter((label, index, values) => label && values.indexOf(label) === index);
35684
+ return labels.join(", ");
35685
+ });
35686
+ };
35687
+ var computeEffectiveTargets = ({
35688
+ intervals,
35689
+ breakMinutes,
35690
+ pphThreshold,
35691
+ rounding = "round"
35692
+ }) => {
35693
+ return intervals.map((interval, idx) => {
35694
+ const intervalMinutes = Number(interval?.minutes) || 0;
35695
+ const breakMins = Number(breakMinutes?.[idx]) || 0;
35696
+ const plannedWorkMinutes = Math.max(0, intervalMinutes - breakMins);
35697
+ if (!Number.isFinite(pphThreshold) || pphThreshold <= 0) return 0;
35698
+ if (plannedWorkMinutes <= 0) return 0;
35699
+ return roundTarget(pphThreshold * plannedWorkMinutes / 60, rounding);
35700
+ });
35701
+ };
35702
+ var buildHourlyTargetPlan = ({
35703
+ shiftStart,
35704
+ shiftEnd,
35705
+ breaks = [],
35706
+ pphThreshold,
35707
+ bucketMinutes = 60,
35708
+ fallbackHours = 11,
35709
+ rounding = "round"
35710
+ }) => {
35711
+ const intervals = buildHourlyIntervals({
35712
+ shiftStart,
35713
+ shiftEnd,
35714
+ bucketMinutes,
35715
+ fallbackHours
35716
+ });
35717
+ const breakMinutes = computeBreakMinutesByInterval({
35718
+ intervals,
35719
+ shiftStart,
35720
+ breaks
35721
+ });
35722
+ const breakRemarks = computeBreakRemarksByInterval({
35723
+ intervals,
35724
+ shiftStart,
35725
+ breaks
35726
+ });
35727
+ const productiveMinutes = intervals.map((interval, idx) => Math.max(0, (Number(interval?.minutes) || 0) - (Number(breakMinutes[idx]) || 0)));
35728
+ const targets = computeEffectiveTargets({
35729
+ intervals,
35730
+ breakMinutes,
35731
+ pphThreshold,
35732
+ rounding
35733
+ });
35734
+ return {
35735
+ intervals,
35736
+ breakMinutes,
35737
+ breakRemarks,
35738
+ productiveMinutes,
35739
+ targets
35740
+ };
35741
+ };
35742
+ var isHourlyIntervalComplete = ({
35743
+ reportDate,
35744
+ shiftStart,
35745
+ shiftEnd,
35746
+ interval,
35747
+ timeZone = "Asia/Kolkata",
35748
+ now: now4 = /* @__PURE__ */ new Date()
35749
+ }) => {
35750
+ if (!reportDate) return true;
35751
+ const snapshot = getZonedNowSnapshot(timeZone, now4);
35752
+ const shiftStartMinutes = parseTimeToMinutes2(shiftStart);
35753
+ const shiftEndMinutes = shiftEnd ? parseTimeToMinutes2(shiftEnd) : Number.NaN;
35754
+ const wrapsMidnight = Number.isFinite(shiftStartMinutes) && Number.isFinite(shiftEndMinutes) && shiftEndMinutes <= shiftStartMinutes;
35755
+ if (reportDate === snapshot.dateKey) {
35756
+ return interval.end <= snapshot.minutesOfDay;
35757
+ }
35758
+ if (wrapsMidnight && reportDate === shiftDateKey(snapshot.dateKey, -1)) {
35759
+ return interval.end <= snapshot.minutesOfDay + MINUTES_PER_DAY;
35760
+ }
35761
+ return reportDate < snapshot.dateKey;
35762
+ };
35763
+ var isShiftInProgressForReportDate = ({
35764
+ reportDate,
35765
+ shiftStart,
35766
+ shiftEnd,
35767
+ timeZone = "Asia/Kolkata",
35768
+ now: now4 = /* @__PURE__ */ new Date()
35769
+ }) => {
35770
+ if (!reportDate || !shiftStart || !shiftEnd) return false;
35771
+ const shiftStartMinutes = parseTimeToMinutes2(shiftStart);
35772
+ const shiftEndMinutesRaw = parseTimeToMinutes2(shiftEnd);
35773
+ if (!Number.isFinite(shiftStartMinutes) || !Number.isFinite(shiftEndMinutesRaw)) {
35774
+ return false;
35775
+ }
35776
+ let shiftEndMinutes = shiftEndMinutesRaw;
35777
+ const wrapsMidnight = shiftEndMinutes <= shiftStartMinutes;
35778
+ if (wrapsMidnight) {
35779
+ shiftEndMinutes += MINUTES_PER_DAY;
35780
+ }
35781
+ const snapshot = getZonedNowSnapshot(timeZone, now4);
35782
+ let currentMinutes = null;
35783
+ if (reportDate === snapshot.dateKey) {
35784
+ currentMinutes = snapshot.minutesOfDay;
35785
+ } else if (wrapsMidnight && reportDate === shiftDateKey(snapshot.dateKey, -1)) {
35786
+ currentMinutes = snapshot.minutesOfDay + MINUTES_PER_DAY;
35787
+ }
35788
+ if (currentMinutes === null) {
35789
+ return false;
35790
+ }
35791
+ return shiftStartMinutes <= currentMinutes && currentMinutes < shiftEndMinutes;
35792
+ };
35217
35793
  var padTime = (value) => value.toString().padStart(2, "0");
35218
35794
  var parseTime = (timeValue) => {
35219
35795
  if (!timeValue) return null;
@@ -35595,6 +36171,7 @@ var HourlyOutputChartComponent = ({
35595
36171
  hourlyTargetOutput,
35596
36172
  shiftStart,
35597
36173
  shiftEnd,
36174
+ shiftBreaks = [],
35598
36175
  showIdleTime = false,
35599
36176
  idleTimeHourly,
35600
36177
  shiftDate,
@@ -35832,13 +36409,41 @@ var HourlyOutputChartComponent = ({
35832
36409
  end: index === skuTimelineSegments.length - 1 ? Math.max(segment.start, targetLineEndOffset) : segment.end
35833
36410
  })).filter((segment) => segment.end > segment.start);
35834
36411
  }, [skuTimelineSegments, targetLineEndOffset]);
35835
- const hasHourlyTargetOutputProp = React144__namespace.default.useMemo(
36412
+ const hasExplicitHourlyTargetOutputProp = React144__namespace.default.useMemo(
35836
36413
  () => hourlyTargetOutput !== void 0,
35837
36414
  [hourlyTargetOutput]
35838
36415
  );
36416
+ const fallbackHourlyTargetOutput = React144__namespace.default.useMemo(() => {
36417
+ if (hasExplicitHourlyTargetOutputProp) return void 0;
36418
+ if (skuTimelineSegments.length > 0) return void 0;
36419
+ const plan = buildHourlyTargetPlan({
36420
+ shiftStart,
36421
+ shiftEnd,
36422
+ breaks: shiftBreaks,
36423
+ pphThreshold,
36424
+ rounding: "floor"
36425
+ });
36426
+ if (!plan.targets.length) return void 0;
36427
+ return plan.targets.map((value) => Number.isFinite(value) ? value : null);
36428
+ }, [
36429
+ hasExplicitHourlyTargetOutputProp,
36430
+ skuTimelineSegments.length,
36431
+ shiftStart,
36432
+ shiftEnd,
36433
+ shiftBreaks,
36434
+ pphThreshold
36435
+ ]);
36436
+ const effectiveHourlyTargetOutput = React144__namespace.default.useMemo(
36437
+ () => hasExplicitHourlyTargetOutputProp ? hourlyTargetOutput : fallbackHourlyTargetOutput,
36438
+ [hasExplicitHourlyTargetOutputProp, hourlyTargetOutput, fallbackHourlyTargetOutput]
36439
+ );
36440
+ const hasHourlyTargetOutputProp = React144__namespace.default.useMemo(
36441
+ () => effectiveHourlyTargetOutput !== void 0,
36442
+ [effectiveHourlyTargetOutput]
36443
+ );
35839
36444
  const hasExplicitHourlyTargets = React144__namespace.default.useMemo(
35840
- () => Array.isArray(hourlyTargetOutput) && hourlyTargetOutput.some((value) => value !== null && value !== void 0),
35841
- [hourlyTargetOutput]
36445
+ () => Array.isArray(effectiveHourlyTargetOutput) && effectiveHourlyTargetOutput.some((value) => value !== null && value !== void 0),
36446
+ [effectiveHourlyTargetOutput]
35842
36447
  );
35843
36448
  const hourlyTargetSegments = React144__namespace.default.useMemo(() => {
35844
36449
  if (!hasExplicitHourlyTargets) return [];
@@ -35852,7 +36457,7 @@ var HourlyOutputChartComponent = ({
35852
36457
  runValue = null;
35853
36458
  };
35854
36459
  for (let i = 0; i < SHIFT_DURATION; i += 1) {
35855
- const rawValue = Array.isArray(hourlyTargetOutput) ? hourlyTargetOutput[i] : null;
36460
+ const rawValue = Array.isArray(effectiveHourlyTargetOutput) ? effectiveHourlyTargetOutput[i] : null;
35856
36461
  const value = rawValue === null || rawValue === void 0 ? null : Number(rawValue);
35857
36462
  if (value === null || !Number.isFinite(value)) {
35858
36463
  flush(i);
@@ -35871,7 +36476,7 @@ var HourlyOutputChartComponent = ({
35871
36476
  }
35872
36477
  flush(SHIFT_DURATION);
35873
36478
  return segments.filter((segment) => segment.end > segment.start);
35874
- }, [SHIFT_DURATION, hasExplicitHourlyTargets, hourlyTargetOutput]);
36479
+ }, [SHIFT_DURATION, hasExplicitHourlyTargets, effectiveHourlyTargetOutput]);
35875
36480
  const activeSkuHourIndices = React144__namespace.default.useMemo(() => {
35876
36481
  const indices = /* @__PURE__ */ new Set();
35877
36482
  const targets = Array(SHIFT_DURATION).fill(pphThreshold);
@@ -35909,7 +36514,7 @@ var HourlyOutputChartComponent = ({
35909
36514
  const { indices, targets, hasTimeline } = activeSkuHourIndices;
35910
36515
  return Array.from({ length: SHIFT_DURATION }, (_, i) => {
35911
36516
  const idleSlot = idleSlots[i];
35912
- const explicitTarget = hasHourlyTargetOutputProp ? hourlyTargetOutput?.[i] ?? null : void 0;
36517
+ const explicitTarget = hasHourlyTargetOutputProp ? effectiveHourlyTargetOutput?.[i] ?? null : void 0;
35913
36518
  const currentTarget = hasHourlyTargetOutputProp ? explicitTarget : targets[i] || pphThreshold;
35914
36519
  const comparisonTarget = currentTarget === null || currentTarget === void 0 ? targets[i] || pphThreshold : currentTarget;
35915
36520
  return {
@@ -35928,7 +36533,7 @@ var HourlyOutputChartComponent = ({
35928
36533
  isDimmed: hasTimeline && !!activeSkuId && !indices.has(i)
35929
36534
  };
35930
36535
  });
35931
- }, [animatedData, data, pphThreshold, idleSlots, SHIFT_DURATION, activeSkuHourIndices, activeSkuId, hourlyTargetOutput, hasHourlyTargetOutputProp]);
36536
+ }, [animatedData, data, pphThreshold, idleSlots, SHIFT_DURATION, activeSkuHourIndices, activeSkuId, effectiveHourlyTargetOutput, hasHourlyTargetOutputProp]);
35932
36537
  const renderSkuTimelineRail = React144__namespace.default.useCallback((props) => {
35933
36538
  if (!skuTimelineSegments.length || SHIFT_DURATION <= 0) return null;
35934
36539
  const offset = props?.offset;
@@ -36587,6 +37192,16 @@ var HourlyOutputChart = React144__namespace.default.memo(
36587
37192
  if (!prevProps.data.every((val, idx) => val === nextProps.data[idx])) {
36588
37193
  return false;
36589
37194
  }
37195
+ const prevHasHourlyTargetOutputProp = prevProps.hourlyTargetOutput !== void 0;
37196
+ const nextHasHourlyTargetOutputProp = nextProps.hourlyTargetOutput !== void 0;
37197
+ if (prevHasHourlyTargetOutputProp !== nextHasHourlyTargetOutputProp) {
37198
+ return false;
37199
+ }
37200
+ if (prevProps.hourlyTargetOutput === null || nextProps.hourlyTargetOutput === null) {
37201
+ if (prevProps.hourlyTargetOutput !== nextProps.hourlyTargetOutput) {
37202
+ return false;
37203
+ }
37204
+ }
36590
37205
  const prevHourlyTargets = prevProps.hourlyTargetOutput || [];
36591
37206
  const nextHourlyTargets = nextProps.hourlyTargetOutput || [];
36592
37207
  if (prevHourlyTargets.length !== nextHourlyTargets.length) {
@@ -36597,6 +37212,18 @@ var HourlyOutputChart = React144__namespace.default.memo(
36597
37212
  return false;
36598
37213
  }
36599
37214
  }
37215
+ const prevShiftBreaks = prevProps.shiftBreaks || [];
37216
+ const nextShiftBreaks = nextProps.shiftBreaks || [];
37217
+ if (prevShiftBreaks.length !== nextShiftBreaks.length) {
37218
+ return false;
37219
+ }
37220
+ for (let i = 0; i < prevShiftBreaks.length; i += 1) {
37221
+ const prevBreak = prevShiftBreaks[i] || {};
37222
+ const nextBreak = nextShiftBreaks[i] || {};
37223
+ if (prevBreak.startTime !== nextBreak.startTime || prevBreak.endTime !== nextBreak.endTime || prevBreak.duration !== nextBreak.duration || prevBreak.remarks !== nextBreak.remarks) {
37224
+ return false;
37225
+ }
37226
+ }
36600
37227
  const prevIdle = prevProps.idleTimeHourly || {};
36601
37228
  const nextIdle = nextProps.idleTimeHourly || {};
36602
37229
  const prevKeys = Object.keys(prevIdle);
@@ -36689,6 +37316,9 @@ var isLowWipGreenOverride = (workspace, legend = DEFAULT_EFFICIENCY_LEGEND) => {
36689
37316
  if (workspace.scheduled_break_active === true) {
36690
37317
  return false;
36691
37318
  }
37319
+ if (workspace.recent_flow_forced_zero_after_shift === true) {
37320
+ return false;
37321
+ }
36692
37322
  if (!hasVideoGridRecentFlow(workspace) || !isVideoGridWipGated(workspace)) {
36693
37323
  return false;
36694
37324
  }
@@ -36934,7 +37564,7 @@ var VideoCard = React144__namespace.default.memo(({
36934
37564
  }
36935
37565
  );
36936
37566
  }, (prevProps, nextProps) => {
36937
- if (prevProps.workspace.efficiency !== nextProps.workspace.efficiency || prevProps.workspace.assembly_enabled !== nextProps.workspace.assembly_enabled || prevProps.workspace.video_grid_metric_mode !== nextProps.workspace.video_grid_metric_mode || prevProps.workspace.recent_flow_percent !== nextProps.workspace.recent_flow_percent || prevProps.workspace.recent_flow_effective_end_at !== nextProps.workspace.recent_flow_effective_end_at || prevProps.workspace.scheduled_break_active !== nextProps.workspace.scheduled_break_active || prevProps.workspace.incoming_wip_current !== nextProps.workspace.incoming_wip_current || prevProps.workspace.incoming_wip_buffer_name !== nextProps.workspace.incoming_wip_buffer_name || prevProps.workspace.trend !== nextProps.workspace.trend || prevProps.workspace.performance_score !== nextProps.workspace.performance_score || prevProps.workspace.pph !== nextProps.workspace.pph) {
37567
+ if (prevProps.workspace.efficiency !== nextProps.workspace.efficiency || prevProps.workspace.assembly_enabled !== nextProps.workspace.assembly_enabled || prevProps.workspace.video_grid_metric_mode !== nextProps.workspace.video_grid_metric_mode || prevProps.workspace.recent_flow_percent !== nextProps.workspace.recent_flow_percent || prevProps.workspace.recent_flow_effective_end_at !== nextProps.workspace.recent_flow_effective_end_at || prevProps.workspace.recent_flow_forced_zero_after_shift !== nextProps.workspace.recent_flow_forced_zero_after_shift || prevProps.workspace.scheduled_break_active !== nextProps.workspace.scheduled_break_active || prevProps.workspace.incoming_wip_current !== nextProps.workspace.incoming_wip_current || prevProps.workspace.incoming_wip_buffer_name !== nextProps.workspace.incoming_wip_buffer_name || prevProps.workspace.trend !== nextProps.workspace.trend || prevProps.workspace.performance_score !== nextProps.workspace.performance_score || prevProps.workspace.pph !== nextProps.workspace.pph) {
36938
37568
  return false;
36939
37569
  }
36940
37570
  if (prevProps.workspace.workspace_uuid !== nextProps.workspace.workspace_uuid || prevProps.workspace.workspace_name !== nextProps.workspace.workspace_name || prevProps.workspace.line_id !== nextProps.workspace.line_id) {
@@ -37253,6 +37883,15 @@ var VideoGridView = React144__namespace.default.memo(({
37253
37883
  }
37254
37884
  console.error(`[VideoGridView] Stream failed for workspace: ${workspaceId}`);
37255
37885
  setFailedStreams((prev) => new Set(prev).add(workspaceId));
37886
+ captureHandledFrontendException(new Error("Video stream failed after recovery attempts"), {
37887
+ surface: "video_grid_stream",
37888
+ route: typeof window !== "undefined" ? window.location.pathname : "unknown",
37889
+ extras: {
37890
+ workspace_id: workspaceId,
37891
+ stream_source: isR2Stream ? "r2" : "media_config",
37892
+ has_fallback: hasFallback
37893
+ }
37894
+ });
37256
37895
  trackCoreEvent("Video Stream Error", {
37257
37896
  workspace_id: workspaceId,
37258
37897
  view_type: "video_grid",
@@ -41920,11 +42559,14 @@ var SilentErrorBoundary = class extends React144__namespace.default.Component {
41920
42559
  componentStack: errorInfo.componentStack,
41921
42560
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
41922
42561
  });
41923
- captureSentryException(error, {
42562
+ captureHandledFrontendException(error, {
41924
42563
  surface: "react_error_boundary",
41925
- component_stack: errorInfo.componentStack,
41926
- error_count: this.state.errorCount + 1,
41927
- pathname: typeof window !== "undefined" ? window.location.pathname : "unknown"
42564
+ route: typeof window !== "undefined" ? window.location.pathname : "unknown",
42565
+ severity: "error",
42566
+ extras: {
42567
+ component_stack: errorInfo.componentStack,
42568
+ error_count: this.state.errorCount + 1
42569
+ }
41928
42570
  });
41929
42571
  this.setState((prev) => ({
41930
42572
  errorCount: prev.errorCount + 1,
@@ -44461,6 +45103,11 @@ var OVERLAY_ICON_COLOR_BY_PALETTE = {
44461
45103
  cyan: "text-cyan-300",
44462
45104
  slate: "text-slate-200"
44463
45105
  };
45106
+ var buildPlaybackCaptureError = (recoverable) => {
45107
+ return new Error(
45108
+ recoverable ? "Clip playback failed after retry exhaustion" : "Clip playback failed with non-recoverable media error"
45109
+ );
45110
+ };
44464
45111
  var BottlenecksContent = ({
44465
45112
  workspaceId,
44466
45113
  workspaceName,
@@ -44915,6 +45562,16 @@ var BottlenecksContent = ({
44915
45562
  }
44916
45563
  } catch (err) {
44917
45564
  console.error("Error loading first video for category:", err);
45565
+ captureHandledFrontendException(err, {
45566
+ surface: "clips_initial_load",
45567
+ route: typeof window !== "undefined" ? window.location.pathname : "unknown",
45568
+ extras: {
45569
+ workspace_id: workspaceId,
45570
+ category: targetCategory,
45571
+ date: effectiveDateString,
45572
+ shift_id: effectiveShiftId
45573
+ }
45574
+ });
44918
45575
  if (isMountedRef.current) {
44919
45576
  setError({
44920
45577
  type: "fatal",
@@ -45452,6 +46109,15 @@ var BottlenecksContent = ({
45452
46109
  console.log(`[BottlenecksContent] Loaded clip ${clipId} (${clickedClipIndex + 1}/${metadataArray.length})`);
45453
46110
  } catch (error2) {
45454
46111
  console.error(`[BottlenecksContent] Error loading clip by ID (${clipId}):`, error2);
46112
+ captureHandledFrontendException(error2, {
46113
+ surface: "clips_selected_load",
46114
+ route: typeof window !== "undefined" ? window.location.pathname : "unknown",
46115
+ extras: {
46116
+ workspace_id: workspaceId,
46117
+ clip_id: clipId,
46118
+ category: categoryId
46119
+ }
46120
+ });
45455
46121
  if (isMountedRef.current) {
45456
46122
  setError({
45457
46123
  type: "fatal",
@@ -46051,6 +46717,18 @@ var BottlenecksContent = ({
46051
46717
  errorCode,
46052
46718
  errorMessage
46053
46719
  });
46720
+ captureHandledFrontendException(buildPlaybackCaptureError(false), {
46721
+ surface: "clips_video_playback",
46722
+ route: typeof window !== "undefined" ? window.location.pathname : "unknown",
46723
+ extras: {
46724
+ workspace_id: workspaceId,
46725
+ category: activeFilterRef.current,
46726
+ video_id: currentVideo?.id,
46727
+ error_code: errorCode,
46728
+ player_error_message: errorMessage,
46729
+ recoverable: false
46730
+ }
46731
+ });
46054
46732
  return;
46055
46733
  }
46056
46734
  if (videoRetryCountRef.current < 3 && currentVideo) {
@@ -46094,6 +46772,19 @@ var BottlenecksContent = ({
46094
46772
  errorMessage,
46095
46773
  attempts: 3
46096
46774
  });
46775
+ captureHandledFrontendException(buildPlaybackCaptureError(true), {
46776
+ surface: "clips_video_playback",
46777
+ route: typeof window !== "undefined" ? window.location.pathname : "unknown",
46778
+ extras: {
46779
+ workspace_id: workspaceId,
46780
+ category: activeFilterRef.current,
46781
+ video_id: currentVideo?.id,
46782
+ error_code: errorCode,
46783
+ player_error_message: errorMessage,
46784
+ recoverable: true,
46785
+ attempts: 3
46786
+ }
46787
+ });
46097
46788
  }
46098
46789
  }, [currentVideo, workspaceId, clearLoadingState, clearRetryTimeout, restartCurrentClipPlayback]);
46099
46790
  React144.useEffect(() => {
@@ -48425,6 +49116,14 @@ var LinePdfExportButton = ({
48425
49116
  pdf.save(`${fileName}.pdf`);
48426
49117
  } catch (error) {
48427
49118
  console.error("PDF Export Error:", error);
49119
+ captureHandledFrontendException(error, {
49120
+ surface: "line_pdf_export",
49121
+ route: typeof window !== "undefined" ? window.location.pathname : "unknown",
49122
+ extras: {
49123
+ file_name: fileName,
49124
+ target_type: typeof targetElement === "string" ? "selector" : "element"
49125
+ }
49126
+ });
48428
49127
  alert("An error occurred while exporting to PDF. Please try again.");
48429
49128
  } finally {
48430
49129
  setIsExporting(false);
@@ -50247,6 +50946,16 @@ var LineMonthlyPdfGenerator = ({
50247
50946
  doc.save(fileName);
50248
50947
  } catch (error) {
50249
50948
  console.error("Line Monthly PDF generation failed:", error);
50949
+ captureHandledFrontendException(error, {
50950
+ surface: "line_monthly_pdf_generation",
50951
+ route: typeof window !== "undefined" ? window.location.pathname : "unknown",
50952
+ extras: {
50953
+ line_name: lineName || null,
50954
+ selected_month: selectedMonth,
50955
+ selected_year: selectedYear,
50956
+ selected_shift_id: selectedShiftId
50957
+ }
50958
+ });
50250
50959
  } finally {
50251
50960
  setIsGenerating(false);
50252
50961
  }
@@ -50311,261 +51020,6 @@ Underperforming Workspaces: ${lineInfo.metrics.underperforming_workspaces} / ${l
50311
51020
  }
50312
51021
  );
50313
51022
  };
50314
-
50315
- // src/lib/utils/hourlyTargets.ts
50316
- var stripSeconds2 = (timeStr) => timeStr ? timeStr.slice(0, 5) : timeStr;
50317
- var MINUTES_PER_DAY = 24 * 60;
50318
- var parseTimeToMinutes2 = (timeString) => {
50319
- const normalized = stripSeconds2(timeString || "");
50320
- if (!normalized || !/^[0-2]\d:[0-5]\d$/.test(normalized)) return Number.NaN;
50321
- const [hours, minutes] = normalized.split(":").map(Number);
50322
- return hours * 60 + minutes;
50323
- };
50324
- var normalizeBreaksOnShiftTimeline = (shiftStart, breaks) => {
50325
- const shiftStartMinutes = parseTimeToMinutes2(shiftStart);
50326
- if (!Number.isFinite(shiftStartMinutes)) return [];
50327
- const normalizedBreaks = [];
50328
- for (const entry of breaks) {
50329
- const startRaw = parseTimeToMinutes2(entry.startTime);
50330
- const endRaw = parseTimeToMinutes2(entry.endTime);
50331
- if (!Number.isFinite(startRaw) || !Number.isFinite(endRaw)) continue;
50332
- let start = startRaw;
50333
- let end = endRaw;
50334
- if (end <= start) {
50335
- end += 24 * 60;
50336
- }
50337
- if (start < shiftStartMinutes) {
50338
- start += 24 * 60;
50339
- end += 24 * 60;
50340
- }
50341
- const label = entry.remarks?.trim() || "Break";
50342
- normalizedBreaks.push({ start, end, label });
50343
- }
50344
- return normalizedBreaks;
50345
- };
50346
- var roundTarget = (value, mode) => {
50347
- if (!Number.isFinite(value)) return 0;
50348
- switch (mode) {
50349
- case "floor":
50350
- return Math.floor(value);
50351
- case "ceil":
50352
- return Math.ceil(value);
50353
- case "round":
50354
- default:
50355
- return Math.round(value);
50356
- }
50357
- };
50358
- var formatDateKey = (date) => {
50359
- const year = date.getUTCFullYear();
50360
- const month = `${date.getUTCMonth() + 1}`.padStart(2, "0");
50361
- const day = `${date.getUTCDate()}`.padStart(2, "0");
50362
- return `${year}-${month}-${day}`;
50363
- };
50364
- var shiftDateKey = (dateKey, deltaDays) => {
50365
- const [yearPart, monthPart, dayPart] = dateKey.split("-").map(Number);
50366
- const year = Number.isFinite(yearPart) ? yearPart : 1970;
50367
- const month = Number.isFinite(monthPart) ? monthPart : 1;
50368
- const day = Number.isFinite(dayPart) ? dayPart : 1;
50369
- const date = new Date(Date.UTC(year, month - 1, day));
50370
- date.setUTCDate(date.getUTCDate() + deltaDays);
50371
- return formatDateKey(date);
50372
- };
50373
- var getZonedNowSnapshot = (timeZone, now4) => {
50374
- const formatter = new Intl.DateTimeFormat("en-US", {
50375
- timeZone,
50376
- year: "numeric",
50377
- month: "2-digit",
50378
- day: "2-digit",
50379
- hour: "2-digit",
50380
- minute: "2-digit",
50381
- hourCycle: "h23"
50382
- });
50383
- const parts = formatter.formatToParts(now4).reduce((acc, part) => {
50384
- if (part.type !== "literal") {
50385
- acc[part.type] = part.value;
50386
- }
50387
- return acc;
50388
- }, {});
50389
- const year = Number(parts.year);
50390
- const month = Number(parts.month);
50391
- const day = Number(parts.day);
50392
- const hour = Number(parts.hour);
50393
- const minute = Number(parts.minute);
50394
- return {
50395
- dateKey: `${String(year).padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`,
50396
- minutesOfDay: (Number.isFinite(hour) ? hour : 0) * 60 + (Number.isFinite(minute) ? minute : 0)
50397
- };
50398
- };
50399
- var getDateKeyInTimeZone = (timeZone, now4 = /* @__PURE__ */ new Date()) => getZonedNowSnapshot(timeZone, now4).dateKey;
50400
- var buildHourlyIntervals = ({
50401
- shiftStart,
50402
- shiftEnd,
50403
- bucketMinutes = 60,
50404
- fallbackHours = 11
50405
- }) => {
50406
- const startMinutes = parseTimeToMinutes2(shiftStart);
50407
- if (!Number.isFinite(startMinutes)) return [];
50408
- const bucket = Number.isFinite(bucketMinutes) && bucketMinutes > 0 ? Math.floor(bucketMinutes) : 60;
50409
- let totalMinutes;
50410
- const endRaw = shiftEnd ? parseTimeToMinutes2(shiftEnd) : Number.NaN;
50411
- if (!Number.isFinite(endRaw)) {
50412
- totalMinutes = Math.max(0, Math.round(fallbackHours * 60));
50413
- } else {
50414
- let endMinutes = endRaw;
50415
- if (endMinutes <= startMinutes) {
50416
- endMinutes += 24 * 60;
50417
- }
50418
- totalMinutes = endMinutes - startMinutes;
50419
- }
50420
- if (!Number.isFinite(totalMinutes) || totalMinutes <= 0) return [];
50421
- const count = Math.ceil(totalMinutes / bucket);
50422
- const shiftEndMinutes = startMinutes + totalMinutes;
50423
- const intervals = [];
50424
- for (let i = 0; i < count; i += 1) {
50425
- const start = startMinutes + i * bucket;
50426
- const end = Math.min(start + bucket, shiftEndMinutes);
50427
- const minutes = Math.max(0, end - start);
50428
- if (minutes <= 0) continue;
50429
- intervals.push({ start, end, minutes });
50430
- }
50431
- return intervals;
50432
- };
50433
- var computeBreakMinutesByInterval = ({
50434
- intervals,
50435
- shiftStart,
50436
- breaks
50437
- }) => {
50438
- if (!intervals.length || !breaks.length) return intervals.map(() => 0);
50439
- const normalizedBreaks = normalizeBreaksOnShiftTimeline(shiftStart, breaks);
50440
- return intervals.map((interval) => {
50441
- if (!normalizedBreaks.length) return 0;
50442
- let total = 0;
50443
- for (const brk of normalizedBreaks) {
50444
- const overlap = Math.max(0, Math.min(interval.end, brk.end) - Math.max(interval.start, brk.start));
50445
- total += overlap;
50446
- if (total >= interval.minutes) return interval.minutes;
50447
- }
50448
- return Math.min(interval.minutes, total);
50449
- });
50450
- };
50451
- var computeBreakRemarksByInterval = ({
50452
- intervals,
50453
- shiftStart,
50454
- breaks
50455
- }) => {
50456
- if (!intervals.length || !breaks.length) return intervals.map(() => "");
50457
- const normalizedBreaks = normalizeBreaksOnShiftTimeline(shiftStart, breaks);
50458
- return intervals.map((interval) => {
50459
- const labels = normalizedBreaks.filter((brk) => Math.max(0, Math.min(interval.end, brk.end) - Math.max(interval.start, brk.start)) > 0).map((brk) => brk.label).filter((label, index, values) => label && values.indexOf(label) === index);
50460
- return labels.join(", ");
50461
- });
50462
- };
50463
- var computeEffectiveTargets = ({
50464
- intervals,
50465
- breakMinutes,
50466
- pphThreshold,
50467
- rounding = "round"
50468
- }) => {
50469
- return intervals.map((interval, idx) => {
50470
- const intervalMinutes = Number(interval?.minutes) || 0;
50471
- const breakMins = Number(breakMinutes?.[idx]) || 0;
50472
- const plannedWorkMinutes = Math.max(0, intervalMinutes - breakMins);
50473
- if (!Number.isFinite(pphThreshold) || pphThreshold <= 0) return 0;
50474
- if (plannedWorkMinutes <= 0) return 0;
50475
- return roundTarget(pphThreshold * plannedWorkMinutes / 60, rounding);
50476
- });
50477
- };
50478
- var buildHourlyTargetPlan = ({
50479
- shiftStart,
50480
- shiftEnd,
50481
- breaks = [],
50482
- pphThreshold,
50483
- bucketMinutes = 60,
50484
- fallbackHours = 11,
50485
- rounding = "round"
50486
- }) => {
50487
- const intervals = buildHourlyIntervals({
50488
- shiftStart,
50489
- shiftEnd,
50490
- bucketMinutes,
50491
- fallbackHours
50492
- });
50493
- const breakMinutes = computeBreakMinutesByInterval({
50494
- intervals,
50495
- shiftStart,
50496
- breaks
50497
- });
50498
- const breakRemarks = computeBreakRemarksByInterval({
50499
- intervals,
50500
- shiftStart,
50501
- breaks
50502
- });
50503
- const productiveMinutes = intervals.map((interval, idx) => Math.max(0, (Number(interval?.minutes) || 0) - (Number(breakMinutes[idx]) || 0)));
50504
- const targets = computeEffectiveTargets({
50505
- intervals,
50506
- breakMinutes,
50507
- pphThreshold,
50508
- rounding
50509
- });
50510
- return {
50511
- intervals,
50512
- breakMinutes,
50513
- breakRemarks,
50514
- productiveMinutes,
50515
- targets
50516
- };
50517
- };
50518
- var isHourlyIntervalComplete = ({
50519
- reportDate,
50520
- shiftStart,
50521
- shiftEnd,
50522
- interval,
50523
- timeZone = "Asia/Kolkata",
50524
- now: now4 = /* @__PURE__ */ new Date()
50525
- }) => {
50526
- if (!reportDate) return true;
50527
- const snapshot = getZonedNowSnapshot(timeZone, now4);
50528
- const shiftStartMinutes = parseTimeToMinutes2(shiftStart);
50529
- const shiftEndMinutes = shiftEnd ? parseTimeToMinutes2(shiftEnd) : Number.NaN;
50530
- const wrapsMidnight = Number.isFinite(shiftStartMinutes) && Number.isFinite(shiftEndMinutes) && shiftEndMinutes <= shiftStartMinutes;
50531
- if (reportDate === snapshot.dateKey) {
50532
- return interval.end <= snapshot.minutesOfDay;
50533
- }
50534
- if (wrapsMidnight && reportDate === shiftDateKey(snapshot.dateKey, -1)) {
50535
- return interval.end <= snapshot.minutesOfDay + MINUTES_PER_DAY;
50536
- }
50537
- return reportDate < snapshot.dateKey;
50538
- };
50539
- var isShiftInProgressForReportDate = ({
50540
- reportDate,
50541
- shiftStart,
50542
- shiftEnd,
50543
- timeZone = "Asia/Kolkata",
50544
- now: now4 = /* @__PURE__ */ new Date()
50545
- }) => {
50546
- if (!reportDate || !shiftStart || !shiftEnd) return false;
50547
- const shiftStartMinutes = parseTimeToMinutes2(shiftStart);
50548
- const shiftEndMinutesRaw = parseTimeToMinutes2(shiftEnd);
50549
- if (!Number.isFinite(shiftStartMinutes) || !Number.isFinite(shiftEndMinutesRaw)) {
50550
- return false;
50551
- }
50552
- let shiftEndMinutes = shiftEndMinutesRaw;
50553
- const wrapsMidnight = shiftEndMinutes <= shiftStartMinutes;
50554
- if (wrapsMidnight) {
50555
- shiftEndMinutes += MINUTES_PER_DAY;
50556
- }
50557
- const snapshot = getZonedNowSnapshot(timeZone, now4);
50558
- let currentMinutes = null;
50559
- if (reportDate === snapshot.dateKey) {
50560
- currentMinutes = snapshot.minutesOfDay;
50561
- } else if (wrapsMidnight && reportDate === shiftDateKey(snapshot.dateKey, -1)) {
50562
- currentMinutes = snapshot.minutesOfDay + MINUTES_PER_DAY;
50563
- }
50564
- if (currentMinutes === null) {
50565
- return false;
50566
- }
50567
- return shiftStartMinutes <= currentMinutes && currentMinutes < shiftEndMinutes;
50568
- };
50569
51023
  var formatOperationalDateKey = (dateKey, options) => {
50570
51024
  const [yearPart, monthPart, dayPart] = dateKey.split("-").map(Number);
50571
51025
  const year = Number.isFinite(yearPart) ? yearPart : 1970;
@@ -51284,6 +51738,15 @@ var LinePdfGenerator = ({
51284
51738
  doc.save(fileName);
51285
51739
  } catch (error) {
51286
51740
  console.error("PDF generation failed:", error);
51741
+ captureHandledFrontendException(error, {
51742
+ surface: "line_pdf_generation",
51743
+ route: typeof window !== "undefined" ? window.location.pathname : "unknown",
51744
+ extras: {
51745
+ line_id: lineInfo.line_id,
51746
+ date: lineInfo.date,
51747
+ shift_id: lineInfo.shift_id
51748
+ }
51749
+ });
51287
51750
  } finally {
51288
51751
  setIsGenerating(false);
51289
51752
  }
@@ -51356,6 +51819,14 @@ var WorkspacePdfExportButton = ({
51356
51819
  pdf.save(`${fileName}.pdf`);
51357
51820
  } catch (error) {
51358
51821
  console.error("PDF Export Error:", error);
51822
+ captureHandledFrontendException(error, {
51823
+ surface: "workspace_pdf_export",
51824
+ route: typeof window !== "undefined" ? window.location.pathname : "unknown",
51825
+ extras: {
51826
+ file_name: fileName,
51827
+ target_type: typeof targetElement === "string" ? "selector" : "element"
51828
+ }
51829
+ });
51359
51830
  alert("An error occurred while exporting to PDF. Please try again.");
51360
51831
  } finally {
51361
51832
  setIsExporting(false);
@@ -53205,6 +53676,16 @@ var WorkspacePdfGenerator = ({
53205
53676
  doc.save(fileName);
53206
53677
  } catch (error) {
53207
53678
  console.error("PDF generation failed:", error);
53679
+ captureHandledFrontendException(error, {
53680
+ surface: "workspace_pdf_generation",
53681
+ route: typeof window !== "undefined" ? window.location.pathname : "unknown",
53682
+ extras: {
53683
+ workspace_id: workspace.workspace_id,
53684
+ line_id: workspace.line_id,
53685
+ date: workspace.date,
53686
+ shift_id: workspace.shift_id
53687
+ }
53688
+ });
53208
53689
  } finally {
53209
53690
  setIsGenerating(false);
53210
53691
  }
@@ -53612,6 +54093,18 @@ var WorkspaceMonthlyPdfGenerator = ({
53612
54093
  doc.save(fileName);
53613
54094
  } catch (error) {
53614
54095
  console.error("Monthly PDF generation failed:", error);
54096
+ captureHandledFrontendException(error, {
54097
+ surface: "workspace_monthly_pdf_generation",
54098
+ route: typeof window !== "undefined" ? window.location.pathname : "unknown",
54099
+ extras: {
54100
+ workspace_id: workspaceId,
54101
+ workspace_name: workspaceName,
54102
+ line_name: lineName || null,
54103
+ selected_month: selectedMonth,
54104
+ selected_year: selectedYear,
54105
+ selected_shift_id: selectedShiftId
54106
+ }
54107
+ });
53615
54108
  } finally {
53616
54109
  setIsGenerating(false);
53617
54110
  }
@@ -62884,6 +63377,16 @@ var useLiveMonitorBootstrap = ({
62884
63377
  if (requestId !== activeRequestIdRef.current) {
62885
63378
  return;
62886
63379
  }
63380
+ captureHandledFrontendException(fetchError, {
63381
+ surface: "live_monitor_bootstrap",
63382
+ route: "/api/dashboard/monitor-bootstrap",
63383
+ extras: {
63384
+ company_id: resolvedCompanyId,
63385
+ line_ids: normalizedLineIds,
63386
+ force_refresh: force,
63387
+ pathname: typeof window !== "undefined" ? window.location.pathname : "unknown"
63388
+ }
63389
+ });
62887
63390
  setError(fetchError instanceof Error ? fetchError : new Error("Failed to load live monitor bootstrap"));
62888
63391
  } finally {
62889
63392
  if (requestId === activeRequestIdRef.current) {
@@ -63196,11 +63699,14 @@ var NotificationService = class {
63196
63699
  }
63197
63700
  return response;
63198
63701
  } catch (error) {
63199
- captureHandledFrontendException(error, {
63702
+ addSentryBreadcrumb("Notification service request failed", {
63200
63703
  surface: "notification_service",
63201
- url,
63202
- method: options.method || "GET",
63203
- pathname: typeof window !== "undefined" ? window.location.pathname : "unknown"
63704
+ route: url,
63705
+ severity: "warning",
63706
+ extras: {
63707
+ method: options.method || "GET",
63708
+ pathname: typeof window !== "undefined" ? window.location.pathname : "unknown"
63709
+ }
63204
63710
  });
63205
63711
  throw error;
63206
63712
  }
@@ -64708,7 +65214,7 @@ var buildLineInfoSnapshot = (lineDetails, metrics2) => {
64708
65214
  underperforming_workspace_uuids: metrics2.underperforming_workspace_uuids || [],
64709
65215
  output_array: metrics2.output_array || [],
64710
65216
  output_hourly: metrics2.output_hourly,
64711
- hourly_target_output: metrics2.hourly_target_output ?? null,
65217
+ hourly_target_output: metrics2.hourly_target_output,
64712
65218
  line_threshold: metrics2.line_threshold ?? 0,
64713
65219
  threshold_pph: metrics2.threshold_pph ?? 0,
64714
65220
  shift_start: metrics2.shift_start || "06:00",
@@ -64795,7 +65301,7 @@ var transformLineMetrics = (lineId, detailResponse, queryDate, queryShiftId) =>
64795
65301
  underperforming_workspace_names: [],
64796
65302
  underperforming_workspace_uuids: [],
64797
65303
  output_array: [],
64798
- hourly_target_output: null,
65304
+ hourly_target_output: void 0,
64799
65305
  line_threshold: 0,
64800
65306
  threshold_pph: 0,
64801
65307
  shift_start: "06:00",
@@ -65834,6 +66340,7 @@ var BottomSection = React144.memo(({
65834
66340
  hourlyOutputData,
65835
66341
  hourlyThreshold,
65836
66342
  hourlyTargetOutput,
66343
+ shiftBreaks,
65837
66344
  idleTimeHourly,
65838
66345
  timezone,
65839
66346
  urlDate,
@@ -66009,6 +66516,7 @@ var BottomSection = React144.memo(({
66009
66516
  hourlyTargetOutput,
66010
66517
  shiftStart: lineInfo.metrics.shift_start || "06:00",
66011
66518
  shiftEnd: lineInfo.metrics.shift_end,
66519
+ shiftBreaks,
66012
66520
  idleTimeHourly,
66013
66521
  shiftDate: lineInfo.date,
66014
66522
  timezone,
@@ -66032,6 +66540,9 @@ var BottomSection = React144.memo(({
66032
66540
  if (prevProps.lineInfo.monitoring_mode !== nextProps.lineInfo.monitoring_mode) return false;
66033
66541
  if (prevProps.skuAware !== nextProps.skuAware) return false;
66034
66542
  if (prevProps.activeSkuId !== nextProps.activeSkuId) return false;
66543
+ if (JSON.stringify(prevProps.shiftBreaks || []) !== JSON.stringify(nextProps.shiftBreaks || [])) {
66544
+ return false;
66545
+ }
66035
66546
  if (JSON.stringify(prevProps.hourlyTargetOutput || []) !== JSON.stringify(nextProps.hourlyTargetOutput || [])) {
66036
66547
  return false;
66037
66548
  }
@@ -66633,7 +67144,7 @@ var KPIDetailView = ({
66633
67144
  underperforming_workspace_uuids: metrics2.underperforming_workspace_uuids || [],
66634
67145
  output_array: metrics2.output_array || [],
66635
67146
  output_hourly: metrics2.output_hourly,
66636
- hourly_target_output: metrics2.hourly_target_output ?? null,
67147
+ hourly_target_output: metrics2.hourly_target_output,
66637
67148
  line_threshold: metrics2.line_threshold ?? 0,
66638
67149
  threshold_pph: metrics2.threshold_pph ?? 0,
66639
67150
  shift_start: metrics2.shift_start || "06:00",
@@ -67597,7 +68108,8 @@ var KPIDetailView = ({
67597
68108
  workspaceDisplayNames,
67598
68109
  hourlyOutputData,
67599
68110
  hourlyThreshold,
67600
- hourlyTargetOutput: chartMetrics?.hourly_target_output ?? null,
68111
+ hourlyTargetOutput: chartMetrics?.hourly_target_output,
68112
+ shiftBreaks: shiftConfig?.shifts?.find((shift) => shift.shiftId === resolvedLineInfo.shift_id)?.breaks || [],
67601
68113
  idleTimeHourly: chartMetrics?.idle_time_hourly,
67602
68114
  timezone: lineTimezone,
67603
68115
  urlDate,
@@ -75361,6 +75873,7 @@ var WorkspaceDetailView = ({
75361
75873
  hourlyTargetOutput: workspace.hourly_target_output,
75362
75874
  shiftStart: workspace.shift_start || "06:00",
75363
75875
  shiftEnd: workspace.shift_end,
75876
+ shiftBreaks: shiftConfig?.shifts?.find((shift2) => shift2.shiftId === workspace.shift_id)?.breaks || [],
75364
75877
  showIdleTime: showChartIdleTime,
75365
75878
  idleTimeHourly: workspace.idle_time_hourly,
75366
75879
  idleTimeClips,
@@ -75508,6 +76021,7 @@ var WorkspaceDetailView = ({
75508
76021
  hourlyTargetOutput: workspace.hourly_target_output,
75509
76022
  shiftStart: workspace.shift_start || "06:00",
75510
76023
  shiftEnd: workspace.shift_end,
76024
+ shiftBreaks: shiftConfig?.shifts?.find((shift2) => shift2.shiftId === workspace.shift_id)?.breaks || [],
75511
76025
  showIdleTime: showChartIdleTime,
75512
76026
  idleTimeHourly: workspace.idle_time_hourly,
75513
76027
  idleTimeClips,
@@ -77347,11 +77861,14 @@ var TicketService = class {
77347
77861
  }
77348
77862
  return response;
77349
77863
  } catch (error) {
77350
- captureHandledFrontendException(error, {
77864
+ addSentryBreadcrumb("Ticket service request failed", {
77351
77865
  surface: "ticket_service",
77352
- url,
77353
- method: options.method || "GET",
77354
- pathname: typeof window !== "undefined" ? window.location.pathname : "unknown"
77866
+ route: url,
77867
+ severity: "warning",
77868
+ extras: {
77869
+ method: options.method || "GET",
77870
+ pathname: typeof window !== "undefined" ? window.location.pathname : "unknown"
77871
+ }
77355
77872
  });
77356
77873
  throw error;
77357
77874
  }
@@ -83131,10 +83648,33 @@ var useOperationsOverviewRefresh = ({
83131
83648
  if (controller.signal.aborted || requestIdsRef.current[section] !== requestId || isAbortError2(error)) {
83132
83649
  return;
83133
83650
  }
83651
+ const sentryContext = {
83652
+ surface: "operations_overview_refresh",
83653
+ route: `/api/dashboard/operations-overview/${section}`,
83654
+ extras: {
83655
+ section,
83656
+ reason,
83657
+ company_id: companyId,
83658
+ line_ids: lineIds,
83659
+ start_date: startKey,
83660
+ end_date: endKey,
83661
+ trend_mode: trendMode,
83662
+ comparison_strategy: comparisonStrategy || null
83663
+ }
83664
+ };
83665
+ if (section === "improvements") {
83666
+ addSentryBreadcrumb("Operations overview optional improvements refresh failed", {
83667
+ ...sentryContext,
83668
+ severity: "warning",
83669
+ category: "operations_overview"
83670
+ });
83671
+ } else {
83672
+ captureHandledFrontendException(error, sentryContext);
83673
+ }
83134
83674
  onError(error instanceof Error ? error.message : `Failed to refresh ${section}`);
83135
83675
  }
83136
83676
  },
83137
- [companyId, enabled, lineIds.length, supabase]
83677
+ [companyId, comparisonStrategy, enabled, endKey, lineIds, startKey, supabase, trendMode]
83138
83678
  );
83139
83679
  const refreshSnapshot = React144__namespace.default.useCallback(
83140
83680
  async (reason) => {
@@ -84736,6 +85276,9 @@ exports.RegistryProvider = RegistryProvider;
84736
85276
  exports.RoleBadge = RoleBadge;
84737
85277
  exports.S3ClipsService = S3ClipsSupabaseService;
84738
85278
  exports.S3Service = S3Service;
85279
+ exports.SENTRY_HANDLED_EVENT_SESSION_LIMIT = SENTRY_HANDLED_EVENT_SESSION_LIMIT;
85280
+ exports.SENTRY_HANDLED_EVENT_WINDOW_MS = SENTRY_HANDLED_EVENT_WINDOW_MS;
85281
+ exports.SENTRY_QUOTA_STORAGE_KEY = SENTRY_QUOTA_STORAGE_KEY;
84739
85282
  exports.SKUManagementView = SKUManagementView;
84740
85283
  exports.SOPComplianceChart = SOPComplianceChart;
84741
85284
  exports.SSEChatClient = SSEChatClient;
@@ -84812,6 +85355,7 @@ exports.WorkspacePdfExportButton = WorkspacePdfExportButton;
84812
85355
  exports.WorkspacePdfGenerator = WorkspacePdfGenerator;
84813
85356
  exports.WorkspaceWhatsAppShareButton = WorkspaceWhatsAppShareButton;
84814
85357
  exports.actionService = actionService;
85358
+ exports.addSentryBreadcrumb = addSentryBreadcrumb;
84815
85359
  exports.aggregateKPIsFromLineMetricsRows = aggregateKPIsFromLineMetricsRows;
84816
85360
  exports.alertsService = alertsService;
84817
85361
  exports.apiUtils = apiUtils;
@@ -84997,6 +85541,7 @@ exports.realtimeService = realtimeService;
84997
85541
  exports.refreshWorkspaceDisplayNames = refreshWorkspaceDisplayNames;
84998
85542
  exports.resetCoreMixpanel = resetCoreMixpanel;
84999
85543
  exports.resetFailedUrl = resetFailedUrl;
85544
+ exports.resetSentryQuotaForTests = resetSentryQuotaForTests;
85000
85545
  exports.resetSubscriptionManager = resetSubscriptionManager;
85001
85546
  exports.resolveDefaultSkuId = resolveDefaultSkuId;
85002
85547
  exports.resolveLiveSkuId = resolveLiveSkuId;