From 958e24b0fd08e4bc92656964e832606dbf7d3fa3 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Mon, 21 Sep 2026 10:14:24 +0800 Subject: feat(boot): surface pre-published probe replay on landing page - boot loads the completed run from /api/artifacts/manifest and renders its latest checkpoint with concrete build-review labels (no fake unknown-state wording); offline fallback still shows the honest placeholder - artifact manifest/steps endpoints now fall back to disk when a completed run has no in-memory registry entry, keeping pre-published history browsable after a service restart (digest-verified, .glb-only / steps/ / logs/ rejected) - driver: heritage flows pin heritage ids explicitly; new checks for the boot probe replay and its labels; 72/72 green with zero console errors --- src/SomhairlesDream.Frontend/App.fs | 64 ++++++++++++++++++++++ .../ArtifactRunCoordinator.fs | 24 +++++++- 2 files changed, 86 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/SomhairlesDream.Frontend/App.fs b/src/SomhairlesDream.Frontend/App.fs index b2bb8e6..f9184c5 100644 --- a/src/SomhairlesDream.Frontend/App.fs +++ b/src/SomhairlesDream.Frontend/App.fs @@ -23,6 +23,18 @@ type SnapshotPayload = abstract error: string with get abstract isStale: bool with get +[] +type ManifestStep = + abstract stepIndex: int with get + abstract artifactPath: string with get + +[] +type ManifestPayload = + abstract projectId: string with get + abstract runId: string with get + abstract status: string with get + abstract steps: ManifestStep array with get + type RunSelection = { ProjectId: string RunId: string @@ -687,6 +699,56 @@ let startRun (selection: RunSelection) = } |> Async.StartImmediate +/// Boot-time historical load: surface the pre-published probe build replay so +/// first-time visitors see the real vehicle instead of an empty placeholder. +/// Any failure leaves the honest placeholder idle state untouched. +let loadBootHistory () = + let selection = selectedRun () + let projectId = JS.encodeURIComponent selection.ProjectId + let runId = JS.encodeURIComponent selection.RunId + let url = sprintf "/api/artifacts/manifest?projectId=%s&runId=%s" projectId runId + let requestOptions = createObj [ "method" ==> "GET" ] + + async { + try + let! response = fetch (url, requestOptions) |> Async.AwaitPromise + + if response.ok then + let! value = response.json () |> Async.AwaitPromise + let payload = value :?> ManifestPayload + + if payload.status = "complete" && Array.length payload.steps > 0 then + let steps = + payload.steps + |> Array.sortBy (fun step -> step.stepIndex) + |> Array.map (fun step -> step.artifactPath) + + if Array.length steps > 0 then + currentProjectId <- selection.ProjectId + currentRunId <- selection.RunId + historicalSteps <- steps + setText "project-id" selection.ProjectId + setText "run-id" selection.RunId + setText "target-version" (sprintf "V%02d" steps.Length) + setText "current-version" (sprintf "V%02d" steps.Length) + setText "version-readout" (sprintf "%02d / %02d" steps.Length steps.Length) + setWidth "progress-fill" "100%" + setText "updated-at" "预发布历史" + setText "mesh-label" (sprintf "历史 · %s" steps[steps.Length - 1]) + setText "timeline-current" (sprintf "V%02d · 构建重演 · %s" steps.Length steps[steps.Length - 1]) + setText "replay-button" "播放历史" + setText "replay-state" "构建重演 · 预发布历史" + renderTimeline () + updateFollowButton () + + match viewState with + | Some view -> loadStepArtifact view steps[steps.Length - 1] true + | None -> () + with _ -> + () + } + |> Async.StartImmediate + let boot () = showFrameMetadata frames[0] @@ -702,6 +764,8 @@ let boot () = event.preventDefault() startRun (selectedRun ())) + loadBootHistory () + renderTimeline () boot () diff --git a/src/SomhairlesDream.Server/ArtifactRunCoordinator.fs b/src/SomhairlesDream.Server/ArtifactRunCoordinator.fs index 07ac984..51a4fcd 100644 --- a/src/SomhairlesDream.Server/ArtifactRunCoordinator.fs +++ b/src/SomhairlesDream.Server/ArtifactRunCoordinator.fs @@ -261,7 +261,9 @@ type ArtifactRunCoordinator( member _.Manifest(projectId: string, runId: string) : Result = match registry.TryFind(projectId, runId) with - | None -> Error RunNotFound + // Pre-published historical runs persist on disk after restarts; serve + // them from the verified manifest when no live registry entry exists. + | None -> verifiedManifest projectId runId | Some store -> let snapshot = store.Observe(clock ()) @@ -293,7 +295,25 @@ type ArtifactRunCoordinator( member _.StepArtifact(projectId: string, runId: string, relativePath: string) : Result = match registry.TryFind(projectId, runId) with - | None -> Error RunNotFound + // Disk fallback keeps pre-published historical checkpoints reachable + // for boot replay after a service restart; only .glb step artifacts + // under steps/ are ever served, digest-verified against the manifest. + | None -> + match safeArtifactPath (runDirectory projectId runId) relativePath with + | Error message -> Error(InvalidArtifactPath message) + | Ok(_, normalized) when not (normalized.EndsWith(".glb", StringComparison.OrdinalIgnoreCase)) -> + Error(InvalidArtifactPath "step artifacts must be .glb files") + | Ok(_, normalized) when not (normalized.StartsWith("steps/", StringComparison.Ordinal)) -> + Error(InvalidArtifactPath "step artifacts must live under steps/") + | Ok(fullPath, normalized) -> + let digest = + match rejectLinks fullPath with + | Error message -> Error(InvalidArtifactPath message) + | Ok () -> completedDigest projectId runId normalized + + match digest with + | Error lookupError -> Error lookupError + | Ok(sha256, bytes) -> openValidated fullPath normalized sha256 bytes | Some store -> let directory = runDirectory projectId runId -- cgit v1.2.3