@optifye/dashboard-core 6.12.29 → 6.12.31

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.
@@ -5,6 +5,9 @@ declare const normalizeVideoGridMetricMode: (value: unknown, assemblyEnabled?: b
5
5
  declare const isRecentFlowVideoGridMetricMode: (value: unknown, assemblyEnabled?: boolean) => boolean;
6
6
  declare const isWipGatedVideoGridMetricMode: (value: unknown, assemblyEnabled?: boolean) => boolean;
7
7
 
8
+ type IdleReasonPaletteToken = 'red' | 'amber' | 'blue' | 'violet' | 'emerald' | 'cyan' | 'slate';
9
+ type IdleReasonIconToken = 'alert-triangle' | 'refresh-cw' | 'package' | 'clock' | 'user-x' | 'wrench' | 'activity' | 'clipboard-x' | 'help-circle';
10
+
8
11
  /** One row of per-SKU workspace metrics. Backend computes; frontend renders. */
9
12
  interface SkuBreakdownItem {
10
13
  /** UUID — `skus.id`. Stable identifier for selectors and threshold writes. */
@@ -149,6 +152,18 @@ interface PoorPerformingWorkspace {
149
152
  action_count: number;
150
153
  action_threshold: number;
151
154
  }
155
+ interface VideoGridStatusBadge {
156
+ kind: 'idle_reason' | 'no_plan';
157
+ label: string | null;
158
+ display_name: string;
159
+ palette_token: IdleReasonPaletteToken;
160
+ icon_token: IdleReasonIconToken;
161
+ is_known?: boolean;
162
+ title?: string;
163
+ anchor_minute?: number;
164
+ reason_minute?: number;
165
+ shift_elapsed_fraction?: number;
166
+ }
152
167
  interface WorkspaceMetrics {
153
168
  company_id: string;
154
169
  line_id: string;
@@ -170,6 +185,7 @@ interface WorkspaceMetrics {
170
185
  monitoring_mode?: 'output' | 'uptime';
171
186
  idle_time?: number;
172
187
  idle_time_hourly?: Record<string, any> | null;
188
+ idle_reason_hourly?: Record<string, any> | null;
173
189
  shift_start?: string;
174
190
  shift_end?: string;
175
191
  assembly_enabled?: boolean;
@@ -196,10 +212,12 @@ interface WorkspaceMetrics {
196
212
  video_grid_green_streak_minutes?: number | null;
197
213
  video_grid_green_streak_anchor_at?: string | null;
198
214
  video_grid_green_streak_started_at?: string | null;
215
+ video_grid_green_streak_observed_at?: string | null;
199
216
  scheduled_break_active?: boolean;
200
217
  incoming_wip_current?: number | null;
201
218
  incoming_wip_effective_at?: string | null;
202
219
  incoming_wip_buffer_name?: string | null;
220
+ video_grid_badges?: VideoGridStatusBadge[];
203
221
  /**
204
222
  * When present, controls whether the UI should show the exclamation indicator for this workstation.
205
223
  * - Flow-configured lines: true only when a WIP alert is active for a buffer that outputs to this workstation.
@@ -477,4 +495,4 @@ interface RecentFlowSnapshotGridProps {
477
495
  }
478
496
  declare const RecentFlowSnapshotGrid: React__default.FC<RecentFlowSnapshotGridProps>;
479
497
 
480
- export { type ActionThreshold as A, type CurrentWorkspaceSKU as C, type DashboardKPIs as D, type EfficiencyLegendUpdate as E, type KpiTrend as K, type LineSignal as L, type MonthlyTrendSummary as M, type PoorPerformingWorkspace as P, type RecentFlowSnapshotGridProps as R, type SkuBreakdownItem as S, type VideoGridMetricMode as V, type WorkspaceMetrics as W, type LineInfo as a, type WorkspaceDetailedMetrics as b, type SkuSegmentItem as c, type LineSkuBreakdownItem as d, type WorkspaceVideoStream as e, type Workspace as f, type WorkspaceCameraIpInfo as g, type WorkspaceActionUpdate as h, type ShiftConfiguration as i, type KpiSignal as j, type LineIssueResolutionSummary as k, type LineDetailedMetrics as l, type LineSignalSource as m, type KpiSignalSource as n, type KpiSignalMode as o, type ValueDelta as p, type PercentageDelta as q, type CountDelta as r, type DurationDelta as s, type WorkspaceCropRect as t, type LineThreshold as u, type ShiftConfigurationRecord as v, normalizeVideoGridMetricMode as w, isRecentFlowVideoGridMetricMode as x, isWipGatedVideoGridMetricMode as y, RecentFlowSnapshotGrid as z };
498
+ export { type ActionThreshold as A, RecentFlowSnapshotGrid as B, type CurrentWorkspaceSKU as C, type DashboardKPIs as D, type EfficiencyLegendUpdate as E, type KpiTrend as K, type LineSignal as L, type MonthlyTrendSummary as M, type PoorPerformingWorkspace as P, type RecentFlowSnapshotGridProps as R, type SkuBreakdownItem as S, type VideoGridMetricMode as V, type WorkspaceMetrics as W, type LineInfo as a, type WorkspaceDetailedMetrics as b, type SkuSegmentItem as c, type LineSkuBreakdownItem as d, type WorkspaceVideoStream as e, type Workspace as f, type WorkspaceCameraIpInfo as g, type WorkspaceActionUpdate as h, type ShiftConfiguration as i, type KpiSignal as j, type LineIssueResolutionSummary as k, type LineDetailedMetrics as l, type VideoGridStatusBadge as m, type LineSignalSource as n, type KpiSignalSource as o, type KpiSignalMode as p, type ValueDelta as q, type PercentageDelta as r, type CountDelta as s, type DurationDelta as t, type WorkspaceCropRect as u, type LineThreshold as v, type ShiftConfigurationRecord as w, normalizeVideoGridMetricMode as x, isRecentFlowVideoGridMetricMode as y, isWipGatedVideoGridMetricMode as z };
@@ -5,6 +5,9 @@ declare const normalizeVideoGridMetricMode: (value: unknown, assemblyEnabled?: b
5
5
  declare const isRecentFlowVideoGridMetricMode: (value: unknown, assemblyEnabled?: boolean) => boolean;
6
6
  declare const isWipGatedVideoGridMetricMode: (value: unknown, assemblyEnabled?: boolean) => boolean;
7
7
 
8
+ type IdleReasonPaletteToken = 'red' | 'amber' | 'blue' | 'violet' | 'emerald' | 'cyan' | 'slate';
9
+ type IdleReasonIconToken = 'alert-triangle' | 'refresh-cw' | 'package' | 'clock' | 'user-x' | 'wrench' | 'activity' | 'clipboard-x' | 'help-circle';
10
+
8
11
  /** One row of per-SKU workspace metrics. Backend computes; frontend renders. */
9
12
  interface SkuBreakdownItem {
10
13
  /** UUID — `skus.id`. Stable identifier for selectors and threshold writes. */
@@ -149,6 +152,18 @@ interface PoorPerformingWorkspace {
149
152
  action_count: number;
150
153
  action_threshold: number;
151
154
  }
155
+ interface VideoGridStatusBadge {
156
+ kind: 'idle_reason' | 'no_plan';
157
+ label: string | null;
158
+ display_name: string;
159
+ palette_token: IdleReasonPaletteToken;
160
+ icon_token: IdleReasonIconToken;
161
+ is_known?: boolean;
162
+ title?: string;
163
+ anchor_minute?: number;
164
+ reason_minute?: number;
165
+ shift_elapsed_fraction?: number;
166
+ }
152
167
  interface WorkspaceMetrics {
153
168
  company_id: string;
154
169
  line_id: string;
@@ -170,6 +185,7 @@ interface WorkspaceMetrics {
170
185
  monitoring_mode?: 'output' | 'uptime';
171
186
  idle_time?: number;
172
187
  idle_time_hourly?: Record<string, any> | null;
188
+ idle_reason_hourly?: Record<string, any> | null;
173
189
  shift_start?: string;
174
190
  shift_end?: string;
175
191
  assembly_enabled?: boolean;
@@ -196,10 +212,12 @@ interface WorkspaceMetrics {
196
212
  video_grid_green_streak_minutes?: number | null;
197
213
  video_grid_green_streak_anchor_at?: string | null;
198
214
  video_grid_green_streak_started_at?: string | null;
215
+ video_grid_green_streak_observed_at?: string | null;
199
216
  scheduled_break_active?: boolean;
200
217
  incoming_wip_current?: number | null;
201
218
  incoming_wip_effective_at?: string | null;
202
219
  incoming_wip_buffer_name?: string | null;
220
+ video_grid_badges?: VideoGridStatusBadge[];
203
221
  /**
204
222
  * When present, controls whether the UI should show the exclamation indicator for this workstation.
205
223
  * - Flow-configured lines: true only when a WIP alert is active for a buffer that outputs to this workstation.
@@ -477,4 +495,4 @@ interface RecentFlowSnapshotGridProps {
477
495
  }
478
496
  declare const RecentFlowSnapshotGrid: React__default.FC<RecentFlowSnapshotGridProps>;
479
497
 
480
- export { type ActionThreshold as A, type CurrentWorkspaceSKU as C, type DashboardKPIs as D, type EfficiencyLegendUpdate as E, type KpiTrend as K, type LineSignal as L, type MonthlyTrendSummary as M, type PoorPerformingWorkspace as P, type RecentFlowSnapshotGridProps as R, type SkuBreakdownItem as S, type VideoGridMetricMode as V, type WorkspaceMetrics as W, type LineInfo as a, type WorkspaceDetailedMetrics as b, type SkuSegmentItem as c, type LineSkuBreakdownItem as d, type WorkspaceVideoStream as e, type Workspace as f, type WorkspaceCameraIpInfo as g, type WorkspaceActionUpdate as h, type ShiftConfiguration as i, type KpiSignal as j, type LineIssueResolutionSummary as k, type LineDetailedMetrics as l, type LineSignalSource as m, type KpiSignalSource as n, type KpiSignalMode as o, type ValueDelta as p, type PercentageDelta as q, type CountDelta as r, type DurationDelta as s, type WorkspaceCropRect as t, type LineThreshold as u, type ShiftConfigurationRecord as v, normalizeVideoGridMetricMode as w, isRecentFlowVideoGridMetricMode as x, isWipGatedVideoGridMetricMode as y, RecentFlowSnapshotGrid as z };
498
+ export { type ActionThreshold as A, RecentFlowSnapshotGrid as B, type CurrentWorkspaceSKU as C, type DashboardKPIs as D, type EfficiencyLegendUpdate as E, type KpiTrend as K, type LineSignal as L, type MonthlyTrendSummary as M, type PoorPerformingWorkspace as P, type RecentFlowSnapshotGridProps as R, type SkuBreakdownItem as S, type VideoGridMetricMode as V, type WorkspaceMetrics as W, type LineInfo as a, type WorkspaceDetailedMetrics as b, type SkuSegmentItem as c, type LineSkuBreakdownItem as d, type WorkspaceVideoStream as e, type Workspace as f, type WorkspaceCameraIpInfo as g, type WorkspaceActionUpdate as h, type ShiftConfiguration as i, type KpiSignal as j, type LineIssueResolutionSummary as k, type LineDetailedMetrics as l, type VideoGridStatusBadge as m, type LineSignalSource as n, type KpiSignalSource as o, type KpiSignalMode as p, type ValueDelta as q, type PercentageDelta as r, type CountDelta as s, type DurationDelta as t, type WorkspaceCropRect as u, type LineThreshold as v, type ShiftConfigurationRecord as w, normalizeVideoGridMetricMode as x, isRecentFlowVideoGridMetricMode as y, isWipGatedVideoGridMetricMode as z };
@@ -1,2 +1,2 @@
1
- export { z as RecentFlowSnapshotGrid, R as RecentFlowSnapshotGridProps, W as WorkspaceMetrics, e as WorkspaceVideoStream } from './automation-DYa3jNEf.mjs';
1
+ export { B as RecentFlowSnapshotGrid, R as RecentFlowSnapshotGridProps, W as WorkspaceMetrics, e as WorkspaceVideoStream } from './automation-ZIumB5W9.mjs';
2
2
  import 'react';
@@ -1,2 +1,2 @@
1
- export { z as RecentFlowSnapshotGrid, R as RecentFlowSnapshotGridProps, W as WorkspaceMetrics, e as WorkspaceVideoStream } from './automation-DYa3jNEf.js';
1
+ export { B as RecentFlowSnapshotGrid, R as RecentFlowSnapshotGridProps, W as WorkspaceMetrics, e as WorkspaceVideoStream } from './automation-ZIumB5W9.js';
2
2
  import 'react';
@@ -1734,6 +1734,99 @@ var trackCoreEvent = (eventName, properties) => {
1734
1734
  return;
1735
1735
  }
1736
1736
  };
1737
+ var DEFAULT_PALETTE_TOKEN = "slate";
1738
+ var DEFAULT_ICON_TOKEN = "help-circle";
1739
+ var PALETTE_CONFIG = {
1740
+ red: {
1741
+ hex: "#dc2626",
1742
+ textClass: "text-red-600",
1743
+ bgClass: "bg-red-50",
1744
+ borderClass: "border-red-200"
1745
+ },
1746
+ amber: {
1747
+ hex: "#f59e0b",
1748
+ textClass: "text-amber-600",
1749
+ bgClass: "bg-amber-50",
1750
+ borderClass: "border-amber-200"
1751
+ },
1752
+ blue: {
1753
+ hex: "#3b82f6",
1754
+ textClass: "text-blue-600",
1755
+ bgClass: "bg-blue-50",
1756
+ borderClass: "border-blue-200"
1757
+ },
1758
+ violet: {
1759
+ hex: "#8b5cf6",
1760
+ textClass: "text-violet-600",
1761
+ bgClass: "bg-violet-50",
1762
+ borderClass: "border-violet-200"
1763
+ },
1764
+ emerald: {
1765
+ hex: "#10b981",
1766
+ textClass: "text-emerald-600",
1767
+ bgClass: "bg-emerald-50",
1768
+ borderClass: "border-emerald-200"
1769
+ },
1770
+ cyan: {
1771
+ hex: "#0891b2",
1772
+ textClass: "text-cyan-600",
1773
+ bgClass: "bg-cyan-50",
1774
+ borderClass: "border-cyan-200"
1775
+ },
1776
+ slate: {
1777
+ hex: "#64748b",
1778
+ textClass: "text-slate-600",
1779
+ bgClass: "bg-slate-50",
1780
+ borderClass: "border-slate-200"
1781
+ }
1782
+ };
1783
+ var ICON_CONFIG = {
1784
+ "alert-triangle": lucideReact.AlertTriangle,
1785
+ "refresh-cw": lucideReact.RefreshCw,
1786
+ package: lucideReact.Package,
1787
+ clock: lucideReact.Clock,
1788
+ "user-x": lucideReact.UserX,
1789
+ wrench: lucideReact.Wrench,
1790
+ activity: lucideReact.Activity,
1791
+ "clipboard-x": lucideReact.ClipboardX,
1792
+ "help-circle": lucideReact.HelpCircle
1793
+ };
1794
+ var humanizeIdleReasonLabel = (value) => {
1795
+ const text = String(value || "").trim();
1796
+ if (!text) {
1797
+ return "Unknown";
1798
+ }
1799
+ return text.replace(/_/g, " ").split(/\s+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ");
1800
+ };
1801
+ var normalizePaletteToken = (value) => {
1802
+ const token = String(value || "").trim().toLowerCase();
1803
+ if (token in PALETTE_CONFIG) {
1804
+ return token;
1805
+ }
1806
+ return DEFAULT_PALETTE_TOKEN;
1807
+ };
1808
+ var normalizeIconToken = (value) => {
1809
+ const token = String(value || "").trim().toLowerCase();
1810
+ if (token in ICON_CONFIG) {
1811
+ return token;
1812
+ }
1813
+ return DEFAULT_ICON_TOKEN;
1814
+ };
1815
+ var getIdleReasonPresentation = (metadata) => {
1816
+ const label = typeof metadata?.label === "string" && metadata.label.trim() ? metadata.label.trim() : null;
1817
+ const displayName = typeof metadata?.displayName === "string" && metadata.displayName.trim() ? metadata.displayName.trim() : humanizeIdleReasonLabel(label);
1818
+ const paletteToken = normalizePaletteToken(metadata?.paletteToken);
1819
+ const iconToken = normalizeIconToken(metadata?.iconToken);
1820
+ return {
1821
+ label,
1822
+ displayName,
1823
+ paletteToken,
1824
+ iconToken,
1825
+ isKnown: Boolean(metadata?.isKnown),
1826
+ Icon: ICON_CONFIG[iconToken],
1827
+ ...PALETTE_CONFIG[paletteToken]
1828
+ };
1829
+ };
1737
1830
 
1738
1831
  // src/lib/constants/videoGridMetricMode.ts
1739
1832
  var VALID_VIDEO_GRID_METRIC_MODES = /* @__PURE__ */ new Set([
@@ -1875,6 +1968,52 @@ var getVideoGridColorState = (workspace, legend = DEFAULT_EFFICIENCY_LEGEND, blu
1875
1968
  }
1876
1969
  return baseColor;
1877
1970
  };
1971
+ var getStatusBadgeSignature = (workspace) => {
1972
+ const badges = workspace.video_grid_badges || [];
1973
+ return badges.map((badge) => [
1974
+ badge.kind,
1975
+ badge.label,
1976
+ badge.display_name,
1977
+ badge.palette_token,
1978
+ badge.icon_token,
1979
+ badge.anchor_minute,
1980
+ badge.reason_minute,
1981
+ badge.shift_elapsed_fraction,
1982
+ badge.title
1983
+ ].join(":")).join("|");
1984
+ };
1985
+ var VALID_STATUS_BADGE_ICON_TOKENS = /* @__PURE__ */ new Set([
1986
+ "alert-triangle",
1987
+ "refresh-cw",
1988
+ "package",
1989
+ "clock",
1990
+ "user-x",
1991
+ "wrench",
1992
+ "activity",
1993
+ "clipboard-x"
1994
+ ]);
1995
+ var normalizeStatusBadgeToken = (value) => String(value || "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
1996
+ var resolveVideoGridStatusBadgeIconToken = (badge) => {
1997
+ if (badge.kind === "no_plan") {
1998
+ return "clipboard-x";
1999
+ }
2000
+ const reasonTokens = /* @__PURE__ */ new Set([
2001
+ normalizeStatusBadgeToken(badge.label),
2002
+ normalizeStatusBadgeToken(badge.display_name),
2003
+ normalizeStatusBadgeToken(badge.title)
2004
+ ]);
2005
+ if (reasonTokens.has("no_material")) {
2006
+ return "package";
2007
+ }
2008
+ if (reasonTokens.has("machine_maintenance") || reasonTokens.has("machine_downtime")) {
2009
+ return "wrench";
2010
+ }
2011
+ const iconToken = normalizeStatusBadgeToken(badge.icon_token);
2012
+ if (VALID_STATUS_BADGE_ICON_TOKENS.has(iconToken)) {
2013
+ return iconToken;
2014
+ }
2015
+ return "activity";
2016
+ };
1878
2017
  function getTrendArrowAndColor(trend) {
1879
2018
  if (trend > 0) {
1880
2019
  return { arrow: "\u2191", color: "text-green-400" };
@@ -1906,6 +2045,7 @@ var VideoCard = React__default.default.memo(({
1906
2045
  }) => {
1907
2046
  const videoRef = React.useRef(null);
1908
2047
  const canvasRef = React.useRef(null);
2048
+ const statusBadgeIdPrefix = React.useId();
1909
2049
  const effectiveLegend = legend || DEFAULT_EFFICIENCY_LEGEND;
1910
2050
  const { isStale: isStreamStale } = useHlsStreamWithCropping(videoRef, canvasRef, {
1911
2051
  src: hlsUrl,
@@ -1932,6 +2072,7 @@ var VideoCard = React__default.default.memo(({
1932
2072
  const shouldRenderMetricBadge = hasDisplayMetric;
1933
2073
  const badgeTitle = isHighEfficiencyOverride ? `Efficiency ${Math.round(videoGridDisplayValue ?? 0)}%` : hasVideoGridRecentFlow(workspace) ? `Flow ${Math.round(videoGridDisplayValue ?? 0)}%` : isRecentFlowCard ? "Flow unavailable" : `Efficiency ${Math.round(videoGridDisplayValue ?? 0)}%`;
1934
2074
  const badgeLabel = `${Math.round(videoGridDisplayValue ?? 0)}%`;
2075
+ const statusBadges = workspace.video_grid_badges || [];
1935
2076
  const efficiencyOverlayClass = videoGridColorState === "green" ? "bg-[#00D654]/25" : videoGridColorState === "blue" ? "bg-[#0EA5E9]/30" : videoGridColorState === "yellow" ? "bg-[#FFD700]/30" : videoGridColorState === "red" ? "bg-[#FF2D0A]/30" : "bg-transparent";
1936
2077
  const efficiencyBarClass = videoGridColorState === "green" ? "bg-[#00AB45]" : videoGridColorState === "blue" ? "bg-[#0EA5E9]" : videoGridColorState === "yellow" ? "bg-[#FFB020]" : videoGridColorState === "red" ? "bg-[#E34329]" : "bg-gray-500/70";
1937
2078
  const efficiencyStatus = videoGridColorState === "green" ? "High" : videoGridColorState === "blue" ? "Best" : videoGridColorState === "yellow" ? "Medium" : videoGridColorState === "red" ? "Low" : "Neutral";
@@ -2010,6 +2151,61 @@ var VideoCard = React__default.default.memo(({
2010
2151
  children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: `${compact ? "text-[10px]" : "text-xs"} font-semibold`, children: badgeLabel })
2011
2152
  }
2012
2153
  ) }),
2154
+ statusBadges.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
2155
+ "div",
2156
+ {
2157
+ "data-testid": "video-card-status-badges",
2158
+ className: `absolute ${compact ? "top-1.5 left-1.5 gap-1" : "top-2 left-2 gap-1.5"} z-30 flex items-center`,
2159
+ children: statusBadges.map((badge, index) => {
2160
+ const presentation = getIdleReasonPresentation({
2161
+ label: badge.label,
2162
+ displayName: badge.display_name,
2163
+ paletteToken: badge.palette_token,
2164
+ iconToken: resolveVideoGridStatusBadgeIconToken(badge),
2165
+ isKnown: badge.is_known
2166
+ });
2167
+ const Icon = presentation.Icon;
2168
+ const tooltipText = presentation.displayName;
2169
+ const tooltipId = `video-card-status-tooltip-${statusBadgeIdPrefix}-${badge.kind}-${index}`;
2170
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2171
+ "div",
2172
+ {
2173
+ "data-testid": `video-card-status-badge-${badge.kind}`,
2174
+ "aria-label": tooltipText,
2175
+ "aria-describedby": tooltipId,
2176
+ className: `group relative inline-flex shrink-0 items-center justify-center rounded-full bg-slate-950/70 border-2 shadow-[0_3px_10px_rgba(0,0,0,0.34),inset_0_0_0_1px_rgba(255,255,255,0.18)] ${compact ? "h-10 w-10" : "h-11 w-11"}`,
2177
+ style: {
2178
+ borderColor: presentation.hex
2179
+ },
2180
+ children: [
2181
+ /* @__PURE__ */ jsxRuntime.jsx(
2182
+ Icon,
2183
+ {
2184
+ "aria-hidden": "true",
2185
+ className: `${compact ? "h-5 w-5" : "h-6 w-6"} text-white`,
2186
+ strokeWidth: 2.4
2187
+ }
2188
+ ),
2189
+ /* @__PURE__ */ jsxRuntime.jsxs(
2190
+ "span",
2191
+ {
2192
+ id: tooltipId,
2193
+ role: "tooltip",
2194
+ "data-testid": `video-card-status-tooltip-${badge.kind}`,
2195
+ className: "pointer-events-none absolute left-0 top-full mt-2 whitespace-nowrap rounded-md border border-white/10 bg-slate-950/95 px-2 py-1 text-[11px] font-semibold leading-none text-white opacity-0 shadow-[0_6px_18px_rgba(0,0,0,0.34)] transition-opacity duration-150 group-hover:opacity-100",
2196
+ children: [
2197
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "absolute -top-1 left-3 h-2 w-2 rotate-45 border-l border-t border-white/10 bg-slate-950/95" }),
2198
+ tooltipText
2199
+ ]
2200
+ }
2201
+ )
2202
+ ]
2203
+ },
2204
+ `${badge.kind}-${badge.label || index}-${badge.reason_minute ?? index}`
2205
+ );
2206
+ })
2207
+ }
2208
+ ),
2013
2209
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: `absolute bottom-0 left-0 right-0 ${compact ? "h-0.5" : "h-1"} bg-black/50 z-30`, children: /* @__PURE__ */ jsxRuntime.jsx(
2014
2210
  "div",
2015
2211
  {
@@ -2046,7 +2242,7 @@ var VideoCard = React__default.default.memo(({
2046
2242
  }
2047
2243
  );
2048
2244
  }, (prevProps, nextProps) => {
2049
- 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) {
2245
+ 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 || getStatusBadgeSignature(prevProps.workspace) !== getStatusBadgeSignature(nextProps.workspace) || prevProps.workspace.trend !== nextProps.workspace.trend || prevProps.workspace.performance_score !== nextProps.workspace.performance_score || prevProps.workspace.pph !== nextProps.workspace.pph) {
2050
2246
  return false;
2051
2247
  }
2052
2248
  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) {
@@ -1,5 +1,5 @@
1
- import React, { useRef, useCallback, useEffect, useState, useMemo } from 'react';
2
- import { Camera, AlertTriangle } from 'lucide-react';
1
+ import React, { useRef, useId, useCallback, useEffect, useState, useMemo } from 'react';
2
+ import { Camera, AlertTriangle, HelpCircle, ClipboardX, Activity, Wrench, UserX, Clock, Package, RefreshCw } from 'lucide-react';
3
3
  import Hls from 'hls.js';
4
4
  import 'mixpanel-browser';
5
5
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
@@ -1727,6 +1727,99 @@ var trackCoreEvent = (eventName, properties) => {
1727
1727
  return;
1728
1728
  }
1729
1729
  };
1730
+ var DEFAULT_PALETTE_TOKEN = "slate";
1731
+ var DEFAULT_ICON_TOKEN = "help-circle";
1732
+ var PALETTE_CONFIG = {
1733
+ red: {
1734
+ hex: "#dc2626",
1735
+ textClass: "text-red-600",
1736
+ bgClass: "bg-red-50",
1737
+ borderClass: "border-red-200"
1738
+ },
1739
+ amber: {
1740
+ hex: "#f59e0b",
1741
+ textClass: "text-amber-600",
1742
+ bgClass: "bg-amber-50",
1743
+ borderClass: "border-amber-200"
1744
+ },
1745
+ blue: {
1746
+ hex: "#3b82f6",
1747
+ textClass: "text-blue-600",
1748
+ bgClass: "bg-blue-50",
1749
+ borderClass: "border-blue-200"
1750
+ },
1751
+ violet: {
1752
+ hex: "#8b5cf6",
1753
+ textClass: "text-violet-600",
1754
+ bgClass: "bg-violet-50",
1755
+ borderClass: "border-violet-200"
1756
+ },
1757
+ emerald: {
1758
+ hex: "#10b981",
1759
+ textClass: "text-emerald-600",
1760
+ bgClass: "bg-emerald-50",
1761
+ borderClass: "border-emerald-200"
1762
+ },
1763
+ cyan: {
1764
+ hex: "#0891b2",
1765
+ textClass: "text-cyan-600",
1766
+ bgClass: "bg-cyan-50",
1767
+ borderClass: "border-cyan-200"
1768
+ },
1769
+ slate: {
1770
+ hex: "#64748b",
1771
+ textClass: "text-slate-600",
1772
+ bgClass: "bg-slate-50",
1773
+ borderClass: "border-slate-200"
1774
+ }
1775
+ };
1776
+ var ICON_CONFIG = {
1777
+ "alert-triangle": AlertTriangle,
1778
+ "refresh-cw": RefreshCw,
1779
+ package: Package,
1780
+ clock: Clock,
1781
+ "user-x": UserX,
1782
+ wrench: Wrench,
1783
+ activity: Activity,
1784
+ "clipboard-x": ClipboardX,
1785
+ "help-circle": HelpCircle
1786
+ };
1787
+ var humanizeIdleReasonLabel = (value) => {
1788
+ const text = String(value || "").trim();
1789
+ if (!text) {
1790
+ return "Unknown";
1791
+ }
1792
+ return text.replace(/_/g, " ").split(/\s+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ");
1793
+ };
1794
+ var normalizePaletteToken = (value) => {
1795
+ const token = String(value || "").trim().toLowerCase();
1796
+ if (token in PALETTE_CONFIG) {
1797
+ return token;
1798
+ }
1799
+ return DEFAULT_PALETTE_TOKEN;
1800
+ };
1801
+ var normalizeIconToken = (value) => {
1802
+ const token = String(value || "").trim().toLowerCase();
1803
+ if (token in ICON_CONFIG) {
1804
+ return token;
1805
+ }
1806
+ return DEFAULT_ICON_TOKEN;
1807
+ };
1808
+ var getIdleReasonPresentation = (metadata) => {
1809
+ const label = typeof metadata?.label === "string" && metadata.label.trim() ? metadata.label.trim() : null;
1810
+ const displayName = typeof metadata?.displayName === "string" && metadata.displayName.trim() ? metadata.displayName.trim() : humanizeIdleReasonLabel(label);
1811
+ const paletteToken = normalizePaletteToken(metadata?.paletteToken);
1812
+ const iconToken = normalizeIconToken(metadata?.iconToken);
1813
+ return {
1814
+ label,
1815
+ displayName,
1816
+ paletteToken,
1817
+ iconToken,
1818
+ isKnown: Boolean(metadata?.isKnown),
1819
+ Icon: ICON_CONFIG[iconToken],
1820
+ ...PALETTE_CONFIG[paletteToken]
1821
+ };
1822
+ };
1730
1823
 
1731
1824
  // src/lib/constants/videoGridMetricMode.ts
1732
1825
  var VALID_VIDEO_GRID_METRIC_MODES = /* @__PURE__ */ new Set([
@@ -1868,6 +1961,52 @@ var getVideoGridColorState = (workspace, legend = DEFAULT_EFFICIENCY_LEGEND, blu
1868
1961
  }
1869
1962
  return baseColor;
1870
1963
  };
1964
+ var getStatusBadgeSignature = (workspace) => {
1965
+ const badges = workspace.video_grid_badges || [];
1966
+ return badges.map((badge) => [
1967
+ badge.kind,
1968
+ badge.label,
1969
+ badge.display_name,
1970
+ badge.palette_token,
1971
+ badge.icon_token,
1972
+ badge.anchor_minute,
1973
+ badge.reason_minute,
1974
+ badge.shift_elapsed_fraction,
1975
+ badge.title
1976
+ ].join(":")).join("|");
1977
+ };
1978
+ var VALID_STATUS_BADGE_ICON_TOKENS = /* @__PURE__ */ new Set([
1979
+ "alert-triangle",
1980
+ "refresh-cw",
1981
+ "package",
1982
+ "clock",
1983
+ "user-x",
1984
+ "wrench",
1985
+ "activity",
1986
+ "clipboard-x"
1987
+ ]);
1988
+ var normalizeStatusBadgeToken = (value) => String(value || "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
1989
+ var resolveVideoGridStatusBadgeIconToken = (badge) => {
1990
+ if (badge.kind === "no_plan") {
1991
+ return "clipboard-x";
1992
+ }
1993
+ const reasonTokens = /* @__PURE__ */ new Set([
1994
+ normalizeStatusBadgeToken(badge.label),
1995
+ normalizeStatusBadgeToken(badge.display_name),
1996
+ normalizeStatusBadgeToken(badge.title)
1997
+ ]);
1998
+ if (reasonTokens.has("no_material")) {
1999
+ return "package";
2000
+ }
2001
+ if (reasonTokens.has("machine_maintenance") || reasonTokens.has("machine_downtime")) {
2002
+ return "wrench";
2003
+ }
2004
+ const iconToken = normalizeStatusBadgeToken(badge.icon_token);
2005
+ if (VALID_STATUS_BADGE_ICON_TOKENS.has(iconToken)) {
2006
+ return iconToken;
2007
+ }
2008
+ return "activity";
2009
+ };
1871
2010
  function getTrendArrowAndColor(trend) {
1872
2011
  if (trend > 0) {
1873
2012
  return { arrow: "\u2191", color: "text-green-400" };
@@ -1899,6 +2038,7 @@ var VideoCard = React.memo(({
1899
2038
  }) => {
1900
2039
  const videoRef = useRef(null);
1901
2040
  const canvasRef = useRef(null);
2041
+ const statusBadgeIdPrefix = useId();
1902
2042
  const effectiveLegend = legend || DEFAULT_EFFICIENCY_LEGEND;
1903
2043
  const { isStale: isStreamStale } = useHlsStreamWithCropping(videoRef, canvasRef, {
1904
2044
  src: hlsUrl,
@@ -1925,6 +2065,7 @@ var VideoCard = React.memo(({
1925
2065
  const shouldRenderMetricBadge = hasDisplayMetric;
1926
2066
  const badgeTitle = isHighEfficiencyOverride ? `Efficiency ${Math.round(videoGridDisplayValue ?? 0)}%` : hasVideoGridRecentFlow(workspace) ? `Flow ${Math.round(videoGridDisplayValue ?? 0)}%` : isRecentFlowCard ? "Flow unavailable" : `Efficiency ${Math.round(videoGridDisplayValue ?? 0)}%`;
1927
2067
  const badgeLabel = `${Math.round(videoGridDisplayValue ?? 0)}%`;
2068
+ const statusBadges = workspace.video_grid_badges || [];
1928
2069
  const efficiencyOverlayClass = videoGridColorState === "green" ? "bg-[#00D654]/25" : videoGridColorState === "blue" ? "bg-[#0EA5E9]/30" : videoGridColorState === "yellow" ? "bg-[#FFD700]/30" : videoGridColorState === "red" ? "bg-[#FF2D0A]/30" : "bg-transparent";
1929
2070
  const efficiencyBarClass = videoGridColorState === "green" ? "bg-[#00AB45]" : videoGridColorState === "blue" ? "bg-[#0EA5E9]" : videoGridColorState === "yellow" ? "bg-[#FFB020]" : videoGridColorState === "red" ? "bg-[#E34329]" : "bg-gray-500/70";
1930
2071
  const efficiencyStatus = videoGridColorState === "green" ? "High" : videoGridColorState === "blue" ? "Best" : videoGridColorState === "yellow" ? "Medium" : videoGridColorState === "red" ? "Low" : "Neutral";
@@ -2003,6 +2144,61 @@ var VideoCard = React.memo(({
2003
2144
  children: /* @__PURE__ */ jsx("span", { className: `${compact ? "text-[10px]" : "text-xs"} font-semibold`, children: badgeLabel })
2004
2145
  }
2005
2146
  ) }),
2147
+ statusBadges.length > 0 && /* @__PURE__ */ jsx(
2148
+ "div",
2149
+ {
2150
+ "data-testid": "video-card-status-badges",
2151
+ className: `absolute ${compact ? "top-1.5 left-1.5 gap-1" : "top-2 left-2 gap-1.5"} z-30 flex items-center`,
2152
+ children: statusBadges.map((badge, index) => {
2153
+ const presentation = getIdleReasonPresentation({
2154
+ label: badge.label,
2155
+ displayName: badge.display_name,
2156
+ paletteToken: badge.palette_token,
2157
+ iconToken: resolveVideoGridStatusBadgeIconToken(badge),
2158
+ isKnown: badge.is_known
2159
+ });
2160
+ const Icon = presentation.Icon;
2161
+ const tooltipText = presentation.displayName;
2162
+ const tooltipId = `video-card-status-tooltip-${statusBadgeIdPrefix}-${badge.kind}-${index}`;
2163
+ return /* @__PURE__ */ jsxs(
2164
+ "div",
2165
+ {
2166
+ "data-testid": `video-card-status-badge-${badge.kind}`,
2167
+ "aria-label": tooltipText,
2168
+ "aria-describedby": tooltipId,
2169
+ className: `group relative inline-flex shrink-0 items-center justify-center rounded-full bg-slate-950/70 border-2 shadow-[0_3px_10px_rgba(0,0,0,0.34),inset_0_0_0_1px_rgba(255,255,255,0.18)] ${compact ? "h-10 w-10" : "h-11 w-11"}`,
2170
+ style: {
2171
+ borderColor: presentation.hex
2172
+ },
2173
+ children: [
2174
+ /* @__PURE__ */ jsx(
2175
+ Icon,
2176
+ {
2177
+ "aria-hidden": "true",
2178
+ className: `${compact ? "h-5 w-5" : "h-6 w-6"} text-white`,
2179
+ strokeWidth: 2.4
2180
+ }
2181
+ ),
2182
+ /* @__PURE__ */ jsxs(
2183
+ "span",
2184
+ {
2185
+ id: tooltipId,
2186
+ role: "tooltip",
2187
+ "data-testid": `video-card-status-tooltip-${badge.kind}`,
2188
+ className: "pointer-events-none absolute left-0 top-full mt-2 whitespace-nowrap rounded-md border border-white/10 bg-slate-950/95 px-2 py-1 text-[11px] font-semibold leading-none text-white opacity-0 shadow-[0_6px_18px_rgba(0,0,0,0.34)] transition-opacity duration-150 group-hover:opacity-100",
2189
+ children: [
2190
+ /* @__PURE__ */ jsx("span", { className: "absolute -top-1 left-3 h-2 w-2 rotate-45 border-l border-t border-white/10 bg-slate-950/95" }),
2191
+ tooltipText
2192
+ ]
2193
+ }
2194
+ )
2195
+ ]
2196
+ },
2197
+ `${badge.kind}-${badge.label || index}-${badge.reason_minute ?? index}`
2198
+ );
2199
+ })
2200
+ }
2201
+ ),
2006
2202
  /* @__PURE__ */ jsx("div", { className: `absolute bottom-0 left-0 right-0 ${compact ? "h-0.5" : "h-1"} bg-black/50 z-30`, children: /* @__PURE__ */ jsx(
2007
2203
  "div",
2008
2204
  {
@@ -2039,7 +2235,7 @@ var VideoCard = React.memo(({
2039
2235
  }
2040
2236
  );
2041
2237
  }, (prevProps, nextProps) => {
2042
- 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) {
2238
+ 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 || getStatusBadgeSignature(prevProps.workspace) !== getStatusBadgeSignature(nextProps.workspace) || prevProps.workspace.trend !== nextProps.workspace.trend || prevProps.workspace.performance_score !== nextProps.workspace.performance_score || prevProps.workspace.pph !== nextProps.workspace.pph) {
2043
2239
  return false;
2044
2240
  }
2045
2241
  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) {