thundercode commited on
Commit
64bac50
·
verified ·
1 Parent(s): 280cc90

release: add docs/FRONTEND.md

Browse files
Files changed (1) hide show
  1. docs/FRONTEND.md +1396 -0
docs/FRONTEND.md ADDED
@@ -0,0 +1,1396 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SatQuery AI — Frontend
2
+
3
+ **Chapter scope.** This chapter documents the SatQuery AI web frontend end to end: the static tier
4
+ and every page in it, the staging and deploy path that publishes it, the Analyze console in full
5
+ depth (every DOM handle, every state, every event), the eight-event execution protocol and the trace
6
+ bar it drives, the REAL-vs-PREVIEW driver split, the captured-run page, the Hugging Face header
7
+ link, cache-busting, and the Cloudflare platform traps that shape all of the above.
8
+
9
+ **Grounding.** Every claim below is taken from a file that was read for this chapter. Where a claim
10
+ comes from code, the file is cited inline, e.g. `(frontend/assets/js/mission.js)`. Where a number is
11
+ quoted it is a number that appears in a file; none is estimated. Where the evidence does not exist,
12
+ the text says exactly: `UNKNOWN — not established from the available evidence`.
13
+
14
+ **Status vocabulary** follows `release/DOCS_STYLE_GUIDE.md` §2: `IMPLEMENTED` · `VERIFIED` ·
15
+ `MEASURED` · `ATTEMPTED` · `NOT RUN` · `BLOCKED` · `DEFERRED` · `REJECTED` · `OPEN` · `RESOLVED` ·
16
+ `CLOSED`.
17
+
18
+ **Nothing in this chapter is a system-level accuracy claim.** Per `release/DOCS_STYLE_GUIDE.md` §3
19
+ there is **no end-to-end benchmark** for SatQuery AI; the frontend is a *client* of the service, and
20
+ the only system-level numbers quoted here are the ones the delivery documents themselves recorded
21
+ (live validation 3 passes × 8 cases, 8/8 each, 24 runs, 0 mock nodes, trace fill 94.4444 %).
22
+
23
+ ---
24
+
25
+ ## 1. What the frontend is, and what it is not
26
+
27
+ SatQuery AI's frontend is a **static site**. It is HTML, CSS, and ES modules served from Cloudflare
28
+ Pages. There is no build step that compiles application code, no bundler, no framework, no server
29
+ rendering, and no runtime dependency on a Node process. The staging tool
30
+ (`scripts/stage_pages.mjs`) copies a *reference-closed subset* of `frontend/` into an output
31
+ directory and then hands that directory to `wrangler`.
32
+
33
+ The site is **hermetic except for one page**. The staging tool computes and prints a reference
34
+ integrity and external-dependency audit, and it reports `HERMETIC` when a page's reference closure
35
+ contains zero network dependencies (`scripts/stage_pages.mjs`). The single deliberate exception is
36
+ `frontend/mission.html`, the Analyze console, which carries a live API base in a `<meta>` tag and can
37
+ call the deployed service. Everything else — the homepage, the essay, the atlas, the architecture
38
+ tour, the benchmark and research pages, the captured-run page — is designed to render without any
39
+ network call beyond its own assets.
40
+
41
+ > **Honesty note (drift recorded, not hidden).** An older comment inside `frontend/_headers` claimed
42
+ > the site was "100% static, zero network calls". That claim is **stale** and is not repeated here as
43
+ > current truth: `mission.html` is a live-calling page, and `mission.html` is one of the eleven
44
+ > shipped pages. The correct current statement is: *ten of eleven pages are hermetic; `mission.html`
45
+ > is the one live-calling page.*
46
+
47
+ ### 1.1 The design law the frontend was built under
48
+
49
+ `frontend/HANDOFF.md` is the governing design document for the frontend. Its §1 states the design
50
+ law; §2 defines the token system as CSS custom properties on `:root`; §3–§10 lay out build phases
51
+ A–G; §9 names the integration seam (`SQ.run().ingest`); §13 lists known hard limits; §14 lists the
52
+ real SatQuery schema type names that the frontend is allowed to speak.
53
+
54
+ The practical consequences of that design law, as they appear in the shipped code:
55
+
56
+ - **No fabricated imagery is presented as real.** Synthetic imagery produced at runtime carries a
57
+ `synthetic: true` flag (`frontend/assets/js/core.js`, `SQ.scene`), and pages that use placeholder
58
+ numbers say so in their own prose (e.g. `frontend/atlas.html` states its numbers are placeholders).
59
+ - **The eight-event vocabulary is fixed.** The frontend may not invent event names; it emits exactly
60
+ the eight names declared in `SQ.EVENT_NAMES` (`frontend/assets/js/core.js`).
61
+ - **The event stream is the seam.** Any driver — mock, live, or a captured replay — talks to the UI
62
+ only by calling `ingest(type, payload)`. Nothing else may mutate the console.
63
+
64
+ ---
65
+
66
+ ## 2. The static tier: file layout
67
+
68
+ The shipped frontend is a flat set of pages plus three asset trees.
69
+
70
+ ```
71
+ frontend/
72
+ *.html top-level pages (the staging seed set)
73
+ _headers Cloudflare Pages header rules (see §12)
74
+ HANDOFF.md the frontend design/handoff document
75
+ assets/
76
+ css/ stylesheets
77
+ js/
78
+ core.js SQ namespace: rng, scene synthesis, policy router,
79
+ event names, mock run driver, shared components
80
+ live.js the real HTTP ingestion client (assets + infer)
81
+ mission.js the Analyze console driver (PREVIEW + LIVE)
82
+ run.js the captured-run ("Anatomy of a Run") driver
83
+ <page drivers> per-page behaviour
84
+ data/
85
+ anatomy-run.js the captured real ResultEnvelope (sanitized)
86
+ img/ real EO imagery (eo/…), plates, thumbnails
87
+ video/ the launch film and clips
88
+ fonts/ webfonts
89
+ ```
90
+
91
+ Two facts about this layout matter for deployment:
92
+
93
+ 1. The **staging seed** is the set of top-level `frontend/*.html` files
94
+ (`scripts/stage_pages.mjs`). Pages are discovered from HTML, and then their reference closure
95
+ (CSS `@import`/`url()`, JS `import`/`export … from`, and dynamic imports) is walked so that only
96
+ referenced assets ship.
97
+ 2. Because the closure is reference-driven, **an asset that is not referenced by a reachable page
98
+ does not ship**. This is deliberate: it keeps the uploaded tree small and it makes dead assets
99
+ visible (they simply do not appear in the staged tree report).
100
+
101
+ ---
102
+
103
+ ## 3. The eleven pages
104
+
105
+ Eleven HTML pages ship. Each was read for this chapter. The table gives the page's purpose and its
106
+ `data-view` (the attribute each page's `<body>` carries, which the CSS uses to scope page-specific
107
+ rules).
108
+
109
+ | # | File | Purpose | Notes |
110
+ |---|---|---|---|
111
+ | 1 | `frontend/index.html` | Homepage / front door. Orbit hero, invitation form, open questions, essay film, discover/evidence/understand/measure/atlas sections. | Carries the launch video and the delta pair. |
112
+ | 2 | `frontend/mission.html` | **The Analyze console.** Query box, two upload widgets, intent panel, viewer, comparison, answer, evidence, confidence, provenance, trace bar, event drawer. | The **only** live-calling page. Carries `<meta name="satquery-api-base">`. |
113
+ | 3 | `frontend/architecture.html` | Architecture tour: how a query becomes an answer, stage by stage. | Footer discloses that its transmission is driven by the prototype mock event stream. |
114
+ | 4 | `frontend/run.html` | **Anatomy of a Run** — renders a real captured `ResultEnvelope` (`run_d124d8b9adea`). | Driven by `frontend/assets/js/run.js` over `frontend/assets/data/anatomy-run.js`. |
115
+ | 5 | `frontend/benchmark.html` | Benchmark page: measured results with an evidence-state legend. | Legend vocabulary: VERIFIED / SUPPORTED / UNVERIFIED / BLOCKED / NOT RUN. |
116
+ | 6 | `frontend/research.html` | Research notes: methods, calibration, honest caveats. | Links into the measurement story. |
117
+ | 7 | `frontend/journey.html` | The build journey / narrative page. | Carries the HF + GitHub header links. |
118
+ | 8 | `frontend/atlas.html` | Atlas of four real EO thumbnails. | Page states its numbers are placeholders. |
119
+ | 9 | `frontend/references.html` | References / citations page. | — |
120
+ | 10 | `frontend/video.html` | Video library: four clips, three planned shorts named. | — |
121
+ | 11 | `frontend/404.html` | Not-found page. | Prose says "Ten pages exist" but links five — drift, recorded in §13. |
122
+
123
+ ### 3.1 Page-by-page detail
124
+
125
+ **`index.html` (homepage).** 424 lines. Structure: a navigation bar carrying the GitHub and Hugging
126
+ Face links; an orbit hero using `assets/img/eo/nile-wide.jpg`; an invitation form whose action is
127
+ `mission.html`; an "open questions" list containing three `mission.html?q=…` links (so a visitor can
128
+ land in the Analyze console with a question pre-filled); an essay-film section using
129
+ `assets/video/satquery-launch-50s.mp4` with eight `data-chapters` markers; an "ask" section using
130
+ `assets/img/eo/delta-plain.jpg`; a "discover" section that presents the **delta-growth t0/t1 pair** as
131
+ a wipe slider (`delta-growth-t0-720` / `delta-growth-t1-720`); an "evidence" section using
132
+ `delta-growth-t2-2075.jpg`; an "understand" section listing six layers; a "measure" section with
133
+ benchmark and research cards; and an "atlas" section with four real EO thumbnails. The footer notes
134
+ name the event span `QUERY_RECEIVED` → `RESULT_ASSEMBLED`, i.e. the first and last of the eight
135
+ events.
136
+
137
+ **`mission.html` (Analyze console).** 297 lines. This is the page this chapter spends most of its
138
+ length on; see §5.
139
+
140
+ **`architecture.html`.** The architecture tour. It walks the reader from a natural-language query
141
+ through routing, planning, specialists, evidence, confidence, and result assembly. Its footer makes
142
+ an explicit honesty disclosure: the transmission shown on the page is driven by the **prototype mock
143
+ event stream**, not by a live run. That disclosure is the page doing the right thing — the animation
144
+ is real UI driven by the same eight-event seam, but the data behind it on this page is the mock
145
+ driver's.
146
+
147
+ **`run.html` ("Anatomy of a Run").** Renders a **real captured** envelope. See §9.
148
+
149
+ **`benchmark.html`.** Presents measured results. It carries an evidence-state legend whose
150
+ vocabulary is VERIFIED / SUPPORTED / UNVERIFIED / BLOCKED / NOT RUN, and it notes the measured
151
+ reliability curve. Per `release/DOCS_STYLE_GUIDE.md` §3, this page is the right place for the
152
+ per-artifact numbers (grounding under two protocols, change IoU, optical-SAR accuracy-with-macro-F1,
153
+ change-VQA two test sets, router validation-only), and it must never present them as a system-level
154
+ score.
155
+
156
+ **`research.html`.** Research notes: method, calibration, and caveats. This is where the calibration
157
+ result belongs, and per the style guide it must be stated correctly: ECE went **0.013755 → 0.014929
158
+ — worse**, and the transform is retained only because it is in the frozen config.
159
+
160
+ **`journey.html`.** Narrative page for the build process.
161
+
162
+ **`atlas.html`.** Four real EO thumbnails. The page states in its own prose that its numbers are
163
+ placeholders. That statement is correct and must be preserved: the atlas is a *gallery*, not a
164
+ measurement.
165
+
166
+ **`references.html`.** Citations.
167
+
168
+ **`video.html`.** Lists four clips and names three planned shorts. The planned shorts are labelled as
169
+ planned, not shipped.
170
+
171
+ **`404.html`.** The not-found page. Its prose says "Ten pages exist" while eleven do, and it links
172
+ five. This is documentation drift inside a shipped page; it is recorded here and in §13 rather than
173
+ silently corrected, because correcting it would be an edit outside this chapter's scope.
174
+
175
+ ### 3.2 The Hugging Face header link — present on all eleven pages
176
+
177
+ Every one of the eleven pages carries, in its navigation, both:
178
+
179
+ - a **GitHub** link to `https://github.com/Anish-lab-blip/SatQuery-AI`, and
180
+ - a **Hugging Face** link to `https://huggingface.co/thundercode/SatQuery`.
181
+
182
+ This was verified by searching all `frontend/*.html` for `huggingface.co` and `github.com` and
183
+ confirming a match in each of: `index`, `journey`, `mission`, `404`, `video`, `atlas`,
184
+ `architecture`, `benchmark`, `run`, `research`, `references` — eleven files, eleven matches each.
185
+ `docs/FINAL_DELIVERY_TODO.md` records this as a post-handoff sprint outcome ("the HF link on all 11
186
+ pages").
187
+
188
+ The reason this is called out as its own subsection: the public release is *GitHub + Hugging Face*,
189
+ and the requirement that the HF link appear on **all** pages (not just the homepage) is a delivery
190
+ requirement, so it is stated as a verified fact with the method of verification.
191
+
192
+ ---
193
+
194
+ ## 4. Staging and deploy path
195
+
196
+ ### 4.1 `scripts/stage_pages.mjs` — the reference-closed staging tool
197
+
198
+ `scripts/stage_pages.mjs` is 378 lines and is the tool that turns the working `frontend/` directory
199
+ into a deployable tree. It is deliberately conservative.
200
+
201
+ **Constants.**
202
+
203
+ | Constant | Value | Meaning |
204
+ |---|---|---|
205
+ | `PAGES_FILE_LIMIT` | `26214400` (25 MiB) | Cloudflare Pages per-file hard limit. |
206
+ | `BIG_WARN_BYTES` | `10485760` (10 MiB) | Warn threshold for a large file. |
207
+
208
+ **Reference extraction.** The tool uses a small set of regexes to find references inside each file
209
+ type:
210
+
211
+ - `RE_HTML` — HTML references (`<script src>`, `<link href>`, `<img src>`, etc.)
212
+ - `RE_CSS_IMPORT` — CSS `@import`
213
+ - `RE_CSS_URL` — CSS `url(...)`
214
+ - `RE_JS_IMPORT` — JS `import … from`
215
+ - `RE_JS_EXPORT` — JS `export … from`
216
+ - `RE_JS_DYN` — JS dynamic `import(...)`
217
+
218
+ Supporting helpers: `stripComments()` (so a reference inside a comment does not become a false
219
+ edge), `extractRefs()`, `isExternal()` (absolute URLs and protocol-relative URLs are not followed),
220
+ `stripQueryHash()` (so `app.js?v=2` resolves to `app.js`), and `insideFrontend()` (a guard so a
221
+ reference cannot escape the `frontend/` root).
222
+
223
+ **Algorithm.**
224
+
225
+ 1. **Seed.** Take the set of top-level `frontend/*.html` files.
226
+ 2. **Closure walk.** For each file in the frontier, extract its references, resolve each to a
227
+ path inside `frontend/`, and add the new ones to the frontier. Repeat until the frontier is empty.
228
+ 3. **Copy.** Copy every file in the closure into the output root, preserving relative paths.
229
+ 4. **Size gate.** If any file exceeds `PAGES_FILE_LIMIT` (25 MiB), hard-fail with **exit code 2**.
230
+ Files above `BIG_WARN_BYTES` (10 MiB) produce a warning.
231
+ 5. **Verify.** Re-walk the *staged* tree and confirm the closure is intact (no dangling reference).
232
+ Failure is **exit code 3**.
233
+ 6. **Report.** Print four report blocks:
234
+ - `=== STAGED TREE ===`
235
+ - `=== REFERENCE INTEGRITY ===`
236
+ - `=== EXTERNAL DEPENDENCY AUDIT ===` — prints `HERMETIC` when zero network dependencies are
237
+ found.
238
+ - `=== OPTIONS APPLIED ===`
239
+ 7. **Hint.** Print the deploy command to run next:
240
+ `npx wrangler pages deploy "<OUT_ROOT>" --project-name <name>`.
241
+
242
+ **Why the exit codes matter.** A staging run that silently produced an incomplete tree would deploy
243
+ a broken site; a staging run that silently produced an over-limit tree would deploy a site that
244
+ Cloudflare rejects. The tool therefore fails loudly *before* upload (exit 2 for size, exit 3 for
245
+ integrity) rather than letting `wrangler` discover the problem.
246
+
247
+ **Argument parsing.** `parseArgs()` handles the CLI surface and `usage()` prints help. The tool is
248
+ invoked as a Node script (`node scripts/stage_pages.mjs …`).
249
+
250
+ ### 4.2 The deploy command
251
+
252
+ The tool's own final hint is the deploy step:
253
+
254
+ ```
255
+ npx wrangler pages deploy "<OUT_ROOT>" --project-name <name>
256
+ ```
257
+
258
+ Deployment is therefore: **stage to a directory → `wrangler pages deploy` that directory**. There is
259
+ no compile step between the two. The deployed frontend HEAD recorded in the delivery documents is
260
+ `2d7ae53b482d` (`docs/FINAL_DELIVERY_TODO.md`, `release/DOCS_STYLE_GUIDE.md` §3).
261
+
262
+ > **Superseded-topology note.** `docs/DEPLOYMENT_ARCHITECTURE.md` opens with a superseded-topology
263
+ > banner, and its body still names Railway / HF-Space hosts while the active topology is
264
+ > Render / Codespace (`docs/DEPLOYMENT_TOPOLOGY.md`). For the frontend specifically, the host is
265
+ > Cloudflare Pages in both readings; the drift concerns the *backend* hosts, not the static tier.
266
+
267
+ ---
268
+
269
+ ## 5. The Analyze console (`frontend/mission.html`) in depth
270
+
271
+ The Analyze console is the frontend's centre of gravity. This section documents its markup (every
272
+ handle), its state machine, its two drivers, and its event rendering.
273
+
274
+ ### 5.1 Markup and DOM handles
275
+
276
+ `frontend/mission.html` is 297 lines. Its `<head>` carries the live API base:
277
+
278
+ ```html
279
+ <meta name="satquery-api-base" content="https://satquery-backend-m4yv.onrender.com">
280
+ ```
281
+
282
+ That meta tag is the second entry in the API-base resolution order (see §5.4). The page loads three
283
+ scripts, in order:
284
+
285
+ ```html
286
+ <script src="assets/js/core.js"></script>
287
+ <script src="assets/js/live.js"></script>
288
+ <script src="assets/js/mission.js"></script>
289
+ ```
290
+
291
+ `core.js` defines the `SQ` namespace and the mock driver; `live.js` defines the real HTTP client;
292
+ `mission.js` is the page driver that decides which of the two to use. The load order is significant:
293
+ `mission.js` runs last because it consumes both.
294
+
295
+ The console's handles, by region:
296
+
297
+ **Query and run.**
298
+
299
+ | Handle | Role |
300
+ |---|---|
301
+ | `#qtext` | The natural-language query input. |
302
+ | `#btnRun` | The Run button. |
303
+ | `#runid` | Displays the run identifier for the current run. |
304
+
305
+ **Observation / upload.**
306
+
307
+ | Handle | Role |
308
+ |---|---|
309
+ | `#obsTail` | The observation status tail. Its **initial text content is `none`** — i.e. no asset loaded yet. |
310
+ | `#dropZone` | The drop target for a file. |
311
+ | `#fileInput` | The primary file input (the single observation). |
312
+ | `#obsNote` | The note under the observation widget. |
313
+
314
+ **Metadata.**
315
+
316
+ | Handle | Role |
317
+ |---|---|
318
+ | `#metaHost` | Container for the metadata readout. |
319
+ | `#mFile` | Metadata: file name. |
320
+ | `#mAcq` | Metadata: acquisition date (one of the `#m*` fields inside `#metaHost`). |
321
+ | `#metaEmpty` | The empty-state placeholder for the metadata block. |
322
+
323
+ **Intent panel.**
324
+
325
+ | Handle | Role |
326
+ |---|---|
327
+ | `#intentTail` | Intent status tail. |
328
+ | `#intentHost` | Container for the parsed intent (task, assets, route). |
329
+ | `#pairTail` | Pair status tail (for the two-asset change tasks). |
330
+ | `#pairNote` | Note under the pair widget. |
331
+ | `#fileInputT0` | The **second** file input — the `t0` (before) image for paired tasks. |
332
+
333
+ **Viewer.**
334
+
335
+ | Handle | Role |
336
+ |---|---|
337
+ | `#vbtns` | Viewer mode buttons. |
338
+ | `#viewerState` | Viewer state label. |
339
+ | `#plate` | The plate container. |
340
+ | `#plateImg` | The plate image; its `src` is `assets/img/eo/reservoir-low.jpg`. |
341
+ | `#ev` | The evidence overlay layer on the plate. |
342
+ | `#evNote` | Note under the evidence overlay. |
343
+ | `#plateCreditLead` | Plate credit lead-in text. |
344
+ | `#plateCredit` | Plate credit text. |
345
+
346
+ **Comparison (paired tasks).**
347
+
348
+ | Handle | Role |
349
+ |---|---|
350
+ | `#cmpWrap` | Comparison wrapper. |
351
+ | `#cmpT0` | The t0 pane. |
352
+ | `#cmpT1` | The t1 pane. |
353
+ | `#cmpRange` | The comparison range/slider control. |
354
+ | `#cmpCredit` | Comparison credit. |
355
+ | `#cmpEmpty` | Comparison empty state. |
356
+
357
+ **Answer.**
358
+
359
+ | Handle | Role |
360
+ |---|---|
361
+ | `#answerHost` | Container for the rendered answer. |
362
+ | `#ansTail` | Answer status tail. |
363
+
364
+ **Evidence.**
365
+
366
+ | Handle | Role |
367
+ |---|---|
368
+ | `#evHost` | Container for the evidence list. |
369
+ | `#evEmpty` | Evidence empty state. |
370
+ | `#evTail` | Evidence status tail. |
371
+
372
+ **Confidence.**
373
+
374
+ | Handle | Role |
375
+ |---|---|
376
+ | `#confHost` | Container for the confidence readout. |
377
+ | `#confC` | The confidence value. |
378
+ | `#confNote` | Note under the confidence value. |
379
+
380
+ **Provenance.**
381
+
382
+ | Handle | Role |
383
+ |---|---|
384
+ | `#provHost` | Container for provenance. |
385
+ | `#pRun` | Provenance: run id. |
386
+ | `#pPolicy` | Provenance: policy. |
387
+ | `#pProtocol` | Provenance: protocol. |
388
+ | `#pSchema` | Provenance: schema version. |
389
+
390
+ **Report and trace.**
391
+
392
+ | Handle | Role |
393
+ |---|---|
394
+ | `#btnReport` | The report button. |
395
+ | `#ctrace` | The trace container. |
396
+ | `#traceNow` | The "now" label on the trace bar. |
397
+ | `#trace` | The trace bar (the element whose width is animated). |
398
+ | `#traceNote` | Note under the trace bar. |
399
+
400
+ **Event drawer.**
401
+
402
+ | Handle | Role |
403
+ |---|---|
404
+ | `#drawer` | The event-log drawer. |
405
+ | `#evlog` | The event log list. |
406
+ | `#btnClose` | Close-drawer button. |
407
+ | `#btnEvents` | Open-drawer button. |
408
+
409
+ ### 5.2 The intent panel
410
+
411
+ The intent panel (`#intentHost`, `#intentTail`) is rendered by `renderIntent()` in
412
+ `frontend/assets/js/mission.js`. It shows the *interpreted* query: which task the router chose, which
413
+ assets the task requires, and which route (live vs mock) will be taken.
414
+
415
+ The interpretation itself is `interpret()` in `mission.js` — a lexical router that runs in the
416
+ browser. Its notable features, as read from the file:
417
+
418
+ - a change stem `/chang/` (no `\b` word boundary) so "change"/"changed"/"changes" all match;
419
+ - a `newAsChange` rule so phrasing like "new …" can be read as a change request;
420
+ - a caption regex for caption/describe phrasings.
421
+
422
+ `interpret()` is deliberately simple and deterministic. It exists so the console can show the user a
423
+ *reason* for the task it is about to run, and so the console can decide which file inputs are
424
+ relevant. It is **not** the server-side router: the server has its own deterministic policy planner
425
+ (see the `SERVING.md` chapter and `core/controller.py`). The browser-side `interpret()` is a UI
426
+ affordance; the authoritative routing decision is the server's, and the console renders what the
427
+ server returns.
428
+
429
+ ### 5.3 Task selection and asset requirements
430
+
431
+ `mission.js` maps the interpreted intent onto a server task name via `ROUTE_TASK_TO_SERVER`. The
432
+ paired tasks are declared in `PAIRED_TASKS`:
433
+
434
+ ```js
435
+ PAIRED_TASKS = { change, change_vqa, optical_sar }
436
+ ```
437
+
438
+ These three tasks need **two** assets (a before/after pair), which is why the console has a second
439
+ file input (`#fileInputT0`) and a comparison region (`#cmpWrap`). When a paired task is selected but
440
+ only one asset is available, the console falls back to a single-asset task via
441
+ `SINGLE_ASSET_FALLBACK = 'vqa'`. This is a UI-level fallback: rather than failing the run, the
442
+ console narrows the request to something one image can answer.
443
+
444
+ For optical-SAR there is a dedicated precondition check, `validateOpticalSar()`, because that task
445
+ has modality-specific requirements. `assetsForTask()` assembles the asset list the chosen task needs.
446
+
447
+ ### 5.4 API-base resolution
448
+
449
+ `frontend/assets/js/live.js` defines the resolution order for the API base in
450
+ `SQ.live.baseUrl()`:
451
+
452
+ 1. `window.SATQUERY_API_BASE` (a runtime override, useful for testing), then
453
+ 2. `<meta name="satquery-api-base">` (the page's declared base — on `mission.html` this is
454
+ `https://satquery-backend-m4yv.onrender.com`), then
455
+ 3. the default `/api` (a same-origin path).
456
+
457
+ `_normalizeBase()` normalises trailing slashes, and `SQ.live.url()` composes the final URL.
458
+ `SQ.ENDPOINTS` names the four endpoints the client talks to:
459
+
460
+ ```js
461
+ SQ.ENDPOINTS = { assets: '/assets', infer: '/infer', capabilities: '/capabilities', health: '/health' }
462
+ ```
463
+
464
+ With the default `/api` base these resolve to `/api/assets`, `/api/infer`, `/api/capabilities`, and
465
+ `/api/health`. On the deployed configuration the base is the Render orchestrator host, which is the
466
+ `/api/*` mirror of the four-endpoint contract (see the `SERVING.md` chapter).
467
+
468
+ ### 5.5 Upload widgets
469
+
470
+ Two file inputs exist: `#fileInput` (primary) and `#fileInputT0` (the before image for paired
471
+ tasks). Both are wired through `handleFile()` in `mission.js`, and both feed
472
+ `SQ.live.uploadAsset()` in `live.js`.
473
+
474
+ `live.js` declares the accepted content types:
475
+
476
+ ```js
477
+ SQ.CONTENT_TYPES = { tif, tiff, png, jpg, jpeg }
478
+ ```
479
+
480
+ and maps a file to its MIME type via `SQ.contentTypeFor()`. The upload is a **raw-bytes POST with a
481
+ `Content-Type` header** — not a multipart form. This mirrors the server contract: `POST /v1/assets`
482
+ takes the file as the request body with its content type in the header, and `POST /v1/analyze` takes
483
+ JSON (multipart is explicitly *not* implemented — see `docs/API_CONTRACT.md` §2.4 and the `SERVING.md`
484
+ chapter).
485
+
486
+ `uploadAsset()` asserts that the response contains an `asset_id`; `uploadAssets()` uploads a list
487
+ **sequentially** (so the second upload cannot race the first). The returned `asset_id` is an opaque
488
+ handle — the client never parses it, it only passes it back. The asset store's TTL and the fact that
489
+ handles are ephemeral are documented in `SERVING.md`.
490
+
491
+ ### 5.6 The observation tail: `none` → ready
492
+
493
+ `#obsTail` starts with text content `none`. When an asset is uploaded successfully, the tail is
494
+ updated to a ready state. This is the console's way of making the *precondition* for a run visible:
495
+ a query can be typed at any time, but a run that requires an asset cannot produce evidence until an
496
+ asset is present. The `#obsNote` field carries the supporting note.
497
+
498
+ ### 5.7 The Run button, `#runid`, and `#answerHost`
499
+
500
+ Pressing `#btnRun` calls `runQuery()` in `mission.js`. `runQuery()` decides between the two drivers
501
+ (§6) and then dispatches. `#runid` is populated with the run identifier the service returns
502
+ (`run_…`); `#answerHost` receives the rendered answer.
503
+
504
+ ### 5.8 The trace bar and the eight events
505
+
506
+ The console's most load-bearing UI element is the trace bar. It is driven entirely by the eight
507
+ execution events.
508
+
509
+ **The eight event names** are declared once, in `frontend/assets/js/core.js`:
510
+
511
+ ```js
512
+ SQ.EVENT_NAMES = [
513
+ 'QUERY_RECEIVED',
514
+ 'QUERY_UNDERSTOOD',
515
+ 'ROUTE_SELECTED',
516
+ 'SPECIALIST_STARTED',
517
+ 'SPECIALIST_COMPLETED',
518
+ 'EVIDENCE_GENERATED',
519
+ 'CONFIDENCE_COMPUTED',
520
+ 'RESULT_ASSEMBLED'
521
+ ]
522
+ ```
523
+
524
+ (Declared at `core.js:616–620`.) These names are the protocol between any driver and the UI. The
525
+ `architecture.html` footer's disclosure — that its transmission is driven by the mock event stream —
526
+ is a statement about *which driver* feeds these names, not about the names themselves.
527
+
528
+ **The nine UI states.** `mission.js` declares `STATES` (nine `ControllerState` values) and maps each
529
+ event to a state via `EVENT_TO_STATE`, with per-state explanatory text in `STATE_NOTE`. Nine states
530
+ over eight events is not an inconsistency: there is a state for "idle / not started" plus the eight
531
+ event-driven states.
532
+
533
+ **The fill formula.** `markState()` sets the trace bar width with:
534
+
535
+ ```js
536
+ traceFill.style.width = ((traceProgress + 0.5) / STATES.length) * 100 + '%'
537
+ ```
538
+
539
+ With eight events completed against nine states, the final fill is
540
+ `((8 + 0.5) / 9) × 100` = **94.4444 %**. This is why the delivery documents record the trace fill as
541
+ 94.4444 %: it is the arithmetic consequence of the formula, not a measurement of a rendering. The
542
+ `+ 0.5` means the bar advances *half a step* on entry to each state, so a completed eight-event run
543
+ lands at 8.5/9 rather than 8/9 or 9/9. The remaining 5.5556 % corresponds to the ninth state, which
544
+ a completed run does not enter.
545
+
546
+ `buildTrace()` constructs the trace bar's segments; `logEvent()` appends to the event log
547
+ (`#evlog`); `resetUI()` clears the console back to its initial state (including resetting `#obsTail`
548
+ to `none`).
549
+
550
+ ### 5.9 The event drawer
551
+
552
+ `#drawer` is the event log, opened by `#btnEvents` and closed by `#btnClose`. `#evlog` is the list
553
+ itself. Each event appended by `logEvent()` records the event type and its payload summary, so a
554
+ reader can see the full ordered sequence rather than only the current state. The drawer is what makes
555
+ the "0 mock nodes" / "9 preview nodes" distinction auditable by a human: the live driver's log
556
+ contains no mock nodes; the preview driver's log contains nine.
557
+
558
+ ---
559
+
560
+ ## 6. REAL vs PREVIEW: two drivers, one event seam
561
+
562
+ `mission.js` opens with the comment "TWO DRIVERS, ONE EVENT SEAM". That is the whole design: two
563
+ driver implementations, one `ingest()` seam, one UI.
564
+
565
+ ### 6.1 The seam
566
+
567
+ `SQ.run(opts)` in `core.js` owns an `ingest()` switch (lines ~742–788) that dispatches each of the
568
+ eight event types to the UI handlers. Any driver that wants to drive the console calls
569
+ `ingest(type, payload)`; it does not touch the DOM. The console boot sequence builds the engine with
570
+ `engine = SQ.run(...)`, then calls `runMock(QUERY)` to paint an initial state, then
571
+ `loadCapabilities()` to fetch the service's capability block.
572
+
573
+ ### 6.2 PREVIEW (`runMock`)
574
+
575
+ `runMock()` is the **preview** driver. Its properties, as read from `mission.js` and `core.js`:
576
+
577
+ - It emits **empty payloads** — the payloads carry the shape of the data but not real values, because
578
+ there is no real run behind it.
579
+ - It labels the console as a preview (`is-mock`).
580
+ - It emits **nine mock nodes** — the event log for a preview run contains nine mock nodes.
581
+ - It drives the trace bar through the same `markState()` path, so the fill arithmetic is identical.
582
+
583
+ `core.js`'s `startMock()` drives the sequence with `setTimeout` timings, so the preview is *animated*:
584
+ each event arrives after a short delay, which is what makes the trace bar and the event drawer move.
585
+
586
+ **What preview does not emit.** The preview driver emits **no specialist events** — i.e. no
587
+ `SPECIALIST_STARTED` / `SPECIALIST_COMPLETED` for a real specialist. This is the honest distinction
588
+ between the two paths: the preview can show the *envelope* of a run, but it cannot show a specialist
589
+ that actually ran, because no specialist ran.
590
+
591
+ ### 6.3 REAL (`runLive`)
592
+
593
+ `runLive()` is the **live** driver. Its properties:
594
+
595
+ - It makes **real HTTP calls** via `SQ.live` (`live.js`).
596
+ - It sets a `liveRun` flag.
597
+ - It reads two response headers from `SQ.live.infer()`: `X-SatQuery-State` and
598
+ `x-satquery-transport`. The state header carries the controller's state (see the nine
599
+ `ControllerState` values); the transport header records how the response was carried (the tunnel
600
+ transport vs a direct/forwarded transport).
601
+ - It translates failures with `translateError()` and, for upload/inference failures,
602
+ `SQ.live.describeFailure()` / `LiveError` in `live.js`.
603
+ - A live run shows **0 mock nodes** — the event log contains no mock nodes at all.
604
+
605
+ ### 6.4 Why the 0-vs-9 distinction is the honesty test
606
+
607
+ The delivery documents record that live validation produced **24 runs** (3 passes × 8 cases, 8/8 each)
608
+ with **0 mock nodes**. That number is only meaningful because the preview path *does* produce mock
609
+ nodes (nine of them). The console's event drawer therefore lets a reader distinguish, from the UI
610
+ alone, whether what they are looking at is a real run or a preview. This is the frontend's
611
+ contribution to the project's truthfulness discipline: the same eight-event vocabulary is used for
612
+ both, and the drawer is what tells them apart.
613
+
614
+ ### 6.5 `loadCapabilities()` and `setMode()`
615
+
616
+ `loadCapabilities()` calls `SQ.live.capabilities()` (i.e. `GET /api/capabilities` on the deployed
617
+ base) and renders the capability block. `setMode()` switches the console between modes. Because
618
+ capabilities are fetched live, the console can show which tasks are available *right now* on the
619
+ deployed service — which matters because the deployed device is CPU and because some capabilities
620
+ are gated on artifacts that may be absent (the `SERVING.md` chapter documents the capability adapter
621
+ and the five-word vocabulary it emits).
622
+
623
+ ### 6.6 The test hook
624
+
625
+ `mission.js` exposes `window.SQ_MISSION` as a test hook. It lets an automated harness drive the
626
+ console (select a task, inject a file, press run) without synthesising DOM events. This is how the
627
+ live validation runs in the delivery documents were executed against the page.
628
+
629
+ ### 6.7 `translateError()`
630
+
631
+ `translateError()` maps a service error into human-readable text in the console. It is the frontend
632
+ half of the error contract: the service returns a machine code and an HTTP status
633
+ (`docs/API_CONTRACT.md` §5.1–§5.3; `gateway/policy.py` `_CODE_STATUS`), and the console turns that
634
+ into a sentence a person can act on. The console does not invent codes; it renders the ones it
635
+ receives. One consequence worth stating: a `422` from the service is *not* necessarily a validation
636
+ failure of the user's data — see the G-1 annotation-scope defect in the `SERVING.md` chapter, where a
637
+ `422 {"detail":[{"loc":["query","request"]}]}` is a *server-side* bug that masquerades as a client
638
+ validation error. `translateError()` will render it as an error; only the backend fix removes it.
639
+
640
+ ---
641
+
642
+ ## 7. The captured-run page: "Anatomy of a Run" (`run.html`)
643
+
644
+ `frontend/run.html` renders a **real captured** `ResultEnvelope`. This is the page that lets a reader
645
+ inspect an actual run without running anything.
646
+
647
+ ### 7.1 The captured envelope
648
+
649
+ The data lives in `frontend/assets/data/anatomy-run.js` (329 lines), assigned to
650
+ `window.SATQUERY_ANATOMY_RUN`. Its header states the provenance:
651
+
652
+ - `_source`: "Captured live 2026-09-25 … Sanitized".
653
+
654
+ The fields that matter, all read from the file:
655
+
656
+ | Field | Value |
657
+ |---|---|
658
+ | `run_id` | `run_d124d8b9adea` |
659
+ | `task` | `grounding` |
660
+ | `query` | "Where is the reservoir?" |
661
+ | `answer` | "[grounding] Located 3 candidate region(s) … Highest objectness 0.61." |
662
+ | `config_hash` | `78f1e3700da15aa1` |
663
+ | `transport` | `tunnel` |
664
+ | `intent.source` | `forced` |
665
+ | plan | `step_001` grounding, `requires_assets` |
666
+ | steps | 8 steps, `RECEIVE` → `RESPOND` |
667
+ | `selected_models` | ViT-B-32 (RemoteCLIP path) → GroundingHead (`params=1052677`) |
668
+ | evidence | 4 items: 3 `bounding_box` + 1 `statistic` |
669
+ | regions | 3 (`region_1cd3973de749`, …) |
670
+ | confidence | raw `0.5231253252136926` / calibrated `0.5236623182649384` (`temperature_scaling`) |
671
+ | calibration component | `temperature: 0.9772731820958189`, `calibration_samples: 16441.0` |
672
+ | timings | `step_001: 209.873` |
673
+ | geospatial | 730×730, `has_crs false` |
674
+ | warnings | 2 — no CRS; contradictory spatial claims |
675
+
676
+ ### 7.2 How the page renders it
677
+
678
+ `frontend/assets/js/run.js` (380 lines) is the driver. It reads `window.SATQUERY_ANATOMY_RUN` and
679
+ exposes the envelope through a set of named views: `QUERY`, `TASK`, `RUN_ID`, `MODELS`, `EVIDENCE`,
680
+ `CONF`, `TIMINGS`, `GEO`, `INTENT`, `PLAN`, `HASH`, `PLATE`.
681
+
682
+ - `REGIONS` is built from `A.regions`, so the three captured regions drive the plate overlays.
683
+ - `paintAll()` loads the **real plate image** and clears the `t0` and `diff` layers (this run has no
684
+ before/after pair, so those layers are empty rather than faked).
685
+ - `buildEvidence()` renders the four evidence items; `evCandidates()`, `evLock()`, and
686
+ `evConfirmed()` render the three stages of the evidence story (candidates → locked → confirmed).
687
+ - `SPECIALISTS_FOR_TASK` maps the task to the specialists that would run, so the page can show the
688
+ specialist panel even though this run's only specialist is grounding.
689
+ - `buildLattice()` builds the step lattice from the 8 captured steps.
690
+ - `DATA` is a table of eight key/value views, one per stage, and **each entry names the event** that
691
+ corresponds to that stage — i.e. the captured page is wired to the same eight-event vocabulary.
692
+ - `setStage()`, `resetEvidence()`, and `gotoStep()` drive the page as the reader scrolls or uses the
693
+ keyboard.
694
+
695
+ ### 7.3 What the page proves, and what it does not
696
+
697
+ It **proves**: a real grounding run was captured, sanitized, and shipped with its full envelope —
698
+ run id, task, query, answer, config hash, transport, intent source, plan, steps, selected models with
699
+ parameter counts, evidence with types, regions, raw and calibrated confidence with the calibration
700
+ component and sample count, timings, geospatial facts, and warnings. A reader can verify that the
701
+ number shown as "confidence" on the page is a *calibrated* value with a documented temperature and a
702
+ documented calibration-sample count.
703
+
704
+ It **does not prove**: any system-level accuracy. One captured run is one run. Per
705
+ `release/DOCS_STYLE_GUIDE.md` §3 there is **no end-to-end benchmark**, and this page does not create
706
+ one. The calibrated confidence `0.5236623182649384` is a per-run confidence, not an accuracy.
707
+
708
+ The two captured warnings are also part of the honest record: `has_crs false` (the imagery had no
709
+ coordinate reference system) and "contradictory spatial claims". Both are shown rather than
710
+ suppressed.
711
+
712
+ ---
713
+
714
+ ## 8. Benchmark, Research, and Lab pages
715
+
716
+ The frontend has a measurement-facing tier whose job is to present numbers *with their status*.
717
+
718
+ - **`benchmark.html`** carries the evidence-state legend: **VERIFIED / SUPPORTED / UNVERIFIED /
719
+ BLOCKED / NOT RUN**. It notes the measured reliability curve. This page is where the per-artifact
720
+ results live, and per the style guide each must be stated with its correct qualification:
721
+ grounding under **two protocols** (canonical 0.2838 / matched6 0.2566) and **two decode variants**
722
+ (head_argmax 0.1215, zero-shot 0.0972) — never one alone; optical-SAR accuracy **0.931 with
723
+ macro-F1 0.434161**, ruling **OPEN**; change-VQA **two** test sets (test 0.697626/0.378373 and
724
+ test2 0.651469/0.372309), ruling **OPEN**; router **0.965116 = validation, ungated, n = 86**, test
725
+ split **NOT RUN**; the VLM adapter **usable** (exact_match 0.963) but **ACCEPTANCE-REJECTED**.
726
+ - **`research.html`** carries the method and caveat material, including the calibration result stated
727
+ correctly: ECE **0.013755 → 0.014929 — worse**.
728
+ - **The Lab page.** The brief for this chapter names a "Lab" page. `frontend/HANDOFF.md` §12 gives the
729
+ file map, and the eleven shipped pages are enumerated in §3 above. A page named "Lab" is **not**
730
+ among the eleven HTML files read for this chapter. The nearest things are the Analyze console
731
+ (`mission.html`) and the captured-run page (`run.html`), which are the pages where a reader can
732
+ "do" or "inspect" work. Whether a page named "Lab" existed at any point and was renamed or dropped
733
+ is `UNKNOWN — not established from the available evidence`.
734
+
735
+ ---
736
+
737
+ ## 9. The Hugging Face header link (delivery requirement)
738
+
739
+ Stated separately because it is a delivery requirement with a verification method. See §3.2: the
740
+ Hugging Face link `https://huggingface.co/thundercode/SatQuery` and the GitHub link
741
+ `https://github.com/Anish-lab-blip/SatQuery-AI` are present in the navigation of **all eleven**
742
+ pages, verified by searching every `frontend/*.html` for both hostnames.
743
+
744
+ ---
745
+
746
+ ## 10. Cache-busting behaviour
747
+
748
+ The frontend uses **URL-versioned assets** plus **header rules** to control caching. The header rules
749
+ live in `frontend/_headers` (a Cloudflare Pages file), and the versioning is visible in the markup.
750
+
751
+ ### 10.1 The `_headers` rules
752
+
753
+ `frontend/_headers` declares:
754
+
755
+ | Path pattern | Rule |
756
+ |---|---|
757
+ | `/*` | Baseline security headers: `X-Content-Type-Options`, `Referrer-Policy`, `X-Frame-Options`, `Cross-Origin-Opener-Policy`. |
758
+ | `/` and `/*.html` | Revalidate. |
759
+ | `/assets/video/*` | `max-age=604800` (7 days). |
760
+ | `/assets/fonts/*` | `max-age=31536000` (1 year). |
761
+ | `/assets/css/*` | `public, max-age=0, must-revalidate`. |
762
+ | `/assets/js/*` | `public, max-age=0, must-revalidate`. |
763
+ | `/assets/img/*` | `max-age=604800` (7 days). |
764
+
765
+ ### 10.2 The rule that matters for correctness
766
+
767
+ **JS and CSS are served `public, max-age=0, must-revalidate`.** That is the safe setting for code:
768
+ the browser may cache a copy but must revalidate before using it. This is what makes a code change
769
+ take effect without asking users to hard-refresh. Images, fonts, and video get long lifetimes because
770
+ they are content, not logic — and because a *changed* image is given a **new URL** rather than
771
+ overwriting the old one.
772
+
773
+ ### 10.3 The delta-growth pair as the worked example
774
+
775
+ `frontend/_headers` itself carries a note that the delta-growth image pair was given **new URLs**
776
+ when it changed (`delta-growth-t0-720` / `delta-growth-t1-720`, referenced from `index.html`). That is
777
+ the correct pattern for a long-cached asset: change the URL, keep the long `max-age`. The homepage's
778
+ wipe slider uses that pair, and the "evidence" section uses `delta-growth-t2-2075.jpg` — a third URL
779
+ in the same family.
780
+
781
+ ---
782
+
783
+ ## 11. Cloudflare platform traps
784
+
785
+ Two Cloudflare behaviours shape this frontend. Both are recorded because a future maintainer will hit
786
+ them.
787
+
788
+ ### 11.1 `_headers` rules CONCATENATE (they do not override)
789
+
790
+ This is the single most surprising Cloudflare Pages behaviour in this project, and
791
+ `frontend/_headers` documents it **verbatim in a comment inside the file**. The rule is: when more
792
+ than one `_headers` rule matches a path, Cloudflare **concatenates** the header values rather than
793
+ letting the more specific rule override the more general one.
794
+
795
+ The practical consequence: if two rules both set `Cache-Control`, the client receives **two**
796
+ `Cache-Control` values. Chromium honours the **first** `max-age` it sees. So a broad rule that sets
797
+ `max-age=0` and a specific rule that sets `max-age=604800` do not "resolve" to the specific one — the
798
+ client sees both, in order, and takes the first.
799
+
800
+ `docs/FINAL_DELIVERY_TODO.md` §1.7 lists this as known blocker item 9. The mitigation, as evidenced
801
+ by the shipped `_headers`, is to **scope the patterns so that they do not overlap** where the value
802
+ must be exact — i.e. write one rule per asset tree rather than a general rule plus an override. The
803
+ `/assets/js/*` and `/assets/css/*` rules are separate from `/assets/img/*` precisely so that each
804
+ tree has exactly one matching rule and there is nothing to concatenate.
805
+
806
+ ### 11.2 The 308 `.html` → extensionless redirect
807
+
808
+ Cloudflare Pages issues a **308** redirect from a path that ends in `.html` to the extensionless
809
+ path: a request for `/run.html` redirects to `/run`. A 308 preserves the method (unlike 301/302 in
810
+ some clients), so a `POST` is not silently turned into a `GET`, but the redirect still happens and the
811
+ final URL differs from the requested one.
812
+
813
+ The second, related trap is the **trailing-slash 307**: Starlette's `redirect_slashes` behaviour
814
+ issues a **307** when a request's trailing slash does not match the route. This is documented for the
815
+ *API* in `docs/API_CONTRACT.md` §5.1 as a footgun, and it matters to the frontend because the
816
+ frontend is the caller: `SQ.live.url()` and `_normalizeBase()` exist partly to make the client's URL
817
+ composition predictable so that the client is not relying on a redirect to reach an endpoint.
818
+
819
+ Both traps share a lesson: **the frontend must link to the canonical URL.** A page that links to
820
+ `/run` (extensionless) never triggers the 308; a page that links to `/run.html` does.
821
+
822
+ ---
823
+
824
+ ## 12. Accessibility and UX caveats
825
+
826
+ This section states what can be established from the files read, and marks the rest.
827
+
828
+ ### 12.1 What is established
829
+
830
+ - **Keyboard driving exists on the captured-run page.** `run.js` supports keyboard input to move
831
+ between steps (`gotoStep()` plus key handling), so `run.html` is operable without a mouse.
832
+ - **Reduced-motion and focus styling** are governed by the token system in `frontend/HANDOFF.md` §2
833
+ (CSS custom properties on `:root`). The handoff document is the design authority for the token
834
+ layer.
835
+ - **The event drawer is a named, focusable pair of controls** (`#btnEvents` / `#btnClose`) with a
836
+ labelled region (`#drawer` → `#evlog`), so the event log is not hover-only.
837
+ - **The upload widgets are real `<input type="file">` elements** (`#fileInput`, `#fileInputT0`),
838
+ which are natively keyboard- and screen-reader-operable, and they are paired with a `#dropZone`
839
+ for pointer drag-and-drop. Drag-and-drop is an *addition* to the file input, not a replacement.
840
+
841
+ ### 12.2 What is not established
842
+
843
+ - **A formal accessibility audit** (axe / Lighthouse / WCAG conformance level) has not been
844
+ performed: `UNKNOWN — not established from the available evidence`.
845
+ - **Contrast ratios** for the token palette: `UNKNOWN — not established from the available evidence`.
846
+ - **Screen-reader behaviour** of the trace bar's animated width (whether a live region announces each
847
+ state transition): `UNKNOWN — not established from the available evidence`. The trace bar is a
848
+ visual affordance driven by `markState()`; whether its state changes are announced is not
849
+ determinable from the code read.
850
+ - **Mobile/responsive breakpoints** beyond what the CSS declares: `UNKNOWN — not established from the
851
+ available evidence`.
852
+ - **The 404 page's page count** is stale: `404.html` says "Ten pages exist" and links five, while
853
+ eleven ship. This is drift, recorded here and not silently repaired.
854
+
855
+ ---
856
+
857
+ ## 13. Documentation drift recorded (not propagated as current truth)
858
+
859
+ Per the project's practice (mirrored from `P10-T02`), drift found during this chapter's research is
860
+ recorded honestly rather than smoothed over:
861
+
862
+ | Location | Stale claim | Correct current statement |
863
+ |---|---|---|
864
+ | `frontend/_headers` comment | "100% static, zero network calls" | Ten of eleven pages are hermetic; `mission.html` calls the live service. |
865
+ | `frontend/404.html` prose | "Ten pages exist" (links five) | Eleven pages ship. |
866
+ | `frontend/mission.html` / `_headers` relationship | (implicit) | The live API base is declared in `<meta name="satquery-api-base">`, which is a *live* dependency the hermeticity audit must be read as exempting. |
867
+
868
+ None of these is a code defect; each is a documentation statement inside a shipped file that no
869
+ longer matches the tree. They are listed so a reader is not misled by them.
870
+
871
+ ---
872
+
873
+ ## 14. What the frontend does NOT do
874
+
875
+ Stated explicitly, because the depth of §5–§7 could otherwise imply more capability than exists:
876
+
877
+ - **No framework and no build step for application code.** Pages are hand-written HTML plus ES
878
+ modules; `scripts/stage_pages.mjs` copies, it does not compile.
879
+ - **No client-side model inference.** The browser never runs a model. All inference happens on the
880
+ service (`POST /api/infer` → the tunnel → the inference service).
881
+ - **No multipart upload.** Uploads are raw-bytes POSTs with a `Content-Type` header, matching the
882
+ server contract (`docs/API_CONTRACT.md` §2.4: multipart is *not* implemented).
883
+ - **No streaming.** There is no server-sent-events or websocket channel. The eight events are
884
+ *client-side UI states*; on a live run they are derived from the single inference response (plus
885
+ the two response headers `X-SatQuery-State` and `x-satquery-transport`), not pushed from the
886
+ server. (See the `SERVING.md` chapter: the service does not stream.)
887
+ - **No authentication UI.** The service has no auth (`docs/API_CONTRACT.md` §7), so there is no login.
888
+ - **No persistence of runs.** Nothing in the frontend stores a run; the console's state is in-memory,
889
+ and the captured-run page reads a static data file.
890
+ - **No offline mode** beyond the fact that ten pages need no network.
891
+
892
+ ---
893
+
894
+ ## 4.3 The staging tool in detail: closure algorithm and report format
895
+
896
+ This subsection expands §4.1 because the staging tool is the *only* build-like step in the frontend
897
+ and its behaviour determines what ships.
898
+
899
+ ### 4.3.1 Why a closure walk instead of "copy the directory"
900
+
901
+ Copying `frontend/` wholesale would ship unreferenced assets: draft images, superseded JS, experiment
902
+ files. A closure walk ships exactly the transitive set of files reachable from the eleven seed pages.
903
+ The consequences are worth stating precisely:
904
+
905
+ - **Adding a page is a deliberate act.** Because the seed set is `frontend/*.html` (top level only),
906
+ a page placed in a subdirectory is *not* a seed. It ships only if a seed page references it.
907
+ - **Removing a reference removes a file from the deploy.** If the last page that used
908
+ `assets/img/eo/old.jpg` stops referencing it, that image silently stops shipping. This is a feature
909
+ (smaller tree) and a hazard (an asset can disappear without an error) — which is exactly why the
910
+ tool prints the staged tree and the integrity report, so the disappearance is visible in the build
911
+ log rather than only in production.
912
+ - **Query strings and hashes are normalised away.** `stripQueryHash()` means `app.js?v=3` and
913
+ `app.js` are the same edge, so versioned references do not create phantom files.
914
+ - **External URLs are not followed.** `isExternal()` stops the walk at `https://…` and `//…`, which
915
+ is why the external-dependency audit can report `HERMETIC`: any external URL that *was* followed
916
+ would show up as a network dependency.
917
+ - **References cannot escape the root.** `insideFrontend()` rejects a resolved path that leaves
918
+ `frontend/`, so a stray `../../secret` reference cannot pull a file from outside the tree.
919
+
920
+ ### 4.3.2 The four report blocks
921
+
922
+ The tool prints four blocks. Reading them in order answers the four questions a deployer has.
923
+
924
+ 1. `=== STAGED TREE ===` — *what will be uploaded?* A listing of every file copied into the output
925
+ root, with sizes. Files over `BIG_WARN_BYTES` (10 MiB) are flagged.
926
+ 2. `=== REFERENCE INTEGRITY ===` — *is the closure complete?* The staged tree is re-walked and every
927
+ reference must resolve inside it. A dangling reference fails with exit code 3. This is the check
928
+ that catches the case where a file was referenced but not copied (e.g. because of a
929
+ case-sensitivity difference between the developer's filesystem and Linux).
930
+ 3. `=== EXTERNAL DEPENDENCY AUDIT ===` — *is the site hermetic?* External URLs found in the closure
931
+ are listed. When the list is empty the block prints `HERMETIC`. This is the check that keeps the
932
+ "ten of eleven pages are hermetic" claim honest: if a page gained a CDN script, the audit would
933
+ stop printing `HERMETIC`.
934
+ 4. `=== OPTIONS APPLIED ===` — *what flags were used?* The effective options, so a build log is
935
+ self-describing.
936
+
937
+ ### 4.3.3 The two hard gates and their exit codes
938
+
939
+ | Condition | Exit code | Why it is fatal |
940
+ |---|---|---|
941
+ | Any file exceeds `PAGES_FILE_LIMIT` (25 MiB) | **2** | Cloudflare Pages rejects a file over the limit; deploying would fail *after* upload. Failing before upload is cheaper and clearer. |
942
+ | Staged tree fails reference-integrity re-walk | **3** | A dangling reference means a broken page in production. |
943
+
944
+ The deliberate design choice is **fail before upload**. Both gates run locally, on the staged tree,
945
+ before `wrangler` is invoked. A non-zero exit stops a shell pipeline (`&&`) before the deploy command
946
+ can run.
947
+
948
+ ### 4.3.4 The deploy hint
949
+
950
+ The last thing the tool prints is the command to run:
951
+
952
+ ```
953
+ npx wrangler pages deploy "<OUT_ROOT>" --project-name <name>
954
+ ```
955
+
956
+ Note that the tool does **not** run the deploy itself. Staging and deploying are separate steps, which
957
+ means a human (or CI) can inspect the staged tree between them. This is consistent with the project's
958
+ general posture: make the artifact inspectable before it is published.
959
+
960
+ ---
961
+
962
+ ## 5.10 The nine console states
963
+
964
+ `mission.js` declares nine `ControllerState` values in `STATES`, an `EVENT_TO_STATE` map from the
965
+ eight event names onto those states, and a `STATE_NOTE` table of human-readable text per state.
966
+ `markState()` is the single function that advances the UI from one state to the next, and it is the
967
+ only place the trace-bar width is written.
968
+
969
+ The relationship between the nine states and the eight events is:
970
+
971
+ - One state is the **idle / pre-run** state — the state the console is in before `QUERY_RECEIVED`.
972
+ `resetUI()` returns the console to it (and resets `#obsTail` to `none`).
973
+ - The other eight states are entered by the eight events, in order. `EVENT_TO_STATE` is the mapping,
974
+ so the console's state names and the protocol's event names are kept in one place rather than
975
+ duplicated across `if` branches.
976
+
977
+ `STATE_NOTE` gives each state a sentence, which is what `#traceNow` and `#traceNote` display while the
978
+ run progresses. The point of the separate state text is that the event name is protocol
979
+ (`SPECIALIST_STARTED`) while the state text is human ("running the grounding specialist"). The console
980
+ shows both: the event name in the drawer's log, the state text in the trace region.
981
+
982
+ ### 5.10.1 Why the fill formula uses `(traceProgress + 0.5) / STATES.length`
983
+
984
+ The formula is:
985
+
986
+ ```js
987
+ traceFill.style.width = ((traceProgress + 0.5) / STATES.length) * 100 + '%'
988
+ ```
989
+
990
+ Three observations about it:
991
+
992
+ 1. **`STATES.length` is 9, not 8.** The denominator is the number of states, which includes the idle
993
+ state. So the maximum reachable fill from events alone is `(8 + 0.5) / 9 = 94.4444 %`.
994
+ 2. **The `+ 0.5` is a half-step lead.** Entering state *n* shows the bar at *(n + 0.5)/9*, i.e. the
995
+ midpoint of that state's band. The bar therefore never sits exactly on a boundary, which reads
996
+ better visually and means the bar is always "inside" a labelled state.
997
+ 3. **The remaining 5.5556 % is the idle state's band.** A completed run does not enter idle, so a
998
+ completed run does not fill the bar. This is the arithmetic origin of the 94.4444 % figure the
999
+ delivery documents record.
1000
+
1001
+ Stated as a status: the **formula** is `IMPLEMENTED`; the 94.4444 % figure is `MEASURED` *as the
1002
+ arithmetic consequence of the formula against nine states*, and it is corroborated by the live
1003
+ validation runs recorded in the delivery documents. It is not a claim about anything else.
1004
+
1005
+ ### 5.10.2 `onEvent()` — the single funnel
1006
+
1007
+ `onEvent()` is the console's event handler: every event delivered through the `ingest()` seam passes
1008
+ through it. It is responsible for
1009
+
1010
+ - appending to the event log via `logEvent()` (which writes to `#evlog` in the drawer),
1011
+ - advancing the state via `markState()` (which writes the trace bar),
1012
+ - routing the payload to the appropriate renderer (`renderEvidence()`, `renderConfidence()`,
1013
+ `renderIntent()`, the answer renderer into `#answerHost`, and the provenance writers into
1014
+ `#pRun` / `#pPolicy` / `#pProtocol` / `#pSchema`).
1015
+
1016
+ Having a single funnel is what makes the REAL/PREVIEW distinction safe: both drivers call the same
1017
+ `onEvent()`, so the rendering path is identical and only the *payload source* differs. It is also why
1018
+ the "0 mock nodes vs 9 mock nodes" property is checkable at one place — the drawer's contents are
1019
+ produced by one function.
1020
+
1021
+ ---
1022
+
1023
+ ## 5.11 The viewer and comparison regions
1024
+
1025
+ ### 5.11.1 The viewer
1026
+
1027
+ The viewer is the plate at the top of the console's results area. Handles: `#vbtns` (mode buttons),
1028
+ `#viewerState` (state label), `#plate` (container), `#plateImg` (the image, `src` initially
1029
+ `assets/img/eo/reservoir-low.jpg`), `#ev` (evidence overlay), `#evNote` (overlay note),
1030
+ `#plateCreditLead` and `#plateCredit` (attribution).
1031
+
1032
+ The initial `src` is a **real EO image** (`reservoir-low.jpg`), not a synthetic one. That matters for
1033
+ honesty: before any run, the console shows a real image with a credit, so a visitor is never looking
1034
+ at invented imagery while the console is idle.
1035
+
1036
+ `#vbtns` selects a viewer mode and `#viewerState` names it. The evidence overlay `#ev` is where the
1037
+ grounding result's regions are drawn — for the captured grounding run there are three regions
1038
+ (`region_1cd3973de749`, …), which is why the overlay is a layer separate from the plate image rather
1039
+ than something painted into the image.
1040
+
1041
+ ### 5.11.2 The comparison region
1042
+
1043
+ For paired tasks (`change`, `change_vqa`, `optical_sar`), the console shows a comparison region:
1044
+ `#cmpWrap` (wrapper), `#cmpT0` (before pane), `#cmpT1` (after pane), `#cmpRange` (the range/slider
1045
+ control), `#cmpCredit` (attribution), `#cmpEmpty` (empty state).
1046
+
1047
+ The two panes are fed from the two upload widgets (`#fileInput` for t1, `#fileInputT0` for t0). When
1048
+ only one asset is available for a paired task, `SINGLE_ASSET_FALLBACK = 'vqa'` narrows the request so
1049
+ the console can still produce an answer instead of failing. `#cmpEmpty` is the state shown when there
1050
+ is nothing to compare.
1051
+
1052
+ The homepage uses the same visual idiom for its delta-growth wipe slider, which is a nice consistency:
1053
+ the *idea* of "two dates, one place, slide to compare" appears both as a landing-page illustration and
1054
+ as a functional control in the console.
1055
+
1056
+ ---
1057
+
1058
+ ## 5.12 Provenance and report controls
1059
+
1060
+ ### 5.12.1 The provenance block
1061
+
1062
+ `#provHost` contains four fields that together answer "what exactly produced this?":
1063
+
1064
+ | Handle | Field | Meaning |
1065
+ |---|---|---|
1066
+ | `#pRun` | run id | The `run_…` identifier. |
1067
+ | `#pPolicy` | policy | The routing/planning policy that produced the plan. |
1068
+ | `#pProtocol` | protocol | The protocol under which the result was produced. |
1069
+ | `#pSchema` | schema | The schema version of the response. |
1070
+
1071
+ The reason a provenance block is worth four fields: the project's measurement discipline depends on
1072
+ being able to say *which* protocol a number came from. The style guide's grounding rule — that
1073
+ grounding was measured under **two protocols** (canonical 0.2838 / matched6 0.2566) and **two decode
1074
+ variants** (head_argmax 0.1215, zero-shot 0.0972) — is exactly the kind of fact that a protocol field
1075
+ exists to disambiguate. A result rendered without its protocol is a result that cannot be compared to
1076
+ anything.
1077
+
1078
+ ### 5.12.2 The report button and the event drawer
1079
+
1080
+ `#btnReport` triggers the console's report action. `#btnEvents` opens `#drawer`, whose `#evlog`
1081
+ contains the ordered event log; `#btnClose` closes it. The drawer is the console's audit surface: it
1082
+ is the one place where a reader can count events and check for mock nodes.
1083
+
1084
+ ---
1085
+
1086
+ ## 5.13 The mock data model inside `core.js`
1087
+
1088
+ `core.js` (1029 lines) is not only the event seam; it is also the source of everything the preview
1089
+ driver draws. Its internals, as read:
1090
+
1091
+ **Utilities.** `SQ.util` provides `rnd` (random), `rng` (a seeded random-number generator — which is
1092
+ what makes the preview *deterministic* across reloads), `pad`, and `ms` (formatting).
1093
+
1094
+ **Raster synthesis.** The synthetic imagery path:
1095
+
1096
+ | Symbol | Role |
1097
+ |---|---|
1098
+ | `BIOMES` | The biome definitions the synthesised terrain is drawn from. |
1099
+ | `CANON` | `{w: 900, h: 600}` — the canonical raster size. |
1100
+ | `buildMasks()` | Builds the masks (land/water/etc.) the raster is composed from. |
1101
+ | `fbm` | Fractal Brownian motion — the noise function that gives the terrain texture. |
1102
+ | `SQ.scene` | Produces a scene; it sets a **`synthetic: true` flag** and fills placeholder `gsd`, `aoi`, and `dates`. |
1103
+
1104
+ The `synthetic: true` flag is the load-bearing honesty mechanism: synthetic imagery is *labelled* as
1105
+ synthetic in the data, so any renderer can disclose it. The placeholder `gsd` (ground sample
1106
+ distance), `aoi` (area of interest), and `dates` are placeholders, not measurements — and the flag
1107
+ says so.
1108
+
1109
+ **Imagery.** `SQ.imagery` resolves which image to show.
1110
+
1111
+ **Stages.** `SQ.STAGES` is the eight-stage list from `QUERY` through `ANSWER`. This is the *narrative*
1112
+ stage list (what a human sees), distinct from the eight *event* names (the protocol). The two are
1113
+ aligned but not identical: `SQ.STAGES` is the visual progression; `SQ.EVENT_NAMES` is the wire
1114
+ vocabulary.
1115
+
1116
+ **The deterministic policy.** `SQ.policy()` is a deterministic router used by the preview. Its
1117
+ documented quirks: a **`where`-first** fix (a query containing "where" is routed to grounding before
1118
+ other rules are considered), the removal of a `built` keyword, and the `newAsChange` rule. Because it
1119
+ is deterministic and seeded, the same query produces the same preview every time — which is what makes
1120
+ the preview useful as a UI demo and useless as a measurement.
1121
+
1122
+ **Answer material.** `SQ.ANSWER_BANK` supplies canned answers for the preview; `SQ.COMPONENTS` lists
1123
+ **seven components** with their model strings, which is what the preview's model panel shows.
1124
+
1125
+ **Shared components.** `SQ.frame`, `SQ.reliabilityPlot`, and `SQ.chip` are reusable renderers. The
1126
+ `SQ.reliabilityPlot` is the component that draws the reliability curve referenced on
1127
+ `benchmark.html`.
1128
+
1129
+ **The run engine.** `SQ.run(opts)` is the engine; its `ingest()` switch (lines ~742–788) dispatches
1130
+ the eight event types to handlers. `startMock()` drives a preview run by calling `ingest()` on a
1131
+ schedule of `setTimeout` delays.
1132
+
1133
+ > **Honesty note on `SQ.policy()` vs the server router.** The browser's `SQ.policy()` and
1134
+ > `mission.js`'s `interpret()` are **UI-side** interpretations. The authoritative router is
1135
+ > server-side (`core/controller.py`, `core/registry.py`). The preview's routing can therefore differ
1136
+ > from what the server would do, and that is acceptable precisely because the preview is labelled a
1137
+ > preview and emits no specialist events. A reader must not read `SQ.policy()` as the routing
1138
+ > specification.
1139
+
1140
+ ---
1141
+
1142
+ ## 6.8 `live.js` API surface reference
1143
+
1144
+ `frontend/assets/js/live.js` is 392 lines. Its header documents the end-to-end flow and **three
1145
+ design rules**. The module's public surface, as read:
1146
+
1147
+ | Symbol | Kind | Behaviour |
1148
+ |---|---|---|
1149
+ | `SQ.ENDPOINTS` | const | `{ assets: '/assets', infer: '/infer', capabilities: '/capabilities', health: '/health' }`. |
1150
+ | `SQ.CONTENT_TYPES` | const | `{ tif, tiff, png, jpg, jpeg }` — the accepted upload types. |
1151
+ | `SQ.contentTypeFor(file)` | fn | Maps a file to its MIME type; used to set the upload `Content-Type`. |
1152
+ | `SQ.live.baseUrl()` | fn | Resolution order: `window.SATQUERY_API_BASE` → `<meta name="satquery-api-base">` ��� `/api`. |
1153
+ | `_normalizeBase(base)` | fn (internal) | Normalises the base (trailing slashes). |
1154
+ | `SQ.live.url(endpoint)` | fn | Composes the final URL from the normalised base and an endpoint. |
1155
+ | `LiveError` | class | The client's error type, carrying enough detail for `describeFailure()`. |
1156
+ | `describeFailure(err)` | fn | Turns a `LiveError` into human-readable text. |
1157
+ | `SQ.live.uploadAsset(file)` | fn | Raw-bytes POST with `Content-Type`; **asserts** the response contains `asset_id`. |
1158
+ | `SQ.live.uploadAssets(files)` | fn | Uploads a list **sequentially** (no parallel uploads). |
1159
+ | `SQ.live.infer(request)` | fn | POSTs the analysis request; **reads `X-SatQuery-State` and `x-satquery-transport`** from the response. |
1160
+ | `SQ.live.run(opts)` | fn | Composes upload + infer into one run. |
1161
+ | `SQ.live.capabilities()` | fn | `GET` the capability block (used by `loadCapabilities()`). |
1162
+
1163
+ ### 6.8.1 The three design rules (as stated in the file's header)
1164
+
1165
+ The file's header states three rules that govern the client. They are worth restating because they
1166
+ explain several behaviours that would otherwise look arbitrary:
1167
+
1168
+ 1. **Raw bytes, not multipart.** The upload is a body-with-content-type POST because that is what the
1169
+ service accepts (`docs/API_CONTRACT.md` §2.4: multipart is *not* implemented). A client that sent
1170
+ multipart would be rejected.
1171
+ 2. **Sequential uploads.** `uploadAssets()` uploads one at a time because the server's asset store is
1172
+ a small ephemeral store with a file cap (`SERVING.md`: default `_asset_max_files()` = 32), and
1173
+ because a paired task's second upload depends on the first succeeding. Parallel uploads would make
1174
+ partial failure harder to reason about.
1175
+ 3. **The asset handle is opaque.** The client asserts the handle exists and passes it back
1176
+ unexamined. The handle's shape (`asset_<32 hex>`) and its TTL are server facts; the client must not
1177
+ depend on either.
1178
+
1179
+ ### 6.8.2 The two response headers
1180
+
1181
+ `SQ.live.infer()` reads two custom headers:
1182
+
1183
+ | Header | Meaning |
1184
+ |---|---|
1185
+ | `X-SatQuery-State` | The controller state for the response (the same vocabulary as the console's `STATES`). |
1186
+ | `x-satquery-transport` | How the response was carried (the captured envelope records `transport: "tunnel"`). |
1187
+
1188
+ These two headers are how the console can display a state and a transport *without* a streaming
1189
+ channel. They are the reason the console can show a live run's progress truthfully: the state and the
1190
+ transport come from the server's own response, not from a client-side guess.
1191
+
1192
+ ---
1193
+
1194
+ ## 7.4 The captured envelope, field by field
1195
+
1196
+ This subsection expands §7.1 into a complete inventory, because the captured envelope is the frontend's
1197
+ single richest piece of real data and a reader should be able to reconstruct it.
1198
+
1199
+ **Provenance and identity.**
1200
+
1201
+ | Field | Value | Note |
1202
+ |---|---|---|
1203
+ | `_source` | "Captured live 2026-09-25 … Sanitized" | The capture date and the fact that the payload was sanitized before shipping. |
1204
+ | `run_id` | `run_d124d8b9adea` | The run identifier. |
1205
+ | `config_hash` | `78f1e3700da15aa1` | The frozen config hash — the same value recorded in the style guide §3. |
1206
+
1207
+ **Request.**
1208
+
1209
+ | Field | Value |
1210
+ |---|---|
1211
+ | `query` | "Where is the reservoir?" |
1212
+ | `task` | `grounding` |
1213
+ | `intent.source` | `forced` (the task was forced rather than inferred). |
1214
+ | `transport` | `tunnel` |
1215
+
1216
+ **Plan.**
1217
+
1218
+ | Field | Value |
1219
+ |---|---|
1220
+ | plan | `step_001`, task `grounding`, `requires_assets` |
1221
+ | steps | 8 steps, `RECEIVE` → `RESPOND` |
1222
+ | `timings.step_001` | `209.873` (ms) |
1223
+
1224
+ **Models.**
1225
+
1226
+ | Field | Value |
1227
+ |---|---|
1228
+ | `selected_models` | ViT-B-32 (the RemoteCLIP path) → GroundingHead |
1229
+ | GroundingHead `params` | `1052677` |
1230
+
1231
+ **Result.**
1232
+
1233
+ | Field | Value |
1234
+ |---|---|
1235
+ | `answer` | "[grounding] Located 3 candidate region(s) … Highest objectness 0.61." |
1236
+ | evidence | 4 items: 3 × `bounding_box`, 1 × `statistic` |
1237
+ | regions | 3, e.g. `region_1cd3973de749` |
1238
+
1239
+ **Confidence.**
1240
+
1241
+ | Field | Value |
1242
+ |---|---|
1243
+ | raw | `0.5231253252136926` |
1244
+ | calibrated | `0.5236623182649384` |
1245
+ | method | `temperature_scaling` |
1246
+ | `temperature` | `0.9772731820958189` |
1247
+ | `calibration_samples` | `16441.0` |
1248
+
1249
+ The `calibration_samples` value `16441.0` is the size of the validation set the temperature was fitted
1250
+ on; `docs/API_CONTRACT.md` §4 records the same figure as 16,441 Val rows. The temperature
1251
+ `0.9772731820958189` is also recorded in `docs/API_CONTRACT.md` §4. This is a real cross-check: the
1252
+ number on the public page matches the number in the API contract.
1253
+
1254
+ **Geospatial and warnings.**
1255
+
1256
+ | Field | Value |
1257
+ |---|---|
1258
+ | geospatial | 730 × 730, `has_crs false` |
1259
+ | warnings | 2 — no CRS; contradictory spatial claims |
1260
+
1261
+ The presence of the warnings in the shipped envelope is itself a design statement: the capture was not
1262
+ cleaned up to look better than it was.
1263
+
1264
+ **Why this page is important to the release.** It is the one place where a reader can see a complete,
1265
+ real, sanitized result envelope — including its imperfections — rendered by the same event vocabulary
1266
+ the live console uses. It is a *sample of one*, and the page does not present it as more than that.
1267
+
1268
+ ---
1269
+
1270
+ ## 11.3 A worked path through the platform traps
1271
+
1272
+ The following Mermaid diagram shows where the two traps (§11.1, §11.2) bite. It is a description of
1273
+ the behaviours documented in `frontend/_headers`, `docs/API_CONTRACT.md` §5.1, and the Cloudflare
1274
+ redirect behaviour recorded in the delivery documents — not a measurement.
1275
+
1276
+ ```mermaid
1277
+ flowchart TD
1278
+ A["Browser requests /run.html"] --> B{"Cloudflare Pages"}
1279
+ B -->|"308 (method preserved)"| C["/run"]
1280
+ C --> D["run.html served from the staged tree"]
1281
+
1282
+ E["Browser loads the page"] --> F{"Assets referenced"}
1283
+ F -->|"/assets/js/run.js"| G["Rule: /assets/js/*"]
1284
+ F -->|"/assets/img/…"| H["Rule: /assets/img/*"]
1285
+ G --> I["Cache-Control: public, max-age=0, must-revalidate"]
1286
+ H --> J["Cache-Control: max-age=604800"]
1287
+
1288
+ K["If two rules matched one path"] --> L["Values CONCATENATE"]
1289
+ L --> M["Chromium honours the FIRST max-age"]
1290
+ M --> N["Mitigation: scope patterns so they do not overlap"]
1291
+ ```
1292
+
1293
+ Read together, the traps say: **link to the canonical extensionless URL** (so the 308 never fires for
1294
+ an internal navigation) and **give each asset tree exactly one matching `_headers` rule** (so there is
1295
+ nothing to concatenate).
1296
+
1297
+ The third, API-side trap — the Starlette trailing-slash **307** documented in `docs/API_CONTRACT.md`
1298
+ §5.1 — is the client's concern rather than the static tier's: `_normalizeBase()` and `SQ.live.url()`
1299
+ exist so the client composes a URL that matches the route exactly, rather than relying on a redirect
1300
+ to reach it.
1301
+
1302
+ ---
1303
+
1304
+ ## 14.1 What the frontend is a client *of*
1305
+
1306
+ Because this chapter documents a client, it is worth stating precisely what contract the client is
1307
+ written against, so a reader can follow the thread into the `SERVING.md` chapter.
1308
+
1309
+ - The client posts **raw bytes** to `/api/assets` and receives an opaque `asset_id`
1310
+ (`docs/API_CONTRACT.md` §2.5).
1311
+ - The client posts **JSON** to `/api/infer` (`docs/API_CONTRACT.md` §2.4) and receives a
1312
+ `ResultEnvelope`.
1313
+ - The client reads `GET /api/capabilities` (`docs/API_CONTRACT.md` §2.2) to know which tasks are
1314
+ available now.
1315
+ - The client may read `GET /api/health` (`docs/API_CONTRACT.md` §2.1) for the service's health block.
1316
+ - The client reads two custom response headers (`X-SatQuery-State`, `x-satquery-transport`).
1317
+ - The client renders errors from the service's machine codes (`docs/API_CONTRACT.md` §5.2 — a 23-code
1318
+ taxonomy in `core/errors.py`, plus the gateway-origin `rate_limited`, mapped by
1319
+ `gateway/policy.py` `_CODE_STATUS`).
1320
+ - The client sends **no credentials**; the service has no auth (`docs/API_CONTRACT.md` §7). CORS is
1321
+ configured on the orchestrator (`deploy/render/main.py` `_PRODUCTION_ORIGINS` includes the Pages
1322
+ origin).
1323
+
1324
+ Every one of those six interactions is documented from the *server* side in the `SERVING.md` chapter,
1325
+ which is the other half of this pair.
1326
+
1327
+ ---
1328
+
1329
+ ## 15. Status summary
1330
+
1331
+ | Subsystem | Status |
1332
+ |---|---|
1333
+ | Static tier (11 pages, CSS, JS modules, assets) | `IMPLEMENTED` |
1334
+ | Staging tool `scripts/stage_pages.mjs` (closure, size gate, integrity gate, hermeticity report) | `IMPLEMENTED` |
1335
+ | Deploy path (`wrangler pages deploy` of the staged tree) | `IMPLEMENTED`; deployed HEAD `2d7ae53b482d` |
1336
+ | Analyze console (`mission.html` + `mission.js` + `live.js` + `core.js`) | `IMPLEMENTED` |
1337
+ | Eight-event protocol + trace bar (94.4444 % fill) | `IMPLEMENTED`; fill `MEASURED` as the arithmetic consequence of the formula |
1338
+ | PREVIEW driver (`runMock`, 9 mock nodes, no specialist events) | `IMPLEMENTED` |
1339
+ | REAL driver (`runLive`, real HTTP, 0 mock nodes) | `IMPLEMENTED`; exercised in the 24-run live validation recorded in the delivery docs |
1340
+ | Captured-run page (`run.html` over `anatomy-run.js`) | `IMPLEMENTED`; data is a real sanitized capture (`run_d124d8b9adea`) |
1341
+ | Hugging Face + GitHub header links on all 11 pages | `VERIFIED` by search across `frontend/*.html` |
1342
+ | Cache-busting (`_headers` rules + URL versioning) | `IMPLEMENTED` |
1343
+ | `_headers` concatenation trap | `KNOWN` (blocker item 9 in `docs/FINAL_DELIVERY_TODO.md` §1.7); mitigated by non-overlapping patterns |
1344
+ | Accessibility audit | `NOT RUN` |
1345
+
1346
+ ---
1347
+
1348
+ ## 16. NOT RUN / OPEN / BLOCKED (frontend)
1349
+
1350
+ Per `release/DOCS_STYLE_GUIDE.md` §4, every doc ends with this list.
1351
+
1352
+ **NOT RUN**
1353
+ - No formal accessibility audit (axe / Lighthouse / WCAG conformance level).
1354
+ - No measured contrast-ratio audit of the token palette.
1355
+ - No screen-reader behaviour verification for the trace bar's state transitions.
1356
+ - No responsive-breakpoint verification beyond the CSS as written.
1357
+ - No end-to-end benchmark of the system (this is project-wide, per `release/DOCS_STYLE_GUIDE.md`
1358
+ §3 — it is *not* a frontend gap, it is a project-level fact that the frontend must not contradict).
1359
+
1360
+ **OPEN**
1361
+ - `frontend/404.html` prose says "Ten pages exist" while eleven ship — documentation drift, OPEN.
1362
+ - The `_headers` concatenation behaviour remains a known platform trap (blocker item 9); the shipped
1363
+ rules avoid overlap, but the underlying platform behaviour is unchanged and OPEN as a hazard.
1364
+ - A page named "Lab" is not among the eleven shipped pages; whether it existed is
1365
+ `UNKNOWN — not established from the available evidence`.
1366
+ - Accessibility conformance level: `UNKNOWN — not established from the available evidence`.
1367
+
1368
+ **BLOCKED**
1369
+ - Nothing in the frontend is blocked. The frontend's live path depends on the backend, and the
1370
+ backend's own blockers (e.g. B-07, tunnel gaps; patch prepared, NOT deployed) are recorded in the
1371
+ `SERVING.md` chapter and the delivery documents. A backend blocker surfaces in the console only as
1372
+ an error rendered by `translateError()`.
1373
+
1374
+ ---
1375
+
1376
+ ## 17. Where the evidence lives
1377
+
1378
+ | Claim area | Evidence file(s) |
1379
+ |---|---|
1380
+ | Page inventory, purposes, `data-view`, section structure | `frontend/*.html` (11 files, each read) |
1381
+ | Homepage structure, video chapters, delta pair, open-question links | `frontend/index.html` |
1382
+ | Analyze console markup and every DOM handle | `frontend/mission.html` |
1383
+ | Console driver, `interpret()`, `chooseTask()`, `assetsForTask()`, `validateOpticalSar()`, `translateError()`, `routeSpecialists()`, `renderIntent()`, `STATES`/`EVENT_TO_STATE`/`STATE_NOTE`, `markState()` (fill formula), `buildTrace()`, `logEvent()`, `resetUI()`, `renderEvidence()`, `renderConfidence()`, `onEvent()`, `runMock()`, `runLive()`, `runQuery()`, `loadCapabilities()`, `setMode()`, `handleFile()`, `window.SQ_MISSION` | `frontend/assets/js/mission.js` |
1384
+ | `SQ` namespace, rng/util, synthetic scene flag, `SQ.STAGES`, `SQ.EVENT_NAMES`, `SQ.policy()`, `SQ.run().ingest()`, `startMock()`, `SQ.COMPONENTS` | `frontend/assets/js/core.js` |
1385
+ | Live client: `SQ.ENDPOINTS`, `SQ.CONTENT_TYPES`, `SQ.contentTypeFor()`, `SQ.live.baseUrl()`, `_normalizeBase()`, `SQ.live.url()`, `LiveError`, `describeFailure()`, `uploadAsset()`, `uploadAssets()`, `infer()` (header reads), `run()`, `capabilities()` | `frontend/assets/js/live.js` |
1386
+ | Captured-run driver: `QUERY…PLATE`, `REGIONS`, `paintAll()`, `buildEvidence()`, `evCandidates()/evLock()/evConfirmed()`, `SPECIALISTS_FOR_TASK`, `buildLattice()`, `DATA` (8 KV tables with event names), `setStage()`, `resetEvidence()`, `gotoStep()` | `frontend/assets/js/run.js` |
1387
+ | Captured envelope (run id, task, answer, config hash, transport, plan, steps, models, evidence, regions, confidence + calibration, timings, geospatial, warnings) | `frontend/assets/data/anatomy-run.js` |
1388
+ | Cache rules and the concatenation trap (verbatim comment) | `frontend/_headers` |
1389
+ | Design law, token system, phases A–G, file map, hard limits, schema types, `ingest` seam, 8 event names | `frontend/HANDOFF.md` |
1390
+ | Staging tool: constants, regexes, closure walk, exit codes 2/3, report blocks, `HERMETIC`, deploy hint | `scripts/stage_pages.mjs` |
1391
+ | Deployed frontend HEAD `2d7ae53b482d`; HF link on all 11 pages; blocker item 9 | `docs/FINAL_DELIVERY_TODO.md` |
1392
+ | Live topology and per-component responsibilities | `docs/DEPLOYMENT_TOPOLOGY.md` |
1393
+ | Superseded-topology banner; entrypoint requirements; failure-mode table | `docs/DEPLOYMENT_ARCHITECTURE.md` |
1394
+ | API contract the client speaks (endpoints, enums, confidence, errors, no-auth, CORS, multipart-not-implemented, 307 footgun, 23-code taxonomy) | `docs/API_CONTRACT.md` |
1395
+ | Captured run ids per task; metrics table; blockers | `docs/FINAL_DELIVERY_REPORT.md` |
1396
+ | Style, grounding rules, status vocabulary, facts-that-must-not-be-wrong, 94.4444 % trace fill | `release/DOCS_STYLE_GUIDE.md` |