summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/SomhairlesDream.Frontend/App.fs64
-rw-r--r--src/SomhairlesDream.Server/ArtifactRunCoordinator.fs24
2 files changed, 86 insertions, 2 deletions
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
+[<Erase>]
+type ManifestStep =
+ abstract stepIndex: int with get
+ abstract artifactPath: string with get
+
+[<Erase>]
+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<ArtifactManifest, ArtifactLookupError> =
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<ArtifactFile, ArtifactLookupError> =
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