Files
wifi-densepose/ui/app.js
ruv b7e0f07e6e feat: Sensing-only UI mode with Gaussian splat visualization and Rust migration ADR
- Add Python WebSocket sensing server (ws_server.py) with ESP32 UDP CSI
  and Windows RSSI auto-detect collectors on port 8765
- Add Three.js Gaussian splat renderer with custom GLSL shaders for
  real-time WiFi signal field visualization (blue→green→red gradient)
- Add SensingTab component with RSSI sparkline, feature meters, and
  motion classification badge
- Add sensing.service.js WebSocket client with reconnect and simulation fallback
- Implement sensing-only mode: suppress all DensePose API calls when
  FastAPI backend (port 8000) is not running, clean console output
- ADR-019: Document sensing-only UI architecture and data flow
- ADR-020: Migrate AI/model inference to Rust with RuVector ONNX Runtime,
  replacing ~2.7GB Python stack with ~50MB static binary
- Add ruvnet/ruvector as upstream remote for RuVector crate ecosystem

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-02-28 14:37:29 -05:00

318 lines
9.4 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// WiFi DensePose Application - Main Entry Point
import { TabManager } from './components/TabManager.js';
import { DashboardTab } from './components/DashboardTab.js';
import { HardwareTab } from './components/HardwareTab.js';
import { LiveDemoTab } from './components/LiveDemoTab.js';
import { SensingTab } from './components/SensingTab.js';
import { apiService } from './services/api.service.js';
import { wsService } from './services/websocket.service.js';
import { healthService } from './services/health.service.js';
import { backendDetector } from './utils/backend-detector.js';
class WiFiDensePoseApp {
constructor() {
this.components = {};
this.isInitialized = false;
}
// Initialize application
async init() {
try {
console.log('Initializing WiFi DensePose UI...');
// Set up error handling
this.setupErrorHandling();
// Initialize services
await this.initializeServices();
// Initialize UI components
this.initializeComponents();
// Set up global event listeners
this.setupEventListeners();
this.isInitialized = true;
console.log('WiFi DensePose UI initialized successfully');
} catch (error) {
console.error('Failed to initialize application:', error);
this.showGlobalError('Failed to initialize application. Please refresh the page.');
}
}
// Initialize services
async initializeServices() {
// Add request interceptor for error handling
apiService.addResponseInterceptor(async (response, url) => {
if (!response.ok && response.status === 401) {
console.warn('Authentication required for:', url);
// Handle authentication if needed
}
return response;
});
// Detect backend availability and initialize accordingly
const useMock = await backendDetector.shouldUseMockServer();
if (useMock) {
console.log('🧪 Initializing with mock server for testing');
// Import and start mock server only when needed
const { mockServer } = await import('./utils/mock-server.js');
mockServer.start();
// Show notification to user
this.showBackendStatus('Mock server active - testing mode', 'warning');
} else {
console.log('🔌 Initializing with real backend');
// Verify backend is actually working
try {
const health = await healthService.checkLiveness();
console.log('✅ Backend is available and responding:', health);
this.showBackendStatus('Connected to real backend', 'success');
} catch (error) {
// DensePose API backend not running — sensing-only mode
backendDetector.sensingOnlyMode = true;
console.log(' DensePose API not running — sensing-only mode via WebSocket on :8765');
this.showBackendStatus('Sensing mode — live WiFi data via WebSocket', 'success');
}
}
}
// Initialize UI components
initializeComponents() {
const container = document.querySelector('.container');
if (!container) {
throw new Error('Main container not found');
}
// Initialize tab manager
this.components.tabManager = new TabManager(container);
this.components.tabManager.init();
// Initialize tab components
this.initializeTabComponents();
// Set up tab change handling
this.components.tabManager.onTabChange((newTab, oldTab) => {
this.handleTabChange(newTab, oldTab);
});
}
// Initialize individual tab components
initializeTabComponents() {
// Skip DensePose-dependent tabs in sensing-only mode
const sensingOnly = backendDetector.sensingOnlyMode;
// Dashboard tab
const dashboardContainer = document.getElementById('dashboard');
if (dashboardContainer) {
this.components.dashboard = new DashboardTab(dashboardContainer);
if (!sensingOnly) {
this.components.dashboard.init().catch(error => {
console.error('Failed to initialize dashboard:', error);
});
}
}
// Hardware tab
const hardwareContainer = document.getElementById('hardware');
if (hardwareContainer) {
this.components.hardware = new HardwareTab(hardwareContainer);
if (!sensingOnly) this.components.hardware.init();
}
// Live demo tab
const demoContainer = document.getElementById('demo');
if (demoContainer) {
this.components.demo = new LiveDemoTab(demoContainer);
if (!sensingOnly) this.components.demo.init();
}
// Sensing tab
const sensingContainer = document.getElementById('sensing');
if (sensingContainer) {
this.components.sensing = new SensingTab(sensingContainer);
}
// Architecture tab - static content, no component needed
// Performance tab - static content, no component needed
// Applications tab - static content, no component needed
}
// Handle tab changes
handleTabChange(newTab, oldTab) {
console.log(`Tab changed from ${oldTab} to ${newTab}`);
// Stop demo if leaving demo tab
if (oldTab === 'demo' && this.components.demo) {
this.components.demo.stopDemo();
}
// Update components based on active tab
switch (newTab) {
case 'dashboard':
// Dashboard auto-updates when visible
break;
case 'hardware':
// Hardware visualization is always active
break;
case 'demo':
// Demo starts manually
break;
case 'sensing':
// Lazy-init sensing tab on first visit
if (this.components.sensing && !this.components.sensing.splatRenderer) {
this.components.sensing.init().catch(error => {
console.error('Failed to initialize sensing tab:', error);
});
}
break;
}
}
// Set up global event listeners
setupEventListeners() {
// Handle window resize
window.addEventListener('resize', () => {
this.handleResize();
});
// Handle visibility change
document.addEventListener('visibilitychange', () => {
this.handleVisibilityChange();
});
// Handle before unload
window.addEventListener('beforeunload', () => {
this.cleanup();
});
}
// Handle window resize
handleResize() {
// Update canvas sizes if needed
const canvases = document.querySelectorAll('canvas');
canvases.forEach(canvas => {
const rect = canvas.parentElement.getBoundingClientRect();
if (canvas.width !== rect.width || canvas.height !== rect.height) {
canvas.width = rect.width;
canvas.height = rect.height;
}
});
}
// Handle visibility change
handleVisibilityChange() {
if (document.hidden) {
// Pause updates when page is hidden
console.log('Page hidden, pausing updates');
healthService.stopHealthMonitoring();
} else {
// Resume updates when page is visible
console.log('Page visible, resuming updates');
healthService.startHealthMonitoring();
}
}
// Set up error handling
setupErrorHandling() {
window.addEventListener('error', (event) => {
if (event.error) {
console.error('Global error:', event.error);
this.showGlobalError('An unexpected error occurred');
}
});
window.addEventListener('unhandledrejection', (event) => {
if (event.reason) {
console.error('Unhandled promise rejection:', event.reason);
this.showGlobalError('An unexpected error occurred');
}
});
}
// Show backend status notification
showBackendStatus(message, type) {
// Create status notification if it doesn't exist
let statusToast = document.getElementById('backendStatusToast');
if (!statusToast) {
statusToast = document.createElement('div');
statusToast.id = 'backendStatusToast';
statusToast.className = 'backend-status-toast';
document.body.appendChild(statusToast);
}
statusToast.textContent = message;
statusToast.className = `backend-status-toast ${type}`;
statusToast.classList.add('show');
// Auto-hide success messages, keep warnings and errors longer
const timeout = type === 'success' ? 3000 : 8000;
setTimeout(() => {
statusToast.classList.remove('show');
}, timeout);
}
// Show global error message
showGlobalError(message) {
// Create error toast if it doesn't exist
let errorToast = document.getElementById('globalErrorToast');
if (!errorToast) {
errorToast = document.createElement('div');
errorToast.id = 'globalErrorToast';
errorToast.className = 'error-toast';
document.body.appendChild(errorToast);
}
errorToast.textContent = message;
errorToast.classList.add('show');
setTimeout(() => {
errorToast.classList.remove('show');
}, 5000);
}
// Clean up resources
cleanup() {
console.log('Cleaning up application resources...');
// Dispose all components
Object.values(this.components).forEach(component => {
if (component && typeof component.dispose === 'function') {
component.dispose();
}
});
// Disconnect all WebSocket connections
wsService.disconnectAll();
// Stop health monitoring
healthService.dispose();
}
// Public API
getComponent(name) {
return this.components[name];
}
isReady() {
return this.isInitialized;
}
}
// Initialize app when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
window.wifiDensePoseApp = new WiFiDensePoseApp();
window.wifiDensePoseApp.init();
});
// Export for testing
export { WiFiDensePoseApp };