Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/fleet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -908,6 +908,14 @@ which appears whenever the floor on screen has something waiting. It shows:
Everything except that button and the zone editor below is a read. Nothing you
do here changes any floor until you promote.

**Leaving it** is `back to map` in the pane head, or Escape. Review is a mode
rather than a fourth column — it stands the operations panes down at every
width — and the tab bar exists only below 760 px, so on a desk the pane's own
head is the way out. Both are held off while a zone edit is up, as the revision
list is: an edit has no autosave, so `cancel` (or `save as candidate`) ends it
first. A promotion leaves for you, the decision it was opened for having been
made.

### Naming the rooms on it, before you promote it

`pixi run segment-map` finds the rooms of a map but cannot know what they are
Expand Down
24 changes: 22 additions & 2 deletions mote_fleet/server/ui/app.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -288,11 +288,27 @@ function onReviewJump() {
if (state.mapKey) review.open(state.mapKey);
}

// Out of it again, by the pane's own control or by Escape: above 760 px the tab
// bar is hidden, so these are the only exits.
function onReviewBack() {
if (!review.leavable()) return;
panes.show('map');
}

function onKey(event) {
if (event.key !== 'Escape') return;
if (panes.current() !== 'review') return;
onReviewBack();
}

// A promotion happened in the review pane: this pane's basemap is now a
// different map, so re-resolve it rather than keep drawing the old one.
function onPromoted() {
// different map, so re-resolve it rather than keep drawing the old one. The
// review is then over — except when the announcement failed, whose note is
// readable only in the pane that wrote it.
function onPromoted(site, floor, revision, announced) {
state.mapKey = null;
scheduleRender();
if (announced) panes.show('map');
}
Comment on lines 304 to 312

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: a successful promotion navigates away before its own confirmation is visible, and lands on the wrong floor.

In promote()'s success branch, the outcome note ("<site>/<floor> is on <rev>") is written into #review-note and then this.onPromoted(site, floor, revision, Boolean(body.announced)) is called:

: `promoted, but not announced: ${body.detail}`,
!body.announced,
);
this.onPromoted(site, floor, revision, Boolean(body.announced));
} catch (error) {
this.note(error.message, true);
}

onPromoted here ignores its own site/floor args and, when announced is true, immediately calls panes.show('map'), which hides the review pane (.review-pane { display: none } in style.css) in the same tick — so the success note is written into a pane that's already hidden, and gets cleared (this.note('')) the next time review is opened. It's never seen.

Worse, the map pane doesn't re-resolve to the promoted floor: ensureMap() derives the basemap from the selected robot's pose/health site+floor, unrelated to what was just promoted via the review pane's own floor picker. In exactly the scenario this pane exists for — promoting a candidate for a floor no robot is currently reporting — the operator is dropped onto an unrelated floor (or "no floor reported"), with no visible evidence the promotion succeeded or which revision landed.

This is a regression from this PR: before it, onPromoted() only did state.mapKey = null; scheduleRender(); and the pane stayed open, so the note was readable.

Suggested direction: only auto-navigate when the promoted floor matches state.mapKey, or drive the map pane to the promoted site/floor instead of discarding those arguments, or surface the outcome on something that survives the pane switch.


// -- rendering -----------------------------------------------------------
Expand Down Expand Up @@ -576,6 +592,7 @@ function bind() {
reviewCanvas: 'review-canvas',
reviewMapLabel: 'review-map-label',
reviewPromote: 'review-promote',
reviewBack: 'review-back',
reviewFit: 'review-fit',
reviewNote: 'review-note',
zonesEdit: 'zones-edit',
Expand Down Expand Up @@ -620,6 +637,7 @@ export async function boot() {
zoneSource: dom.reviewZoneSource,
mapLabel: dom.reviewMapLabel,
promote: dom.reviewPromote,
back: dom.reviewBack,
fit: dom.reviewFit,
note: dom.reviewNote,
// The zone editor's own controls. It lives in this pane because it edits
Expand All @@ -645,6 +663,8 @@ export async function boot() {
});
dom.zone.addEventListener('change', onZone);
dom.reviewJump.addEventListener('click', onReviewJump);
dom.reviewBack.addEventListener('click', onReviewBack);
document.addEventListener('keydown', onKey);
dom.fit.addEventListener('click', () => {
mapView.follow(null);
dom.follow.checked = false;
Expand Down
3 changes: 3 additions & 0 deletions mote_fleet/server/ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ <h2>map</h2>
is the map about to be promoted and not the one already published. -->
<section class="pane review-pane" data-pane="review">
<div class="pane-head">
<!-- Above 760 px the tab bar is hidden and review stands every other
pane down, so this button and Escape are the only exits. -->
<button id="review-back" type="button">back to map</button>
<h2>review</h2>
<span id="review-map-label" class="dim">—</span>
<button id="review-fit" type="button">fit</button>
Expand Down
7 changes: 6 additions & 1 deletion mote_fleet/server/ui/review.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,10 @@ export class ReviewView {
return Boolean(this.selected && this.map.map);
}

leavable() {
return !this.editing;
}

// Editing is a mode on the selected revision, so while it is on, the things
// that would swap that revision out from under it are disabled rather than
// racing it. There is no autosave: an unsaved edit is lost to `cancel`, and
Expand All @@ -384,6 +388,7 @@ export class ReviewView {
this.dom.zoneSave.hidden = !this.editing;
this.dom.zoneCancel.hidden = !this.editing;
this.dom.floor.disabled = this.editing;
this.dom.back.disabled = this.editing;
for (const row of this.dom.revisions.querySelectorAll('button')) {
row.disabled = this.editing;
}
Expand Down Expand Up @@ -577,7 +582,7 @@ export class ReviewView {
: `promoted, but not announced: ${body.detail}`,
!body.announced,
);
this.onPromoted(site, floor, revision);
this.onPromoted(site, floor, revision, Boolean(body.announced));
} catch (error) {
this.note(error.message, true);
}
Expand Down
5 changes: 3 additions & 2 deletions mote_fleet/server/ui/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -792,9 +792,10 @@ main {
}

/* Editing holds the selected revision still, so the controls that would swap it
are disabled — and have to *look* disabled, or the pane reads as ignoring
clicks. */
or leave it are disabled — and have to *look* disabled, or the pane reads as
ignoring clicks. */
.zones-head button:disabled,
#review-back:disabled,
.revision-row:disabled {
opacity: 0.5;
cursor: not-allowed;
Expand Down
55 changes: 55 additions & 0 deletions mote_fleet/test/browser_check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,40 @@ try {
`${candidateDrawn} painted pixels`,
);

// The pane shipped with no exit above 760 px, where the tab bar is hidden: an
// operator who opened it left by resizing the window or reloading the page.
const exits = await session.evaluate(`(() => {
const active = () => [...document.querySelectorAll('.pane')]
.filter(pane => pane.classList.contains('active'))
.map(pane => pane.dataset.pane).join(',');
const out = { width: innerWidth, tabs: getComputedStyle(document.querySelector('.panes')).display };
document.getElementById('review-back').click();
out.button = active();
out.shown = getComputedStyle(document.querySelector('.map-pane')).display;
document.getElementById('review-jump').click();
out.reopened = active();
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
out.escape = active();
return out;
})()`);
check(
'the review pane can be left at desk width, by its button and by Escape',
exits.tabs === 'none' &&
exits.button === 'map' &&
exits.shown !== 'none' &&
exits.reopened === 'review' &&
exits.escape === 'map',
JSON.stringify(exits),
);

// Back in, for the rest of the review checks.
await session.evaluate(`document.getElementById('review-jump').click()`);
await settle(
session,
`document.getElementById('review-map-label').textContent`,
(label) => /\d{8}T\d{6}/.test(label),
);

// The fixture's candidate is the published map mirrored, so a review pane
// that fetched the canonical image — the defect this replaces — would draw a
// perfectly convincing map. The URL is what separates the two.
Expand Down Expand Up @@ -374,6 +408,22 @@ try {
JSON.stringify({ ends: editing.ends, adds: editing.adds }),
);

const held = await session.evaluate(`(() => {
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
document.getElementById('review-back').click();
return {
disabled: document.getElementById('review-back').disabled,
pane: [...document.querySelectorAll('.pane')]
.filter(pane => pane.classList.contains('active'))
.map(pane => pane.dataset.pane).join(','),
};
})()`);
check(
'an edit in progress holds the exit, by button and by Escape',
held.disabled && held.pane === 'review',
JSON.stringify(held),
);

// One list, one shape: the rows are the same rows, in the same place, the
// same height, with the same cells — `edit zones` puts controls in them and
// changes nothing else. Every part of that has been wrong at least once: a
Expand Down Expand Up @@ -605,6 +655,11 @@ try {
(note) => /is on \d{8}T\d{6}/.test(note),
);
check('the first promotion on a floor goes through', /is on /.test(promoted), promoted);

const landed = await session.evaluate(`[...document.querySelectorAll('.pane')]
.filter(pane => pane.classList.contains('active'))
.map(pane => pane.dataset.pane).join(',')`);
check('a promotion hands the screen back to the operations map', landed === 'map', landed);
}

// -- the phone ----------------------------------------------------------
Expand Down
28 changes: 28 additions & 0 deletions mote_fleet/test/ui_test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -937,6 +937,33 @@ test('review is a mode: opening it stands the operations panes down', () => {
assert.match(css, /\.review-pane\.active\s*\{\s*display:\s*flex/);
});

test('the review pane has a way out, and it leads to the map', () => {
const html = read('index.html');
const review = html.slice(
html.indexOf('class="pane review-pane"'),
html.indexOf('class="pane detail-pane"'),
);
assert.ok(review.includes('id="review-back"'), 'the review pane has no exit control');

// An exit only if it names a pane that is *not* this one: `show('review')`
// on a button labelled `back` looks right in the markup and does nothing.
const app = read('app.mjs');
const leave = app.slice(app.indexOf('function onReviewBack('));
assert.match(leave.slice(0, leave.indexOf('\n}')), /panes\.show\('map'\)/);
assert.match(app, /dom\.reviewBack\.addEventListener\('click', onReviewBack\)/);
const key = app.slice(app.indexOf('function onKey('));
const body = key.slice(0, key.indexOf('\n}'));
assert.match(body, /event\.key !== 'Escape'/);
assert.match(body, /onReviewBack\(\)/);
});

test('an edit in progress holds the exit, as it holds the revision list', () => {
const source = read('review.mjs');
const controls = source.slice(source.indexOf('renderEditControls()'));
assert.match(controls.slice(0, controls.indexOf('\n }')), /this\.dom\.back\.disabled = this\.editing/);
assert.match(source, /leavable\(\) \{\s*return !this\.editing;/);
});

test('the review pane has every element app.mjs binds to it', () => {
const html = read('index.html');
for (const id of [
Expand All @@ -953,6 +980,7 @@ test('the review pane has every element app.mjs binds to it', () => {
'review-canvas',
'review-map-label',
'review-promote',
'review-back',
'review-fit',
'review-note',
]) {
Expand Down