mirror of
https://github.com/ruvnet/RuView
synced 2026-07-30 18:41:42 +00:00
fix: WebSocket race condition, data source indicators, auto-start pose detection (#96)
* feat: RVF training pipeline & UI integration (ADR-036) Implement full model training, management, and inference pipeline: Backend (Rust): - recording.rs: CSI recording API (start/stop/list/download/delete) - model_manager.rs: RVF model loading, LoRA profile switching, model library - training_api.rs: Training API with WebSocket progress streaming, simulated training mode with realistic loss curves, auto-RVF export on completion - main.rs: Wire new modules, recording hooks in all CSI paths, data dirs UI (new components): - ModelPanel.js: Dark-mode model library with load/unload, LoRA dropdown - TrainingPanel.js: Recording controls, training config, live Canvas charts - model.service.js: Model REST API client with events - training.service.js: Training + recording API client with WebSocket progress UI (enhancements): - LiveDemoTab: Model selector, LoRA profile switcher, A/B split view toggle, training quick-panel with 60s recording shortcut - SettingsPanel: Full dark mode conversion (issue #92), model configuration (device, threads, auto-load), training configuration (epochs, LR, patience) - PoseDetectionCanvas: 10-frame pose trail with ghost keypoints and motion trajectory lines, cyan trail toggle button - pose.service.js: Model-inference confidence thresholds UI (plumbing): - index.html: Training tab (8th tab) - app.js: Panel initialization and tab routing - style.css: ~250 lines of training/model panel dark-mode styles 191 Rust tests pass, 0 failures. Closes #92. Refs: ADR-036, #93 Co-Authored-By: claude-flow <ruv@ruv.net> * fix: real RuVector training pipeline + UI service fixes Training pipeline (training_api.rs): - Replace simulated training with real signal-based training loop - Load actual CSI data from .csi.jsonl recordings or live frame history - Extract 180 features per frame: subcarrier amplitudes, temporal variance, Goertzel frequency analysis (9 bands), motion gradients, global stats - Train calibrated linear CSI-to-pose mapping via mini-batch gradient descent with L2 regularization (ridge regression), Xavier init, cosine LR decay - Self-supervised: teacher targets from derive_pose_from_sensing() heuristics - Real validation metrics: MSE and PCK@0.2 on 80/20 train/val split - Export trained .rvf with real weights, feature normalization stats, witness - Add infer_pose_from_model() for live inference from trained model - 16 new tests covering features, training, inference, serialization UI fixes: - Fix double-URL bug in model.service.js and training.service.js (buildApiUrl was called twice — once in service, once in apiService) - Fix route paths to match Rust backend (/api/v1/train/*, /api/v1/recording/*) - Fix request body formats (session_name, nested config object) - Fix top-level await in LiveDemoTab.js blocking module graph - Dynamic imports for ModelPanel/TrainingPanel in app.js - Center nav tabs with flex-wrap for 8-tab layout Co-Authored-By: claude-flow <ruv@ruv.net> * fix: WebSocket onOpen race condition, data source indicators, auto-start pose detection - Fix WebSocket onOpen race condition in websocket.service.js where setupEventHandlers replaced onopen after socket was already open, preventing pose service from receiving connection signal - Add 4-state data source indicator (LIVE/SIMULATED/RECONNECTING/OFFLINE) across Dashboard, Sensing, and Live Demo tabs via sensing.service.js - Add hot-plug ESP32 auto-detection in sensing server (auto mode runs both UDP listener and simulation, switches on ESP32_TIMEOUT) - Auto-start pose detection when backend is reachable - Hide duplicate PoseDetectionCanvas controls when enableControls=false - Add standalone Demo button in LiveDemoTab for offline animated demo - Add data source banner and status styling Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
+196
-38
@@ -55,7 +55,23 @@ export class SettingsPanel {
|
||||
// Advanced settings
|
||||
heartbeatInterval: 30000,
|
||||
maxReconnectAttempts: 10,
|
||||
enableSmoothing: true
|
||||
enableSmoothing: true,
|
||||
|
||||
// Model settings
|
||||
defaultModelPath: 'data/models/',
|
||||
autoLoadModel: false,
|
||||
inferenceDevice: 'CPU',
|
||||
inferenceThreads: 4,
|
||||
progressiveLoading: true,
|
||||
|
||||
// Training settings
|
||||
defaultEpochs: 100,
|
||||
defaultBatchSize: 32,
|
||||
defaultLearningRate: 0.0003,
|
||||
earlyStoppingPatience: 15,
|
||||
checkpointDirectory: 'data/models/',
|
||||
autoExportOnCompletion: true,
|
||||
recordingDirectory: 'data/recordings/'
|
||||
};
|
||||
|
||||
this.callbacks = {
|
||||
@@ -245,6 +261,67 @@ export class SettingsPanel {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model Settings -->
|
||||
<div class="settings-section">
|
||||
<h4>Model Configuration</h4>
|
||||
<div class="setting-row">
|
||||
<label for="default-model-path-${this.containerId}">Default Model Path:</label>
|
||||
<input type="text" id="default-model-path-${this.containerId}" class="setting-input setting-input-wide" placeholder="data/models/">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label for="auto-load-model-${this.containerId}">Auto-load Model on Startup:</label>
|
||||
<input type="checkbox" id="auto-load-model-${this.containerId}" class="setting-checkbox">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label for="inference-device-${this.containerId}">Inference Device:</label>
|
||||
<select id="inference-device-${this.containerId}" class="setting-select">
|
||||
<option value="CPU">CPU</option>
|
||||
<option value="GPU">GPU</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label for="inference-threads-${this.containerId}">Inference Threads:</label>
|
||||
<input type="number" id="inference-threads-${this.containerId}" class="setting-input" min="1" max="16">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label for="progressive-loading-${this.containerId}">Progressive Loading:</label>
|
||||
<input type="checkbox" id="progressive-loading-${this.containerId}" class="setting-checkbox">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Training Settings -->
|
||||
<div class="settings-section">
|
||||
<h4>Training Configuration</h4>
|
||||
<div class="setting-row">
|
||||
<label for="default-epochs-${this.containerId}">Default Epochs:</label>
|
||||
<input type="number" id="default-epochs-${this.containerId}" class="setting-input" min="1" max="10000">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label for="default-batch-size-${this.containerId}">Default Batch Size:</label>
|
||||
<input type="number" id="default-batch-size-${this.containerId}" class="setting-input" min="1" max="512">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label for="default-learning-rate-${this.containerId}">Default Learning Rate:</label>
|
||||
<input type="number" id="default-learning-rate-${this.containerId}" class="setting-input" min="0.000001" max="1" step="0.0001">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label for="early-stopping-patience-${this.containerId}">Early Stopping Patience:</label>
|
||||
<input type="number" id="early-stopping-patience-${this.containerId}" class="setting-input" min="1" max="100">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label for="checkpoint-directory-${this.containerId}">Checkpoint Directory:</label>
|
||||
<input type="text" id="checkpoint-directory-${this.containerId}" class="setting-input setting-input-wide" placeholder="data/models/">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label for="auto-export-on-completion-${this.containerId}">Auto-export on Completion:</label>
|
||||
<input type="checkbox" id="auto-export-on-completion-${this.containerId}" class="setting-checkbox">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label for="recording-directory-${this.containerId}">Recording Directory:</label>
|
||||
<input type="text" id="recording-directory-${this.containerId}" class="setting-input setting-input-wide" placeholder="data/recordings/">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-toggle">
|
||||
<button class="btn btn-sm" id="toggle-advanced-${this.containerId}">Show Advanced</button>
|
||||
</div>
|
||||
@@ -267,11 +344,12 @@ export class SettingsPanel {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.settings-panel {
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
background: #0d1117;
|
||||
border: 1px solid rgba(56, 68, 89, 0.6);
|
||||
border-radius: 8px;
|
||||
font-family: Arial, sans-serif;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
overflow: hidden;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
@@ -279,13 +357,13 @@ export class SettingsPanel {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #ddd;
|
||||
background: rgba(15, 20, 35, 0.95);
|
||||
border-bottom: 1px solid rgba(56, 68, 89, 0.6);
|
||||
}
|
||||
|
||||
.settings-header h3 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
color: #e0e0e0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -297,26 +375,43 @@ export class SettingsPanel {
|
||||
|
||||
.settings-content {
|
||||
padding: 20px;
|
||||
max-height: 400px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.settings-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.settings-content::-webkit-scrollbar-track {
|
||||
background: rgba(15, 20, 35, 0.5);
|
||||
}
|
||||
|
||||
.settings-content::-webkit-scrollbar-thumb {
|
||||
background: rgba(56, 68, 89, 0.8);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.settings-content::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(80, 96, 120, 0.9);
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
margin-bottom: 25px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 16px;
|
||||
background: rgba(17, 24, 39, 0.9);
|
||||
border: 1px solid rgba(56, 68, 89, 0.4);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.settings-section:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.settings-section h4 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
color: #8899aa;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
@@ -332,7 +427,7 @@ export class SettingsPanel {
|
||||
|
||||
.setting-row label {
|
||||
flex: 1;
|
||||
color: #666;
|
||||
color: #8899aa;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -340,9 +435,26 @@ export class SettingsPanel {
|
||||
.setting-input, .setting-select {
|
||||
flex: 0 0 120px;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #ddd;
|
||||
border: 1px solid rgba(56, 68, 89, 0.6);
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
background: rgba(15, 20, 35, 0.8);
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.setting-input:focus, .setting-select:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.15);
|
||||
}
|
||||
|
||||
.setting-input-wide {
|
||||
flex: 0 0 160px;
|
||||
}
|
||||
|
||||
.setting-select option {
|
||||
background: #1a2234;
|
||||
color: #c8d0dc;
|
||||
}
|
||||
|
||||
.setting-range {
|
||||
@@ -353,41 +465,45 @@ export class SettingsPanel {
|
||||
.setting-value {
|
||||
flex: 0 0 40px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
color: #b0b8c8;
|
||||
text-align: center;
|
||||
background: #f8f9fa;
|
||||
background: rgba(15, 20, 35, 0.8);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #ddd;
|
||||
border: 1px solid rgba(56, 68, 89, 0.6);
|
||||
}
|
||||
|
||||
.setting-checkbox {
|
||||
flex: 0 0 auto;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: #667eea;
|
||||
}
|
||||
|
||||
.setting-color {
|
||||
flex: 0 0 50px;
|
||||
height: 30px;
|
||||
border: 1px solid #ddd;
|
||||
border: 1px solid rgba(56, 68, 89, 0.6);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
background: rgba(15, 20, 35, 0.8);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border: 1px solid rgba(56, 68, 89, 0.6);
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
background: rgba(30, 40, 60, 0.8);
|
||||
color: #b0b8c8;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #f8f9fa;
|
||||
border-color: #adb5bd;
|
||||
background: rgba(40, 55, 80, 0.9);
|
||||
border-color: rgba(80, 96, 120, 0.8);
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
@@ -398,32 +514,32 @@ export class SettingsPanel {
|
||||
.settings-toggle {
|
||||
text-align: center;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
border-top: 1px solid rgba(56, 68, 89, 0.4);
|
||||
}
|
||||
|
||||
.settings-footer {
|
||||
padding: 10px 20px;
|
||||
background: #f8f9fa;
|
||||
border-top: 1px solid #ddd;
|
||||
background: rgba(15, 20, 35, 0.95);
|
||||
border-top: 1px solid rgba(56, 68, 89, 0.6);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.settings-status {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
color: #6b7a8d;
|
||||
}
|
||||
|
||||
.advanced-section {
|
||||
background: #f9f9f9;
|
||||
background: rgba(20, 28, 45, 0.9);
|
||||
margin: 0 -20px 25px -20px;
|
||||
padding: 20px;
|
||||
border: none;
|
||||
border-top: 1px solid #ddd;
|
||||
border-bottom: 1px solid #ddd;
|
||||
border-top: 1px solid rgba(56, 68, 89, 0.4);
|
||||
border-bottom: 1px solid rgba(56, 68, 89, 0.4);
|
||||
}
|
||||
|
||||
.advanced-section h4 {
|
||||
color: #dc3545;
|
||||
color: #ef4444;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -492,7 +608,9 @@ export class SettingsPanel {
|
||||
const checkboxes = [
|
||||
'auto-reconnect', 'show-keypoints', 'show-skeleton', 'show-bounding-box',
|
||||
'show-confidence', 'show-zones', 'show-debug-info', 'enable-validation',
|
||||
'enable-performance-tracking', 'enable-debug-logging', 'enable-smoothing'
|
||||
'enable-performance-tracking', 'enable-debug-logging', 'enable-smoothing',
|
||||
'auto-load-model', 'progressive-loading',
|
||||
'auto-export-on-completion'
|
||||
];
|
||||
|
||||
checkboxes.forEach(id => {
|
||||
@@ -503,12 +621,14 @@ export class SettingsPanel {
|
||||
});
|
||||
});
|
||||
|
||||
// Number inputs
|
||||
// Number inputs (integers)
|
||||
const numberInputs = [
|
||||
'connection-timeout', 'max-persons', 'max-fps',
|
||||
'heartbeat-interval', 'max-reconnect-attempts'
|
||||
'connection-timeout', 'max-persons', 'max-fps',
|
||||
'heartbeat-interval', 'max-reconnect-attempts',
|
||||
'inference-threads', 'default-epochs', 'default-batch-size',
|
||||
'early-stopping-patience'
|
||||
];
|
||||
|
||||
|
||||
numberInputs.forEach(id => {
|
||||
const input = document.getElementById(`${id}-${this.containerId}`);
|
||||
input?.addEventListener('change', (e) => {
|
||||
@@ -517,6 +637,32 @@ export class SettingsPanel {
|
||||
});
|
||||
});
|
||||
|
||||
// Float number inputs
|
||||
const floatInputs = ['default-learning-rate'];
|
||||
floatInputs.forEach(id => {
|
||||
const input = document.getElementById(`${id}-${this.containerId}`);
|
||||
input?.addEventListener('change', (e) => {
|
||||
const settingKey = this.camelCase(id);
|
||||
this.updateSetting(settingKey, parseFloat(e.target.value));
|
||||
});
|
||||
});
|
||||
|
||||
// Text inputs
|
||||
const textInputs = ['default-model-path', 'checkpoint-directory', 'recording-directory'];
|
||||
textInputs.forEach(id => {
|
||||
const input = document.getElementById(`${id}-${this.containerId}`);
|
||||
input?.addEventListener('change', (e) => {
|
||||
const settingKey = this.camelCase(id);
|
||||
this.updateSetting(settingKey, e.target.value);
|
||||
});
|
||||
});
|
||||
|
||||
// Inference device select
|
||||
const inferenceDeviceSelect = document.getElementById(`inference-device-${this.containerId}`);
|
||||
inferenceDeviceSelect?.addEventListener('change', (e) => {
|
||||
this.updateSetting('inferenceDevice', e.target.value);
|
||||
});
|
||||
|
||||
// Color inputs
|
||||
const colorInputs = ['skeleton-color', 'keypoint-color', 'bounding-box-color'];
|
||||
colorInputs.forEach(id => {
|
||||
@@ -696,7 +842,19 @@ export class SettingsPanel {
|
||||
enableDebugLogging: false,
|
||||
heartbeatInterval: 30000,
|
||||
maxReconnectAttempts: 10,
|
||||
enableSmoothing: true
|
||||
enableSmoothing: true,
|
||||
defaultModelPath: 'data/models/',
|
||||
autoLoadModel: false,
|
||||
inferenceDevice: 'CPU',
|
||||
inferenceThreads: 4,
|
||||
progressiveLoading: true,
|
||||
defaultEpochs: 100,
|
||||
defaultBatchSize: 32,
|
||||
defaultLearningRate: 0.0003,
|
||||
earlyStoppingPatience: 15,
|
||||
checkpointDirectory: 'data/models/',
|
||||
autoExportOnCompletion: true,
|
||||
recordingDirectory: 'data/recordings/'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user