summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--public/App.js65
-rw-r--r--public/index.html18
-rw-r--r--src/SomhairlesDream.Frontend/App.fs64
-rw-r--r--src/SomhairlesDream.Server/ArtifactRunCoordinator.fs24
-rwxr-xr-xtools/acceptance/e2e_acceptance.py28
5 files changed, 183 insertions, 16 deletions
diff --git a/public/App.js b/public/App.js
index ab4ae59..a401102 100644
--- a/public/App.js
+++ b/public/App.js
@@ -1,8 +1,8 @@
import { Union, Record } from "./fable_modules/fable-library-js.4.24.0/Types.js";
import { union_type, class_type, record_type, bool_type, string_type } from "./fable_modules/fable-library-js.4.24.0/Reflection.js";
import { Replay_frames } from "./SomhairlesDream.Shared/Replay.js";
-import { defaultOf, equals, createAtom } from "./fable_modules/fable-library-js.4.24.0/Util.js";
-import { equalsWith, tryFind, iterateIndexed, item } from "./fable_modules/fable-library-js.4.24.0/Array.js";
+import { comparePrimitives, defaultOf, equals, createAtom } from "./fable_modules/fable-library-js.4.24.0/Util.js";
+import { sortBy, map, equalsWith, tryFind, iterateIndexed, item } from "./fable_modules/fable-library-js.4.24.0/Array.js";
import { join, isNullOrWhiteSpace, printf, toText } from "./fable_modules/fable-library-js.4.24.0/String.js";
import { MeshData_triangleIndices, MeshData_positionData } from "./SomhairlesDream.Shared/Domain.js";
import { min, max } from "./fable_modules/fable-library-js.4.24.0/Double.js";
@@ -881,6 +881,66 @@ export function startRun(selection) {
})));
}
+/**
+ * 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.
+ */
+export function loadBootHistory() {
+ const selection = selectedRun();
+ const projectId = encodeURIComponent(selection.ProjectId);
+ const runId = encodeURIComponent(selection.RunId);
+ const url = toText(printf("/api/artifacts/manifest?projectId=%s&runId=%s"))(projectId)(runId);
+ const requestOptions = {
+ method: "GET",
+ };
+ startImmediate(singleton.Delay(() => singleton.TryWith(singleton.Delay(() => singleton.Bind(awaitPromise(fetch(url, requestOptions)), (_arg) => {
+ const response = _arg;
+ return response.ok ? singleton.Bind(awaitPromise(response.json()), (_arg_1) => {
+ let arg_2, arg_3, arg_4, arg_5, arg_6, arg_7, arg_8;
+ const payload = _arg_1;
+ if ((payload.status === "complete") && (payload.steps.length > 0)) {
+ const steps = map((step_1) => step_1.artifactPath, sortBy((step) => step.stepIndex, payload.steps, {
+ Compare: comparePrimitives,
+ }));
+ if (steps.length > 0) {
+ currentProjectId(selection.ProjectId);
+ currentRunId(selection.RunId);
+ historicalSteps(steps);
+ setText("project-id", selection.ProjectId);
+ setText("run-id", selection.RunId);
+ setText("target-version", (arg_2 = (steps.length | 0), toText(printf("V%02d"))(arg_2)));
+ setText("current-version", (arg_3 = (steps.length | 0), toText(printf("V%02d"))(arg_3)));
+ setText("version-readout", (arg_4 = (steps.length | 0), (arg_5 = (steps.length | 0), toText(printf("%02d / %02d"))(arg_4)(arg_5))));
+ setWidth("progress-fill", "100%");
+ setText("updated-at", "预发布历史");
+ setText("mesh-label", (arg_6 = item(steps.length - 1, steps), toText(printf("历史 · %s"))(arg_6)));
+ setText("timeline-current", (arg_7 = (steps.length | 0), (arg_8 = item(steps.length - 1, steps), toText(printf("V%02d · 构建重演 · %s"))(arg_7)(arg_8))));
+ setText("replay-button", "播放历史");
+ setText("replay-state", "构建重演 · 预发布历史");
+ renderTimeline();
+ updateFollowButton();
+ if (viewState() == null) {
+ return singleton.Zero();
+ }
+ else {
+ loadStepArtifact(viewState(), item(steps.length - 1, steps), true);
+ return singleton.Zero();
+ }
+ }
+ else {
+ return singleton.Zero();
+ }
+ }
+ else {
+ return singleton.Zero();
+ }
+ }) : singleton.Zero();
+ })), (_arg_2) => {
+ return singleton.Zero();
+ })));
+}
+
export function boot() {
showFrameMetadata(item(0, frames));
try {
@@ -902,6 +962,7 @@ export function boot() {
event.preventDefault();
startRun(selectedRun());
});
+ loadBootHistory();
renderTimeline();
}
diff --git a/public/index.html b/public/index.html
index 71e9e62..d6966b1 100644
--- a/public/index.html
+++ b/public/index.html
@@ -32,11 +32,11 @@
<section class="mission-strip" aria-label="当前任务">
<div>
<span class="eyebrow">PROJECT</span>
- <strong id="project-id">heritage-001</strong>
+ <strong id="project-id">probe-001</strong>
</div>
<div>
<span class="eyebrow">RUN</span>
- <strong id="run-id">run-001</strong>
+ <strong id="run-id">run-probe-real-2</strong>
</div>
<div class="mission-note">
<span class="eyebrow">MODE</span>
@@ -58,19 +58,19 @@
<div class="control-grid">
<label>
<span>项目 ID</span>
- <input id="project-id-input" value="heritage-001" autocomplete="off">
+ <input id="project-id-input" value="probe-001" autocomplete="off">
</label>
<label>
<span>运行 ID</span>
- <input id="run-id-input" value="run-001" autocomplete="off">
+ <input id="run-id-input" value="run-probe-real-2" autocomplete="off">
</label>
<label>
<span>基准 ID</span>
- <input id="base-id-input" value="base-000" autocomplete="off">
+ <input id="base-id-input" value="probe-base-000" autocomplete="off">
</label>
<label>
<span>目标 ID</span>
- <input id="target-id-input" value="target-003" autocomplete="off">
+ <input id="target-id-input" value="probe-target-007" autocomplete="off">
</label>
</div>
<div class="control-actions">
@@ -85,7 +85,7 @@
<div class="version-block">
<div class="label-row">
<span>设计版本</span>
- <strong id="version-readout">00 / 03</strong>
+ <strong id="version-readout">07 / 07</strong>
</div>
<div class="progress-track" aria-hidden="true">
<span id="progress-fill" class="progress-fill" style="width: 0%"></span>
@@ -192,8 +192,8 @@
</section>
<footer class="footer-note">
- <span>状态快照 · 原子发布 · 心跳监视</span>
- <span>OPEN CODE / F# FIRST</span>
+ <span>首页构建重演:预发布历史透视探针(非飞行设计 · 盘式天线 / RTG / 科学吊臂 简化几何)</span>
+ <span>heritage 参考线保留占位演示 · F# / OPEN CODE</span>
</footer>
</main>
<script type="module" src="/App.js"></script>
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
diff --git a/tools/acceptance/e2e_acceptance.py b/tools/acceptance/e2e_acceptance.py
index 8271857..b6aa74e 100755
--- a/tools/acceptance/e2e_acceptance.py
+++ b/tools/acceptance/e2e_acceptance.py
@@ -198,6 +198,9 @@ def assert_geometry_fit(geo, label, results):
def run_flow(page, base_url, run_id, viewport_label, results, evidence, shot_prefix):
page.goto(f"{base_url}/?freeze=1", wait_until="domcontentloaded")
+ page.fill("#project-id-input", "heritage-001")
+ page.fill("#base-id-input", "base-000")
+ page.fill("#target-id-input", "target-003")
wait_text(page, "#status-label", "待机")
results.append((True, f"{viewport_label}: idle 待机 visible"))
body = page.inner_text("body")
@@ -272,6 +275,9 @@ def stale_load_regression(browser, base_url, track, results, evidence):
def hold_route(route):
q = urllib.parse.parse_qs(urllib.parse.urlparse(route.request.url).query)
+ if (q.get("runId") or [""])[0] == "run-probe-real-2":
+ route.continue_()
+ return
held.append({
"route": route,
"project": (q.get("projectId") or [""])[0],
@@ -282,6 +288,9 @@ def stale_load_regression(browser, base_url, track, results, evidence):
page.route("**/api/artifacts/steps*", hold_route)
page.goto(f"{base_url}/?freeze=1", wait_until="domcontentloaded")
+ page.fill("#project-id-input", "heritage-001")
+ page.fill("#base-id-input", "base-000")
+ page.fill("#target-id-input", "target-003")
wait_text(page, "#status-label", "待机")
def start_run(run_id):
@@ -522,6 +531,9 @@ def historical_replay_regression(browser, base_url, track, results, evidence):
def hold_route(route):
q = urllib.parse.parse_qs(urllib.parse.urlparse(route.request.url).query)
+ if (q.get("runId") or [""])[0] == "run-probe-real-2":
+ route.continue_()
+ return
held.append({
"route": route,
"project": (q.get("projectId") or [""])[0],
@@ -532,6 +544,9 @@ def historical_replay_regression(browser, base_url, track, results, evidence):
page.route("**/api/artifacts/steps*", hold_route)
page.goto(f"{base_url}/?freeze=1", wait_until="domcontentloaded")
+ page.fill("#project-id-input", "heritage-001")
+ page.fill("#base-id-input", "base-000")
+ page.fill("#target-id-input", "target-003")
wait_text(page, "#status-label", "待机")
def start_run(run_id):
@@ -573,13 +588,20 @@ def historical_replay_regression(browser, base_url, track, results, evidence):
return g.get("boxSize") and all(
isinstance(s, (int, float)) and s > 0 for s in g["boxSize"])
- # Idle fallback must be clearly labeled as placeholder, never as history.
+ # Boot must surface the pre-published probe build replay with honest labels.
+ wait_text(page, "#viewport-note", "历史几何 · steps/probe-07-livery.glb")
+ results.append((True,
+ "historical: boot loads pre-published probe replay "
+ "(历史几何 · steps/probe-07-livery.glb)"))
idle_state = page.inner_text("#replay-state")
idle_button = page.inner_text("#replay-button")
- results.append(("占位" in idle_state and "占位" in idle_button,
- f"historical: idle fallback clearly labeled placeholder "
+ results.append(("历史" in idle_state and "历史" in idle_button,
+ f"historical: boot historical labels are honest "
f"(state='{idle_state}', button='{idle_button}')"))
+ # The placeholder fallback stays honest whenever real history is absent.
+ # (Placeholder idle is ensured on pages where the manifest fetch fails;
+ # see run_flow heritage pinning plus the standalone offline guard above.)
# Phase I: pin a checkpoint mid-run; live snapshot updates must not unpin it.
run_id = f"run-replay-{stamp}"
start_run(run_id)