Skip to content

Take 3D inspection off Preflight, and stop CI measuring the machine - #1

Merged
kmuz-dev merged 7 commits into
mainfrom
feature/preflight-no-3d
Aug 22, 2026
Merged

Take 3D inspection off Preflight, and stop CI measuring the machine#1
kmuz-dev merged 7 commits into
mainfrom
feature/preflight-no-3d

Conversation

@kmuz-dev

@kmuz-dev kmuz-dev commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Five commits: one product change the user asked for, three test fixes needed to prove it, and one real UI defect the third of those uncovered.

Take 3D inspection off Preflight (b2e7f6e)

Preflight answers two questions in order - which aircraft am I making, and is my paper ready - and then offers one action. An Inspect in 3D control on the hero offered a second thing to do at the exact moment the screen exists to make one choice, and what it offered was a mouse-orbited model of the aircraft the cadet is about to build with their hands.

OrbitViewer becomes AircraftHero: production artwork, the engine's build-time SVG beneath it for when the image will not load, and nothing to operate. The renderer, the intersection observer, the retry path and the reset control all go with the button, so Preflight now never reaches for the renderer chunk at all rather than merely deferring it - lazy-renderer.spec.ts says so in those words.

The Hangar keeps live inspection, where browsing the fleet is the point, and the fold engine stays authoritative for Tutorial geometry. The five strings the Hangar borrowed from preflight.* move to hangar.*, now their only caller; preflight.inspect3d and preflight.orbitHint have no caller left and are dropped from all twelve locales.

The Hangar's Reset view was buried under Start folding (ed05f63)

The per-plane Preflight inspection baselines went with the control, so the display stage is now baselined in the Hangar bay instead - and covering the Hangar's reset control for the first time is what found this.

Clicking where Reset view appears navigates to the selected plane's Preflight instead. .hangar-bay-status was anchored bottom-left, which is where the placard already lives: measured overlap with .hangar-primary is 63px at 1024x768, 1280x720, 1280x800 and 1440x900 - every desktop width - and the button is not merely unclickable but invisible, painted behind the red CTA.

The z-index reads 18 against the placard's 16 and that comparison never happens: the placard hangs off .hangar-main while the status row hangs off .hangar-bay inside .hangar-bay-column, so the two indices resolve in different stacking contexts and never meet. The narrow breakpoint had already worked this out and put the row 8px under the control that opens inspection; this is the same relationship at desktop metrics.

Three thresholds that were measuring the machine

30b9bde - the tutorial holds one shape on every fold measures fourteen static layouts and was sitting through fourteen real WebGL fold animations to reach them, until it hit its 180s ceiling. Under prefers-reduced-motion the engine jumps straight to each settled state, so the walk costs rasterisation alone.

244fd50 - smoking the deployed site lost two to four journey specs a run, on a different plane each time. One race: the fold selector is a controlled React <select> on a statically exported page, so between the HTML arriving and hydration finishing it is visible and operable with nothing listening. A change fired in that window is dropped and React then restores value from its own state.

bab4804 - hangar-perf asserted > 20 frames in 3000ms for a live scene, which is a 150ms-per-frame budget: the exact thing that test's own header says it must not assert. It scored 10 on a 2-core CI runner and 4 on a busy laptop, neither run carrying a defect. The floor now sits where a stall lives.

Verification

  • typecheck, lint clean
  • 747 unit and component tests pass
  • i18n audit: 419 keys consistent across 12 locales
  • 141 E2E pass locally in 1.8 minutes, 0 failed
  • 135 E2E pass against the deployed site, 0 failed
  • Reset view hit-tested at 1024x768, 1280x720, 1440x900 and 390x844: the click lands on the button at every width

🤖 Generated with Claude Code

Preflight answers two questions in order — which aircraft am I making,
and is my paper ready — and then offers one action. An Inspect in 3D
control sitting on the hero offered a second thing to do at the exact
moment the screen exists to make one choice, and what it offered was a
mouse-orbited model of the aircraft the cadet is about to build with
their hands. The Hangar keeps live inspection, where browsing the fleet
is the point; the fold engine remains authoritative for Tutorial
geometry.

So `OrbitViewer` becomes `AircraftHero`: production artwork, the
engine's build-time SVG beneath it for the case where the image will not
load, and nothing to operate. The renderer, the intersection observer,
the retry path and the reset control all go with the button, and
Preflight now never reaches for the renderer chunk at all rather than
merely deferring it — `lazy-renderer.spec.ts` says so in those words.

The five strings the Hangar borrowed from `preflight.*` move to
`hangar.*`, which is now their only caller; `preflight.inspect3d` and
`preflight.orbitHint` have no caller left and are dropped from all
twelve locales. The per-plane inspection visual baselines go too, and
the display stage — standing aircraft, graded backdrop, contact shadow —
is baselined in the Hangar bay instead, which is where it still lives.
CI failed on `the tutorial holds one shape on every fold, on desktop`,
and had the laptop and phone widths, a tutorial resume and a hangar
cadence assertion flaky beside it. None of them is a product defect:
the E2E suite rasterises every WebGL frame in software on a two-worker
runner, the full run took 16.2 minutes, and the heaviest test in it sat
through fourteen real Hammer folds three times over before running out
of its 180 second ceiling.

The subject of that test is where the rail's controls sit at each
settled step — fourteen static layouts, not fourteen animations. Under
`prefers-reduced-motion` the engine jumps straight to each settled state
(`REDUCED_MOTION_SECONDS` is 1ms), so the walk costs rasterisation and
nothing else. The 180s ceiling stays as what a genuine hang hits.

The same guard was already on the visual-baseline spec and had been lost
from it in this branch, which is what made three approved tutorial
baselines appear to drift; restored, they match byte for byte.

Local suite: 4.3 minutes to 1.5, with the Preflight inspection specs
gone as well.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 22, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
flight-school 089e898 Aug 22 2026, 06:23 PM

Smoking the deployed site lost two to four journey specs a run, on a
different plane each time, always the same way: the step readout the
test asserts on never appeared. It reproduced nowhere locally, and CI
had been losing the same assertion in tutorial.spec for weeks with the
same unattributable look.

One race, one cause. The rail's fold selector is a controlled React
`<select>` on a statically exported page, so between the HTML arriving
and hydration finishing it is real, visible and operable with nothing
listening. A `change` fired in that window is dropped, and React's first
render restores `value` from its own state, so the page quietly stays on
the fold it was already showing. Locally that window is a few
milliseconds; over a network it is wide enough to lose.

`data-material-ready` is the product's own answer to whether the stage
is live — set from an effect on both the WebGL path and the static
fallback, so it cannot be true before React is running. `chooseFold`
waits for it and then selects, which is a gate rather than a retry: the
test now waits for the same thing a person waits for.

Live suite against https://flight-school-chi.vercel.app: 135 passed,
0 failed, where the run before it lost four.
Two follow-ups to the hydration fix, both about a threshold describing
the machine instead of the product.

`chooseFold` gated on `data-material-ready`, which is correct and costs
a SwiftShader engine boot per call - four minutes across the suite for a
wait no assertion afterwards needs. The step readout is the cheapest
thing on the page only React can produce: the rail re-renders on the
step change without waiting for the stage. Same gate, without the boot.

`hangar-perf` asserted `> 20 frames in 3000ms` for the live scene, which
is a 150ms-per-frame budget - the exact thing that test's own header
says it must not assert, having been burnt by it twice already. It
scored 10 on a 2-core CI runner under 4x throttling and 4 on a busy
laptop, neither run carrying a defect. The failure it exists to catch is
a stall, which produces one frame or none, so the floor is set there and
the cadence stays in the annotation where a human can read it.
@kmuz-dev kmuz-dev changed the title Take 3D inspection off Preflight Take 3D inspection off Preflight, and stop CI measuring the machine Aug 22, 2026
CI caught this on the new inspection spec, and it is a real defect, not
a test artefact: clicking where Reset view appears in the Hangar
navigates to the selected plane's Preflight instead.

`.hangar-bay-status` was anchored bottom-left, which is where the
placard already lives. Measured overlap with `.hangar-primary` is 63px
at 1024x768, 1280x720, 1280x800 and 1440x900 — every desktop width —
and the button is not merely unclickable but invisible, painted behind
the red Start folding CTA. `document.elementFromPoint` at the centre of
Reset view returns `a.hangar-primary`, and a raw click there lands on
/en/plane/dart/.

The z-index reads 18 against the placard's 16, and that comparison
never happens: the placard hangs off `.hangar-main` while the status row
hangs off `.hangar-bay` inside `.hangar-bay-column`, so the two indices
resolve in different stacking contexts and never meet. Raising the
number is not the fix, and would not be the fix even if it worked — two
controls competing for one patch of screen is a layout bug, not a paint
order one.

The narrow breakpoint had already worked this out and put the row 8px
under the control that opens inspection. This is the same relationship
at desktop metrics, which also makes Reset view read as what it is:
part of the inspection cluster, beside Show artwork, rather than a
stray control in the corner. The mobile override's `bottom: auto` went
with the base rule's bottom anchor.

Verified at 1024x768, 1280x720, 1440x900 and 390x844: the hit test at
the button's centre returns the button, the gap under the toggle is 8px
at every width, and it is on screen.
The desktop width failed all three attempts on CI, at three different
lines including a bare `page.evaluate`. That is not a hang, it is a
pegged main thread, and the cause is upstream of the test: the stage's
render loop has no dirty check, so it re-rasterises for as long as it is
on screen whether or not anything has moved. A test that sits on the
tutorial for two minutes therefore holds a WebGL surface at 60fps for
two minutes, and `fullyParallel` had put the two most expensive widths
on a two-core runner at once.

Serialising the three takes the contention out. Scoped to a describe
rather than the file, so the cheap layout tests around them still run in
parallel.

The loop itself is the real defect and is now written down in
docs/KNOWN-ISSUES.md rather than fixed here: a still aeroplane redrawn
sixty times a second is a warm laptop and a shorter battery on the
screen a beginner spends longest on, and PRD criterion 8 already asks
for the offscreen and hidden half of the same argument. Fixing it means
marking dirty on pose, camera, resize, material load and control
damping, and keeping the loop alive for OrbitControls damping, the
display stage's breathing and lift, and any in-flight timeline. That is
a renderer change with real edges, not something to squeeze into a
layout fix.

Local: layout.spec 14 passed in 34s, desktop 16.0s against a 180s
ceiling.
The desktop width kept timing out on CI, and the failure snapshot said
why: Playwright reported it could not resolve the Next fold button,
while the page capture taken at that moment shows `button "Next fold"
[active]` sitting there on step 11 of 14. The element was never missing.
The main thread was too busy to answer the query.

Too busy with what: `FoldRenderer.startLoop` has no dirty check, so an
on-screen stage re-rasterises every frame whether or not anything moved,
and a test that sits on the tutorial for three minutes holds a WebGL
surface at 60fps for three minutes. On this laptop that is free — the
same walk finishes in 13s at 6x CPU throttling — because there is a GPU
here and none on the runner. Serialising the three widths was not
enough, because the loop still competes with whatever the other worker
is doing.

So suspend it, through the product's own path rather than a stub:
`useRenderActivity` pauses a stage while the document is hidden, which
is PRD acceptance criterion 8, and a hidden document changes no layout
at all — which is the entire subject of this test. Rendering is still
exercised once per step, because `gotoStep` draws a frame explicitly
when the loop is inactive.

Measured, with a probe counting the frames the page will give it over
two seconds: 52 with the loop running, 121 with it suspended, and the
canvas still changes on every step in both. Locally the desktop walk
goes 15.6s to 2s, layout.spec 34.2s to 10.2s, and the full E2E suite
141 passed in 1.3 minutes.

The loop itself remains the real defect and stays recorded in
docs/KNOWN-ISSUES.md; this makes the suite honest about what it is
measuring rather than fixing the renderer from a layout test.
@kmuz-dev
kmuz-dev merged commit 3f1ea63 into main Aug 22, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant