mirror of
https://github.com/screentinker/screentinker.git
synced 2026-05-15 07:32:23 -06:00
Add device groups UI, group commands, proxy IP fix, and web player detection
- Dashboard now organizes devices by group with colored section headers - Group command endpoint (POST /groups/:id/command) sends to all members - Manage modal with multi-group confirmation prompt - Destructive commands (reboot/shutdown) require confirmation - Ungrouped devices shown separately at bottom - trust proxy + X-Forwarded-For for real client IPs behind Nginx - Hide Android-only telemetry (battery/storage/RAM/CPU/WiFi) for web players Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e7081a579c
commit
faa437881f
|
|
@ -91,6 +91,15 @@ export const api = {
|
|||
body: JSON.stringify({ order })
|
||||
}),
|
||||
|
||||
// Device Groups
|
||||
getGroups: () => request('/groups'),
|
||||
createGroup: (name, color) => request('/groups', { method: 'POST', body: JSON.stringify({ name, color }) }),
|
||||
deleteGroup: (id) => request(`/groups/${id}`, { method: 'DELETE' }),
|
||||
getGroupDevices: (id) => request(`/groups/${id}/devices`),
|
||||
addDeviceToGroup: (groupId, device_id) => request(`/groups/${groupId}/devices`, { method: 'POST', body: JSON.stringify({ device_id }) }),
|
||||
removeDeviceFromGroup: (groupId, deviceId) => request(`/groups/${groupId}/devices/${deviceId}`, { method: 'DELETE' }),
|
||||
sendGroupCommand: (groupId, type, payload) => request(`/groups/${groupId}/command`, { method: 'POST', body: JSON.stringify({ type, payload }) }),
|
||||
|
||||
// Admin - Users
|
||||
getUsers: () => request('/auth/users'),
|
||||
deleteUser: (id) => request(`/auth/users/${id}`, { method: 'DELETE' }),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,16 @@ import { api } from '../api.js';
|
|||
import { on, off, requestScreenshot } from '../socket.js';
|
||||
import { showToast } from '../components/toast.js';
|
||||
|
||||
const DESTRUCTIVE_COMMANDS = ['reboot', 'shutdown'];
|
||||
const GROUP_COMMANDS = [
|
||||
{ type: 'screen_on', label: 'Screen On' },
|
||||
{ type: 'screen_off', label: 'Screen Off' },
|
||||
{ type: 'launch', label: 'Restart App' },
|
||||
{ type: 'update', label: 'Check Update' },
|
||||
{ type: 'reboot', label: 'Reboot', destructive: true },
|
||||
{ type: 'shutdown', label: 'Shutdown', destructive: true },
|
||||
];
|
||||
|
||||
let statusHandler = null;
|
||||
let screenshotHandler = null;
|
||||
let refreshInterval = null;
|
||||
|
|
@ -94,6 +104,33 @@ function renderDeviceCard(device) {
|
|||
`;
|
||||
}
|
||||
|
||||
function renderGroupSection(group, devices) {
|
||||
const onlineCount = devices.filter(d => d.status === 'online').length;
|
||||
return `
|
||||
<div class="group-section" data-group-id="${group.id}" style="margin-bottom:24px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;padding:8px 12px;background:var(--bg-secondary);border-radius:8px;border-left:4px solid ${group.color || '#3B82F6'}">
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<strong style="font-size:15px">${group.name}</strong>
|
||||
<span style="color:var(--text-muted);font-size:12px">${devices.length} device${devices.length !== 1 ? 's' : ''} · ${onlineCount} online</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;align-items:center">
|
||||
${devices.length > 0 ? `
|
||||
<select class="input group-cmd-select" data-group-id="${group.id}" data-group-name="${group.name}" data-device-count="${devices.length}" style="width:150px;padding:4px 8px;font-size:12px;background:var(--bg-input)">
|
||||
<option value="">Send Command...</option>
|
||||
${GROUP_COMMANDS.map(c => `<option value="${c.type}" ${c.destructive ? 'style="color:var(--danger)"' : ''}>${c.label}</option>`).join('')}
|
||||
</select>
|
||||
` : ''}
|
||||
<button class="btn" data-group-manage="${group.id}" style="padding:4px 10px;font-size:12px" title="Add/remove devices">Manage</button>
|
||||
<button class="btn" data-group-delete="${group.id}" style="padding:4px 8px;font-size:12px;color:var(--danger)" title="Delete group">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="device-grid">
|
||||
${devices.length > 0 ? devices.map(renderDeviceCard).join('') : '<div style="color:var(--text-muted);font-size:13px;padding:8px 12px">No devices in this group. Click Manage to add some.</div>'}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export function render(container) {
|
||||
container.innerHTML = `
|
||||
<div class="page-header">
|
||||
|
|
@ -101,6 +138,8 @@ export function render(container) {
|
|||
<h1>Displays <span class="help-tip" data-tip="Your paired display devices. Green = online, red = offline. Click a device to manage its playlist, view telemetry, or use remote control.">?</span></h1>
|
||||
<div class="subtitle">Manage your remote displays</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px">
|
||||
<button class="btn" id="createGroupBtn">+ Group</button>
|
||||
<button class="btn btn-primary" id="addDeviceBtn">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
||||
|
|
@ -108,6 +147,7 @@ export function render(container) {
|
|||
Add Display
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="dashStats" style="display:flex;gap:12px;margin-bottom:16px"></div>
|
||||
<div style="display:flex;gap:12px;margin-bottom:16px;align-items:center">
|
||||
<input type="text" id="deviceSearch" class="input" placeholder="Search displays..." style="max-width:300px">
|
||||
|
|
@ -117,16 +157,7 @@ export function render(container) {
|
|||
<option value="offline">Offline</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="device-grid" id="deviceGrid">
|
||||
<div class="empty-state">
|
||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
|
||||
<line x1="8" y1="21" x2="16" y2="21"/>
|
||||
<line x1="12" y1="17" x2="12" y2="21"/>
|
||||
</svg>
|
||||
<h3>Loading displays...</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div id="groupedDevices"></div>
|
||||
`;
|
||||
|
||||
const addBtn = container.querySelector('#addDeviceBtn');
|
||||
|
|
@ -166,41 +197,51 @@ export function render(container) {
|
|||
await api.pairDevice(code, name || undefined);
|
||||
document.getElementById('addDeviceModal').style.display = 'none';
|
||||
showToast('Display paired successfully!', 'success');
|
||||
loadDevices();
|
||||
loadDashboard();
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
// Load devices
|
||||
loadDevices();
|
||||
// Create group
|
||||
container.querySelector('#createGroupBtn').addEventListener('click', async () => {
|
||||
const name = prompt('Group name:');
|
||||
if (!name) return;
|
||||
try {
|
||||
await api.createGroup(name);
|
||||
showToast('Group created', 'success');
|
||||
loadDashboard();
|
||||
} catch (e) { showToast(e.message, 'error'); }
|
||||
});
|
||||
|
||||
// Load everything
|
||||
loadDashboard();
|
||||
|
||||
// Real-time updates
|
||||
statusHandler = (data) => {
|
||||
const card = document.querySelector(`[data-device-id="${data.device_id}"]`);
|
||||
if (card) {
|
||||
const cards = document.querySelectorAll(`[data-device-id="${data.device_id}"]`);
|
||||
cards.forEach(card => {
|
||||
const statusEl = card.querySelector('.device-card-status');
|
||||
statusEl.innerHTML = `<span class="status-dot ${data.status}"></span><span>${data.status}</span>`;
|
||||
}
|
||||
if (statusEl) statusEl.innerHTML = `<span class="status-dot ${data.status}"></span><span>${data.status}</span>`;
|
||||
});
|
||||
};
|
||||
|
||||
screenshotHandler = (data) => {
|
||||
const preview = document.getElementById(`preview-${data.device_id}`);
|
||||
if (preview) {
|
||||
// Update all instances of this device's preview (may appear in multiple groups)
|
||||
document.querySelectorAll(`#preview-${data.device_id}`).forEach(preview => {
|
||||
const imgSrc = data.image_data || (data.url + '&token=' + localStorage.getItem('token'));
|
||||
const img = preview.querySelector('img');
|
||||
if (img) {
|
||||
img.src = imgSrc;
|
||||
} else {
|
||||
preview.innerHTML = `<img src="${imgSrc}" alt="Screenshot" loading="lazy">` +
|
||||
preview.querySelector('.device-card-status').outerHTML;
|
||||
}
|
||||
const statusHtml = preview.querySelector('.device-card-status')?.outerHTML || '';
|
||||
preview.innerHTML = `<img src="${imgSrc}" alt="Screenshot" loading="lazy">${statusHtml}`;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Device added/removed - refresh the whole list
|
||||
const deviceAddedHandler = () => loadDevices();
|
||||
const deviceRemovedHandler = () => loadDevices();
|
||||
const deviceAddedHandler = () => loadDashboard();
|
||||
const deviceRemovedHandler = () => loadDashboard();
|
||||
|
||||
on('device-status', statusHandler);
|
||||
on('screenshot-ready', screenshotHandler);
|
||||
|
|
@ -214,7 +255,6 @@ export function render(container) {
|
|||
});
|
||||
}, 2000);
|
||||
|
||||
// Refresh screenshots periodically
|
||||
refreshInterval = setInterval(() => {
|
||||
document.querySelectorAll('.device-card').forEach(card => {
|
||||
requestScreenshot(card.dataset.deviceId);
|
||||
|
|
@ -222,14 +262,14 @@ export function render(container) {
|
|||
}, 30000);
|
||||
}
|
||||
|
||||
async function loadDevices() {
|
||||
const grid = document.getElementById('deviceGrid');
|
||||
if (!grid) return;
|
||||
async function loadDashboard() {
|
||||
const main = document.getElementById('groupedDevices');
|
||||
if (!main) return;
|
||||
|
||||
try {
|
||||
const devices = await api.getDevices();
|
||||
const [devices, groups] = await Promise.all([api.getDevices(), api.getGroups()]);
|
||||
|
||||
// Stats cards
|
||||
// Stats
|
||||
const online = devices.filter(d => d.status === 'online').length;
|
||||
const offline = devices.filter(d => d.status === 'offline').length;
|
||||
const provisioning = devices.filter(d => d.status === 'provisioning').length;
|
||||
|
|
@ -256,9 +296,9 @@ async function loadDevices() {
|
|||
`;
|
||||
}
|
||||
|
||||
if (devices.length === 0) {
|
||||
grid.innerHTML = `
|
||||
<div class="empty-state" style="grid-column: 1/-1">
|
||||
if (devices.length === 0 && groups.length === 0) {
|
||||
main.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
|
||||
<line x1="8" y1="21" x2="16" y2="21"/>
|
||||
|
|
@ -268,12 +308,159 @@ async function loadDevices() {
|
|||
<p>Install the ScreenTinker app on your Apolosign TV and pair it using the button above.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch group memberships
|
||||
const groupsWithDevices = await Promise.all(groups.map(async g => {
|
||||
const members = await api.getGroupDevices(g.id);
|
||||
const memberIds = new Set(members.map(m => m.id));
|
||||
// Use full device data from the main devices list (has telemetry/screenshots)
|
||||
const fullDevices = devices.filter(d => memberIds.has(d.id));
|
||||
return { ...g, devices: fullDevices, memberIds };
|
||||
}));
|
||||
|
||||
// Find ungrouped devices
|
||||
const allGroupedIds = new Set();
|
||||
groupsWithDevices.forEach(g => g.memberIds.forEach(id => allGroupedIds.add(id)));
|
||||
const ungrouped = devices.filter(d => !allGroupedIds.has(d.id));
|
||||
|
||||
let html = '';
|
||||
|
||||
// Render each group with its devices
|
||||
for (const g of groupsWithDevices) {
|
||||
html += renderGroupSection(g, g.devices);
|
||||
}
|
||||
|
||||
// Render ungrouped devices
|
||||
if (ungrouped.length > 0) {
|
||||
html += `
|
||||
<div style="margin-bottom:24px">
|
||||
${groups.length > 0 ? `
|
||||
<div style="display:flex;align-items:center;margin-bottom:10px;padding:8px 12px;background:var(--bg-secondary);border-radius:8px;border-left:4px solid var(--text-muted)">
|
||||
<strong style="font-size:15px;color:var(--text-muted)">Ungrouped</strong>
|
||||
<span style="color:var(--text-muted);font-size:12px;margin-left:10px">${ungrouped.length} device${ungrouped.length !== 1 ? 's' : ''}</span>
|
||||
</div>` : ''}
|
||||
<div class="device-grid">
|
||||
${ungrouped.map(renderDeviceCard).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
main.innerHTML = html;
|
||||
attachGroupHandlers(groupsWithDevices, devices);
|
||||
|
||||
} catch (err) {
|
||||
main.innerHTML = `<div class="empty-state"><h3>Failed to load displays</h3><p>${err.message}</p></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function attachGroupHandlers(groupsWithDevices, allDevices) {
|
||||
// Command select handlers
|
||||
document.querySelectorAll('.group-cmd-select').forEach(select => {
|
||||
select.addEventListener('change', async (e) => {
|
||||
const type = e.target.value;
|
||||
if (!type) return;
|
||||
const groupId = e.target.dataset.groupId;
|
||||
const groupName = e.target.dataset.groupName;
|
||||
const count = e.target.dataset.deviceCount;
|
||||
|
||||
if (DESTRUCTIVE_COMMANDS.includes(type)) {
|
||||
if (!confirm(`${type.toUpperCase()} all ${count} device${count !== '1' ? 's' : ''} in "${groupName}"?\n\nThis cannot be undone.`)) {
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await api.sendGroupCommand(groupId, type);
|
||||
showToast(`${type} sent to ${result.sent}/${result.total} devices${result.offline > 0 ? ` (${result.offline} offline)` : ''}`, result.offline > 0 ? 'warning' : 'success');
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
}
|
||||
e.target.value = '';
|
||||
});
|
||||
});
|
||||
|
||||
// Delete group
|
||||
document.querySelectorAll('[data-group-delete]').forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
const id = btn.dataset.groupDelete;
|
||||
if (!confirm('Delete this group? Devices will not be affected.')) return;
|
||||
try {
|
||||
await api.deleteGroup(id);
|
||||
showToast('Group deleted', 'success');
|
||||
loadDashboard();
|
||||
} catch (e) { showToast(e.message, 'error'); }
|
||||
});
|
||||
});
|
||||
|
||||
// Manage group (add/remove devices)
|
||||
document.querySelectorAll('[data-group-manage]').forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
const groupId = btn.dataset.groupManage;
|
||||
const group = groupsWithDevices.find(g => g.id === groupId);
|
||||
const memberIds = new Set(group.devices.map(d => d.id));
|
||||
|
||||
// Get all groups for multi-group warning
|
||||
const otherGroups = groupsWithDevices.filter(g => g.id !== groupId);
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:1000';
|
||||
modal.innerHTML = `
|
||||
<div style="background:var(--bg-card);border-radius:12px;padding:24px;max-width:400px;width:90%;max-height:70vh;overflow-y:auto">
|
||||
<h3 style="margin:0 0 4px">${group.name}</h3>
|
||||
<p style="margin:0 0 16px;font-size:12px;color:var(--text-muted)">Check devices to add them to this group</p>
|
||||
<div style="display:flex;flex-direction:column;gap:6px">
|
||||
${allDevices.filter(d => d.status !== 'provisioning').map(d => {
|
||||
const inOther = otherGroups.filter(g => g.memberIds.has(d.id)).map(g => g.name);
|
||||
return `
|
||||
<label style="display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;cursor:pointer;background:var(--bg-secondary)">
|
||||
<input type="checkbox" data-device-id="${d.id}" data-in-groups="${inOther.join(',')}" ${memberIds.has(d.id) ? 'checked' : ''}>
|
||||
<span class="status-dot ${d.status}" style="width:8px;height:8px"></span>
|
||||
<span style="font-size:13px;flex:1">${d.name}</span>
|
||||
${inOther.length > 0 ? `<span style="font-size:10px;color:var(--text-muted);background:var(--bg-primary);padding:1px 6px;border-radius:8px">${inOther.join(', ')}</span>` : ''}
|
||||
</label>
|
||||
`;
|
||||
}).join('')}
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;margin-top:16px;justify-content:flex-end">
|
||||
<button class="btn" id="manageGroupClose">Done</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
modal.querySelector('#manageGroupClose').onclick = () => { modal.remove(); loadDashboard(); };
|
||||
modal.addEventListener('click', (ev) => { if (ev.target === modal) { modal.remove(); loadDashboard(); } });
|
||||
|
||||
modal.querySelectorAll('input[type="checkbox"]').forEach(cb => {
|
||||
cb.addEventListener('change', async () => {
|
||||
const deviceId = cb.dataset.deviceId;
|
||||
const existingGroups = cb.dataset.inGroups;
|
||||
try {
|
||||
if (cb.checked && existingGroups) {
|
||||
if (!confirm(`This device is already in: ${existingGroups}\n\nAdd it to "${group.name}" too?`)) {
|
||||
cb.checked = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (cb.checked) {
|
||||
await api.addDeviceToGroup(groupId, deviceId);
|
||||
} else {
|
||||
grid.innerHTML = devices.map(renderDeviceCard).join('');
|
||||
await api.removeDeviceFromGroup(groupId, deviceId);
|
||||
}
|
||||
} catch (err) {
|
||||
grid.innerHTML = `<div class="empty-state" style="grid-column: 1/-1"><h3>Failed to load displays</h3><p>${err.message}</p></div>`;
|
||||
showToast(err.message, 'error');
|
||||
cb.checked = !cb.checked;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function cleanup() {
|
||||
|
|
|
|||
|
|
@ -200,6 +200,7 @@ async function loadDevice(deviceId, activeTab = null) {
|
|||
<div class="info-card-label">IP Address</div>
|
||||
<div class="info-card-value small">${device.ip_address || '--'}</div>
|
||||
</div>
|
||||
${device.android_version && !device.android_version.startsWith('Web/') ? `
|
||||
<div class="info-card">
|
||||
<div class="info-card-label">Battery</div>
|
||||
<div class="info-card-value" id="telBattery">${latestTelemetry.battery_level != null ? latestTelemetry.battery_level + '%' : '--'}</div>
|
||||
|
|
@ -218,27 +219,38 @@ async function loadDevice(deviceId, activeTab = null) {
|
|||
style="width:${((latestTelemetry.storage_total_mb - latestTelemetry.storage_free_mb) / latestTelemetry.storage_total_mb * 100)}%"></div>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
` : `
|
||||
<div class="info-card">
|
||||
<div class="info-card-label">Player Type</div>
|
||||
<div class="info-card-value small">Web Player</div>
|
||||
</div>
|
||||
`}
|
||||
${device.android_version && !device.android_version.startsWith('Web/') ? `
|
||||
<div class="info-card">
|
||||
<div class="info-card-label">WiFi</div>
|
||||
<div class="info-card-value small" id="telWifi">${latestTelemetry.wifi_ssid || '--'}</div>
|
||||
<div style="font-size:11px;color:var(--text-muted);margin-top:2px" id="telRssi">${latestTelemetry.wifi_rssi ? latestTelemetry.wifi_rssi + ' dBm' : ''}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="info-card">
|
||||
<div class="info-card-label">Uptime</div>
|
||||
<div class="info-card-value small" id="telUptime">${formatUptime(latestTelemetry.uptime_seconds)}</div>
|
||||
</div>
|
||||
${device.android_version && !device.android_version.startsWith('Web/') ? `
|
||||
<div class="info-card">
|
||||
<div class="info-card-label">Android Version</div>
|
||||
<div class="info-card-value small">${device.android_version || '--'}</div>
|
||||
<div class="info-card-value small">${device.android_version}</div>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<div class="info-card-label">App Version</div>
|
||||
<div class="info-card-value small">${device.app_version || '--'}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="info-card">
|
||||
<div class="info-card-label">Screen Resolution</div>
|
||||
<div class="info-card-value small">${device.screen_width && device.screen_height ? device.screen_width + 'x' + device.screen_height : '--'}</div>
|
||||
</div>
|
||||
${device.android_version && !device.android_version.startsWith('Web/') ? `
|
||||
<div class="info-card">
|
||||
<div class="info-card-label">RAM</div>
|
||||
<div class="info-card-value small" id="telRam">${latestTelemetry.ram_free_mb ? formatBytes(latestTelemetry.ram_free_mb) + ' free' : '--'}</div>
|
||||
|
|
@ -247,6 +259,7 @@ async function loadDevice(deviceId, activeTab = null) {
|
|||
<div class="info-card-label">CPU Usage</div>
|
||||
<div class="info-card-value small" id="telCpu">${latestTelemetry.cpu_usage != null ? latestTelemetry.cpu_usage.toFixed(1) + '%' : '--'}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<!-- Uptime Timeline (24h) -->
|
||||
|
|
|
|||
|
|
@ -85,4 +85,38 @@ router.post('/:id/assign-content', (req, res) => {
|
|||
res.json({ success: true, devices_updated: devices.length });
|
||||
});
|
||||
|
||||
// Send command to all devices in a group
|
||||
router.post('/:id/command', (req, res) => {
|
||||
const { type, payload } = req.body;
|
||||
if (!type) return res.status(400).json({ error: 'command type required' });
|
||||
|
||||
// Verify group belongs to user
|
||||
const group = db.prepare('SELECT * FROM device_groups WHERE id = ? AND user_id = ?').get(req.params.id, req.user.id);
|
||||
if (!group) return res.status(404).json({ error: 'group not found' });
|
||||
|
||||
const devices = db.prepare(`
|
||||
SELECT d.id, d.name, d.status FROM devices d
|
||||
JOIN device_group_members dgm ON d.id = dgm.device_id
|
||||
WHERE dgm.group_id = ?
|
||||
`).all(req.params.id);
|
||||
|
||||
const deviceNs = req.app.get('io').of('/device');
|
||||
const results = [];
|
||||
|
||||
for (const device of devices) {
|
||||
const room = deviceNs.adapter.rooms.get(device.id);
|
||||
if (room && room.size > 0) {
|
||||
deviceNs.to(device.id).emit('device:command', { type, payload: payload || {} });
|
||||
results.push({ device_id: device.id, name: device.name, status: 'sent' });
|
||||
} else {
|
||||
results.push({ device_id: device.id, name: device.name, status: 'offline' });
|
||||
}
|
||||
}
|
||||
|
||||
const sent = results.filter(r => r.status === 'sent').length;
|
||||
const offline = results.filter(r => r.status === 'offline').length;
|
||||
console.log(`Group command '${type}' sent to group '${group.name}': ${sent} sent, ${offline} offline`);
|
||||
res.json({ success: true, sent, offline, total: devices.length, results });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ const config = require('./config');
|
|||
});
|
||||
|
||||
const app = express();
|
||||
app.set('trust proxy', true);
|
||||
|
||||
// Determine if SSL certs are available
|
||||
const hasSsl = fs.existsSync(config.sslCert) && fs.existsSync(config.sslKey);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,12 @@ const { getUserPlan, getUserDeviceCount } = require('../middleware/subscription'
|
|||
// In-memory store for latest screenshot per device (avoids disk writes during streaming)
|
||||
let lastScreenshots = {};
|
||||
|
||||
function getClientIp(socket) {
|
||||
const forwarded = socket.handshake.headers['x-forwarded-for'];
|
||||
if (forwarded) return forwarded.split(',')[0].trim();
|
||||
return socket.handshake.address;
|
||||
}
|
||||
|
||||
function logDeviceStatus(deviceId, status) {
|
||||
try {
|
||||
db.prepare('INSERT INTO device_status_log (device_id, status) VALUES (?, ?)').run(deviceId, status);
|
||||
|
|
@ -141,7 +147,7 @@ module.exports = function setupDeviceSocket(io) {
|
|||
if (device) {
|
||||
currentDeviceId = device_id;
|
||||
db.prepare("UPDATE devices SET status = 'online', last_heartbeat = strftime('%s','now'), ip_address = ?, updated_at = strftime('%s','now') WHERE id = ?")
|
||||
.run(socket.handshake.address, device_id);
|
||||
.run(getClientIp(socket), device_id);
|
||||
|
||||
if (device_info) {
|
||||
db.prepare('UPDATE devices SET android_version = ?, app_version = ?, screen_width = ?, screen_height = ? WHERE id = ?')
|
||||
|
|
@ -181,7 +187,7 @@ module.exports = function setupDeviceSocket(io) {
|
|||
INSERT INTO devices (id, pairing_code, status, ip_address, android_version, app_version, screen_width, screen_height, last_heartbeat)
|
||||
VALUES (?, ?, 'provisioning', ?, ?, ?, ?, ?, strftime('%s','now'))
|
||||
`).run(
|
||||
id, pairing_code, socket.handshake.address,
|
||||
id, pairing_code, getClientIp(socket),
|
||||
device_info?.android_version || null,
|
||||
device_info?.app_version || null,
|
||||
device_info?.screen_width || null,
|
||||
|
|
|
|||
Loading…
Reference in a new issue