# FieldHydro Changelog

Semantic versioning. The current version is the single source of truth in
`field/version.js`; `tests/field-version.test.js` asserts this file documents it.

## v0.9.2 — 2026-10-21

### Fixed
- **Cross-section polygons.** The inundation layer is a Leaflet feature group. Loading a GeoJSON polygon fits the map. A layer group has no `getBounds`, so the same file was reported as a parse error after the polygon had already been drawn.
- **Cross-section basemap.** CARTO's dark tiles now return an API-key image. The map uses Esri World Dark Gray, zoomed from the service's native level 16.

## v0.9.1 — 2026-09-24

### Changed
- **Renamed FloodTruth and moved to its own domain.** The suite is served at
  https://floodtruth.com/ and is open to search engines. Old `pe-calc.com/field/…` links
  301 to the same page on floodtruth.com, so QR codes, pitch links and bookmarks keep
  working. Code identifiers (`FieldHydroVersion`, `fieldhydro-*.js`, the IndexedDB and
  cache names) keep the old name so staged areas and installed copies survive the move.

## v0.9.0 — 2026-09-23

### Fixed
- **LSPIV read surface velocity ~7–9% low on faint water texture.** OpenCV's Farnebäck solve
  adds a fixed regularising constant, so on a low-contrast surface the displacement is pulled
  toward zero (synthetic translating texture: −12% at a grey-level SD of 8, −0.3% at SD 20,
  0% at SD 50; clean high-contrast textures were already exact). Every stabilised frame now
  gets one identical affine contrast gain before the flow step (target SD 40, measured on the
  water in frame 0, capped at ×8), shown in the results as "Contrast gain". On the synthetic
  demo flume (true 0.45 m/s) the reading moved from 0.40–0.42 m/s to 0.44–0.45 m/s at 10–15
  fps. At 5 fps (9 px per pair) it still reads ~4% low — the large-displacement regime the
  displacement guard already warns about.
- **Duplicate frames counted as zero motion.** A seek can report `seeked` before the new frame
  is drawn and return the previous image; that pair averaged in as zero velocity. The frame is
  re-grabbed once, and a pair that is still identical is excluded and reported in the status.
- **Video metadata/seek watchdog was 2 s**, which tripped on a loaded desktop decoding an 8 s
  640×360 clip; now 10 s.
- The methods text said Farnebäck ran 7 iterations; the code runs 3.
- **AR depth reticle could never read distant water**: rays within ~1.15° of level were
  rejected, and the Helene river sits ~0.4° below eye level. Only rays at or above the horizon
  are rejected now.
- **Center on polygon aimed at the reach centroid**, which in the Helene demo lies behind a
  hillside. It now aims at flooded ground with terrain line-of-sight (viewshed sweep over the
  DSM), and a real click stops the auto-tour so the aim holds.
- HUD showed "portrait" on landscape desktops; the auto-tour badge showed the raw synthetic
  heading instead of the camera heading.

### Added
- Desktop look controls in the AR viewer: drag, arrow keys (Shift ×3), wheel FOV zoom.
- Plan-view inset (observer, view cone, flood extent, reticle target).
- Helene demo ships a 10 m 3DEP terrain crop (`scenarios/helene_dem_3dep_10m.tif`) instead of
  ~625 live point queries; works offline.
- Stream Flow demo clip with a known answer (`assets/lspiv-demo.webm`,
  `scripts/make-lspiv-demo.py`) that reports true vs measured velocity.
- One navigation bar across the standalone tools; `index.html#ar` / `#sediment` / `#lspiv`.

## v0.8.5 — 2026-09-04

### Fixed
- **Sensors never attached on Chromium 153+.** Chrome 153 ships
  `DeviceOrientationEvent.requestPermission()` (answers `granted` with no gesture and no UI).
  The app treated the mere existence of that function as "this is iOS, wait for a tap", so on
  Chrome 153 the compass and tilt listeners were never registered: the demo sat on its fixed
  110° pose with the auto-tour "tap to enable" prompt, and live mode showed an Enable-Motion
  button that Android never needed. The gate is now decided by behaviour: the function is
  called once with no gesture; `granted` attaches immediately, while iOS's
  `NotAllowedError` (which records no denial) keeps the tap-to-grant flow. Surfaced by CI when
  Playwright 1.63.0 (published today) moved the e2e Chromium from 151 to 153; the rotation
  suite now stubs both shapes so the guard holds on whichever Chromium ships. The e2e harness
  accepts `E2E_CHROMIUM=<chrome.exe>` to pin a build for side-by-side runs.
- **Camera opt-in no longer prompts twice on a device with no camera.** `startCamera()`
  retried an unconstrained `getUserMedia` after `NotFoundError`, but with ideal-only
  constraints that error means there is no video input at all, so the retry could only
  fail again and re-prompt. Surfaced by the reviewer-funnel e2e on the CI runner (no
  webcam), which every push since v0.8.0 had failed; the check passed locally because a
  laptop webcam yields `NotAllowedError`, which was never retried. `NotReadableError`
  (camera busy) and `OverconstrainedError` are still retried once.

## v0.8.4 — 2026-09-04

### Changed
- **Transparent in the calculations, not white-box in the code.** pe-calc.com now deploys from
  a generated `dist/` (`scripts/build-dist.mjs`, run by Netlify): the FieldHydro scripts and
  inline page scripts ship minified with comments and local names stripped (top-level names
  are kept — the pages share one classic-script scope), markup comments are removed, and the
  architecture scaffolds, dev notes, scenario build scripts and the DEM cache no longer reach
  the CDN. What stays public on purpose: every Methods panel (KaTeX formulas with citations),
  the worked examples, `VALIDATION.md`, `UNCERTAINTY.md` and this changelog. The source
  repository is private. The pitch packet's "open, source viewable" wording became "publicly
  deployed, every formula shown and cited"; the Phase I map and the tour no longer link the
  scaffold documents.
- `FIELD_DIR=dist/field npm run test:e2e` drives the minified build with the full suite.
- floodtruth.com (registered today) forwards to the suite: `floodtruth.com/* → pe-calc.com/field/*`.

## v0.8.3 — 2026-09-04

### Fixed
- **Installed clients were running new pages against months-old scripts.** The service
  worker served HTML network-first but every script cache-first under a hand-bumped cache
  name that had not moved since v6 — so a PWA or APK that had visited before got v0.8.x
  HTML with v0.7 JavaScript (the Android emulator showed version.js 0.8.0 under 0.8.2
  pages). The cache is now keyed on `version.js` via `importScripts` (a version bump
  installs a new worker and drops the old cache) and scripts are stale-while-revalidate,
  so even a missed bump self-heals on the next load. Verified: an APK upgrade over an old
  install shows the old version on first load and the new one on the next navigation.
- **APK exports actually save.** Blob downloads are inert in the Capacitor WebView; every
  export (snapshot PNG, captioned exhibit, CSV, GeoJSON, HWM ZIP, debug JSON) now goes
  through a small native plugin (`FieldHydroAndroidPlugin`, MediaStore → Downloads/FieldHydro)
  via `field/fieldhydro-android.js`, which is a no-op in browsers, the PWA and iOS.
- Heading order: the landing page's reviewer sections are H2 under the hero H1, the AR
  landing card's title is H2, and the cross-section Methods sub-heads are H3 under H2.
- The live verifier no longer looks for the export-demo link the funnel redesign removed.

## v0.8.2 — 2026-09-04

UX / UI audit (`tests/e2e/ux-audit.e2e.mjs`): accessibility, phone ergonomics, keyboard,
empty states, complete journeys with screenshots, guided tours, and a throttled-phone budget.

### Fixed
- **Landing page weighed 10.7 MB** because OpenCV.js (9.6 MB) loaded on the reviewer funnel
  before anyone opened the Sediment tab. It now loads on demand when that tab is activated
  (924 KB landing; the 15 s failure timeout starts with the request).
- Guided tour tooltips ended 6–8 px below the screen edge on phones and the welcome tip
  flashed at the document bottom: the pop-in animation was being measured mid-scale. The tip
  is measured by layout size, re-clamped after layout, and hidden until placed.
- The auto-tour badge covered the demo banner's "Switch to live mode" button on phones; it
  now sits above the toolbar, and it — with the 3D toggle and storm stepper — steps aside
  while the More sheet is open. The "Centered" toast no longer covers the reticle readout.
- Tap-to-mark: the Marks button overlapped Export on the phone toolbar; it sits above the row.
- Cross-section: "Load worked example" now scrolls the headline result into view (it was
  below the fold on every screen size). Sediment: the source photo was taller than a desktop
  screen (950 px); capped at 70 vh.
- Accessibility: each tool page has an H1; the reference-bed file input is labelled;
  no visible text under 11 px; every input is ≥16 px on touch devices (iOS zoom) via one
  shared rule; the index tab strip fades its clipped edge so it reads as scrollable.

### Added
- `E2E_ONLY=<suite,...>` to run a subset of the e2e suites.

### Verified
- 0 visible text runs below 3:1 contrast on any page; no AA (4.5:1) misses reported.
- AR demo render loop 44 fps at 4× phone CPU throttling; landing / AR / cross-section load in
  1.0 / 1.5 / 0.9 s throttled.
- Journeys (phone + desktop): landing → demo → More → snapshot PNG → live setup; worked
  example → export; GPS → demo polygon → live AR → tap → evidence sheet; synthetic photo →
  coin calibration by two taps → photo-sieve results; 10-step tour with every tip on screen.

## v0.8.1 — 2026-09-04

Cross-engine and emulator regression pass on v0.8.0.

### Fixed
- **320 px phones (found on WebKit):** the header nav on the LSPIV and tap-to-mark pages did not
  wrap and pushed the page 10–76 px wider than the screen; long inline `code` samples did the
  same on the cross-section page; and WebKit let KaTeX's visually-hidden accessible MathML copy
  widen the document by up to 500 px. Nav wraps under 640 px, `code` wraps, `.katex-mathml` is
  contained, and long display formulas scroll inside their own box.

### Added
- **Engine-selectable e2e harness**: `E2E_BROWSER=webkit|firefox npm run test:e2e` after
  `npx playwright-core install webkit firefox`. Suites needing mobile emulation or the fake
  camera skip themselves on Firefox; headless WebKit (no `navigator.mediaDevices`) exercises
  the soft-fail path of the camera opt-in instead of the count.
- **`tests/e2e/android-emulator.mjs`**: drives the Capacitor APK inside the Android emulator
  over raw CDP (`adb forward … webview_devtools_remote_<pid>`). Steps: `diag` (camera,
  orientation events, rotation, GPS), `rotpose` (real-sensor validation of the screen-angle
  compensation through four quarter turns), `pages` (cross-section worked example, OpenCV
  init, tap-to-mark live start + tap).

### Verified
- Chromium 549 / WebKit 538 / Firefox 343 e2e checks green; 6919 unit.
- Android 15 emulator, real WebView, virtual rear camera 1392×1856, real DeviceOrientation:
  derived heading and pitch unchanged and roll 0° through every rotation the display followed;
  the emulator's idle pose (β=90, γ≈40) is the horizon gimbal case and now reads as zero roll.
  GPS fix ±5 m; conveyance worked example −64.5 % / −3792 cfs; OpenCV initialises; a tap in
  tap-to-mark resolves to a ground point and opens the evidence sheet.
- The debug APK is rebuilt from this code (`FieldHydro-debug.apk` on the Desktop).

## v0.8.0 — 2026-09-04

### Fixed
- **Phone rotation.** The AR viewer and tap-to-mark derived pitch as `β−90` and roll as `γ`,
  a portrait-only reading of the W3C DeviceOrientation angles, and papered over it with a
  `screen.orientation.lock('portrait')` that is a no-op in iOS Safari and in an Android
  browser tab. In landscape the overlay read as a 90° roll. Both pages now build the spec's
  full Z-X'-Y'' rotation matrix and read the camera axis and the *screen's* up vector from
  it (`orientationToCameraHPR` in `fieldhydro-geo.js`, screen angle from
  `screen.orientation.angle` / `window.orientation`). Landscape is supported; the lock is gone.
- **Horizon gimbal lock.** At β≈90 the device y-axis is world-up, so γ is a *yaw* — the
  legacy `heading = 360−α` dropped it and `roll = γ` misapplied it. That was the "compass
  hypersensitivity at the horizon" damped in June. The matrix derivation removes it.
  `?hpr=legacy` restores the old mapping for side-by-side comparison on a phone.
- **Visible field of view.** The FOV slider is now the camera's long-side FOV; the FOV
  actually spanning the viewport is derived each frame from the frame size and the
  `object-fit: cover` crop (`visibleHfovDeg`): a portrait phone sees ~36° of a 70° camera,
  a landscape phone the full 70°. Previously portrait used 70° across, so the overlay was
  drawn too narrow.
- **Stage toolbar was half the screen wide.** `.ar-ctrls` was positioned with `left:50%` +
  translate, which gives an absolutely positioned box only half the stage to shrink-to-fit
  into — on a phone every button wrapped onto its own row and the stack covered the scene.
- **`±NaNm` in the reticle range readout** during every demo session: `isFinite(null)` is
  true, so the demo's null GPS accuracy was fed into the uncertainty budget.
- The 3D/NFHL toggle sat on top of the HUD (same corner); the demo banner covered the HUD.

### Added
- **Phone stage layout** (touch or ≤640 px, portrait and landscape): one-row bottom toolbar
  (Center · Snapshot · More · Stop) with secondary actions behind a "More" sheet; slim
  one-line demo banner; compact tap-to-expand HUD; 3D toggle and storm/offset stepper in the
  bottom corners by the thumbs. Desktop layout unchanged apart from the toolbar-width fix.
- **Screen wake lock** while the AR stage is active (a reach walk is minutes without a tap);
  re-acquired on return from background, and the camera restarts if iOS killed the track.
- **Actionable permission errors**: camera and location denials say where to fix them
  (Safari settings vs. address-bar lock icon); `OverconstrainedError`/`NotFoundError`
  retry with an unconstrained stream; a GPS timeout retries a coarse fix and labels it.
- **iOS compass-accuracy hint** (`webkitCompassAccuracy`): figure-8 prompt, throttled.
- HUD readouts: effective FOV and screen orientation.
- Tests: rotation-matrix unit tests (spec matrix, legacy reduction at γ=0, gimbal case,
  round-trip through the page's own camera convention at 0/90/180/270°), and
  `tests/e2e/rotation.e2e.mjs` (portrait/landscape pose, iOS heading offset, wake lock,
  error text, and a landscape-viewport layout check).

### Known / to verify on a device
- iOS landscape heading uses `webkitCompassHeading + screen angle`; the sign follows Apple's
  "heading of the device top" definition and is unit-tested but not yet confirmed on an iPhone.

## v0.7.2 — 2026-07-08

### Added
- **Real server-side access control** for `/field/*` via a Netlify Edge Function
  (`netlify/edge-functions/field-auth.js`) enforcing HTTP Basic Auth on Netlify's Deno edge
  — content is withheld with a 401 before any byte is served, so it can't be bypassed with
  curl/View-Source/DevTools like the old cosmetic `gate.js`. Opt-in via the `FIELDHYDRO_AUTH`
  env var (`user:pass` pairs, never in the repo); constant-time credential comparison. Pure
  logic in `_auth-core.mjs`, covered by `tests/field-auth.test.js` (the tested logic is the
  deployed logic). Documented in `field/AUTH.md`.

### Changed
- `gate.js` marked superseded (was cosmetic; not wired into any page). `netlify.toml` +
  `FIELD.md` updated to point at the edge function as the real control.

## v0.7.1 — 2026-07-08

### Added
- **Direct closed-loop validation against the authoritative HEC-RAS results.** Extracted a
  small committed ground-truth fixture (`tests/fixtures/anson-ras-reference.json`) from the
  Anson County WWTP HEC-RAS 6.6 2D breach model (NCDS public record) and added V-VAL-9/10/11:
  the demo geojson's peak water surface (452.59 ft NAVD88) matches the RAS-computed 2D
  Maximum Water Surface to 0.01 ft, all demo WSELs fall within the RAS envelope, and every
  scenario label maps to the real RAS plan title. V&V battery now 22 cases.

## v0.7.0 — 2026-07-08

Regulatory-grade rigor pass: verification, validation, uncertainty, and geodesy.

### Added
- **Verification & Validation suite** (`tests/vv-cases.js`, `tests/field-vv.test.js`) with
  declared acceptance tolerances, and an **auto-generated, CI-gated report**
  (`field/VALIDATION.md` via `scripts/gen-validation-report.js`) that cannot drift from the
  code. 19 cases across Verification (analytical oracles) and Validation (real data).
- **Measurement-uncertainty engine** (`field/fieldhydro-uncertainty.js`, `field/UNCERTAINTY.md`):
  ISO/JCGM GUM first-order propagation cross-validated by Monte Carlo. 95% confidence bands
  on AR ground registration, depth, and discharge, surfaced in the AR reticle HUD and the
  cross-section readouts and stamped into debug/EAP exports.
- **Shared hydraulics module** (`field/fieldhydro-hydraulics.js`): the subdivided Manning
  conveyance math as a single verified source of truth (cited to Chow 1959, HEC-RAS, Henderson).
- **Second real validation anchor**: the Anson County WWTP **HEC-RAS 2D breach analysis**
  (NC Dam Safety public record) validated for reprojection (EPSG:6543→WGS84), attribute
  integrity, and physical plausibility — alongside the Helene / USGS 03451500 gauged peak.
- **Rigorous geodesy**: `geoToEnuEcef` (exact geodetic→ECEF→ENU), `geodeticRadii`,
  `degPerMLat`/`degPerMLon`, and vertical-datum helpers `geoidToOrtho` (GEOID18) and
  `ngvd29ToNavd88`.
- **Versioning**: `field/version.js` single source of truth + this changelog.

### Changed
- **Geodesy bias fixed**: `geoToEnu`/`enuToGeo` now use the WGS84 ellipsoidal radii of
  curvature M(φ)/N(φ) instead of the semi-major axis, removing a +0.33% N-S / −0.11% E-W
  distance bias. All AR tools (`ar-prototype`, `ar-tap-to-mark`, `ar-cross-section`)
  converted onto the shared helpers so terrain sampling and feature projection agree.
- **Accessibility (Section 508 / WCAG 2.1)**: removed zoom-blocking viewport flags across
  the field tools (1.4.4 Resize Text), added `prefers-reduced-motion` handling and visible
  focus styles.

## v0.4–v0.6 (pre-changelog)

Prototype era: AR inundation viewer, LSPIV, sediment photo-sieving, cross-section +
conveyance, HEC-RAS adapter, offline area staging, closed-loop tap-to-mark, reviewer tour.
See `field/FIELD.md` for the architecture.
