mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-11 13:49:03 +00:00
447c22af48
Add create_data_directory helper to prepare Docker data volumes with proper permissions and optional SELinux (svirt_sandbox_file_t) support, and use it in install/migrate flows with user guidance on failures. Add comprehensive SYNOLOGY_INSTALLATION.md with Synology/Portainer/container-manager/docker-compose setup, firewall, troubleshooting and update instructions. Update DB adapters (SQLite/Postgres) to treat device_folder_assignments as the single source of truth: clear assignments on folder delete, stop writing folder_id into peer table, and make getUnassignedDeviceCount resilient by trying 'peers' then 'peer' schemas and returning -1 on error. Minor UI tweaks: add console.error on folder assignment failure and add WIP banners/margin adjustments to remote viewer pages.
510 lines
17 KiB
HTML
510 lines
17 KiB
HTML
<%- include('layouts/viewer', {
|
|
title: (device && device.hostname) ? device.hostname : deviceId,
|
|
pageScripts: [],
|
|
body: `
|
|
|
|
<!-- WIP Banner -->
|
|
<div style="position: fixed; top: 0; left: 0; right: 0; z-index: 9999; background: linear-gradient(90deg, #f59e0b, #d97706); color: #fff; text-align: center; padding: 10px 15px; font-weight: 600; font-size: 14px; box-shadow: 0 2px 8px rgba(0,0,0,0.3);">
|
|
🚧 Web Remote Client is under active development and may not function correctly. 🚧
|
|
</div>
|
|
|
|
<!-- BetterDesk Native Remote Desktop Viewer -->
|
|
<div class="bdv-container" id="bdv-container" style="margin-top: 48px;">
|
|
|
|
<!-- Overlay shown before connection -->
|
|
<div class="bdv-overlay" id="bdv-overlay">
|
|
<div class="bdv-overlay-card">
|
|
<div class="bdv-overlay-icon">
|
|
<span class="material-icons">connected_tv</span>
|
|
</div>
|
|
<h2 class="bdv-overlay-title">Remote Desktop</h2>
|
|
<p class="bdv-overlay-device">` + (device && device.hostname ? device.hostname : '') + `</p>
|
|
<p class="bdv-overlay-id">` + deviceId + `</p>
|
|
<div class="bdv-overlay-status" id="bdv-status-area">
|
|
<div class="spinner"></div>
|
|
<span id="bdv-status-text">Connecting to agent…</span>
|
|
</div>
|
|
<div class="bdv-overlay-actions" id="bdv-overlay-actions" style="display:none;">
|
|
<button class="btn btn-primary" id="btnReconnect">
|
|
<span class="material-icons">refresh</span> Reconnect
|
|
</button>
|
|
<a href="/devices" class="btn btn-secondary">
|
|
<span class="material-icons">arrow_back</span> Back
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main canvas — shows JPEG frames -->
|
|
<canvas id="bdv-canvas" class="bdv-canvas" tabindex="0"></canvas>
|
|
|
|
<!-- Top toolbar (visible on hover / always when not streaming) -->
|
|
<div class="bdv-toolbar" id="bdv-toolbar">
|
|
<div class="bdv-toolbar-left">
|
|
<span class="material-icons">connected_tv</span>
|
|
<span id="bdv-toolbar-title">` + (device && device.hostname ? device.hostname : deviceId) + `</span>
|
|
<span class="bdv-toolbar-sep"></span>
|
|
<span class="bdv-badge" id="bdv-fps-badge">—</span>
|
|
</div>
|
|
<div class="bdv-toolbar-right">
|
|
<button class="bdv-btn" id="btnChat" title="Open Chat">
|
|
<span class="material-icons">chat</span>
|
|
</button>
|
|
<button class="bdv-btn" id="btnFullscreen" title="Fullscreen">
|
|
<span class="material-icons">fullscreen</span>
|
|
</button>
|
|
<button class="bdv-btn bdv-btn--danger" id="btnStop" title="Stop session">
|
|
<span class="material-icons">stop_circle</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Chat sidebar -->
|
|
<div class="bdv-chat" id="bdv-chat">
|
|
<div class="bdv-chat-header">
|
|
<span>Chat — ` + deviceId + `</span>
|
|
<button class="bdv-btn" id="btnCloseChat">
|
|
<span class="material-icons">close</span>
|
|
</button>
|
|
</div>
|
|
<div class="bdv-chat-messages" id="bdv-chat-messages"></div>
|
|
<div class="bdv-chat-input-row">
|
|
<input type="text" id="bdv-chat-input" class="bdv-chat-inp" placeholder="Type a message…" autocomplete="off" />
|
|
<button class="bdv-btn" id="btnSendChat">
|
|
<span class="material-icons">send</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
'use strict';
|
|
|
|
// ---- Config ----
|
|
const DEVICE_ID = ${JSON.stringify(deviceId)};
|
|
const WS_BASE = (location.protocol === 'https:' ? 'wss' : 'ws') + '://' + location.host;
|
|
const REMOTE_URL = WS_BASE + '/ws/remote-viewer/' + encodeURIComponent(DEVICE_ID);
|
|
const CHAT_URL = WS_BASE + '/ws/chat-operator/' + encodeURIComponent(DEVICE_ID);
|
|
|
|
// ---- DOM ----
|
|
const canvas = document.getElementById('bdv-canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
const overlay = document.getElementById('bdv-overlay');
|
|
const statusTxt = document.getElementById('bdv-status-text');
|
|
const actions = document.getElementById('bdv-overlay-actions');
|
|
const fpsBadge = document.getElementById('bdv-fps-badge');
|
|
const chatSide = document.getElementById('bdv-chat');
|
|
const chatMsgs = document.getElementById('bdv-chat-messages');
|
|
const chatInp = document.getElementById('bdv-chat-input');
|
|
const toolbar = document.getElementById('bdv-toolbar');
|
|
|
|
// ---- State ----
|
|
let remoteWs = null;
|
|
let chatWs = null;
|
|
let streaming = false;
|
|
let frameCount = 0;
|
|
let lastFpsTick = performance.now();
|
|
let chatOpen = false;
|
|
let mouseButtonsDown = 0;
|
|
|
|
// ---- FPS Counter ----
|
|
setInterval(() => {
|
|
const now = performance.now();
|
|
const dt = (now - lastFpsTick) / 1000;
|
|
const fps = (frameCount / dt).toFixed(1);
|
|
fpsBadge.textContent = streaming ? fps + ' fps' : '—';
|
|
frameCount = 0;
|
|
lastFpsTick = now;
|
|
}, 2000);
|
|
|
|
// ---- Remote WebSocket ----
|
|
function connectRemote() {
|
|
setStatus('Connecting to server…');
|
|
remoteWs = new WebSocket(REMOTE_URL);
|
|
remoteWs.binaryType = 'arraybuffer';
|
|
|
|
remoteWs.onopen = () => {
|
|
setStatus('Waiting for agent…');
|
|
};
|
|
|
|
remoteWs.onclose = (ev) => {
|
|
streaming = false;
|
|
setStatus('Disconnected — ' + (ev.reason || 'connection closed'));
|
|
showActions();
|
|
};
|
|
|
|
remoteWs.onmessage = (ev) => {
|
|
if (ev.data instanceof ArrayBuffer) {
|
|
// Binary = JPEG frame
|
|
handleJpegFrame(ev.data);
|
|
} else {
|
|
handleJsonFrame(JSON.parse(ev.data));
|
|
}
|
|
};
|
|
|
|
remoteWs.onerror = () => {
|
|
setStatus('Connection error');
|
|
showActions();
|
|
};
|
|
}
|
|
|
|
function handleJsonFrame(frame) {
|
|
switch (frame.type) {
|
|
case 'session-info':
|
|
if (frame.agent_ready) {
|
|
setStatus(frame.streaming ? 'Streaming…' : 'Agent ready — starting stream…');
|
|
} else {
|
|
setStatus('Waiting for agent to connect…');
|
|
}
|
|
streaming = frame.streaming;
|
|
break;
|
|
case 'agent-ready':
|
|
setStatus('Agent connected — starting stream…');
|
|
break;
|
|
case 'stream-started':
|
|
streaming = true;
|
|
overlay.style.display = 'none';
|
|
canvas.focus();
|
|
break;
|
|
case 'stream-stopped':
|
|
streaming = false;
|
|
setStatus('Stream stopped — ' + (frame.reason || ''));
|
|
overlay.style.display = 'flex';
|
|
showActions();
|
|
break;
|
|
case 'agent-disconnected':
|
|
streaming = false;
|
|
setStatus('Agent disconnected');
|
|
overlay.style.display = 'flex';
|
|
showActions();
|
|
break;
|
|
}
|
|
}
|
|
|
|
function handleJpegFrame(buffer) {
|
|
const blob = new Blob([buffer], { type: 'image/jpeg' });
|
|
const url = URL.createObjectURL(blob);
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
if (canvas.width !== img.width || canvas.height !== img.height) {
|
|
canvas.width = img.width;
|
|
canvas.height = img.height;
|
|
}
|
|
ctx.drawImage(img, 0, 0);
|
|
URL.revokeObjectURL(url);
|
|
frameCount++;
|
|
};
|
|
img.onerror = () => URL.revokeObjectURL(url);
|
|
img.src = url;
|
|
}
|
|
|
|
// ---- Input forwarding ----
|
|
canvas.addEventListener('mousemove', (e) => {
|
|
if (!streaming) return;
|
|
const { rx, ry } = canvasCoords(e);
|
|
sendRemote({ type: 'input', event_type: 'mouse_move', x: rx, y: ry });
|
|
});
|
|
|
|
canvas.addEventListener('mousedown', (e) => {
|
|
if (!streaming) return;
|
|
mouseButtonsDown |= (1 << e.button);
|
|
const { rx, ry } = canvasCoords(e);
|
|
sendRemote({ type: 'input', event_type: 'mouse_down', x: rx, y: ry, button: btnName(e.button) });
|
|
});
|
|
|
|
canvas.addEventListener('mouseup', (e) => {
|
|
if (!streaming) return;
|
|
mouseButtonsDown &= ~(1 << e.button);
|
|
const { rx, ry } = canvasCoords(e);
|
|
sendRemote({ type: 'input', event_type: 'mouse_up', x: rx, y: ry, button: btnName(e.button) });
|
|
});
|
|
|
|
canvas.addEventListener('wheel', (e) => {
|
|
if (!streaming) return;
|
|
e.preventDefault();
|
|
sendRemote({ type: 'input', event_type: 'wheel', delta_x: Math.round(e.deltaX), delta_y: Math.round(e.deltaY) });
|
|
}, { passive: false });
|
|
|
|
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
|
|
|
|
canvas.addEventListener('keydown', (e) => {
|
|
if (!streaming) return;
|
|
e.preventDefault();
|
|
sendRemote({ type: 'input', event_type: 'key_down', key: e.key });
|
|
});
|
|
|
|
canvas.addEventListener('keyup', (e) => {
|
|
if (!streaming) return;
|
|
e.preventDefault();
|
|
sendRemote({ type: 'input', event_type: 'key_up', key: e.key });
|
|
});
|
|
|
|
function canvasCoords(e) {
|
|
const rect = canvas.getBoundingClientRect();
|
|
const scaleX = canvas.width / rect.width;
|
|
const scaleY = canvas.height / rect.height;
|
|
return {
|
|
rx: Math.round((e.clientX - rect.left) * scaleX),
|
|
ry: Math.round((e.clientY - rect.top) * scaleY),
|
|
};
|
|
}
|
|
|
|
function btnName(b) {
|
|
return b === 2 ? 'right' : b === 1 ? 'middle' : 'left';
|
|
}
|
|
|
|
function sendRemote(obj) {
|
|
if (remoteWs && remoteWs.readyState === WebSocket.OPEN) {
|
|
remoteWs.send(JSON.stringify(obj));
|
|
}
|
|
}
|
|
|
|
// ---- Toolbar / UI ----
|
|
function setStatus(msg) { statusTxt.textContent = msg; }
|
|
function showActions() { actions.style.display = 'flex'; }
|
|
|
|
document.getElementById('btnReconnect').onclick = () => {
|
|
actions.style.display = 'none';
|
|
if (remoteWs) remoteWs.close();
|
|
setTimeout(connectRemote, 200);
|
|
};
|
|
|
|
document.getElementById('btnStop').onclick = () => {
|
|
sendRemote({ type: 'stop' });
|
|
if (remoteWs) remoteWs.close();
|
|
};
|
|
|
|
document.getElementById('btnFullscreen').onclick = () => {
|
|
if (!document.fullscreenElement) {
|
|
document.getElementById('bdv-container').requestFullscreen();
|
|
} else {
|
|
document.exitFullscreen();
|
|
}
|
|
};
|
|
|
|
document.addEventListener('fullscreenchange', () => {
|
|
const icon = document.getElementById('btnFullscreen').querySelector('span');
|
|
icon.textContent = document.fullscreenElement ? 'fullscreen_exit' : 'fullscreen';
|
|
});
|
|
|
|
// Toolbar auto-hide
|
|
let hideTimer;
|
|
document.getElementById('bdv-container').addEventListener('mousemove', () => {
|
|
toolbar.classList.add('bdv-toolbar--visible');
|
|
clearTimeout(hideTimer);
|
|
hideTimer = setTimeout(() => {
|
|
if (streaming) toolbar.classList.remove('bdv-toolbar--visible');
|
|
}, 2500);
|
|
});
|
|
|
|
// ---- Chat ----
|
|
function connectChat() {
|
|
chatWs = new WebSocket(CHAT_URL);
|
|
chatWs.onopen = () => {};
|
|
chatWs.onclose = () => {};
|
|
chatWs.onmessage = (ev) => {
|
|
const frame = JSON.parse(ev.data);
|
|
if (frame.type === 'message') appendChatMsg(frame);
|
|
if (frame.type === 'history') frame.messages.forEach(appendChatMsg);
|
|
};
|
|
}
|
|
|
|
function appendChatMsg(msg) {
|
|
const div = document.createElement('div');
|
|
div.className = 'bdv-chat-msg bdv-chat-msg--' + (msg.from === 'operator' ? 'out' : 'in');
|
|
div.innerHTML = '<span class="bdv-chat-msg-text">' + escHtml(msg.text) + '</span>' +
|
|
'<span class="bdv-chat-msg-time">' + new Date(msg.timestamp).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}) + '</span>';
|
|
chatMsgs.appendChild(div);
|
|
chatMsgs.scrollTop = chatMsgs.scrollHeight;
|
|
}
|
|
|
|
function escHtml(str) {
|
|
return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
}
|
|
|
|
function sendChatMsg() {
|
|
const text = chatInp.value.trim();
|
|
if (!text || !chatWs || chatWs.readyState !== WebSocket.OPEN) return;
|
|
chatWs.send(JSON.stringify({ type: 'message', text }));
|
|
chatInp.value = '';
|
|
}
|
|
|
|
document.getElementById('btnChat').onclick = () => {
|
|
chatOpen = !chatOpen;
|
|
chatSide.classList.toggle('bdv-chat--open', chatOpen);
|
|
};
|
|
|
|
document.getElementById('btnCloseChat').onclick = () => {
|
|
chatOpen = false;
|
|
chatSide.classList.remove('bdv-chat--open');
|
|
};
|
|
|
|
document.getElementById('btnSendChat').onclick = sendChatMsg;
|
|
chatInp.addEventListener('keydown', (e) => { if (e.key === 'Enter') sendChatMsg(); });
|
|
|
|
// ---- Init ----
|
|
connectRemote();
|
|
connectChat();
|
|
})();
|
|
</script>
|
|
|
|
<style>
|
|
/* ---- BetterDesk Remote Viewer styles (scope: this page only) ---- */
|
|
|
|
.bdv-container {
|
|
position: fixed; inset: 0;
|
|
background: #111;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.bdv-canvas {
|
|
display: block;
|
|
max-width: 100%; max-height: 100%;
|
|
object-fit: contain;
|
|
cursor: none;
|
|
outline: none;
|
|
}
|
|
|
|
.bdv-overlay {
|
|
position: absolute; inset: 0;
|
|
background: rgba(0,0,0,.82);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 10;
|
|
}
|
|
|
|
.bdv-overlay-card {
|
|
background: #1a1a2e;
|
|
border: 1px solid #2d2d4a;
|
|
border-radius: 16px;
|
|
padding: 40px 48px;
|
|
text-align: center;
|
|
max-width: 400px;
|
|
width: 90%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 12px;
|
|
color: #e8e8f0;
|
|
}
|
|
|
|
.bdv-overlay-icon .material-icons { font-size: 56px; color: #4f6ef7; }
|
|
.bdv-overlay-title { font-size: 1.4rem; font-weight: 700; }
|
|
.bdv-overlay-device { font-size: 1rem; color: #9898b0; }
|
|
.bdv-overlay-id { font-size: 0.8rem; color: #606078; font-family: monospace; }
|
|
|
|
.bdv-overlay-status {
|
|
display: flex; align-items: center; gap: 10px;
|
|
color: #9898b0; font-size: 0.9rem;
|
|
}
|
|
|
|
.bdv-overlay-actions { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }
|
|
|
|
.bdv-toolbar {
|
|
position: absolute;
|
|
top: 0; left: 0; right: 0;
|
|
height: 48px;
|
|
background: linear-gradient(to bottom, rgba(0,0,0,.8), transparent);
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0 16px;
|
|
gap: 10px;
|
|
z-index: 20;
|
|
opacity: 0;
|
|
transition: opacity .2s;
|
|
}
|
|
|
|
.bdv-toolbar--visible { opacity: 1; }
|
|
|
|
.bdv-toolbar-left, .bdv-toolbar-right {
|
|
display: flex; align-items: center; gap: 8px; color: #e8e8f0;
|
|
}
|
|
.bdv-toolbar-right { margin-left: auto; }
|
|
|
|
.bdv-toolbar-sep { width: 1px; height: 16px; background: #444; }
|
|
.bdv-badge { background: rgba(255,255,255,.1); border-radius: 4px; padding: 2px 8px; font-size: 0.75rem; color: #ccc; }
|
|
|
|
.bdv-btn {
|
|
background: none; border: none; cursor: pointer; color: #e8e8f0;
|
|
padding: 4px; border-radius: 4px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
transition: background .15s;
|
|
}
|
|
.bdv-btn:hover { background: rgba(255,255,255,.15); }
|
|
.bdv-btn--danger:hover { background: rgba(239,68,68,.3); }
|
|
|
|
.bdv-chat {
|
|
position: absolute;
|
|
top: 0; right: -320px; bottom: 0;
|
|
width: 320px;
|
|
background: #1a1a2e;
|
|
border-left: 1px solid #2d2d4a;
|
|
z-index: 30;
|
|
display: flex; flex-direction: column;
|
|
transition: right .25s ease;
|
|
}
|
|
.bdv-chat--open { right: 0; }
|
|
|
|
.bdv-chat-header {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
padding: 12px 16px;
|
|
border-bottom: 1px solid #2d2d4a;
|
|
color: #e8e8f0; font-size: 0.9rem; font-weight: 600;
|
|
}
|
|
|
|
.bdv-chat-messages {
|
|
flex: 1; overflow-y: auto;
|
|
padding: 12px 14px;
|
|
display: flex; flex-direction: column; gap: 8px;
|
|
}
|
|
|
|
.bdv-chat-msg { display: flex; flex-direction: column; max-width: 85%; }
|
|
.bdv-chat-msg--out { align-self: flex-end; }
|
|
.bdv-chat-msg--in { align-self: flex-start; }
|
|
|
|
.bdv-chat-msg-text {
|
|
padding: 8px 12px;
|
|
border-radius: 12px;
|
|
font-size: 0.875rem;
|
|
word-break: break-word;
|
|
}
|
|
.bdv-chat-msg--out .bdv-chat-msg-text { background: #4f6ef7; color: #fff; border-bottom-right-radius: 4px; }
|
|
.bdv-chat-msg--in .bdv-chat-msg-text { background: #222240; color: #e8e8f0; border-bottom-left-radius: 4px; }
|
|
|
|
.bdv-chat-msg-time { font-size: 0.7rem; color: #606078; padding: 2px 4px; }
|
|
.bdv-chat-msg--out .bdv-chat-msg-time { text-align: right; }
|
|
|
|
.bdv-chat-input-row {
|
|
display: flex; gap: 6px; padding: 10px 12px;
|
|
border-top: 1px solid #2d2d4a;
|
|
}
|
|
|
|
.bdv-chat-inp {
|
|
flex: 1;
|
|
background: #222240; border: 1px solid #2d2d4a; border-radius: 8px;
|
|
padding: 8px 12px; color: #e8e8f0; font-size: 0.875rem;
|
|
outline: none;
|
|
}
|
|
.bdv-chat-inp:focus { border-color: #4f6ef7; }
|
|
|
|
/* Spinner reuse from theme.css (fallback inline) */
|
|
.spinner {
|
|
width: 20px; height: 20px;
|
|
border: 2px solid #2d2d4a;
|
|
border-top-color: #4f6ef7;
|
|
border-radius: 50%;
|
|
animation: spin .8s linear infinite;
|
|
display: inline-block;
|
|
}
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
</style>
|
|
|
|
`
|
|
}) %>
|