@optifye/dashboard-core 6.12.15 → 6.12.17

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
@@ -16848,6 +16848,7 @@ function useHlsStream(videoRef, { src, shouldPlay, onFatalError, hlsConfig }) {
16848
16848
  return url.includes(proxyBaseUrl);
16849
16849
  }
16850
16850
  };
16851
+ const isSnapshotStreamUrl = (url) => url.includes("/api/automation/snapshot/stream/");
16851
16852
  const getR2CameraUuid = (url) => {
16852
16853
  try {
16853
16854
  const parsed = new URL(url);
@@ -17436,11 +17437,16 @@ function useHlsStream(videoRef, { src, shouldPlay, onFatalError, hlsConfig }) {
17436
17437
  return;
17437
17438
  }
17438
17439
  if (Hls__default.default.isSupported() && !isNativeHlsRef.current) {
17440
+ const usesSnapshotStream = isSnapshotStreamUrl(resolvedHlsSrc);
17439
17441
  const mergedConfig = {
17440
17442
  ...HLS_CONFIG,
17441
17443
  ...hlsConfig,
17444
+ enableWorker: usesSnapshotStream ? false : hlsConfig?.enableWorker ?? HLS_CONFIG.enableWorker,
17442
17445
  xhrSetup: (xhr, url) => {
17443
17446
  const usesProxy = isProxyUrl(url);
17447
+ if (isSnapshotStreamUrl(url)) {
17448
+ xhr.withCredentials = true;
17449
+ }
17444
17450
  if (isR2WorkerUrl(url, r2WorkerDomain) || usesProxy) {
17445
17451
  if (isManifestUrl(url)) {
17446
17452
  xhr.open("GET", buildCacheBustedUrl(url), true);
@@ -17454,7 +17460,11 @@ function useHlsStream(videoRef, { src, shouldPlay, onFatalError, hlsConfig }) {
17454
17460
  const isR2Url = isR2WorkerUrl(context.url, r2WorkerDomain);
17455
17461
  const isManifestRequest = isManifestUrl(context.url);
17456
17462
  const usesProxy = isProxyUrl(context.url);
17463
+ const isSnapshotStream = isSnapshotStreamUrl(context.url);
17457
17464
  let requestUrl = context.url;
17465
+ if (isSnapshotStream) {
17466
+ initParams.credentials = "include";
17467
+ }
17458
17468
  if (isR2Url || usesProxy) {
17459
17469
  if (authToken) {
17460
17470
  initParams.headers = {
@@ -37594,6 +37604,7 @@ var VideoCard = React144__namespace.default.memo(({
37594
37604
  displayMinuteBucket,
37595
37605
  displayName,
37596
37606
  lastSeenLabel,
37607
+ hasRecentHealthSignal: hasRecentHealthSignal2 = false,
37597
37608
  onMouseEnter,
37598
37609
  onMouseLeave
37599
37610
  }) => {
@@ -37608,7 +37619,7 @@ var VideoCard = React144__namespace.default.memo(({
37608
37619
  useRAF,
37609
37620
  onFatalError: onFatalError ?? (() => throttledReloadDashboard())
37610
37621
  });
37611
- const showOffline = Boolean(isStreamStale);
37622
+ const showOffline = Boolean(isStreamStale && !hasRecentHealthSignal2);
37612
37623
  const lastSeenText = lastSeenLabel || "Unknown";
37613
37624
  const workspaceDisplayName = displayName || workspace.displayName || workspace.workspace_name;
37614
37625
  const videoGridMetricValue = getVideoGridMetricValue(workspace, effectiveLegend);
@@ -37748,6 +37759,9 @@ var VideoCard = React144__namespace.default.memo(({
37748
37759
  if (prevProps.lastSeenLabel !== nextProps.lastSeenLabel) {
37749
37760
  return false;
37750
37761
  }
37762
+ if (prevProps.hasRecentHealthSignal !== nextProps.hasRecentHealthSignal) {
37763
+ return false;
37764
+ }
37751
37765
  if (prevProps.legend !== nextProps.legend) {
37752
37766
  return false;
37753
37767
  }
@@ -37770,10 +37784,17 @@ var DEFAULT_HLS_URL = "https://192.168.5.9:8443/cam1.m3u8";
37770
37784
  var DEBUG_DASHBOARD_LOGS2 = process.env.NEXT_PUBLIC_DEBUG_DASHBOARD === "true";
37771
37785
  var MOBILE_SCROLL_THRESHOLD = 15;
37772
37786
  var MOBILE_BREAKPOINT_PX = 640;
37787
+ var RECENT_HEALTH_SIGNAL_MS = 3 * 60 * 1e3;
37773
37788
  var logDebug2 = (...args) => {
37774
37789
  if (!DEBUG_DASHBOARD_LOGS2) return;
37775
37790
  console.log(...args);
37776
37791
  };
37792
+ var hasRecentHealthSignal = (lastHeartbeat) => {
37793
+ if (!lastHeartbeat) return false;
37794
+ const heartbeatMs = new Date(lastHeartbeat).getTime();
37795
+ if (!Number.isFinite(heartbeatMs)) return false;
37796
+ return Date.now() - heartbeatMs <= RECENT_HEALTH_SIGNAL_MS;
37797
+ };
37777
37798
  var VideoGridView = React144__namespace.default.memo(({
37778
37799
  workspaces,
37779
37800
  selectedLine,
@@ -38085,7 +38106,8 @@ var VideoGridView = React144__namespace.default.memo(({
38085
38106
  const isVisible = visibleWorkspaces.has(workspaceId);
38086
38107
  const workspaceCropping = getWorkspaceCropping(workspaceId, workspace.workspace_name);
38087
38108
  const workspaceStream = videoStreamsByWorkspaceId?.[workspaceId];
38088
- const lastSeenLabel = workspace.workspace_uuid ? lastSeenByWorkspaceId[workspace.workspace_uuid]?.timeSinceLastUpdate : void 0;
38109
+ const workspaceHealth = workspace.workspace_uuid ? lastSeenByWorkspaceId[workspace.workspace_uuid] : void 0;
38110
+ const lastSeenLabel = workspaceHealth?.timeSinceLastUpdate;
38089
38111
  const r2Url = workspaceStream?.hls_url;
38090
38112
  const fallbackUrl = getWorkspaceHlsUrl(workspace.workspace_name, workspace.line_id);
38091
38113
  const hasR2Stream = Boolean(r2Url);
@@ -38104,7 +38126,8 @@ var VideoGridView = React144__namespace.default.memo(({
38104
38126
  hlsUrl,
38105
38127
  isR2Stream,
38106
38128
  shouldPlay,
38107
- lastSeenLabel
38129
+ lastSeenLabel,
38130
+ hasRecentHealthSignal: hasRecentHealthSignal(workspaceHealth?.lastHeartbeat)
38108
38131
  };
38109
38132
  });
38110
38133
  }, [
@@ -38151,6 +38174,7 @@ var VideoGridView = React144__namespace.default.memo(({
38151
38174
  canvasFps: effectiveCanvasFps,
38152
38175
  displayName: resolveWorkspaceDisplayName(card.workspace),
38153
38176
  lastSeenLabel: card.lastSeenLabel,
38177
+ hasRecentHealthSignal: card.hasRecentHealthSignal,
38154
38178
  useRAF: effectiveUseRAF,
38155
38179
  displayMinuteBucket,
38156
38180
  compact: !selectedLine,
@@ -85847,6 +85871,246 @@ var streamProxyConfig = {
85847
85871
  responseLimit: false
85848
85872
  }
85849
85873
  };
85874
+ var MOBILE_SCROLL_THRESHOLD2 = 15;
85875
+ var MOBILE_BREAKPOINT_PX2 = 640;
85876
+ var sortWorkspaces = (left, right) => {
85877
+ if (left.line_id !== right.line_id) {
85878
+ return left.line_id.localeCompare(right.line_id);
85879
+ }
85880
+ const leftMatch = left.workspace_name.match(/WS(\d+)/);
85881
+ const rightMatch = right.workspace_name.match(/WS(\d+)/);
85882
+ if (leftMatch && rightMatch) {
85883
+ return parseInt(leftMatch[1], 10) - parseInt(rightMatch[1], 10);
85884
+ }
85885
+ return left.workspace_name.localeCompare(right.workspace_name, void 0, { numeric: true });
85886
+ };
85887
+ var RecentFlowSnapshotGrid = ({
85888
+ workspaces,
85889
+ videoStreamsByWorkspaceId,
85890
+ legend,
85891
+ className = ""
85892
+ }) => {
85893
+ const containerRef = React144.useRef(null);
85894
+ const readinessRef = React144.useRef(null);
85895
+ const [gridCols, setGridCols] = React144.useState(4);
85896
+ const [gridRows, setGridRows] = React144.useState(1);
85897
+ const [isMobileScrollableGrid, setIsMobileScrollableGrid] = React144.useState(false);
85898
+ const sortedWorkspaces = React144.useMemo(
85899
+ () => [...workspaces || []].sort(sortWorkspaces),
85900
+ [workspaces]
85901
+ );
85902
+ const effectiveLegend = legend || DEFAULT_EFFICIENCY_LEGEND;
85903
+ const displayMinuteBucket = Math.floor(Date.now() / 6e4);
85904
+ const expectedVideoCount = React144.useMemo(
85905
+ () => sortedWorkspaces.filter((workspace) => {
85906
+ const workspaceId = workspace.workspace_uuid || workspace.workspace_name;
85907
+ return Boolean(workspaceId && videoStreamsByWorkspaceId[workspaceId]?.hls_url);
85908
+ }).length,
85909
+ [sortedWorkspaces, videoStreamsByWorkspaceId]
85910
+ );
85911
+ const publishSnapshotReadiness = React144.useCallback((readyVideoCount) => {
85912
+ const status = {
85913
+ expectedVideoCount,
85914
+ readyVideoCount,
85915
+ status: expectedVideoCount === 0 ? "no_videos" : readyVideoCount >= expectedVideoCount ? "ready" : "loading",
85916
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
85917
+ };
85918
+ const isReady = expectedVideoCount > 0 && status.status === "ready";
85919
+ if (typeof window !== "undefined") {
85920
+ window.__OPTIFYE_SNAPSHOT_READY__ = isReady;
85921
+ window.__OPTIFYE_SNAPSHOT_VIDEO_STATUS__ = status;
85922
+ }
85923
+ const marker = readinessRef.current;
85924
+ if (marker) {
85925
+ marker.dataset.ready = isReady ? "true" : "false";
85926
+ marker.dataset.status = status.status;
85927
+ marker.dataset.expectedVideoCount = String(status.expectedVideoCount);
85928
+ marker.dataset.readyVideoCount = String(status.readyVideoCount);
85929
+ marker.dataset.updatedAt = status.updatedAt;
85930
+ }
85931
+ }, [expectedVideoCount]);
85932
+ const calculateOptimalGrid = React144.useCallback(() => {
85933
+ if (!containerRef.current) return;
85934
+ const containerPadding = 16;
85935
+ const rawContainerWidth = containerRef.current.clientWidth;
85936
+ const containerWidth = rawContainerWidth - containerPadding;
85937
+ const containerHeight = containerRef.current.clientHeight - containerPadding;
85938
+ const count = sortedWorkspaces.length;
85939
+ if (count === 0) {
85940
+ setGridCols(1);
85941
+ setGridRows(1);
85942
+ setIsMobileScrollableGrid(false);
85943
+ return;
85944
+ }
85945
+ const shouldUseMobileScroll = rawContainerWidth < MOBILE_BREAKPOINT_PX2 && count >= MOBILE_SCROLL_THRESHOLD2;
85946
+ const optimalLayouts = {
85947
+ 1: 1,
85948
+ 2: 2,
85949
+ 3: 3,
85950
+ 4: 2,
85951
+ 5: 3,
85952
+ 6: 3,
85953
+ 7: 4,
85954
+ 8: 4,
85955
+ 9: 3,
85956
+ 10: 5,
85957
+ 11: 4,
85958
+ 12: 4,
85959
+ 13: 5,
85960
+ 14: 5,
85961
+ 15: 5,
85962
+ 16: 4,
85963
+ 17: 6,
85964
+ 18: 6,
85965
+ 19: 5,
85966
+ 20: 5,
85967
+ 21: 7,
85968
+ 22: 6,
85969
+ 23: 6,
85970
+ 24: 6
85971
+ };
85972
+ let bestCols = optimalLayouts[count] || Math.ceil(Math.sqrt(count));
85973
+ const containerAspectRatio = containerWidth / containerHeight;
85974
+ const targetAspectRatio = 16 / 9;
85975
+ const gap = 8;
85976
+ if (containerAspectRatio > targetAspectRatio * 1.5 && count > 6) {
85977
+ bestCols = Math.min(bestCols + 1, Math.ceil(count / 2));
85978
+ }
85979
+ const minCellWidth = 100;
85980
+ const availableWidth = containerWidth - gap * (bestCols - 1);
85981
+ const cellWidth = availableWidth / bestCols;
85982
+ if (cellWidth < minCellWidth && bestCols > 1) {
85983
+ bestCols = Math.max(1, Math.floor((containerWidth + gap) / (minCellWidth + gap)));
85984
+ }
85985
+ setGridCols(bestCols);
85986
+ setGridRows(Math.ceil(count / bestCols));
85987
+ setIsMobileScrollableGrid(shouldUseMobileScroll);
85988
+ }, [sortedWorkspaces.length]);
85989
+ React144.useEffect(() => {
85990
+ calculateOptimalGrid();
85991
+ window.addEventListener("resize", calculateOptimalGrid);
85992
+ return () => window.removeEventListener("resize", calculateOptimalGrid);
85993
+ }, [calculateOptimalGrid]);
85994
+ React144.useEffect(() => {
85995
+ const attachedVideos = /* @__PURE__ */ new Set();
85996
+ const videoEvents = ["loadeddata", "canplay", "playing", "timeupdate", "error", "stalled"];
85997
+ const hasDecodedFrame = (video) => video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && video.videoWidth > 0 && video.videoHeight > 0;
85998
+ const updateReadiness = () => {
85999
+ const videos = Array.from(containerRef.current?.querySelectorAll("video") ?? []);
86000
+ const readyVideoCount = videos.filter(hasDecodedFrame).length;
86001
+ publishSnapshotReadiness(Math.min(readyVideoCount, expectedVideoCount));
86002
+ for (const video of videos) {
86003
+ if (attachedVideos.has(video)) continue;
86004
+ attachedVideos.add(video);
86005
+ for (const eventName of videoEvents) {
86006
+ video.addEventListener(eventName, updateReadiness);
86007
+ }
86008
+ }
86009
+ };
86010
+ publishSnapshotReadiness(0);
86011
+ updateReadiness();
86012
+ const intervalId = window.setInterval(updateReadiness, 250);
86013
+ return () => {
86014
+ window.clearInterval(intervalId);
86015
+ for (const video of attachedVideos) {
86016
+ for (const eventName of videoEvents) {
86017
+ video.removeEventListener(eventName, updateReadiness);
86018
+ }
86019
+ }
86020
+ if (typeof window !== "undefined") {
86021
+ window.__OPTIFYE_SNAPSHOT_READY__ = false;
86022
+ window.__OPTIFYE_SNAPSHOT_VIDEO_STATUS__ = void 0;
86023
+ }
86024
+ };
86025
+ }, [expectedVideoCount, publishSnapshotReadiness]);
86026
+ if (!sortedWorkspaces.length) {
86027
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
86028
+ /* @__PURE__ */ jsxRuntime.jsx(
86029
+ "div",
86030
+ {
86031
+ ref: readinessRef,
86032
+ "data-testid": "snapshot-video-readiness",
86033
+ "data-ready": "false",
86034
+ "data-status": "no_videos",
86035
+ "data-expected-video-count": "0",
86036
+ "data-ready-video-count": "0",
86037
+ hidden: true
86038
+ }
86039
+ ),
86040
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex min-h-[320px] items-center justify-center rounded-md border border-dashed border-slate-300 bg-slate-50 text-sm font-medium text-slate-500", children: "No workstation snapshot available" })
86041
+ ] });
86042
+ }
86043
+ return /* @__PURE__ */ jsxRuntime.jsxs(
86044
+ "div",
86045
+ {
86046
+ "aria-label": "Recent-flow workstation snapshot",
86047
+ className: `relative h-full min-h-0 w-full overflow-hidden bg-slate-50/30 ${className}`,
86048
+ children: [
86049
+ /* @__PURE__ */ jsxRuntime.jsx(
86050
+ "div",
86051
+ {
86052
+ ref: readinessRef,
86053
+ "data-testid": "snapshot-video-readiness",
86054
+ "data-ready": "false",
86055
+ "data-status": expectedVideoCount === 0 ? "no_videos" : "loading",
86056
+ "data-expected-video-count": expectedVideoCount,
86057
+ "data-ready-video-count": "0",
86058
+ hidden: true
86059
+ }
86060
+ ),
86061
+ /* @__PURE__ */ jsxRuntime.jsx(
86062
+ "div",
86063
+ {
86064
+ ref: containerRef,
86065
+ "data-testid": "video-grid-scroll-container",
86066
+ "data-mobile-scrollable": isMobileScrollableGrid ? "true" : "false",
86067
+ className: `absolute inset-0 w-full overflow-x-hidden px-1 py-1 sm:px-2 sm:py-2 ${isMobileScrollableGrid ? "overflow-y-auto" : "overflow-hidden"}`,
86068
+ children: /* @__PURE__ */ jsxRuntime.jsx(
86069
+ "div",
86070
+ {
86071
+ "data-testid": "video-grid-layout",
86072
+ className: `grid min-w-0 w-full gap-1.5 sm:gap-2 ${isMobileScrollableGrid ? "content-start" : "h-full"}`,
86073
+ style: {
86074
+ gridTemplateColumns: `repeat(${gridCols}, minmax(0, 1fr))`,
86075
+ gridTemplateRows: isMobileScrollableGrid ? void 0 : `repeat(${gridRows}, 1fr)`,
86076
+ gridAutoFlow: "row"
86077
+ },
86078
+ children: sortedWorkspaces.map((workspace) => {
86079
+ const workspaceId = workspace.workspace_uuid || workspace.workspace_name;
86080
+ const stream = workspaceId ? videoStreamsByWorkspaceId[workspaceId] : null;
86081
+ const hlsUrl = stream?.hls_url || "";
86082
+ return /* @__PURE__ */ jsxRuntime.jsx(
86083
+ "div",
86084
+ {
86085
+ "data-workspace-id": workspaceId,
86086
+ className: isMobileScrollableGrid ? "workspace-card relative min-w-0 w-full aspect-video min-h-[92px]" : "workspace-card relative min-w-0 w-full h-full",
86087
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0", children: /* @__PURE__ */ jsxRuntime.jsx(
86088
+ VideoCard,
86089
+ {
86090
+ workspace,
86091
+ hlsUrl,
86092
+ shouldPlay: Boolean(hlsUrl),
86093
+ legend: effectiveLegend,
86094
+ cropping: stream?.crop || void 0,
86095
+ canvasFps: 10,
86096
+ useRAF: false,
86097
+ displayMinuteBucket,
86098
+ displayName: workspace.displayName || workspace.workspace_name,
86099
+ compact: true
86100
+ }
86101
+ ) })
86102
+ },
86103
+ `${workspace.line_id}-${workspaceId}`
86104
+ );
86105
+ })
86106
+ }
86107
+ )
86108
+ }
86109
+ )
86110
+ ]
86111
+ }
86112
+ );
86113
+ };
85850
86114
 
85851
86115
  exports.ACTION_FAMILIES = ACTION_FAMILIES;
85852
86116
  exports.ACTION_NAMES = ACTION_NAMES;
@@ -86003,6 +86267,7 @@ exports.PrefetchStatus = PrefetchStatus;
86003
86267
  exports.PrefetchTimeoutError = PrefetchTimeoutError;
86004
86268
  exports.ProfileView = ProfileView_default;
86005
86269
  exports.ROOT_DASHBOARD_EVENT_NAMES = ROOT_DASHBOARD_EVENT_NAMES;
86270
+ exports.RecentFlowSnapshotGrid = RecentFlowSnapshotGrid;
86006
86271
  exports.RegistryProvider = RegistryProvider;
86007
86272
  exports.RoleBadge = RoleBadge;
86008
86273
  exports.S3ClipsService = S3ClipsSupabaseService;