diff --git a/README.md b/README.md
index 44859f908..59495b637 100644
--- a/README.md
+++ b/README.md
@@ -330,6 +330,13 @@ _The Space Missions layer replaying a Falcon 9 ascent — labeled `RECONSTRUCTED
> Needs a free **Mapillary client token**; the plain-English query box additionally needs an **Anthropic key**. Both go in POWER UP.
+**Getting the keys**
+
+1. **Mapillary client token (required, free).** Sign in at [mapillary.com/dashboard/developers](https://www.mapillary.com/dashboard/developers), choose **Register application**, give it any name and website, tick **Read** access, and copy the **Client Token** (it starts with `MLY|`). Paste it into **POWER UP → Mapillary**, or set `MAPILLARY_CLIENT_TOKEN` in `.env` and restart `npm run dev`.
+2. **Anthropic key (optional).** Create one at [console.anthropic.com](https://console.anthropic.com/settings/keys) and add it in **POWER UP → Anthropic** or as `ANTHROPIC_API_KEY` in `.env`. It stays on the server; only the plain-English query box uses it.
+
+Without the Mapillary token the STREET LEVEL panel reads **KEY REQUIRED** and its controls stay greyed out; without the Anthropic key everything except the query box works.
+
Turn on **Street Level (Mapillary)** under DATA LAYERS → Cameras, or open the STREET LEVEL strip in the right rail. Mapillary's coverage draws as ground-hugging lines (recent captures bright green, older ones dim, 360° panoramas magenta); click a line for its image cones and a cone to open the photo in the embedded MapillaryJS viewer. **EXPAND** grows the viewer to most of the screen and **CAMERA FOLLOWS VIEW** drives the globe camera from the panorama. With an Anthropic key, type a request such as _"stop signs in downtown Detroit"_ and the matching detections stream onto the globe tile by tile with Mapillary's own icons; click one to see the photo with the detection outlined. Mapillary blurs faces and licence plates before publishing imagery, and the globe credits "© Mapillary contributors, CC BY-SA 4.0" while the layer is on.
## 🎖️ Field Missions
diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md
index 690d5ce51..ba056bbc1 100644
--- a/docs/CURRENT-STATE.md
+++ b/docs/CURRENT-STATE.md
@@ -2697,7 +2697,8 @@ its criteria cannot be silently ignored.
Street Level (Mapillary) lives in the right context rail next to CCTV as an
ordinary collapsible panel and starts collapsed. Without `MAPILLARY_CLIENT_TOKEN`
-the panel shows a gated empty state naming the variable; with it, coverage
+the header status reads KEY REQUIRED and the controls stay disabled (key setup
+is documented in the README's Street Level section); with it, coverage
draws as ground-clamped sequence lines (z0–5 overview points from orbit,
z11–14 sequences below 60 km) and the proxy strips the unused `image` point
layer from z14 tiles in transit (12 MB → ~80 KB). The plain-English query box
diff --git a/scripts/qa-mapillary.mjs b/scripts/qa-mapillary.mjs
index 36a1888d6..a862e3bd1 100644
--- a/scripts/qa-mapillary.mjs
+++ b/scripts/qa-mapillary.mjs
@@ -108,7 +108,7 @@ async function main() {
classes: [...el.classList],
width: Math.round(box.width),
right: Math.round(box.right),
- keyless: !document.getElementById('mly-keyless').hidden,
+ status: document.getElementById('mly-status').textContent,
controlsDisabled: document.getElementById('mly-controls').disabled,
bodyDisplay: getComputedStyle(document.getElementById('mly-body'))
.display,
@@ -152,7 +152,7 @@ async function main() {
);
if (!status.configured) {
await step(
- 'keyless install gates the controls and names the token',
+ 'keyless install gates the controls and reports KEY REQUIRED',
async () => {
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('mapillary', true, {
@@ -161,14 +161,8 @@ async function main() {
);
await sleep(800);
const info = await panel();
- assert.equal(info.keyless, true);
assert.equal(info.controlsDisabled, true);
- assert.match(
- await page.evaluate(
- () => document.getElementById('mly-keyless').textContent,
- ),
- /MAPILLARY_CLIENT_TOKEN/,
- );
+ assert.equal(info.status, 'KEY REQUIRED');
},
);
console.log(
@@ -217,7 +211,7 @@ async function main() {
{ timeout: 90_000 },
);
const info = await panel();
- assert.equal(info.keyless, false);
+ assert.equal(info.controlsDisabled, false);
// Cesium paints on-screen credits a frame or two after they register.
await page.waitForFunction(
() => document.body.innerHTML.includes('Mapillary contributors'),
diff --git a/src/layers/mapillary/index.js b/src/layers/mapillary/index.js
index 9cd211b5b..3e84c05bf 100644
--- a/src/layers/mapillary/index.js
+++ b/src/layers/mapillary/index.js
@@ -180,7 +180,8 @@ export function createMapillaryLayer({ source, services = {} }) {
return {
enabled: state.enabled,
keyRequired: state.keyRequired,
- planner: state.status?.planner === true,
+ // null until the status call answers (the layer initialises lazily).
+ planner: state.status ? state.status.planner === true : null,
plannerModel: state.status?.plannerModel || null,
coverage: {
zoom: state.coverage.zoom,
diff --git a/src/mapillaryMarkup.test.mjs b/src/mapillaryMarkup.test.mjs
index 935453017..981a4d6ab 100644
--- a/src/mapillaryMarkup.test.mjs
+++ b/src/mapillaryMarkup.test.mjs
@@ -63,9 +63,9 @@ test('panel styles stay inside GEV conventions: no !important, no fixed panel',
assert.deepEqual(fixed, ['.mly-viewer-wrap-expanded']);
});
-test('keyless and no-planner states are real, gated UI rather than dead buttons', () => {
- assert.match(html, /id="mly-keyless"[^>]*hidden/);
- assert.match(html, /MAPILLARY_CLIENT_TOKEN/);
+test('keyless and no-planner states gate the controls rather than leave dead buttons', () => {
+ // The key requirement is documented (README, .env.example), not repeated in the panel.
+ assert.doesNotMatch(html, /mly-keyless/);
assert.match(html, /