Compare commits

..

No commits in common. "7b9362756dcf18c8edc81bd0e1728fa742ed34ed" and "23dd010a95fd0f902094e969b027e13b1718a1bd" have entirely different histories.

11 changed files with 5 additions and 792 deletions

View File

@ -75,12 +75,6 @@ const DEFAULTS = {
delaySec: 3,
autoStart: true,
hosters: [] // pre-selected hosters, empty = ask via modal
},
remote: {
enabled: false,
port: 9100,
token: '',
allowInput: true
}
},
history: []

View File

@ -1,20 +0,0 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('capture', {
// Get capture source ID from main process (desktopCapturer runs in main)
getSourceId: () => ipcRenderer.invoke('remote:get-capture-source-id'),
// Signaling: receive offer/ICE from main process (relayed from dashboard)
onSignaling: (callback) => {
ipcRenderer.on('remote:signaling-to-capture', (_event, data) => callback(data));
},
// Signaling: send answer/ICE back to main process (relayed to dashboard)
sendSignaling: (data) => ipcRenderer.send('remote:signaling-from-capture', data),
// Input: forward input events from DataChannel to main process
sendInput: (data) => ipcRenderer.send('remote:input-event', data),
// Notify main process of client connection/disconnection
notifyClientCount: (count) => ipcRenderer.send('remote:client-count', count)
});

View File

@ -1,124 +0,0 @@
<!DOCTYPE html>
<html>
<head><title>Remote Capture</title></head>
<body>
<script>
// Maps clientId -> { pc: RTCPeerConnection, dc: RTCDataChannel }
const clients = new Map();
let captureStream = null;
async function getCaptureStream() {
if (captureStream) return captureStream;
// desktopCapturer runs in main process (Electron 33+), we get the source ID via IPC
const sourceId = await window.capture.getSourceId();
if (!sourceId) throw new Error('No capture source ID from main process');
captureStream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: sourceId,
minWidth: 1280,
maxWidth: 1920,
minHeight: 720,
maxHeight: 1080,
maxFrameRate: 30
}
}
});
return captureStream;
}
async function handleOffer(clientId, offer, role) {
const stream = await getCaptureStream();
const pc = new RTCPeerConnection({ iceServers: [] });
clients.set(clientId, { pc, role });
// Add video tracks
for (const track of stream.getTracks()) {
pc.addTrack(track, stream);
}
// Handle DataChannel from dashboard (dashboard creates it as offerer)
pc.ondatachannel = (event) => {
const dc = event.channel;
clients.get(clientId).dc = dc;
dc.onmessage = (msg) => {
try {
const input = JSON.parse(msg.data);
input.clientId = clientId;
input.role = role;
window.capture.sendInput(input);
} catch {}
};
};
// ICE candidates
pc.onicecandidate = (event) => {
if (event.candidate) {
window.capture.sendSignaling({
type: 'ice-candidate',
clientId,
candidate: event.candidate
});
}
};
pc.onconnectionstatechange = () => {
if (pc.connectionState === 'disconnected' || pc.connectionState === 'failed') {
removeClient(clientId);
}
};
await pc.setRemoteDescription(new RTCSessionDescription(offer));
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
window.capture.sendSignaling({
type: 'answer',
clientId,
answer: pc.localDescription
});
window.capture.notifyClientCount(clients.size);
}
function handleIceCandidate(clientId, candidate) {
const client = clients.get(clientId);
if (client && client.pc) {
client.pc.addIceCandidate(new RTCIceCandidate(candidate)).catch(() => {});
}
}
function removeClient(clientId) {
const client = clients.get(clientId);
if (client) {
if (client.dc) client.dc.close();
client.pc.close();
clients.delete(clientId);
window.capture.notifyClientCount(clients.size);
}
}
// Listen for signaling messages from main process
window.capture.onSignaling((data) => {
switch (data.type) {
case 'offer':
handleOffer(data.clientId, data.offer, data.role).catch(err => {
console.error('Failed to handle offer:', err);
});
break;
case 'ice-candidate':
handleIceCandidate(data.clientId, data.candidate);
break;
case 'client-disconnected':
removeClient(data.clientId);
break;
}
});
</script>
</body>
</html>

View File

@ -1,171 +0,0 @@
const { WebSocketServer } = require('ws');
const crypto = require('crypto');
class RemoteServer {
constructor() {
this._wss = null;
this._clients = new Map(); // ws -> { id, role, authenticated }
this._config = null;
this._failedAttempts = new Map(); // ip -> { count, blockedUntil }
}
start(opts) {
return new Promise((resolve, reject) => {
this._config = opts;
this._wss = new WebSocketServer({ port: opts.port }, () => {
resolve();
});
this._wss.on('error', (err) => {
reject(err);
});
this._wss.on('connection', (ws, req) => {
this._handleConnection(ws, req);
});
});
}
stop() {
if (this._wss) {
for (const [ws] of this._clients) {
ws.close(1000, 'Server shutting down');
}
this._clients.clear();
this._wss.close();
this._wss = null;
}
}
getClientCount() {
let count = 0;
for (const [, client] of this._clients) {
if (client.authenticated) count++;
}
return count;
}
getPort() {
if (this._wss && this._wss.address()) {
return this._wss.address().port;
}
return null;
}
_handleConnection(ws, req) {
const ip = req.socket.remoteAddress || 'unknown';
if (this._isBlocked(ip)) {
ws.close(4003, 'Too many failed attempts');
return;
}
const clientId = crypto.randomUUID();
this._clients.set(ws, { id: clientId, role: null, authenticated: false });
let authReceived = false;
const authTimeout = setTimeout(() => {
if (!authReceived) {
ws.close(4001, 'Auth timeout');
this._clients.delete(ws);
}
}, 5000);
ws.on('message', (raw) => {
let msg;
try { msg = JSON.parse(raw); } catch { return; }
const client = this._clients.get(ws);
if (!client) return;
if (!client.authenticated) {
authReceived = true;
clearTimeout(authTimeout);
if (msg.type === 'auth' && msg.token === this._config.token) {
client.authenticated = true;
client.role = msg.role || 'viewer';
ws.send(JSON.stringify({ type: 'auth-ok', clientId }));
if (this.getClientCount() === 1) {
this._config.onCreateCaptureWindow();
}
} else {
this._recordFailedAttempt(ip);
ws.close(4002, 'Invalid token');
this._clients.delete(ws);
}
return;
}
if (msg.type === 'offer' || msg.type === 'ice-candidate') {
msg.clientId = client.id;
msg.role = client.role;
this._config.onSignalingToCapture(msg);
}
});
ws.on('close', () => {
clearTimeout(authTimeout);
const client = this._clients.get(ws);
const wasAuthenticated = client && client.authenticated;
this._clients.delete(ws);
if (wasAuthenticated) {
this._config.onSignalingToCapture({
type: 'client-disconnected',
clientId: client.id
});
if (this.getClientCount() === 0) {
this._config.onDestroyCaptureWindow();
}
}
});
ws.on('error', () => {
this._clients.delete(ws);
});
}
sendToClient(clientId, data) {
for (const [ws, client] of this._clients) {
if (client.id === clientId && client.authenticated) {
ws.send(JSON.stringify(data));
break;
}
}
}
broadcast(data) {
const msg = JSON.stringify(data);
for (const [ws, client] of this._clients) {
if (client.authenticated && ws.readyState === 1) {
ws.send(msg);
}
}
}
_isBlocked(ip) {
const entry = this._failedAttempts.get(ip);
if (!entry) return false;
if (entry.blockedUntil && Date.now() < entry.blockedUntil) return true;
if (entry.blockedUntil && Date.now() >= entry.blockedUntil) {
this._failedAttempts.delete(ip);
return false;
}
return false;
}
_recordFailedAttempt(ip) {
const entry = this._failedAttempts.get(ip) || { count: 0, blockedUntil: null };
entry.count++;
if (entry.count >= 5) {
entry.blockedUntil = Date.now() + 60000;
}
this._failedAttempts.set(ip, entry);
}
}
module.exports = RemoteServer;

217
main.js
View File

@ -11,7 +11,6 @@ const DoodstreamUploader = require('./lib/doodstream-upload');
const { checkForUpdate, installUpdate, abortUpdate } = require('./lib/updater');
const backupCrypto = require('./lib/backup-crypto');
const FolderMonitor = require('./lib/folder-monitor');
const RemoteServer = require('./lib/remote-server');
let mainWindow;
let dropTargetWindow = null;
@ -19,8 +18,6 @@ let tray = null;
const configStore = new ConfigStore(app);
let uploadManager = null;
let folderMonitor = new FolderMonitor();
let remoteServer = null;
let captureWindow = null;
const HEALTH_CHECK_TIMEOUT = 25000;
// --- Debug logging (writes to upload-debug.log next to the app) ---
@ -491,18 +488,6 @@ app.whenReady().then(() => {
debugLog(`folder-monitor auto-start failed: ${err.message}`);
}
// Auto-start remote server if enabled
try {
const remoteConfig = configStore.load().globalSettings && configStore.load().globalSettings.remote;
if (remoteConfig && remoteConfig.enabled) {
startRemoteServer().catch(err => {
debugLog(`remote-server auto-start failed: ${err.message}`);
});
}
} catch (err) {
debugLog(`remote-server auto-start failed: ${err.message}`);
}
// Auto-show drop target if enabled
try {
const dtConfig = configStore.load();
@ -532,10 +517,6 @@ app.on('window-all-closed', () => {
app.on('before-quit', () => {
try { folderMonitor.stop(); } catch {}
try {
if (remoteServer) { remoteServer.stop(); remoteServer = null; }
destroyCaptureWindow();
} catch {}
destroyDropTargetWindow();
});
@ -910,204 +891,6 @@ ipcMain.handle('folder-monitor:select-folder', async () => {
return result.filePaths[0];
});
// --- Remote Control ---
function generateToken() {
const crypto = require('crypto');
return crypto.randomBytes(32).toString('hex');
}
function createCaptureWindow() {
if (captureWindow && !captureWindow.isDestroyed()) return;
captureWindow = new BrowserWindow({
show: false,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
preload: path.join(__dirname, 'lib', 'remote-capture-preload.js')
}
});
captureWindow.loadFile(path.join(__dirname, 'lib', 'remote-capture.html'));
// Crash recovery: if hidden window closes unexpectedly while clients connected, recreate it
captureWindow.on('closed', () => {
captureWindow = null;
if (remoteServer && remoteServer.getClientCount() > 0) {
debugLog('remote: capture window crashed, recreating...');
createCaptureWindow();
}
});
}
function destroyCaptureWindow() {
if (captureWindow && !captureWindow.isDestroyed()) {
captureWindow.close();
captureWindow = null;
}
}
async function startRemoteServer() {
if (remoteServer) {
remoteServer.stop();
remoteServer = null;
}
const config = configStore.load();
const remote = config.globalSettings && config.globalSettings.remote;
if (!remote || !remote.enabled) return;
let token = remote.token;
if (!token) {
token = generateToken();
const gs = { ...config.globalSettings, remote: { ...remote, token } };
await configStore.save({ globalSettings: gs });
}
remoteServer = new RemoteServer();
await remoteServer.start({
port: remote.port || 9100,
token,
allowInput: remote.allowInput !== false,
mainWindow,
onSignalingToCapture: (data) => {
if (captureWindow && !captureWindow.isDestroyed()) {
captureWindow.webContents.send('remote:signaling-to-capture', data);
}
},
onCreateCaptureWindow: () => createCaptureWindow(),
onDestroyCaptureWindow: () => destroyCaptureWindow()
});
debugLog(`remote-server started on port ${remoteServer.getPort()}`);
}
// IPC: Signaling from capture window back to dashboard client
ipcMain.on('remote:signaling-from-capture', (_event, data) => {
if (remoteServer && data.clientId) {
remoteServer.sendToClient(data.clientId, data);
}
});
// IPC: Input events from capture window
ipcMain.on('remote:input-event', (_event, data) => {
if (!mainWindow || mainWindow.isDestroyed()) return;
const config = configStore.load();
const remote = config.globalSettings && config.globalSettings.remote;
if (!remote || !remote.allowInput) return;
if (data.role !== 'admin') return;
const bounds = mainWindow.getContentBounds();
const x = Math.round((data.x || 0) * bounds.width);
const y = Math.round((data.y || 0) * bounds.height);
switch (data.type) {
case 'mousemove':
mainWindow.webContents.sendInputEvent({ type: 'mouseMove', x, y });
break;
case 'mousedown':
mainWindow.webContents.sendInputEvent({
type: 'mouseDown', x, y,
button: data.button === 'right' ? 'right' : 'left',
clickCount: 1
});
break;
case 'mouseup':
mainWindow.webContents.sendInputEvent({
type: 'mouseUp', x, y,
button: data.button === 'right' ? 'right' : 'left',
clickCount: 1
});
break;
case 'scroll':
mainWindow.webContents.sendInputEvent({
type: 'mouseWheel', x, y,
deltaX: data.deltaX || 0,
deltaY: data.deltaY || 0
});
break;
case 'keydown':
mainWindow.webContents.sendInputEvent({
type: 'keyDown',
keyCode: data.key,
modifiers: buildModifiers(data)
});
if (data.key.length === 1) {
mainWindow.webContents.sendInputEvent({
type: 'char',
keyCode: data.key,
modifiers: buildModifiers(data)
});
}
break;
case 'keyup':
mainWindow.webContents.sendInputEvent({
type: 'keyUp',
keyCode: data.key,
modifiers: buildModifiers(data)
});
break;
}
});
function buildModifiers(data) {
const mods = [];
if (data.shift) mods.push('shift');
if (data.ctrl) mods.push('control');
if (data.alt) mods.push('alt');
return mods;
}
// IPC: Get capture source ID (desktopCapturer must run in main process in Electron 33+)
ipcMain.handle('remote:get-capture-source-id', async () => {
if (!mainWindow || mainWindow.isDestroyed()) return null;
const { desktopCapturer } = require('electron');
const sources = await desktopCapturer.getSources({ types: ['window'] });
const title = mainWindow.getTitle();
const source = sources.find(s => s.name === title);
return source ? source.id : null;
});
// IPC: Client count updates from capture window
ipcMain.on('remote:client-count', (_event, count) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('remote:client-count', count);
}
});
// IPC: Remote settings
ipcMain.handle('remote:get-settings', () => {
const config = configStore.load();
return config.globalSettings && config.globalSettings.remote || {};
});
ipcMain.handle('remote:save-settings', async (_event, remoteSettings) => {
const config = configStore.load();
const gs = { ...config.globalSettings, remote: remoteSettings };
await configStore.save({ globalSettings: gs });
if (remoteSettings.enabled) {
await startRemoteServer();
} else if (remoteServer) {
remoteServer.stop();
remoteServer = null;
destroyCaptureWindow();
debugLog('remote-server stopped');
}
return true;
});
ipcMain.handle('remote:generate-token', () => {
return generateToken();
});
ipcMain.handle('remote:status', () => {
return {
running: !!remoteServer,
port: remoteServer ? remoteServer.getPort() : null,
clientCount: remoteServer ? remoteServer.getClientCount() : 0
};
});
// --- Always on top ---
ipcMain.handle('set-always-on-top', async (_event, value) => {
if (mainWindow && !mainWindow.isDestroyed()) {

28
package-lock.json generated
View File

@ -1,16 +1,15 @@
{
"name": "multi-hoster-uploader",
"version": "2.0.6",
"version": "1.8.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "multi-hoster-uploader",
"version": "2.0.6",
"version": "1.8.6",
"dependencies": {
"chokidar": "^3.6.0",
"undici": "^7.16.0",
"ws": "^8.19.0"
"undici": "^7.16.0"
},
"devDependencies": {
"electron": "^33.0.0",
@ -5434,27 +5433,6 @@
"dev": true,
"license": "ISC"
},
"node_modules/ws": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xmlbuilder": {
"version": "15.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",

View File

@ -1,6 +1,6 @@
{
"name": "multi-hoster-uploader",
"version": "2.1.0",
"version": "2.0.6",
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
"main": "main.js",
"scripts": {
@ -12,8 +12,7 @@
},
"dependencies": {
"chokidar": "^3.6.0",
"undici": "^7.16.0",
"ws": "^8.19.0"
"undici": "^7.16.0"
},
"devDependencies": {
"electron": "^33.0.0",

View File

@ -93,15 +93,6 @@ contextBridge.exposeInMainWorld('api', {
onShutdownCountdown: (callback) => {
ipcRenderer.on('shutdown-countdown', (_event, data) => callback(data));
},
// Remote Control
remoteGetSettings: () => ipcRenderer.invoke('remote:get-settings'),
remoteSaveSettings: (settings) => ipcRenderer.invoke('remote:save-settings', settings),
remoteGenerateToken: () => ipcRenderer.invoke('remote:generate-token'),
remoteStatus: () => ipcRenderer.invoke('remote:status'),
onRemoteClientCount: (callback) => {
ipcRenderer.on('remote:client-count', (_event, count) => callback(count));
},
// File path from drag & drop (Electron 33+ compatible)
getPathForFile: (file) => webUtils.getPathForFile(file),
removeAllListeners: () => {
@ -114,6 +105,5 @@ contextBridge.exposeInMainWorld('api', {
ipcRenderer.removeAllListeners('folder-monitor:new-files');
ipcRenderer.removeAllListeners('drop-target:files');
ipcRenderer.removeAllListeners('account-switched');
ipcRenderer.removeAllListeners('remote:client-count');
}
});

View File

@ -2023,97 +2023,6 @@ function renderSettings() {
}
});
// --- Remote Control Panel ---
const remoteSettings = globalSettings.remote || {};
const remotePanel = document.createElement('div');
remotePanel.className = 'hoster-settings-panel';
remotePanel.innerHTML = `
<div class="hoster-panel-header" data-hoster="remote">
<span class="panel-arrow">&#9654;</span>
<span class="panel-title">Fernsteuerung</span>
<span class="panel-status${remoteSettings.enabled ? ' active' : ''}" id="remoteStatusBadge">${remoteSettings.enabled ? 'Aktiv' : 'Inaktiv'}</span>
</div>
<div class="hoster-panel-body" data-panel="remote" style="display:none">
<div class="settings-section-label">Server</div>
<div class="settings-grid-mini">
<div class="settings-row checkbox-row">
<label>Aktiviert</label>
<input type="checkbox" class="settings-autosave" id="remoteEnabledInput" ${remoteSettings.enabled ? 'checked' : ''}>
</div>
<div class="settings-row checkbox-row">
<label>Input erlauben</label>
<input type="checkbox" class="settings-autosave" id="remoteAllowInputInput" ${remoteSettings.allowInput !== false ? 'checked' : ''}>
</div>
</div>
<div class="settings-row">
<label>Port</label>
<input type="number" class="hs-input settings-autosave" id="remotePortInput" value="${remoteSettings.port || 9100}" min="1024" max="65535" style="width:100px">
</div>
<div class="settings-row">
<label>API-Token</label>
<input type="text" class="key-input" id="remoteTokenInput" value="${remoteSettings.token || ''}" readonly style="flex:1">
<button class="btn btn-xs btn-secondary" id="remoteCopyTokenBtn" title="Kopieren">Kopieren</button>
<button class="btn btn-xs btn-secondary" id="remoteRegenerateTokenBtn" title="Neu generieren">Neu</button>
</div>
<div class="settings-section-label">Status</div>
<div class="settings-row">
<span id="remoteConnectionStatus" style="color:#94a3b8">Prüfe...</span>
</div>
</div>
`;
container.appendChild(remotePanel);
// Toggle remote panel
remotePanel.querySelector('.hoster-panel-header').addEventListener('click', () => {
const body = remotePanel.querySelector('.hoster-panel-body');
const arrow = remotePanel.querySelector('.panel-arrow');
const isOpen = body.style.display !== 'none';
body.style.display = isOpen ? 'none' : 'block';
arrow.innerHTML = isOpen ? '&#9654;' : '&#9660;';
});
// Copy token
document.getElementById('remoteCopyTokenBtn').addEventListener('click', async () => {
const token = document.getElementById('remoteTokenInput').value;
if (token) {
await window.api.copyToClipboard(token);
document.getElementById('remoteCopyTokenBtn').textContent = 'Kopiert!';
setTimeout(() => { document.getElementById('remoteCopyTokenBtn').textContent = 'Kopieren'; }, 1500);
}
});
// Regenerate token
document.getElementById('remoteRegenerateTokenBtn').addEventListener('click', async () => {
const newToken = await window.api.remoteGenerateToken();
document.getElementById('remoteTokenInput').value = newToken;
scheduleSettingsSave();
});
// Update status
window.api.remoteStatus().then(status => {
const el = document.getElementById('remoteConnectionStatus');
if (!el) return;
if (status.running) {
el.textContent = `Aktiv auf Port ${status.port}${status.clientCount} Client(s) verbunden`;
el.style.color = '#10b981';
} else {
el.textContent = 'Nicht aktiv';
el.style.color = '#94a3b8';
}
}).catch(() => {});
// Live client count updates
window.api.onRemoteClientCount((count) => {
const el = document.getElementById('remoteConnectionStatus');
if (el && el.style.color === 'rgb(16, 185, 129)') {
window.api.remoteStatus().then(status => {
if (status.running) {
el.textContent = `Aktiv auf Port ${status.port}${status.clientCount} Client(s) verbunden`;
}
}).catch(() => {});
}
});
// --- Backup Panel ---
const backupPanel = document.createElement('div');
backupPanel.className = 'hoster-settings-panel';
@ -2281,12 +2190,6 @@ async function saveSettings(options = {}) {
delaySec: Math.max(1, parseInt(document.getElementById('fmDelaySecInput')?.value || '3', 10) || 3),
autoStart: !!document.getElementById('fmAutoStartInput')?.checked,
hosters: Array.from(document.querySelectorAll('.fm-hoster-checkbox:checked')).map(el => el.dataset.fmHoster)
},
remote: {
enabled: !!document.getElementById('remoteEnabledInput')?.checked,
port: Math.max(1024, Math.min(65535, parseInt(document.getElementById('remotePortInput')?.value || '9100', 10) || 9100)),
token: (document.getElementById('remoteTokenInput')?.value || '').trim(),
allowInput: !!document.getElementById('remoteAllowInputInput')?.checked
}
};
@ -2338,31 +2241,6 @@ async function saveSettings(options = {}) {
if (badge) { badge.textContent = 'Inaktiv'; badge.className = 'panel-status'; }
}
// Start/stop remote server based on settings
const remoteSettings = globalSettings.remote;
const remoteBadge = document.getElementById('remoteStatusBadge');
if (remoteSettings) {
try {
await window.api.remoteSaveSettings(remoteSettings);
if (remoteBadge) {
remoteBadge.textContent = remoteSettings.enabled ? 'Aktiv' : 'Inaktiv';
remoteBadge.className = `panel-status${remoteSettings.enabled ? ' active' : ''}`;
}
// Update status display
const status = await window.api.remoteStatus();
const statusEl = document.getElementById('remoteConnectionStatus');
if (statusEl) {
if (status.running) {
statusEl.textContent = `Aktiv auf Port ${status.port}${status.clientCount} Client(s) verbunden`;
statusEl.style.color = '#10b981';
} else {
statusEl.textContent = 'Nicht aktiv';
statusEl.style.color = '#94a3b8';
}
}
} catch {}
}
const feedback = document.getElementById('saveFeedback');
feedback.textContent = feedbackText;
setTimeout(() => {

View File

@ -1,53 +0,0 @@
const { describe, it } = require('node:test');
const assert = require('node:assert');
const path = require('path');
const fs = require('fs');
const os = require('os');
// Minimal app mock for ConfigStore
function createTestConfigStore() {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mhu-test-'));
const mockApp = {
isPackaged: false,
getPath: (name) => tmpDir,
getPath: () => tmpDir
};
const ConfigStore = require('../lib/config-store');
const store = new ConfigStore(mockApp);
store.filePath = path.join(tmpDir, 'test-config.json');
return { store, tmpDir };
}
describe('remote config defaults', () => {
it('should include remote settings in defaults', () => {
const { store } = createTestConfigStore();
const config = store.load();
const remote = config.globalSettings.remote;
assert.strictEqual(remote.enabled, false);
assert.strictEqual(remote.port, 9100);
assert.strictEqual(typeof remote.token, 'string');
assert.strictEqual(remote.token, '');
assert.strictEqual(remote.allowInput, true);
});
it('should deep-merge remote settings with existing config', async () => {
const { store } = createTestConfigStore();
// Save config with partial remote settings
await store.save({
globalSettings: {
remote: { enabled: true, port: 9200 }
}
});
const config = store.load();
const remote = config.globalSettings.remote;
// Saved values preserved
assert.strictEqual(remote.enabled, true);
assert.strictEqual(remote.port, 9200);
// Defaults merged in
assert.strictEqual(remote.allowInput, true);
assert.strictEqual(remote.token, '');
});
});

View File

@ -1,41 +0,0 @@
const { describe, it, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert');
// Test the module can be required and has the expected API
describe('RemoteServer', () => {
it('should export a class with start/stop methods', () => {
const RemoteServer = require('../lib/remote-server');
assert.strictEqual(typeof RemoteServer, 'function');
assert.strictEqual(typeof RemoteServer.prototype.start, 'function');
assert.strictEqual(typeof RemoteServer.prototype.stop, 'function');
assert.strictEqual(typeof RemoteServer.prototype.getClientCount, 'function');
});
it('should start and stop without errors', async () => {
const RemoteServer = require('../lib/remote-server');
const server = new RemoteServer();
// Mock mainWindow
const mockMainWindow = {
isDestroyed: () => false,
getTitle: () => 'Test Window',
getContentBounds: () => ({ x: 0, y: 0, width: 1920, height: 1080 }),
webContents: {
sendInputEvent: () => {}
}
};
await server.start({
port: 0, // random available port
token: 'test-token-123',
allowInput: true,
mainWindow: mockMainWindow,
onSignalingToCapture: () => {},
onCreateCaptureWindow: () => {},
onDestroyCaptureWindow: () => {}
});
assert.strictEqual(server.getClientCount(), 0);
server.stop();
});
});