Files
michaelschiemer/resources/js/modules/api-manager/DeviceManager.js
Michael Schiemer 55a330b223 Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
2025-08-11 20:13:26 +02:00

704 lines
25 KiB
JavaScript

// modules/api-manager/DeviceManager.js
import { Logger } from '../../core/logger.js';
/**
* Device APIs Manager - Geolocation, Sensors, Battery, Network, Vibration
*/
export class DeviceManager {
constructor(config = {}) {
this.config = config;
this.activeWatchers = new Map();
this.sensorData = new Map();
// Check API support
this.support = {
geolocation: 'geolocation' in navigator,
deviceMotion: 'DeviceMotionEvent' in window,
deviceOrientation: 'DeviceOrientationEvent' in window,
vibration: 'vibrate' in navigator,
battery: 'getBattery' in navigator,
networkInfo: 'connection' in navigator || 'mozConnection' in navigator || 'webkitConnection' in navigator,
wakeLock: 'wakeLock' in navigator,
bluetooth: 'bluetooth' in navigator,
usb: 'usb' in navigator,
serial: 'serial' in navigator
};
Logger.info('[DeviceManager] Initialized with support:', this.support);
// Initialize sensors if available
this.initializeSensors();
}
/**
* Geolocation API
*/
geolocation = {
// Get current position
getCurrent: (options = {}) => {
return new Promise((resolve, reject) => {
if (!this.support.geolocation) {
reject(new Error('Geolocation not supported'));
return;
}
const defaultOptions = {
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 60000,
...options
};
navigator.geolocation.getCurrentPosition(
(position) => {
const location = this.enhanceLocationData(position);
Logger.info('[DeviceManager] Location acquired:', {
lat: location.latitude,
lng: location.longitude,
accuracy: location.accuracy
});
resolve(location);
},
(error) => {
Logger.error('[DeviceManager] Geolocation failed:', error.message);
reject(error);
},
defaultOptions
);
});
},
// Watch position changes
watch: (callback, options = {}) => {
if (!this.support.geolocation) {
throw new Error('Geolocation not supported');
}
const defaultOptions = {
enableHighAccuracy: true,
timeout: 30000,
maximumAge: 10000,
...options
};
const watchId = navigator.geolocation.watchPosition(
(position) => {
const location = this.enhanceLocationData(position);
callback(location);
},
(error) => {
Logger.error('[DeviceManager] Location watch failed:', error.message);
callback({ error });
},
defaultOptions
);
this.activeWatchers.set(`geo_${watchId}`, {
type: 'geolocation',
id: watchId,
stop: () => {
navigator.geolocation.clearWatch(watchId);
this.activeWatchers.delete(`geo_${watchId}`);
}
});
Logger.info('[DeviceManager] Location watch started:', watchId);
return {
id: watchId,
stop: () => {
navigator.geolocation.clearWatch(watchId);
this.activeWatchers.delete(`geo_${watchId}`);
Logger.info('[DeviceManager] Location watch stopped:', watchId);
}
};
},
// Calculate distance between two points
distance: (pos1, pos2) => {
const R = 6371; // Earth's radius in km
const dLat = this.toRadians(pos2.latitude - pos1.latitude);
const dLon = this.toRadians(pos2.longitude - pos1.longitude);
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(this.toRadians(pos1.latitude)) * Math.cos(this.toRadians(pos2.latitude)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
const distance = R * c;
return {
kilometers: distance,
miles: distance * 0.621371,
meters: distance * 1000
};
}
};
/**
* Device Motion and Orientation
*/
motion = {
// Start motion detection
start: (callback, options = {}) => {
if (!this.support.deviceMotion) {
throw new Error('Device Motion not supported');
}
const handler = (event) => {
const motionData = {
acceleration: event.acceleration,
accelerationIncludingGravity: event.accelerationIncludingGravity,
rotationRate: event.rotationRate,
interval: event.interval,
timestamp: event.timeStamp,
// Enhanced data
totalAcceleration: this.calculateTotalAcceleration(event.acceleration),
shake: this.detectShake(event.accelerationIncludingGravity),
orientation: this.getDeviceOrientation(event)
};
callback(motionData);
};
// Request permission for iOS 13+
if (typeof DeviceMotionEvent.requestPermission === 'function') {
DeviceMotionEvent.requestPermission().then(response => {
if (response === 'granted') {
window.addEventListener('devicemotion', handler);
}
});
} else {
window.addEventListener('devicemotion', handler);
}
const watcherId = this.generateId('motion');
this.activeWatchers.set(watcherId, {
type: 'motion',
handler,
stop: () => {
window.removeEventListener('devicemotion', handler);
this.activeWatchers.delete(watcherId);
}
});
Logger.info('[DeviceManager] Motion detection started');
return {
id: watcherId,
stop: () => {
window.removeEventListener('devicemotion', handler);
this.activeWatchers.delete(watcherId);
Logger.info('[DeviceManager] Motion detection stopped');
}
};
},
// Start orientation detection
startOrientation: (callback, options = {}) => {
if (!this.support.deviceOrientation) {
throw new Error('Device Orientation not supported');
}
const handler = (event) => {
const orientationData = {
alpha: event.alpha, // Z axis (0-360)
beta: event.beta, // X axis (-180 to 180)
gamma: event.gamma, // Y axis (-90 to 90)
absolute: event.absolute,
timestamp: event.timeStamp,
// Enhanced data
compass: this.calculateCompass(event.alpha),
tilt: this.calculateTilt(event.beta, event.gamma),
rotation: this.getRotationState(event)
};
callback(orientationData);
};
// Request permission for iOS 13+
if (typeof DeviceOrientationEvent.requestPermission === 'function') {
DeviceOrientationEvent.requestPermission().then(response => {
if (response === 'granted') {
window.addEventListener('deviceorientation', handler);
}
});
} else {
window.addEventListener('deviceorientation', handler);
}
const watcherId = this.generateId('orientation');
this.activeWatchers.set(watcherId, {
type: 'orientation',
handler,
stop: () => {
window.removeEventListener('deviceorientation', handler);
this.activeWatchers.delete(watcherId);
}
});
Logger.info('[DeviceManager] Orientation detection started');
return {
id: watcherId,
stop: () => {
window.removeEventListener('deviceorientation', handler);
this.activeWatchers.delete(watcherId);
Logger.info('[DeviceManager] Orientation detection stopped');
}
};
}
};
/**
* Vibration API
*/
vibration = {
// Simple vibration
vibrate: (pattern) => {
if (!this.support.vibration) {
Logger.warn('[DeviceManager] Vibration not supported');
return false;
}
try {
navigator.vibrate(pattern);
Logger.info('[DeviceManager] Vibration triggered:', pattern);
return true;
} catch (error) {
Logger.error('[DeviceManager] Vibration failed:', error);
return false;
}
},
// Predefined patterns
patterns: {
short: 200,
long: 600,
double: [200, 100, 200],
triple: [200, 100, 200, 100, 200],
sos: [100, 30, 100, 30, 100, 200, 200, 30, 200, 30, 200, 200, 100, 30, 100, 30, 100],
heartbeat: [100, 30, 100, 130, 40, 30, 40, 30, 100],
notification: [200, 100, 200],
success: [100],
error: [300, 100, 300],
warning: [200, 100, 200, 100, 200]
},
// Stop vibration
stop: () => {
if (this.support.vibration) {
navigator.vibrate(0);
Logger.info('[DeviceManager] Vibration stopped');
}
},
// Haptic feedback helpers
success: () => this.vibration.vibrate(this.vibration.patterns.success),
error: () => this.vibration.vibrate(this.vibration.patterns.error),
warning: () => this.vibration.vibrate(this.vibration.patterns.warning),
notification: () => this.vibration.vibrate(this.vibration.patterns.notification)
};
/**
* Battery API
*/
battery = {
// Get battery status
get: async () => {
if (!this.support.battery) {
Logger.warn('[DeviceManager] Battery API not supported');
return null;
}
try {
const battery = await navigator.getBattery();
const batteryInfo = {
level: Math.round(battery.level * 100),
charging: battery.charging,
chargingTime: battery.chargingTime,
dischargingTime: battery.dischargingTime,
// Enhanced data
status: this.getBatteryStatus(battery),
timeRemaining: this.formatBatteryTime(battery)
};
Logger.info('[DeviceManager] Battery status:', batteryInfo);
return batteryInfo;
} catch (error) {
Logger.error('[DeviceManager] Battery status failed:', error);
return null;
}
},
// Watch battery changes
watch: async (callback) => {
if (!this.support.battery) {
throw new Error('Battery API not supported');
}
try {
const battery = await navigator.getBattery();
const events = ['chargingchange', 'levelchange', 'chargingtimechange', 'dischargingtimechange'];
const handlers = [];
events.forEach(eventType => {
const handler = () => {
const batteryInfo = {
level: Math.round(battery.level * 100),
charging: battery.charging,
chargingTime: battery.chargingTime,
dischargingTime: battery.dischargingTime,
status: this.getBatteryStatus(battery),
timeRemaining: this.formatBatteryTime(battery),
event: eventType
};
callback(batteryInfo);
};
battery.addEventListener(eventType, handler);
handlers.push({ event: eventType, handler });
});
const watcherId = this.generateId('battery');
this.activeWatchers.set(watcherId, {
type: 'battery',
battery,
handlers,
stop: () => {
handlers.forEach(({ event, handler }) => {
battery.removeEventListener(event, handler);
});
this.activeWatchers.delete(watcherId);
}
});
Logger.info('[DeviceManager] Battery watch started');
return {
id: watcherId,
stop: () => {
handlers.forEach(({ event, handler }) => {
battery.removeEventListener(event, handler);
});
this.activeWatchers.delete(watcherId);
Logger.info('[DeviceManager] Battery watch stopped');
}
};
} catch (error) {
Logger.error('[DeviceManager] Battery watch failed:', error);
throw error;
}
}
};
/**
* Network Information API
*/
network = {
// Get connection info
get: () => {
if (!this.support.networkInfo) {
Logger.warn('[DeviceManager] Network Information not supported');
return null;
}
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
return {
effectiveType: connection.effectiveType,
downlink: connection.downlink,
rtt: connection.rtt,
saveData: connection.saveData,
// Enhanced data
speed: this.getConnectionSpeed(connection),
quality: this.getConnectionQuality(connection),
recommendation: this.getNetworkRecommendation(connection)
};
},
// Watch network changes
watch: (callback) => {
if (!this.support.networkInfo) {
throw new Error('Network Information not supported');
}
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
const handler = () => {
const networkInfo = {
effectiveType: connection.effectiveType,
downlink: connection.downlink,
rtt: connection.rtt,
saveData: connection.saveData,
speed: this.getConnectionSpeed(connection),
quality: this.getConnectionQuality(connection),
recommendation: this.getNetworkRecommendation(connection)
};
callback(networkInfo);
};
connection.addEventListener('change', handler);
const watcherId = this.generateId('network');
this.activeWatchers.set(watcherId, {
type: 'network',
connection,
handler,
stop: () => {
connection.removeEventListener('change', handler);
this.activeWatchers.delete(watcherId);
}
});
Logger.info('[DeviceManager] Network watch started');
return {
id: watcherId,
stop: () => {
connection.removeEventListener('change', handler);
this.activeWatchers.delete(watcherId);
Logger.info('[DeviceManager] Network watch stopped');
}
};
}
};
/**
* Wake Lock API
*/
wakeLock = {
// Request wake lock
request: async (type = 'screen') => {
if (!this.support.wakeLock) {
Logger.warn('[DeviceManager] Wake Lock not supported');
return null;
}
try {
const wakeLock = await navigator.wakeLock.request(type);
Logger.info(`[DeviceManager] Wake lock acquired: ${type}`);
return {
type: wakeLock.type,
release: () => {
wakeLock.release();
Logger.info(`[DeviceManager] Wake lock released: ${type}`);
}
};
} catch (error) {
Logger.error('[DeviceManager] Wake lock failed:', error);
throw error;
}
}
};
// Helper methods
initializeSensors() {
// Initialize any sensors that need setup
if (this.support.deviceMotion || this.support.deviceOrientation) {
// Store baseline sensor data for comparison
this.sensorData.set('motionBaseline', { x: 0, y: 0, z: 0 });
this.sensorData.set('shakeThreshold', this.config.shakeThreshold || 15);
}
}
enhanceLocationData(position) {
return {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy,
altitude: position.coords.altitude,
altitudeAccuracy: position.coords.altitudeAccuracy,
heading: position.coords.heading,
speed: position.coords.speed,
timestamp: position.timestamp,
// Enhanced data
coordinates: `${position.coords.latitude},${position.coords.longitude}`,
accuracyLevel: this.getAccuracyLevel(position.coords.accuracy),
mapUrl: `https://maps.google.com/?q=${position.coords.latitude},${position.coords.longitude}`
};
}
getAccuracyLevel(accuracy) {
if (accuracy <= 5) return 'excellent';
if (accuracy <= 10) return 'good';
if (accuracy <= 50) return 'fair';
return 'poor';
}
calculateTotalAcceleration(acceleration) {
if (!acceleration) return 0;
const x = acceleration.x || 0;
const y = acceleration.y || 0;
const z = acceleration.z || 0;
return Math.sqrt(x * x + y * y + z * z);
}
detectShake(acceleration) {
if (!acceleration) return false;
const threshold = this.sensorData.get('shakeThreshold');
const x = Math.abs(acceleration.x || 0);
const y = Math.abs(acceleration.y || 0);
const z = Math.abs(acceleration.z || 0);
return (x > threshold || y > threshold || z > threshold);
}
getDeviceOrientation(event) {
const acceleration = event.accelerationIncludingGravity;
if (!acceleration) return 'unknown';
const x = acceleration.x || 0;
const y = acceleration.y || 0;
const z = acceleration.z || 0;
if (Math.abs(x) > Math.abs(y) && Math.abs(x) > Math.abs(z)) {
return x > 0 ? 'landscape-right' : 'landscape-left';
} else if (Math.abs(y) > Math.abs(z)) {
return y > 0 ? 'portrait-upside-down' : 'portrait';
} else {
return z > 0 ? 'face-down' : 'face-up';
}
}
calculateCompass(alpha) {
if (alpha === null) return null;
const directions = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];
const index = Math.round(alpha / 45) % 8;
return {
degrees: Math.round(alpha),
direction: directions[index],
cardinal: this.getCardinalDirection(alpha)
};
}
getCardinalDirection(alpha) {
if (alpha >= 337.5 || alpha < 22.5) return 'North';
if (alpha >= 22.5 && alpha < 67.5) return 'Northeast';
if (alpha >= 67.5 && alpha < 112.5) return 'East';
if (alpha >= 112.5 && alpha < 157.5) return 'Southeast';
if (alpha >= 157.5 && alpha < 202.5) return 'South';
if (alpha >= 202.5 && alpha < 247.5) return 'Southwest';
if (alpha >= 247.5 && alpha < 292.5) return 'West';
if (alpha >= 292.5 && alpha < 337.5) return 'Northwest';
return 'Unknown';
}
calculateTilt(beta, gamma) {
return {
x: Math.round(beta || 0),
y: Math.round(gamma || 0),
magnitude: Math.round(Math.sqrt((beta || 0) ** 2 + (gamma || 0) ** 2))
};
}
getRotationState(event) {
const { alpha, beta, gamma } = event;
// Determine if device is being rotated significantly
const rotationThreshold = 10;
const isRotating = Math.abs(beta) > rotationThreshold || Math.abs(gamma) > rotationThreshold;
return {
isRotating,
intensity: isRotating ? Math.max(Math.abs(beta), Math.abs(gamma)) : 0
};
}
getBatteryStatus(battery) {
const level = battery.level * 100;
if (battery.charging) return 'charging';
if (level <= 10) return 'critical';
if (level <= 20) return 'low';
if (level <= 50) return 'medium';
return 'high';
}
formatBatteryTime(battery) {
const time = battery.charging ? battery.chargingTime : battery.dischargingTime;
if (time === Infinity || isNaN(time)) return 'Unknown';
const hours = Math.floor(time / 3600);
const minutes = Math.floor((time % 3600) / 60);
return `${hours}h ${minutes}m`;
}
getConnectionSpeed(connection) {
const downlink = connection.downlink;
if (downlink >= 10) return 'fast';
if (downlink >= 1.5) return 'good';
if (downlink >= 0.5) return 'slow';
return 'very-slow';
}
getConnectionQuality(connection) {
const effectiveType = connection.effectiveType;
switch (effectiveType) {
case '4g': return 'excellent';
case '3g': return 'good';
case '2g': return 'poor';
case 'slow-2g': return 'very-poor';
default: return 'unknown';
}
}
getNetworkRecommendation(connection) {
const quality = this.getConnectionQuality(connection);
switch (quality) {
case 'excellent':
return 'Full quality content recommended';
case 'good':
return 'Moderate quality content recommended';
case 'poor':
return 'Light content only, avoid large files';
case 'very-poor':
return 'Text-only content recommended';
default:
return 'Monitor connection quality';
}
}
toRadians(degrees) {
return degrees * (Math.PI / 180);
}
generateId(prefix = 'device') {
return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Stop all active watchers
*/
stopAllWatchers() {
this.activeWatchers.forEach(watcher => {
watcher.stop();
});
this.activeWatchers.clear();
Logger.info('[DeviceManager] All watchers stopped');
}
/**
* Get device capabilities summary
*/
getCapabilities() {
return {
support: this.support,
activeWatchers: this.activeWatchers.size,
watcherTypes: Array.from(this.activeWatchers.values()).map(w => w.type)
};
}
}