mirror of
https://github.com/screentinker/screentinker.git
synced 2026-05-15 07:32:23 -06:00
Two dashboard-accuracy improvements for issue #3. Disconnect debounce (5s): - Brief transient flaps (Engine.IO ping miss, eviction-then-reconnect, Wi-Fi blip) no longer immediately flip the device to offline in the dashboard. Disconnect handler now defers the offline transition; register handlers cancel the pending timer if reconnect lands in window. - Existing stale-disconnect guard kept as fast-path for the eviction case (no timer scheduled at all when the active heartbeat conn is already a different socket). - Re-check at timer fire compares socketIds: aborts only if a GENUINELY DIFFERENT socket reclaimed the device. Just the closing socket's own (not-yet-cleaned-up) entry is treated as stale and proceeds with offline transition. - Server-restart mid-grace is handled by the heartbeat checker safety net (existing component): any 'online' row with last_heartbeat older than heartbeatTimeout gets marked offline on next sweep. Truthful single-device command feedback: - dashboard:device-command handler now checks deviceNs.adapter.rooms for an active socket before emitting (matches the group-command route's pattern). - If room is empty, falls through to commandQueue.queueCommand (lazy require - if commit C is reverted, MODULE_NOT_FOUND is cached and every subsequent call gets consistent queued=false behavior). - Returns three-state ack to caller: { delivered, queued, reason }. - Server log line was misleading - now logs 'Command delivered to device X' vs 'Command for offline device X (queued=true/false)'. Frontend: - sendCommand() takes optional callback. Without one, fires-and-forgets (no behavior change for non-wired callers). With one, uses Socket.IO .timeout(5000).emit so the callback always fires (ack or no_ack). - Six device-detail command buttons wired to three-state toasts: reboot, shutdown, screen_off, screen_on, launch, update. - delivered: green/success toast (existing localized message) - queued: amber/warning toast (new generic message) - no_ack: red/error toast - fallback: red/error toast - Two callers intentionally left fire-and-forget: - window._sendCmd (generic remote-overlay keypress/touch helper) - enable_system_capture (has its own visual state machine; out of scope for this commit) Three new i18n keys (en.js only; other locales follow later): - device.toast.command_queued - device.toast.command_undeliverable - device.toast.command_no_ack Refs #3 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
139 lines
4.4 KiB
JavaScript
139 lines
4.4 KiB
JavaScript
let dashboardSocket = null;
|
|
const listeners = new Map();
|
|
|
|
export function connectSocket() {
|
|
const token = localStorage.getItem('token');
|
|
dashboardSocket = io('/dashboard', {
|
|
auth: { token }
|
|
});
|
|
|
|
dashboardSocket.on('connect', () => {
|
|
console.log('Dashboard connected, socket id:', dashboardSocket.id);
|
|
updateConnectionStatus(true);
|
|
emit('connected');
|
|
});
|
|
|
|
dashboardSocket.on('connect_error', (err) => {
|
|
console.error('Dashboard socket connect error:', err.message);
|
|
});
|
|
|
|
dashboardSocket.on('disconnect', (reason) => {
|
|
console.log('Dashboard disconnected:', reason);
|
|
updateConnectionStatus(false);
|
|
emit('disconnected');
|
|
});
|
|
|
|
// Device status updates
|
|
dashboardSocket.on('dashboard:device-status', (data) => {
|
|
emit('device-status', data);
|
|
});
|
|
|
|
// Screenshot ready
|
|
dashboardSocket.on('dashboard:screenshot-ready', (data) => {
|
|
emit('screenshot-ready', data);
|
|
});
|
|
|
|
// Device added
|
|
dashboardSocket.on('dashboard:device-added', (data) => {
|
|
emit('device-added', data);
|
|
});
|
|
|
|
// Device removed
|
|
dashboardSocket.on('dashboard:device-removed', (data) => {
|
|
emit('device-removed', data);
|
|
});
|
|
|
|
// Playback state
|
|
dashboardSocket.on('dashboard:playback-state', (data) => {
|
|
emit('playback-state', data);
|
|
});
|
|
|
|
// Playback progress (play_start with duration — drives device-card progress bars)
|
|
dashboardSocket.on('dashboard:playback-progress', (data) => {
|
|
emit('playback-progress', data);
|
|
});
|
|
|
|
// Wall changed — dashboard refreshes wall cards + device-grouping layout
|
|
dashboardSocket.on('dashboard:wall-changed', () => {
|
|
emit('wall-changed');
|
|
});
|
|
|
|
// Content ack
|
|
dashboardSocket.on('dashboard:content-ack', (data) => {
|
|
emit('content-ack', data);
|
|
});
|
|
|
|
return dashboardSocket;
|
|
}
|
|
|
|
function updateConnectionStatus(connected) {
|
|
const el = document.getElementById('connectionStatus');
|
|
if (!el) return;
|
|
const dot = el.querySelector('.status-dot');
|
|
const text = el.querySelector('span:last-child');
|
|
if (connected) {
|
|
dot.className = 'status-dot online';
|
|
text.textContent = 'Connected';
|
|
} else {
|
|
dot.className = 'status-dot offline';
|
|
text.textContent = 'Disconnected';
|
|
}
|
|
}
|
|
|
|
export function on(event, callback) {
|
|
if (!listeners.has(event)) listeners.set(event, []);
|
|
listeners.get(event).push(callback);
|
|
}
|
|
|
|
export function off(event, callback) {
|
|
if (!listeners.has(event)) return;
|
|
const cbs = listeners.get(event);
|
|
const idx = cbs.indexOf(callback);
|
|
if (idx > -1) cbs.splice(idx, 1);
|
|
}
|
|
|
|
function emit(event, data) {
|
|
const cbs = listeners.get(event);
|
|
if (cbs) cbs.forEach(cb => cb(data));
|
|
}
|
|
|
|
export function requestScreenshot(deviceId) {
|
|
console.log('requestScreenshot:', deviceId, 'socket connected:', dashboardSocket?.connected);
|
|
if (dashboardSocket) dashboardSocket.emit('dashboard:request-screenshot', { device_id: deviceId });
|
|
}
|
|
|
|
export function startRemote(deviceId) {
|
|
console.log('startRemote:', deviceId, 'socket connected:', dashboardSocket?.connected);
|
|
if (dashboardSocket) dashboardSocket.emit('dashboard:remote-start', { device_id: deviceId });
|
|
}
|
|
|
|
export function stopRemote(deviceId) {
|
|
if (dashboardSocket) dashboardSocket.emit('dashboard:remote-stop', { device_id: deviceId });
|
|
}
|
|
|
|
export function sendTouch(deviceId, x, y, action) {
|
|
if (dashboardSocket) dashboardSocket.emit('dashboard:remote-touch', { device_id: deviceId, x, y, action });
|
|
}
|
|
|
|
export function sendKey(deviceId, keycode) {
|
|
if (dashboardSocket) dashboardSocket.emit('dashboard:remote-key', { device_id: deviceId, keycode });
|
|
}
|
|
|
|
// Optional callback receives the server-side ack: { delivered, queued, reason }.
|
|
// Callers without a callback keep firing-and-forgetting (no behavior change).
|
|
// With a callback, we use Socket.IO's .timeout() so the callback always fires -
|
|
// either with the ack or with an Error if the server doesn't respond in 5s.
|
|
export function sendCommand(deviceId, type, payload, callback) {
|
|
if (!dashboardSocket) return;
|
|
if (typeof callback === 'function') {
|
|
dashboardSocket.timeout(5000).emit('dashboard:device-command', { device_id: deviceId, type, payload }, (err, ack) => {
|
|
if (err) callback({ delivered: false, reason: 'no_ack' });
|
|
else callback(ack || { delivered: false, reason: 'no_ack' });
|
|
});
|
|
} else {
|
|
dashboardSocket.emit('dashboard:device-command', { device_id: deviceId, type, payload });
|
|
}
|
|
}
|
|
|
|
export function getSocket() { return dashboardSocket; }
|