feat(adr-105): hide pose canvas in Docker SPA when no model is loaded
PoseDetectionCanvas polls /api/v1/pose/stats every 30 s. When model_loaded === false (the default — no trained pose model present), the canvas is hidden and a "No trained pose model loaded" overlay explains why, pointing the operator at the Sensing / Hardware tabs for the channels that are still active. renderPoseData() also short-circuits on modelLoaded !== true so any WS frames that slip through during the poll interval can't paint a misleading skeleton. Closes the last Open Item in ADR-105. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
parent
c8ac60f6ab
commit
2dcb30a6de
|
|
@ -51,6 +51,17 @@ export class PoseDetectionCanvas {
|
||||||
this.showTrail = false;
|
this.showTrail = false;
|
||||||
this.maxTrailLength = 10;
|
this.maxTrailLength = 10;
|
||||||
|
|
||||||
|
// ADR-105 / ADR-113: model-load gating. The canvas refuses to draw
|
||||||
|
// skeletons until /api/v1/pose/stats reports model_loaded === true,
|
||||||
|
// so an empty/zero-confidence keypoint stream from a model-less
|
||||||
|
// server doesn't paint a misleading "phantom" pose.
|
||||||
|
//
|
||||||
|
// null = "haven't asked yet" (treated as not-loaded for rendering).
|
||||||
|
this.modelLoaded = null;
|
||||||
|
this.modelStatusUrl = options.modelStatusUrl || '/api/v1/pose/stats';
|
||||||
|
this.modelStatusPollMs = options.modelStatusPollMs || 30000;
|
||||||
|
this.modelStatusTimer = null;
|
||||||
|
|
||||||
// Initialize component
|
// Initialize component
|
||||||
this.initializeComponent();
|
this.initializeComponent();
|
||||||
}
|
}
|
||||||
|
|
@ -79,9 +90,79 @@ export class PoseDetectionCanvas {
|
||||||
// Set up pose service subscription
|
// Set up pose service subscription
|
||||||
this.setupPoseServiceSubscription();
|
this.setupPoseServiceSubscription();
|
||||||
|
|
||||||
|
// ADR-105: poll model_loaded so we can hide the canvas when no
|
||||||
|
// trained pose model is on the server.
|
||||||
|
this.checkModelStatus();
|
||||||
|
this.modelStatusTimer = setInterval(
|
||||||
|
() => this.checkModelStatus(),
|
||||||
|
this.modelStatusPollMs
|
||||||
|
);
|
||||||
|
|
||||||
this.logger.info('PoseDetectionCanvas component initialized successfully');
|
this.logger.info('PoseDetectionCanvas component initialized successfully');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch `/api/v1/pose/stats` and update `this.modelLoaded`. On the
|
||||||
|
* leading-edge transitions (null → false, true → false) we hide the
|
||||||
|
* pose canvas and overlay a "No model loaded" notice so the operator
|
||||||
|
* isn't fooled by an empty skeleton renderer.
|
||||||
|
*/
|
||||||
|
async checkModelStatus() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(this.modelStatusUrl, { cache: 'no-store' });
|
||||||
|
if (!resp.ok) {
|
||||||
|
// Server reachable but not surfacing pose stats — be safe.
|
||||||
|
this.setModelLoaded(false, 'pose-stats endpoint error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const json = await resp.json();
|
||||||
|
const loaded = json && json.model_loaded === true;
|
||||||
|
this.setModelLoaded(loaded, null);
|
||||||
|
} catch (e) {
|
||||||
|
// Network blip — don't flip-flop the UI on a transient failure.
|
||||||
|
this.logger.debug('model-status poll failed', { err: e.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setModelLoaded(loaded, errOrNull) {
|
||||||
|
if (this.modelLoaded === loaded) return;
|
||||||
|
this.modelLoaded = loaded;
|
||||||
|
this.logger.info('model-loaded state changed', { loaded, note: errOrNull });
|
||||||
|
this.updateCanvasVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCanvasVisibility() {
|
||||||
|
if (!this.canvas) return;
|
||||||
|
const wrap = this.canvas.parentElement; // .pose-canvas-container
|
||||||
|
const overlayId = `model-overlay-${this.containerId}`;
|
||||||
|
let overlay = document.getElementById(overlayId);
|
||||||
|
if (this.modelLoaded === true) {
|
||||||
|
this.canvas.style.visibility = 'visible';
|
||||||
|
if (overlay) overlay.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// No model — hide the canvas and show a clear notice.
|
||||||
|
this.canvas.style.visibility = 'hidden';
|
||||||
|
if (!overlay && wrap) {
|
||||||
|
overlay = document.createElement('div');
|
||||||
|
overlay.id = overlayId;
|
||||||
|
overlay.className = 'pose-model-missing';
|
||||||
|
overlay.style.cssText =
|
||||||
|
'position:absolute;inset:0;display:flex;align-items:center;' +
|
||||||
|
'justify-content:center;color:#888;font-family:JetBrains Mono,monospace;' +
|
||||||
|
'font-size:13px;text-align:center;padding:20px;background:#0d1117;';
|
||||||
|
overlay.innerHTML =
|
||||||
|
'No trained pose model loaded.<br>' +
|
||||||
|
'<span style="color:#555;font-size:11px;">' +
|
||||||
|
'Pose rendering disabled — sensing channels still active in ' +
|
||||||
|
'the Sensing / Hardware tabs (ADR-105).</span>';
|
||||||
|
wrap.style.position = 'relative';
|
||||||
|
wrap.appendChild(overlay);
|
||||||
|
} else if (overlay) {
|
||||||
|
overlay.style.display = 'flex';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createDOMStructure() {
|
createDOMStructure() {
|
||||||
this.container.innerHTML = `
|
this.container.innerHTML = `
|
||||||
<div class="pose-detection-canvas-wrapper">
|
<div class="pose-detection-canvas-wrapper">
|
||||||
|
|
@ -516,6 +597,13 @@ export class PoseDetectionCanvas {
|
||||||
if (!this.renderer || !this.state.isActive) {
|
if (!this.renderer || !this.state.isActive) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// ADR-105: refuse to paint anything when the server has no trained
|
||||||
|
// pose model — empty/zero-confidence keypoints would otherwise show
|
||||||
|
// up as a misleading skeleton. The overlay from
|
||||||
|
// updateCanvasVisibility() already tells the operator why.
|
||||||
|
if (this.modelLoaded !== true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Render trail before the current frame if enabled
|
// Render trail before the current frame if enabled
|
||||||
|
|
@ -1535,6 +1623,12 @@ export class PoseDetectionCanvas {
|
||||||
this.unsubscribeFunctions.forEach(unsubscribe => unsubscribe());
|
this.unsubscribeFunctions.forEach(unsubscribe => unsubscribe());
|
||||||
this.unsubscribeFunctions = [];
|
this.unsubscribeFunctions = [];
|
||||||
|
|
||||||
|
// ADR-105: stop the model-status poll.
|
||||||
|
if (this.modelStatusTimer) {
|
||||||
|
clearInterval(this.modelStatusTimer);
|
||||||
|
this.modelStatusTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up resize observer
|
// Clean up resize observer
|
||||||
if (this.resizeObserver) {
|
if (this.resizeObserver) {
|
||||||
this.resizeObserver.disconnect();
|
this.resizeObserver.disconnect();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue