fix: retain shell feedback bindings and stop disposed panel work

This commit is contained in:
Sameh Khamis committed 2026-09-13 04:49:02 -07:00
1 parent 03ad8142e6
commit 945a809994
5 files changed
+63 -29

No files matched your search

+36 -2
View File
@@ -33,7 +33,10 @@ try {
const ui = window.__godsEyeView.styleManager;
const counts = {};
const watchObserver = (name) => {
const owner = name === '_draggableResizeObserver' ? ui._panelPosition : ui._panelLayout;
const owner =
name === '_draggableResizeObserver'
? ui._panelPosition
: ui._panelLayout;
const observer = owner[name];
if (!observer) return false;
const disconnect = observer.disconnect.bind(observer);
@@ -110,6 +113,12 @@ try {
if (type === 'resize' && callback === resizeHandler) counts.resize++;
return removeEventListener.call(this, type, callback, options);
};
const { setSplitFlapText } = await import('/src/splitFlap.js');
const feedbackLabel = document.getElementById('global-loading-label');
setSplitFlapText(feedbackLabel, 'CHECKING LIVE DATA');
setSplitFlapText(feedbackLabel, 'LOAD COMPLETE');
const permanentText =
feedbackLabel.querySelector('.gev-flap-text')?.firstChild;
try {
const disposal = ui.dispose();
const focusBefore = document.activeElement;
@@ -167,7 +176,11 @@ try {
counts[field] === 1 && ui._contextControls[field] === null,
),
observersReleased: observed.every(
(name) => counts[name] === 1 && ui[name] === null,
(name) =>
counts[name] === 1 &&
(name === '_draggableResizeObserver'
? ui._panelPosition
: ui._panelLayout)[name] === null,
),
subscriptionReleased:
counts.cctv === 1 && ui._cctvControls._cctvUnsubscribe === null,
@@ -176,6 +189,19 @@ try {
ui._radioControls.destroyed &&
ui._locationControls.destroyed &&
ui._cctvControls.destroyed,
feedbackTextPreserved:
permanentText?.nodeType === Node.TEXT_NODE &&
feedbackLabel.querySelector('.gev-flap-text')?.firstChild ===
permanentText &&
permanentText.data === 'LOAD COMPLETE' &&
!feedbackLabel.classList.contains('gev-flap-active') &&
feedbackLabel.querySelector('.gev-flap-cells')?.childNodes.length ===
0,
shellWorkReleased:
ui._lifetime.destroyed &&
ui._lifetime.frames.size === 0 &&
ui._lifetime.timers.size === 0 &&
ui._lifetime.removers.size === 0,
idempotent: once === JSON.stringify(counts),
};
} finally {
@@ -215,6 +241,14 @@ try {
'Cockpit cleanup releases listeners and returns all Display groups home',
result.cockpitReleased,
);
check(
'feedback teardown preserves the permanent accessible text node',
result.feedbackTextPreserved,
);
check(
'shell teardown cancels deferred work and releases listeners',
result.shellWorkReleased,
);
check('repeated UI disposal is inert', result.idempotent);
check(
'disposal and subsequent resize produce no uncaught browser errors',
+1 -23
View File
@@ -1,3 +1,4 @@
import { setSplitFlapText } from '../splitFlap.js';
import { UiLifetime } from './uiLifetime.js';
import { RecordingControls } from './recordingControls.js';
import { readShellElements } from './shellElements.js';
@@ -86,8 +87,6 @@ import {
stampInitialShareGesture,
} from '../navigationPolicy.js';
/** Versioned localStorage namespace prefix to invalidate stale panel layouts. */
const SHARE_PANEL_STATE_SPECS = Object.freeze([
{ id: 'control-panel', pinnable: true },
{ id: 'location-bar', pinnable: true },
@@ -108,28 +107,7 @@ const COCKPIT_ENTRY_COLLAPSE_PANEL_IDS = Object.freeze([
'global-context-panel',
'radio-panel',
]);
/**
* Position keys are versioned separately from collapsed-state keys so layout
* default changes (e.g. right-rail origin) can reset positions without also
* resetting every panel's open/closed preference.
*/
const DETECTION_ALLOCATION_STORAGE_KEY = 'gev:detection-allocation:v1';
/** Z ladder: panels promote within [100, 139]; voice pill 150, toast 200, clean-view-exit 300. */
/**
* Fixed UI regions that can occupy the left accordion's vertical lane.
* Rectangles are filtered at runtime for visibility and horizontal overlap,
* so right-side/center controls do not reduce the lane unless they actually
* intersect it at the current viewport size.
*/
/**
* Fixed UI regions that can occupy the right control lane. Runtime rectangle
* filtering keeps the rail clear of whichever HUD variant is currently
* visible without tying the layout to one screen height.
*/
/** Display labels shown in the mini-status readout for each active style. */
const STYLE_STATUS_LABELS = {
normal: 'NORMAL',
+11
View File
@@ -1,6 +1,12 @@
/** Own rail measurement, layout scheduling and dock tray observation. */
import { layoutLeftPanelRail, layoutRightPanelRail } from './panelRails.js';
const COCKPIT_LAYOUT_SETTLE_MS = 240;
/**
* Fixed UI regions that can occupy the left accordion's vertical lane.
* Rectangles are filtered at runtime for visibility and horizontal overlap,
* so right-side/center controls do not reduce the lane unless they actually
* intersect it at the current viewport size.
*/
const LEFT_STACK_OBSTACLE_SELECTOR = [
'#cockpit-hud .cockpit-topline',
'#cockpit-hud .cockpit-topline > div',
@@ -26,6 +32,11 @@ const LEFT_STACK_OBSTACLE_SELECTOR = [
'#pp-toggles',
'#param-slider-panel',
].join(', ');
/**
* Fixed UI regions that can occupy the right control lane. Runtime rectangle
* filtering keeps the rail clear of whichever HUD variant is currently
* visible without tying the layout to one screen height.
*/
const RIGHT_STACK_OBSTACLE_SELECTOR = [
'#cockpit-hud .cockpit-topline',
'#cockpit-hud .cockpit-topline > div',
+10 -4
View File
@@ -1,6 +1,13 @@
/** Own panel position preferences, viewport clamping and drag listeners. */
/** Versioned localStorage namespace prefix to invalidate stale panel layouts. */
const PANEL_LAYOUT_STORAGE_VERSION = 'v6';
/**
* Position keys are versioned separately from collapsed-state keys so layout
* default changes (e.g. right-rail origin) can reset positions without also
* resetting every panel's open/closed preference.
*/
const PANEL_POSITION_STORAGE_VERSION = 'v8';
/** Z ladder: panels promote within [100, 139]; voice pill 150, toast 200, clean-view-exit 300. */
const PANEL_Z_BASE = 100;
const PANEL_Z_MAX = 139;
export class PanelPositionControls {
@@ -22,6 +29,7 @@ export class PanelPositionControls {
this.destroyed = false;
}
listen(target, type, callback) {
if (this.destroyed || !target) return;
const listener = (event) => {
if (!this.destroyed) callback(event);
};
@@ -29,6 +37,7 @@ export class PanelPositionControls {
this.removers.push(() => target.removeEventListener(type, listener));
}
_reclampDraggablePanels() {
if (this.destroyed) return;
const el = this._ppToggles;
if (!el || !el.style.top || el.style.top === 'auto') return;
const top = parseInt(el.style.top, 10);
@@ -56,6 +65,7 @@ export class PanelPositionControls {
}
_initPanelDrag() {
if (this.destroyed) return;
const dragSpecs = [
{
id: 'pp-toggles',
@@ -258,10 +268,6 @@ export class PanelPositionControls {
};
const onUp = () => {
cancel();
panelEl.classList.remove('panel-dragging');
window.removeEventListener('pointermove', onMove);
window.removeEventListener('pointerup', onUp);
window.removeEventListener('pointercancel', onUp);
if (panelId === 'pp-toggles') {
this._pinPanelToRight(panelEl);
}
+5
View File
@@ -201,6 +201,11 @@ test('panel disposal cancels a live drag without saving or accepting later point
window.dispatchEvent(new Event('pointerup'));
assert.deepEqual(panel.style, before);
assert.equal(panel.classList.contains('panel-dragging'), false);
owner._makePanelDraggable('sample', panel, handle);
owner._initPanelDrag();
owner._ppToggles.style.top = '10000px';
owner._reclampDraggablePanels();
assert.equal(owner._ppToggles.style.top, '10000px');
assert.equal(owner.removers.length, 0);
assert.deepEqual(f.writes, []);
} finally {