import { toLocalDateKey } from "@/shared/utils/date-time"; import { useEffect, useState } from "react"; import { getWorkbenchProject, type WorkbenchAccess } from "../api/request-context"; import { env } from "@/shared/config/env"; import { type WorkbenchRuntimeSnapshot } from "../api/workbench-runtime-client"; import { fetchRealtimePipeFlows } from "../api/realtime-flow-client"; import { SCHEDULED_CONDITION_REFRESH_INTERVAL_MS, createOperationalScheduledConditions } from "../data/scheduled-conditions"; import { WORKBENCH_SCENARIOS } from "../data/workbench-session"; import type { ScheduledConditionItem } from "../types"; import type { WorkbenchSyncState } from "./use-workbench-sync"; const HEADER_DATA_TIME_STEP_MINUTES = 5; const INITIAL_HEADER_DATA_TIME = "同步中"; const BACKEND_RUNTIME_REFRESH_INTERVAL_MS = 15_000; export function resolveWorkbenchOperationalDate( backendSnapshot: WorkbenchRuntimeSnapshot | null, options: { now?: Date; runtimeMode?: "auto" | "demo" | "history" | "backend"; } = {} ) { const now = options.now ?? new Date(); if ( !["demo", "history"].includes(options.runtimeMode ?? "auto") || !backendSnapshot ) return now; const timestamps = backendSnapshot.workOrders .map((workOrder) => Date.parse(workOrder.scheduledAt)) .filter(Number.isFinite); const historicalTimestamps = timestamps.filter((timestamp) => timestamp <= now.getTime()); const latestTimestamp = historicalTimestamps.length > 0 ? Math.max(...historicalTimestamps) : timestamps.length > 0 ? Math.min(...timestamps) : null; return latestTimestamp === null ? now : new Date(latestTimestamp); } export function formatHeaderDataTime(date: Date, snapToStep = true) { const minutes = snapToStep ? Math.floor(date.getMinutes() / HEADER_DATA_TIME_STEP_MINUTES) * HEADER_DATA_TIME_STEP_MINUTES : date.getMinutes(); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); const hours = String(date.getHours()).padStart(2, "0"); const formattedMinutes = String(minutes).padStart(2, "0"); return `${year}-${month}-${day} ${hours}:${formattedMinutes}`; } export function getMsUntilNextHeaderDataTimeTick(date: Date) { const nextTick = new Date(date); const nextMinutes = Math.floor(date.getMinutes() / HEADER_DATA_TIME_STEP_MINUTES) * HEADER_DATA_TIME_STEP_MINUTES + HEADER_DATA_TIME_STEP_MINUTES; nextTick.setMinutes(nextMinutes, 0, 0); return Math.max(nextTick.getTime() - date.getTime(), 1_000); } export function resolveWorkbenchRuntimeData( mockConditions: ScheduledConditionItem[], backendSnapshot: WorkbenchRuntimeSnapshot | null, options: { operationalDate?: Date; runtimeMode?: "auto" | "demo" | "history" | "backend"; } = {} ) { const operationalDate = options.operationalDate ?? new Date(); const fallbackConditions = mockConditions.filter( (condition) => condition.kind === "condition" && isSameLocalOperationalDate(condition.scheduledAt, operationalDate) ); const backendWorkOrders = (backendSnapshot?.workOrders ?? []).filter( (workOrder) => isSameLocalOperationalDate(workOrder.scheduledAt, operationalDate) ); const backendWorkOrdersConfigured = Boolean( backendSnapshot && backendSnapshot.workOrderAvailability !== "not_configured" ); const resolvedWorkOrders = backendWorkOrdersConfigured ? backendWorkOrders : []; const backendAlertsConfigured = Boolean( backendSnapshot && backendSnapshot.alertAvailability !== "not_configured" ); const useBackendScenarios = Boolean( backendSnapshot && backendSnapshot.scenarioAvailability !== "not_configured" && backendSnapshot.scenarios.length > 0 ); const useBackendArtifact = Boolean( backendSnapshot && backendSnapshot.artifactAvailability !== "not_configured" && backendSnapshot.latestAnalysisArtifact ); return { scheduledConditions: [...fallbackConditions, ...resolvedWorkOrders], workbenchAlerts: backendAlertsConfigured ? backendSnapshot!.alerts : [], alertDataSource: backendAlertsConfigured ? "backend" as const : "none" as const, runtimeDataSource: "mock" as const, scenarios: useBackendScenarios ? backendSnapshot!.scenarios : WORKBENCH_SCENARIOS, scenarioDataSource: useBackendScenarios ? "analysis_runs" as const : "mock" as const, latestAnalysisArtifact: useBackendArtifact ? backendSnapshot!.latestAnalysisArtifact : null, simulationOverlay: useBackendArtifact ? backendSnapshot!.simulationOverlay : null, artifactDataSource: useBackendArtifact ? "analysis_runs" as const : "mock" as const, workOrderDataSource: backendWorkOrdersConfigured ? "backend" as const : "none" as const }; } function isSameLocalOperationalDate(value: string, operationalDate: Date) { const candidate = new Date(value); if (Number.isNaN(candidate.getTime()) || Number.isNaN(operationalDate.getTime())) return false; return candidate.getFullYear() === operationalDate.getFullYear() && candidate.getMonth() === operationalDate.getMonth() && candidate.getDate() === operationalDate.getDate(); } export function useWorkbenchRuntimeData( getAccessToken: WorkbenchAccess | undefined, sync: WorkbenchSyncState ) { const scopedProject = getWorkbenchProject(getAccessToken); const allowDemoScenarios = !scopedProject || scopedProject.gs_workspace === "tjwater_next"; const [headerDataTime, setHeaderDataTime] = useState(INITIAL_HEADER_DATA_TIME); const [mockConditions, setMockConditions] = useState([]); const backendSnapshot: WorkbenchRuntimeSnapshot | null = sync.snapshot?.runtime ?? null; const [realtimePipeFlows, setRealtimePipeFlows] = useState > | null>(null); const [scheduledConditionsLoading, setScheduledConditionsLoading] = useState(true); useEffect(() => { let timeoutId: number; const refreshHeaderDataTime = () => { const now = new Date(); setHeaderDataTime(formatHeaderDataTime(now)); timeoutId = window.setTimeout(refreshHeaderDataTime, getMsUntilNextHeaderDataTimeTick(now)); }; refreshHeaderDataTime(); return () => window.clearTimeout(timeoutId); }, []); useEffect(() => { const refreshScheduledConditions = () => { const operationalDate = resolveWorkbenchOperationalDate(backendSnapshot, { runtimeMode: env.TJWATER_WORKBENCH_RUNTIME_MODE }); setMockConditions(createOperationalScheduledConditions(operationalDate)); setScheduledConditionsLoading(false); }; refreshScheduledConditions(); const intervalId = window.setInterval( refreshScheduledConditions, SCHEDULED_CONDITION_REFRESH_INTERVAL_MS ); return () => window.clearInterval(intervalId); }, [backendSnapshot]); useEffect(() => { const controller = new AbortController(); let timeoutId: number | undefined; let refreshing = false; const refreshRealtimePipeFlows = async () => { if (controller.signal.aborted || refreshing) return; if (document.visibilityState === "hidden") { timeoutId = window.setTimeout(refreshRealtimePipeFlows, BACKEND_RUNTIME_REFRESH_INTERVAL_MS); return; } try { refreshing = true; const snapshot = await fetchRealtimePipeFlows(getAccessToken, controller.signal); if (!controller.signal.aborted) { setRealtimePipeFlows(snapshot.records.length > 0 ? snapshot : null); } } catch { if (!controller.signal.aborted) setRealtimePipeFlows(null); } finally { refreshing = false; if (!controller.signal.aborted) { timeoutId = window.setTimeout( refreshRealtimePipeFlows, BACKEND_RUNTIME_REFRESH_INTERVAL_MS ); } } }; const handleVisibilityChange = () => { if (document.visibilityState !== "visible") return; if (timeoutId !== undefined) window.clearTimeout(timeoutId); void refreshRealtimePipeFlows(); }; void refreshRealtimePipeFlows(); document.addEventListener("visibilitychange", handleVisibilityChange); return () => { controller.abort(); document.removeEventListener("visibilitychange", handleVisibilityChange); if (timeoutId !== undefined) window.clearTimeout(timeoutId); }; }, [getAccessToken]); const operationalDate = resolveWorkbenchOperationalDate(backendSnapshot, { runtimeMode: env.TJWATER_WORKBENCH_RUNTIME_MODE }); const resolvedRuntimeData = resolveWorkbenchRuntimeData(mockConditions, backendSnapshot, { operationalDate, runtimeMode: env.TJWATER_WORKBENCH_RUNTIME_MODE }); const backendDataTime = realtimePipeFlows?.observedAt; const backendRuntimeDataTime = backendSnapshot?.generatedAt; const demoMode = env.TJWATER_WORKBENCH_RUNTIME_MODE === "demo"; const historyMode = env.TJWATER_WORKBENCH_RUNTIME_MODE === "history"; const backendRuntimeActive = resolvedRuntimeData.runtimeDataSource !== "mock"; const backendWorkOrdersActive = resolvedRuntimeData.workOrderDataSource === "backend"; const backendHistoryActive = historyMode && (backendRuntimeActive || backendWorkOrdersActive); return { headerDataTime: backendHistoryActive ? `历史 ${formatHeaderDataTime(operationalDate, false)}` : demoMode || historyMode ? `演示 ${formatHeaderDataTime(operationalDate, false)}` : backendDataTime ? formatHeaderDataTime(new Date(backendDataTime), false) : backendRuntimeActive && backendRuntimeDataTime ? formatHeaderDataTime(new Date(backendRuntimeDataTime), false) : headerDataTime === INITIAL_HEADER_DATA_TIME ? headerDataTime : `演示 ${headerDataTime}`, headerDataSource: backendHistoryActive ? "history" as const : demoMode || historyMode ? "demo" as const : backendDataTime ? "realtime" as const : backendRuntimeActive ? "backend" as const : "mock" as const, operationalDate: toLocalDateKey(operationalDate), operationalCursorTime: operationalDate.toISOString(), scheduledConditionsLoading, realtimePipeFlows, flowDataSource: realtimePipeFlows ? "realtime" as const : "mock" as const, ...resolvedRuntimeData, scenarios: !allowDemoScenarios && resolvedRuntimeData.scenarioDataSource === "mock" ? [] : resolvedRuntimeData.scenarios }; }