ui(mapillary): drop the keyless empty state

The panel no longer shows a separate keyless call-to-action; the fieldset
gate and the layer-row tooltip already cover the missing-key case. The
planner flag is tri-state (null until status answers).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Fable 5.1 committed 2026-09-25 17:51:58 +01:00
1 parent 10a00873ca
commit 7f65a32877
11 files changed
+31 -60

No files matched your search

+7
View File
@@ -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
+2 -1
View File
@@ -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
+4 -10
View File
@@ -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</a> contributors'),
+2 -1
View File
@@ -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,
+3 -3
View File
@@ -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, /<fieldset id="mly-controls"/);
assert.match(html, /<ul id="mly-legend"/);
assert.match(html, /id="mly-error"[^>]*role="alert"/);
-1
View File
@@ -900,7 +900,6 @@ export class StyleManager extends ShellFacade {
}),
setPanelCollapsed: (collapsed, options) =>
this.setPanelCollapsed('mapillary-panel', collapsed, options),
openKeySetup: () => document.getElementById('key-setup-chip')?.click(),
showToast: (message) => this._showToast(message),
},
});
+2 -6
View File
@@ -36,8 +36,6 @@ export class MapillaryControls {
const byId = (id) => this.root?.querySelector(`#${id}`) || null;
return {
status: byId('mly-status'),
keyless: byId('mly-keyless'),
keylessBtn: byId('mly-keyless-btn'),
controls: byId('mly-controls'),
enableBtn: byId('mly-enable-btn'),
lookBtn: byId('mly-look-btn'),
@@ -82,7 +80,6 @@ export class MapillaryControls {
if (!this.root) return;
this.mapillary.attachViewerHost?.(el.viewer);
this.listen(el.keylessBtn, 'click', () => this.actions.openKeySetup?.());
this.listen(el.enableBtn, 'click', () => this._toggleEnabled());
this.listen(el.lookBtn, 'click', async () => {
if (!(await this._ensureEnabled())) return;
@@ -306,7 +303,7 @@ export class MapillaryControls {
const view = presentMapillaryPanel(state);
this._view = view;
this._renderHeader(view);
this._renderEmptyState(view);
this._renderGate(view);
this._renderQuery(view);
this._renderResults(view);
this._renderFilters(view);
@@ -331,9 +328,8 @@ export class MapillaryControls {
}
}
_renderEmptyState(view) {
_renderGate(view) {
const el = this._elements;
if (el.keyless) el.keyless.hidden = !view.keyless.visible;
if (el.controls) el.controls.disabled = view.controlsDisabled;
}
+3 -5
View File
@@ -153,7 +153,9 @@ function presentMeta(state) {
export function presentMapillaryPanel(state) {
const enabled = state.enabled === true;
const keyRequired = state.keyRequired === true;
const planner = state.planner === true;
// Unknown (status not fetched yet) reads as available: the server answers
// a missing key with its own error, and the panel must not claim one is missing.
const planner = state.planner !== false;
const busy = state.query.busy === true;
const error = state.query.error || state.street.error || null;
let answer = state.query.answer || '';
@@ -167,10 +169,6 @@ export function presentMapillaryPanel(state) {
enabled,
keyRequired,
status: presentStatus(state),
keyless: {
visible: keyRequired,
text: 'Street Level needs a free Mapillary client token (MAPILLARY_CLIENT_TOKEN). Coverage, photos and queries stay off until one is added.',
},
controlsDisabled: keyRequired,
enableButton: {
text: enabled ? 'STREET LEVEL ON' : 'STREET LEVEL OFF',
+8 -4
View File
@@ -86,10 +86,8 @@ function deepMerge(target, source) {
return out;
}
test('a missing Mapillary token gates every control and names the variable', () => {
test('a missing Mapillary token gates every control and flags KEY REQUIRED', () => {
const view = presentMapillaryPanel(snapshot({ keyRequired: true }));
assert.equal(view.keyless.visible, true);
assert.match(view.keyless.text, /MAPILLARY_CLIENT_TOKEN/);
assert.equal(view.controlsDisabled, true);
assert.deepEqual(view.status, { text: 'KEY REQUIRED', tone: 'warn' });
});
@@ -103,10 +101,16 @@ test('without an Anthropic key the query box explains itself and stays disabled'
assert.equal(view.query.hintWarn, true);
assert.match(view.query.hint, /Anthropic key/);
assert.ok(view.query.placeholder.length < 40, 'placeholder fits the field');
assert.equal(view.keyless.visible, false);
assert.equal(view.controlsDisabled, false);
});
test('before the status call answers the query box does not claim a missing key', () => {
const view = presentMapillaryPanel(snapshot({ planner: null }));
assert.equal(view.query.inputDisabled, false);
assert.equal(view.query.hintWarn, false);
assert.doesNotMatch(view.query.placeholder, /Anthropic/);
});
test('a running query turns ASK into STOP and shows indeterminate progress per stage', () => {
const planning = presentMapillaryPanel(
snapshot({ enabled: true, query: { busy: true, stage: 'planning' } }),
-25
View File
@@ -110,31 +110,6 @@
pointer-events: none;
}
/* ── Keyless empty state ─────────────────────────────────────────────────── */
.mly-keyless {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px 12px;
border: 1px solid rgba(255, 179, 0, 0.45);
border-radius: 9px;
background: rgba(255, 179, 0, 0.06);
}
.mly-keyless-text {
margin: 0;
font-family: var(--font-sans);
font-size: 0.72rem;
line-height: 1.4;
color: var(--text-primary);
}
.mly-keyless-text code {
font-family: var(--font-mono);
font-size: 0.66rem;
color: #ffb300;
}
/* ── Query ───────────────────────────────────────────────────────────────── */
.mly-query {
display: flex;
-4
View File
@@ -85,10 +85,6 @@
<button class="panel-collapse-btn" data-collapse-target="mapillary-panel" title="Expand panel">◀</button>
</div>
<div id="mly-body" class="mly-body">
<div id="mly-keyless" class="mly-keyless" hidden>
<p class="mly-keyless-text">Street Level needs a free Mapillary client token (<code>MAPILLARY_CLIENT_TOKEN</code>). Coverage, photos and queries stay off until one is added.</p>
<button id="mly-keyless-btn" class="scene-btn" type="button">ADD KEY IN POWER UP</button>
</div>
<fieldset id="mly-controls" class="mly-controls">
<div class="cctv-controls">
<button id="mly-enable-btn" class="scene-btn" type="button" aria-pressed="false" title="Turn the Street Level layer on or off">STREET LEVEL OFF</button>