Files
BetterDesk/web-nodejs/views/remote-cdap.ejs
T
UNITRONIX 4eed88673c fix(updates): atomic binary replace + accurate modal status
- deployServerBinary: use rename(2) for atomic replace, fixes ETXTBSY
  when target Go binary is busy (Linux kernel handles inode swap).
  Falls back to copyFileSync on cross-device rename or non-Linux.
  Windows: rename target out of the way first, then move new in.
- settings.js: mark 'server' phase as error when build succeeded but
  deploy failed (was incorrectly marking 'done' from build alone).
- settings.js: completion modal now shows error title, error message
  and pre-formatted stderr when serverDeploy.success === false.
- i18n: added complete_with_errors, modal_done_with_errors_title in
  en/pl.
2026-04-26 01:23:18 +02:00

190 lines
10 KiB
HTML

<%- include('layouts/viewer', {
title: deviceId,
pageScripts: ['cdap-desktop', 'cdap-audio'],
body: `
<style>
.bd-remote-wrap { position: fixed; inset: 0; display: flex; flex-direction: column; background: radial-gradient(circle at top, #18243a 0%, #0d1117 42%, #070b12 100%); color: #e6edf3; }
.bd-remote-bar { display: flex; align-items: center; gap: 12px; padding: 10px 16px; border-bottom: 1px solid rgba(255,255,255,.08); background: rgba(10,16,27,.8); backdrop-filter: blur(16px); }
.bd-remote-title { display: flex; align-items: center; gap: 10px; min-width: 0; }
.bd-remote-title .material-icons { color: #58a6ff; }
.bd-remote-name { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bd-remote-id { color: #8b949e; font-size: 12px; }
.bd-remote-pill { padding: 4px 10px; border-radius: 999px; background: rgba(88,166,255,.16); color: #c9d1d9; font-size: 12px; }
.bd-remote-grow { flex: 1; }
.bd-remote-actions { display: flex; align-items: center; gap: 8px; }
.bd-remote-actions button,
.bd-remote-actions a { display: inline-flex; align-items: center; gap: 6px; border: 1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.06); color: #e6edf3; border-radius: 10px; padding: 8px 12px; font-size: 13px; text-decoration: none; cursor: pointer; }
.bd-remote-actions button:hover,
.bd-remote-actions a:hover { background: rgba(255,255,255,.12); }
.bd-remote-actions button.recording { background: rgba(248,81,73,.18); border-color: rgba(248,81,73,.45); color: #ffa198; }
.bd-remote-actions button.active { background: rgba(88,166,255,.22); border-color: rgba(88,166,255,.55); color: #79b8ff; }
.bd-remote-stage { flex: 1; padding: 12px; min-height: 0; }
.bd-remote-widget { position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; border-radius: 18px; overflow: hidden; background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.08); box-shadow: 0 28px 80px rgba(0,0,0,.45); }
.bd-remote-widget .cdap-desktop-toolbar { display: flex; align-items: center; gap: 10px; min-height: 52px; padding: 0 16px; background: rgba(7,11,18,.92); border-bottom: 1px solid rgba(255,255,255,.06); }
.bd-remote-widget .cdap-desktop-canvas-wrap { position: relative; flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; background: #020409; }
.bd-remote-widget .cdap-desktop-canvas { max-width: 100%; max-height: 100%; width: 100%; height: 100%; object-fit: contain; outline: none; cursor: default; }
.bd-remote-widget .cdap-desktop-overlay { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; background: linear-gradient(180deg, rgba(2,4,9,.45), rgba(2,4,9,.82)); color: #c9d1d9; text-align: center; }
.bd-remote-widget .cdap-desktop-overlay .material-icons { font-size: 44px; color: #58a6ff; }
.bd-remote-widget .cdap-desktop-connect { position: absolute; right: 20px; bottom: 20px; z-index: 20; }
.bd-remote-widget .cdap-desktop-connect.hidden,
.bd-remote-widget .cdap-desktop-overlay.hidden,
.bd-remote-widget .cdap-desktop-clipboard-indicator.hidden,
.bd-remote-widget .hidden { display: none !important; }
.bd-remote-widget .cdap-desktop-clipboard-indicator { margin-left: auto; font-size: 12px; padding: 4px 10px; border-radius: 999px; background: rgba(88,166,255,.14); color: #c9d1d9; }
.bd-remote-widget .cdap-monitor-selector { display: inline-flex; align-items: center; gap: 6px; margin-left: auto; color: #c9d1d9; }
.bd-remote-widget .cdap-monitor-selector select { background: rgba(255,255,255,.06); color: #e6edf3; border: 1px solid rgba(255,255,255,.12); border-radius: 8px; padding: 6px 10px; }
.bd-remote-hint { font-size: 12px; color: #8b949e; }
@media (max-width: 900px) {
.bd-remote-bar { flex-wrap: wrap; }
.bd-remote-grow { display: none; }
.bd-remote-actions { width: 100%; justify-content: flex-end; }
}
</style>
<div class="bd-remote-wrap">
<div class="bd-remote-bar">
<div class="bd-remote-title">
<span class="material-icons">desktop_windows</span>
<div>
<div class="bd-remote-name">${deviceName || deviceId}</div>
<div class="bd-remote-id">${deviceId}</div>
</div>
</div>
<span class="bd-remote-pill">CDAP Desktop</span>
<span class="bd-remote-hint">Click inside the canvas to capture keyboard and mouse input.</span>
<span class="bd-remote-grow"></span>
<div class="bd-remote-actions">
<button type="button" id="bd-remote-paste" title="Paste clipboard text into the remote session"><span class="material-icons">content_paste</span>Paste</button>
<button type="button" id="bd-remote-audio" title="Toggle remote audio"><span class="material-icons">volume_up</span>Audio</button>
<button type="button" id="bd-remote-record" title="Record session to WebM"><span class="material-icons">fiber_manual_record</span>Record</button>
<button type="button" id="bd-remote-fullscreen" title="Fullscreen + capture system shortcuts"><span class="material-icons">fullscreen</span>Fullscreen</button>
<button type="button" id="bd-remote-reconnect"><span class="material-icons">refresh</span>Reconnect</button>
<a href="/devices"><span class="material-icons">arrow_back</span>Back</a>
</div>
</div>
<div class="bd-remote-stage">
<div class="cdap-desktop-widget bd-remote-widget" id="wval-remote-full">
<div class="cdap-desktop-toolbar">
<span class="cdap-desktop-clipboard-indicator hidden"></span>
<span class="bd-remote-hint">BetterDesk native desktop session over CDAP WebSocket.</span>
</div>
<div class="cdap-desktop-canvas-wrap">
<canvas class="cdap-desktop-canvas" width="1280" height="720"></canvas>
<div class="cdap-desktop-overlay">
<span class="material-icons">hourglass_top</span>
<span>Waiting for desktop session...</span>
</div>
</div>
<div class="cdap-desktop-connect">
<button class="btn cdap-desktop-connect-btn" data-widget="remote-full" type="button">
<span class="material-icons">play_arrow</span>
Connect desktop
</button>
</div>
</div>
<!-- Hidden audio widget element so cdap-audio.js can mount its level
meter / status without rendering a visible UI; controlled from
the toolbar Audio button. -->
<div class="cdap-audio-widget" id="wval-remote-audio" style="display:none" aria-hidden="true">
<div class="cdap-audio-status"></div>
<div class="cdap-audio-level"><div class="cdap-audio-level-fill"></div></div>
</div>
</div>
</div>
<script nonce="${cspNonce}">
(function () {
const deviceId = ${JSON.stringify(deviceId)};
const widgetId = 'remote-full';
function openDesktopSession() {
const connectWrap = document.querySelector('#wval-remote-full .cdap-desktop-connect');
connectWrap?.classList.add('hidden');
if (window.CDAPDesktop) {
window.CDAPDesktop.close(deviceId, widgetId);
window.CDAPDesktop.open(deviceId, widgetId);
}
}
document.addEventListener('DOMContentLoaded', function () {
document.querySelector('.cdap-desktop-connect-btn')?.addEventListener('click', openDesktopSession);
document.getElementById('bd-remote-reconnect')?.addEventListener('click', openDesktopSession);
// Fullscreen + Keyboard Lock (Phase 3.3) ----------------------------
const fsBtn = document.getElementById('bd-remote-fullscreen');
const fsIcon = fsBtn?.querySelector('.material-icons');
fsBtn?.addEventListener('click', async () => {
const fs = await window.CDAPDesktop?.toggleFullscreen(deviceId, widgetId);
if (fsIcon) fsIcon.textContent = fs ? 'fullscreen_exit' : 'fullscreen';
});
document.addEventListener('fullscreenchange', () => {
if (!fsIcon) return;
fsIcon.textContent = window.CDAPDesktop?.isFullscreen(deviceId, widgetId) ? 'fullscreen_exit' : 'fullscreen';
});
// Session recording (Phase 3.4) -------------------------------------
const recBtn = document.getElementById('bd-remote-record');
const recIcon = recBtn?.querySelector('.material-icons');
recBtn?.addEventListener('click', async () => {
if (window.CDAPDesktop?.isRecording(deviceId, widgetId)) {
await window.CDAPDesktop.downloadRecording(deviceId, widgetId);
if (recIcon) recIcon.textContent = 'fiber_manual_record';
recBtn.classList.remove('recording');
} else {
const ok = window.CDAPDesktop?.startRecording(deviceId, widgetId);
if (ok) {
if (recIcon) recIcon.textContent = 'stop_circle';
recBtn.classList.add('recording');
} else {
alert('Recording is not supported in this browser, or the session is not connected yet.');
}
}
});
// Remote audio toggle (Phase 3.8) -----------------------------------
const audioBtn = document.getElementById('bd-remote-audio');
const audioIcon = audioBtn?.querySelector('.material-icons');
const audioWidgetId = 'remote-audio';
let audioActive = false;
audioBtn?.addEventListener('click', () => {
if (!window.CDAPAudio) {
console.warn('[remote-cdap] CDAPAudio module unavailable');
return;
}
if (audioActive) {
window.CDAPAudio.close(deviceId, audioWidgetId);
audioActive = false;
if (audioIcon) audioIcon.textContent = 'volume_up';
audioBtn.classList.remove('active');
} else {
window.CDAPAudio.open(deviceId, audioWidgetId, { direction: 'receive' });
audioActive = true;
if (audioIcon) audioIcon.textContent = 'volume_off';
audioBtn.classList.add('active');
}
});
// Virtual paste (Phase 3.2) ---------------------------------------
// Reads the operator clipboard via navigator.clipboard.readText()
// and types it into the remote session as a single text input
// event. Useful when the device side refuses incoming clipboard
// sync, or when the operator wants to paste from outside the page.
document.getElementById('bd-remote-paste')?.addEventListener('click', async () => {
try {
const ok = await window.CDAPDesktop?.pasteFromClipboard(deviceId, widgetId);
if (!ok) {
alert('Could not read the clipboard. Grant clipboard permission to this site or focus the canvas first.');
}
} catch (err) {
console.warn('[remote-cdap] paste failed:', err && err.message);
}
});
openDesktopSession();
});
})();
</script>
`
}) %>