feat: byse.sx health check + performance optimizations for large queues
- Add byse.sx health check via API upload/server endpoint - Virtual scrolling for queue table (>200 rows renders only visible rows) - O(1) job lookups via index Maps instead of O(n) array.find() - Event delegation on queue tbody instead of per-row listeners - Async config writes to avoid blocking main process - Increase persist debounce to 3s during uploads (was 250ms) - Reduce debug logging to state changes only - Move save button to bottom-right in settings Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
294f6a4de7
commit
7d992206e8
@ -81,13 +81,12 @@ class ConfigStore {
|
|||||||
|
|
||||||
save(config) {
|
save(config) {
|
||||||
const current = this.load();
|
const current = this.load();
|
||||||
// Update hosters credentials
|
|
||||||
if (config.hosters) current.hosters = config.hosters;
|
if (config.hosters) current.hosters = config.hosters;
|
||||||
// Update hoster settings
|
|
||||||
if (config.hosterSettings) current.hosterSettings = config.hosterSettings;
|
if (config.hosterSettings) current.hosterSettings = config.hosterSettings;
|
||||||
// Update global settings
|
|
||||||
if (config.globalSettings) current.globalSettings = config.globalSettings;
|
if (config.globalSettings) current.globalSettings = config.globalSettings;
|
||||||
fs.writeFileSync(this.filePath, JSON.stringify(current, null, 2), 'utf-8');
|
// Async write to avoid blocking main process
|
||||||
|
const data = JSON.stringify(current, null, 2);
|
||||||
|
fs.writeFile(this.filePath, data, 'utf-8', () => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
loadHistory() {
|
loadHistory() {
|
||||||
|
|||||||
48
main.js
48
main.js
@ -245,8 +245,42 @@ async function checkVoeHealth(hosterConfig) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function checkByseHealth(hosterConfig) {
|
||||||
|
const apiKey = hosterConfig && hosterConfig.apiKey
|
||||||
|
? String(hosterConfig.apiKey).trim()
|
||||||
|
: '';
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
return { status: 'error', message: 'API Key fehlt' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiBase = 'https://api.byse.sx';
|
||||||
|
|
||||||
|
const serverRes = await fetch(`${apiBase}/upload/server?key=${encodeURIComponent(apiKey)}`, {
|
||||||
|
method: 'GET',
|
||||||
|
redirect: 'follow'
|
||||||
|
});
|
||||||
|
const serverPayload = await serverRes.json().catch(() => null);
|
||||||
|
|
||||||
|
if (!serverPayload || typeof serverPayload !== 'object') {
|
||||||
|
return { status: 'error', message: 'API lieferte kein gueltiges JSON' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverResult = serverPayload.result;
|
||||||
|
if (typeof serverResult === 'string' && /^https?:\/\//i.test(serverResult.trim())) {
|
||||||
|
return { status: 'ok', message: 'API Key gueltig, Upload-Server verfuegbar' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const msg = String(serverPayload.msg || serverPayload.message || '').trim();
|
||||||
|
if (msg) {
|
||||||
|
return { status: 'error', message: msg };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { status: 'error', message: 'API Key ungueltig oder Server nicht erreichbar' };
|
||||||
|
}
|
||||||
|
|
||||||
async function runHosterHealthCheck(config, requestedHosters) {
|
async function runHosterHealthCheck(config, requestedHosters) {
|
||||||
const allowed = ['doodstream.com', 'vidmoly.me', 'voe.sx'];
|
const allowed = ['doodstream.com', 'vidmoly.me', 'voe.sx', 'byse.sx'];
|
||||||
const source = Array.isArray(requestedHosters) && requestedHosters.length > 0
|
const source = Array.isArray(requestedHosters) && requestedHosters.length > 0
|
||||||
? requestedHosters
|
? requestedHosters
|
||||||
: allowed;
|
: allowed;
|
||||||
@ -290,6 +324,15 @@ async function runHosterHealthCheck(config, requestedHosters) {
|
|||||||
return { hoster, ...result };
|
return { hoster, ...result };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hoster === 'byse.sx') {
|
||||||
|
const result = await withTimeout(
|
||||||
|
checkByseHealth(hosterConfig),
|
||||||
|
HEALTH_CHECK_TIMEOUT,
|
||||||
|
'Byse-Check'
|
||||||
|
);
|
||||||
|
return { hoster, ...result };
|
||||||
|
}
|
||||||
|
|
||||||
return { hoster, status: 'skipped', message: 'Kein Health-Check fuer diesen Hoster' };
|
return { hoster, status: 'skipped', message: 'Kein Health-Check fuer diesen Hoster' };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return {
|
return {
|
||||||
@ -429,7 +472,10 @@ ipcMain.handle('start-upload', (_event, payload) => {
|
|||||||
uploadManager = new UploadManager(config.hosterSettings || {});
|
uploadManager = new UploadManager(config.hosterSettings || {});
|
||||||
|
|
||||||
uploadManager.on('progress', (data) => {
|
uploadManager.on('progress', (data) => {
|
||||||
|
// Only log state changes, not continuous progress updates
|
||||||
|
if (data.status !== 'uploading') {
|
||||||
debugLog(`progress: ${data.fileName} ${data.hoster} ${data.status} ${data.error || ''}`);
|
debugLog(`progress: ${data.fileName} ${data.hoster} ${data.status} ${data.error || ''}`);
|
||||||
|
}
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.webContents.send('upload-progress', data);
|
mainWindow.webContents.send('upload-progress', data);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "multi-hoster-uploader",
|
"name": "multi-hoster-uploader",
|
||||||
"version": "1.3.0",
|
"version": "1.4.0",
|
||||||
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
164
renderer/app.js
164
renderer/app.js
@ -15,6 +15,8 @@ const AUTO_CHECK_PREF_KEY = 'autoHealthCheckBeforeUpload';
|
|||||||
|
|
||||||
// Queue state
|
// Queue state
|
||||||
let queueJobs = []; // { id, file, fileName, hoster, status, bytesUploaded, bytesTotal, speedKbs, elapsed, remaining, error, result, attempt, maxAttempts, link }
|
let queueJobs = []; // { id, file, fileName, hoster, status, bytesUploaded, bytesTotal, speedKbs, elapsed, remaining, error, result, attempt, maxAttempts, link }
|
||||||
|
let _jobIndexById = new Map(); // id -> job (O(1) lookup)
|
||||||
|
let _jobIndexByUploadId = new Map(); // uploadId -> job
|
||||||
let selectedJobIds = new Set();
|
let selectedJobIds = new Set();
|
||||||
let queueSortState = { key: 'filename', direction: 'asc' };
|
let queueSortState = { key: 'filename', direction: 'asc' };
|
||||||
|
|
||||||
@ -288,9 +290,11 @@ async function persistQueueStateNow() {
|
|||||||
|
|
||||||
function persistQueueStateSoon() {
|
function persistQueueStateSoon() {
|
||||||
clearTimeout(queuePersistTimer);
|
clearTimeout(queuePersistTimer);
|
||||||
|
// Use longer debounce during uploads to reduce disk I/O
|
||||||
|
const delay = uploading ? 3000 : 500;
|
||||||
queuePersistTimer = setTimeout(() => {
|
queuePersistTimer = setTimeout(() => {
|
||||||
persistQueueStateNow().catch(() => {});
|
persistQueueStateNow().catch(() => {});
|
||||||
}, 250);
|
}, delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearPersistedQueueStateSoon() {
|
function clearPersistedQueueStateSoon() {
|
||||||
@ -395,51 +399,76 @@ function buildQueuePreview() {
|
|||||||
const hosters = getSelectedHosters();
|
const hosters = getSelectedHosters();
|
||||||
if (hosters.length === 0) {
|
if (hosters.length === 0) {
|
||||||
queueJobs = queueJobs.filter(j => j.status !== 'preview');
|
queueJobs = queueJobs.filter(j => j.status !== 'preview');
|
||||||
|
rebuildJobIndex();
|
||||||
renderQueueTable();
|
renderQueueTable();
|
||||||
persistQueueStateSoon();
|
persistQueueStateSoon();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Remove old preview jobs (status 'preview')
|
// Remove old preview jobs
|
||||||
queueJobs = queueJobs.filter(j => j.status !== 'preview');
|
queueJobs = queueJobs.filter(j => j.status !== 'preview');
|
||||||
|
|
||||||
|
// Build a Set for fast existence checks
|
||||||
|
const existingKeys = new Set();
|
||||||
|
for (const j of queueJobs) {
|
||||||
|
if (j.status !== 'error') existingKeys.add(`${j.file}|${j.hoster}`);
|
||||||
|
}
|
||||||
|
|
||||||
for (const file of selectedFiles) {
|
for (const file of selectedFiles) {
|
||||||
for (const hoster of hosters) {
|
for (const hoster of hosters) {
|
||||||
// Don't add if already in queue (from a previous upload)
|
const key = `${file.path}|${hoster}`;
|
||||||
const exists = queueJobs.find(j => j.file === file.path && j.hoster === hoster && j.status !== 'error');
|
if (!existingKeys.has(key)) {
|
||||||
if (!exists) {
|
const job = {
|
||||||
queueJobs.push({
|
|
||||||
id: `preview-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
id: `preview-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||||
file: file.path, fileName: file.name, hoster,
|
file: file.path, fileName: file.name, hoster,
|
||||||
status: 'preview', bytesUploaded: 0, bytesTotal: file.size || 0,
|
status: 'preview', bytesUploaded: 0, bytesTotal: file.size || 0,
|
||||||
speedKbs: 0, elapsed: 0, remaining: 0,
|
speedKbs: 0, elapsed: 0, remaining: 0,
|
||||||
error: null, result: null, attempt: 0, maxAttempts: 0, link: ''
|
error: null, result: null, attempt: 0, maxAttempts: 0, link: ''
|
||||||
});
|
};
|
||||||
|
queueJobs.push(job);
|
||||||
|
existingKeys.add(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
rebuildJobIndex();
|
||||||
renderQueueTable();
|
renderQueueTable();
|
||||||
persistQueueStateSoon();
|
persistQueueStateSoon();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Queue Table Rendering (debounced) ---
|
// --- Job Index Management ---
|
||||||
|
function rebuildJobIndex() {
|
||||||
|
_jobIndexById.clear();
|
||||||
|
_jobIndexByUploadId.clear();
|
||||||
|
for (const job of queueJobs) {
|
||||||
|
_jobIndexById.set(job.id, job);
|
||||||
|
if (job.uploadId) _jobIndexByUploadId.set(job.uploadId, job);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function indexJob(job) {
|
||||||
|
_jobIndexById.set(job.id, job);
|
||||||
|
if (job.uploadId) _jobIndexByUploadId.set(job.uploadId, job);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeJobFromIndex(job) {
|
||||||
|
_jobIndexById.delete(job.id);
|
||||||
|
if (job.uploadId) _jobIndexByUploadId.delete(job.uploadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Queue Table Rendering (debounced with virtual scrolling) ---
|
||||||
let _renderQueued = false;
|
let _renderQueued = false;
|
||||||
|
let _sortedJobsCache = [];
|
||||||
|
const VIRTUAL_ROW_HEIGHT = 28;
|
||||||
|
const VIRTUAL_OVERSCAN = 10;
|
||||||
|
let _lastVisibleRange = { start: -1, end: -1 };
|
||||||
|
let _queueListenersBound = false;
|
||||||
|
|
||||||
function scheduleQueueRender() {
|
function scheduleQueueRender() {
|
||||||
if (_renderQueued) return;
|
if (_renderQueued) return;
|
||||||
_renderQueued = true;
|
_renderQueued = true;
|
||||||
requestAnimationFrame(() => { _renderQueued = false; renderQueueTable(); });
|
requestAnimationFrame(() => { _renderQueued = false; renderQueueTable(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderQueueTable() {
|
function buildRowHtml(job) {
|
||||||
const tbody = document.getElementById('queueBody');
|
|
||||||
if (!tbody) return;
|
|
||||||
|
|
||||||
// Preserve scroll position
|
|
||||||
const scrollContainer = document.getElementById('queueContainer');
|
|
||||||
const scrollTop = scrollContainer ? scrollContainer.scrollTop : 0;
|
|
||||||
|
|
||||||
const sorted = sortQueueJobs(queueJobs);
|
|
||||||
|
|
||||||
tbody.innerHTML = sorted.map((job) => {
|
|
||||||
const statusClass = `status-${job.status}`;
|
const statusClass = `status-${job.status}`;
|
||||||
const rowClass = `queue-row ${statusClass}${selectedJobIds.has(job.id) ? ' selected' : ''}`;
|
const rowClass = `queue-row ${statusClass}${selectedJobIds.has(job.id) ? ' selected' : ''}`;
|
||||||
const uploadedSize = job.status === 'preview'
|
const uploadedSize = job.status === 'preview'
|
||||||
@ -452,7 +481,7 @@ function renderQueueTable() {
|
|||||||
const pct = Math.min(100, Math.round((job.progress || 0) * 100));
|
const pct = Math.min(100, Math.round((job.progress || 0) * 100));
|
||||||
const link = job.result ? (job.result.download_url || job.result.embed_url || '') : '';
|
const link = job.result ? (job.result.download_url || job.result.embed_url || '') : '';
|
||||||
|
|
||||||
return `<tr class="${rowClass}" data-job-id="${job.id}" data-link="${escapeAttr(link)}">
|
return `<tr class="${rowClass}" data-job-id="${job.id}" data-link="${escapeAttr(link)}" style="height:${VIRTUAL_ROW_HEIGHT}px">
|
||||||
<td class="col-filename" title="${escapeAttr(job.fileName)}">${escapeHtml(job.fileName)}</td>
|
<td class="col-filename" title="${escapeAttr(job.fileName)}">${escapeHtml(job.fileName)}</td>
|
||||||
<td class="col-size">${uploadedSize}</td>
|
<td class="col-size">${uploadedSize}</td>
|
||||||
<td class="col-host">${escapeHtml(job.hoster)}</td>
|
<td class="col-host">${escapeHtml(job.hoster)}</td>
|
||||||
@ -469,22 +498,78 @@ function renderQueueTable() {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('');
|
}
|
||||||
|
|
||||||
// Restore scroll position
|
function renderQueueTable() {
|
||||||
if (scrollContainer) scrollContainer.scrollTop = scrollTop;
|
const tbody = document.getElementById('queueBody');
|
||||||
|
if (!tbody) return;
|
||||||
|
|
||||||
// Attach click handlers
|
_sortedJobsCache = sortQueueJobs(queueJobs);
|
||||||
tbody.querySelectorAll('.queue-row').forEach(row => {
|
const totalRows = _sortedJobsCache.length;
|
||||||
row.addEventListener('click', (e) => handleRowClick(e, row));
|
|
||||||
row.addEventListener('contextmenu', (e) => handleRowContextMenu(e, row));
|
// For small queues (<200 rows), use simple rendering
|
||||||
|
if (totalRows < 200) {
|
||||||
|
tbody.innerHTML = _sortedJobsCache.map(buildRowHtml).join('');
|
||||||
|
_lastVisibleRange = { start: -1, end: -1 };
|
||||||
|
} else {
|
||||||
|
// Virtual scrolling for large queues
|
||||||
|
_renderVirtualRows(tbody);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind event delegation once
|
||||||
|
if (!_queueListenersBound) {
|
||||||
|
_queueListenersBound = true;
|
||||||
|
tbody.addEventListener('click', (e) => {
|
||||||
|
const row = e.target.closest('.queue-row');
|
||||||
|
if (row) handleRowClick(e, row);
|
||||||
});
|
});
|
||||||
|
tbody.addEventListener('contextmenu', (e) => {
|
||||||
|
const row = e.target.closest('.queue-row');
|
||||||
|
if (row) handleRowContextMenu(e, row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Update retry button visibility
|
// Update retry button visibility
|
||||||
const hasFailedJobs = queueJobs.some(j => j.status === 'error');
|
const hasFailedJobs = queueJobs.some(j => j.status === 'error');
|
||||||
document.getElementById('retryFailedBtn').style.display = hasFailedJobs ? 'inline-block' : 'none';
|
document.getElementById('retryFailedBtn').style.display = hasFailedJobs ? 'inline-block' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _renderVirtualRows(tbody) {
|
||||||
|
const scrollContainer = document.getElementById('queueContainer');
|
||||||
|
if (!scrollContainer) return;
|
||||||
|
|
||||||
|
const totalRows = _sortedJobsCache.length;
|
||||||
|
const totalHeight = totalRows * VIRTUAL_ROW_HEIGHT;
|
||||||
|
const scrollTop = scrollContainer.scrollTop;
|
||||||
|
const viewportHeight = scrollContainer.clientHeight;
|
||||||
|
|
||||||
|
const startIdx = Math.max(0, Math.floor(scrollTop / VIRTUAL_ROW_HEIGHT) - VIRTUAL_OVERSCAN);
|
||||||
|
const endIdx = Math.min(totalRows, Math.ceil((scrollTop + viewportHeight) / VIRTUAL_ROW_HEIGHT) + VIRTUAL_OVERSCAN);
|
||||||
|
|
||||||
|
// Only re-render if visible range changed
|
||||||
|
if (startIdx === _lastVisibleRange.start && endIdx === _lastVisibleRange.end) return;
|
||||||
|
_lastVisibleRange = { start: startIdx, end: endIdx };
|
||||||
|
|
||||||
|
const topPad = startIdx * VIRTUAL_ROW_HEIGHT;
|
||||||
|
const bottomPad = (totalRows - endIdx) * VIRTUAL_ROW_HEIGHT;
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
if (topPad > 0) html += `<tr class="virtual-spacer" style="height:${topPad}px"><td colspan="8"></td></tr>`;
|
||||||
|
for (let i = startIdx; i < endIdx; i++) {
|
||||||
|
html += buildRowHtml(_sortedJobsCache[i]);
|
||||||
|
}
|
||||||
|
if (bottomPad > 0) html += `<tr class="virtual-spacer" style="height:${bottomPad}px"><td colspan="8"></td></tr>`;
|
||||||
|
|
||||||
|
tbody.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _onQueueScroll() {
|
||||||
|
if (_sortedJobsCache.length >= 200) {
|
||||||
|
const tbody = document.getElementById('queueBody');
|
||||||
|
if (tbody) _renderVirtualRows(tbody);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function sortQueueJobs(jobs) {
|
function sortQueueJobs(jobs) {
|
||||||
const { key, direction } = queueSortState;
|
const { key, direction } = queueSortState;
|
||||||
const factor = direction === 'asc' ? 1 : -1;
|
const factor = direction === 'asc' ? 1 : -1;
|
||||||
@ -656,7 +741,7 @@ async function startUpload() {
|
|||||||
|
|
||||||
// Auto health check
|
// Auto health check
|
||||||
if (autoHealthCheckEnabled) {
|
if (autoHealthCheckEnabled) {
|
||||||
const checkHosters = hosters.filter(name => name === 'doodstream.com' || name === 'vidmoly.me' || name === 'voe.sx');
|
const checkHosters = hosters.filter(name => name === 'doodstream.com' || name === 'vidmoly.me' || name === 'voe.sx' || name === 'byse.sx');
|
||||||
if (checkHosters.length > 0) {
|
if (checkHosters.length > 0) {
|
||||||
healthCheckRunning = true;
|
healthCheckRunning = true;
|
||||||
try {
|
try {
|
||||||
@ -711,20 +796,20 @@ async function cancelUpload() {
|
|||||||
|
|
||||||
// --- Progress handling ---
|
// --- Progress handling ---
|
||||||
function handleProgress(data) {
|
function handleProgress(data) {
|
||||||
console.log('[upload-progress]', data.status, data.hoster, data.fileName, data.error || '');
|
// Find matching job: O(1) by uploadId, fallback to linear search
|
||||||
// Find matching job by fileName + hoster, or by uploadId
|
let job = data.uploadId ? _jobIndexByUploadId.get(data.uploadId) : null;
|
||||||
let job = data.uploadId ? queueJobs.find(j => j.uploadId === data.uploadId) : null;
|
|
||||||
if (!job) {
|
if (!job) {
|
||||||
// Match by file+hoster for queued/preview jobs (prefer queued, then preview)
|
|
||||||
job = queueJobs.find(j =>
|
job = queueJobs.find(j =>
|
||||||
j.fileName === data.fileName && j.hoster === data.hoster && j.status === 'queued'
|
j.fileName === data.fileName && j.hoster === data.hoster && j.status === 'queued'
|
||||||
) || queueJobs.find(j =>
|
) || queueJobs.find(j =>
|
||||||
j.fileName === data.fileName && j.hoster === data.hoster && j.status === 'preview'
|
j.fileName === data.fileName && j.hoster === data.hoster && j.status === 'preview'
|
||||||
);
|
);
|
||||||
if (job && data.uploadId) job.uploadId = data.uploadId;
|
if (job && data.uploadId) {
|
||||||
|
job.uploadId = data.uploadId;
|
||||||
|
_jobIndexByUploadId.set(data.uploadId, job);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!job) {
|
if (!job) {
|
||||||
// Create new job entry
|
|
||||||
job = {
|
job = {
|
||||||
id: data.uploadId || `job-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
id: data.uploadId || `job-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||||
uploadId: data.uploadId,
|
uploadId: data.uploadId,
|
||||||
@ -734,6 +819,7 @@ function handleProgress(data) {
|
|||||||
error: null, result: null, attempt: 0, maxAttempts: 0, link: ''
|
error: null, result: null, attempt: 0, maxAttempts: 0, link: ''
|
||||||
};
|
};
|
||||||
queueJobs.push(job);
|
queueJobs.push(job);
|
||||||
|
indexJob(job);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update job state
|
// Update job state
|
||||||
@ -792,7 +878,7 @@ function handleBatchDone(summary) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleStats(data) {
|
function handleStats(data) {
|
||||||
console.log('[upload-stats]', data.state, 'active=' + data.activeJobs);
|
// stats logging removed for perf
|
||||||
document.getElementById('sbState').textContent = data.state === 'uploading' ? 'Upload laeuft...' : 'Bereit';
|
document.getElementById('sbState').textContent = data.state === 'uploading' ? 'Upload laeuft...' : 'Bereit';
|
||||||
document.getElementById('sbSpeed').textContent = formatSpeed(data.globalSpeedKbs || 0);
|
document.getElementById('sbSpeed').textContent = formatSpeed(data.globalSpeedKbs || 0);
|
||||||
document.getElementById('sbTotal').textContent = formatSize(data.totalBytes || 0);
|
document.getElementById('sbTotal').textContent = formatSize(data.totalBytes || 0);
|
||||||
@ -854,9 +940,9 @@ async function executeHealthCheck(hosters, mode) {
|
|||||||
|
|
||||||
async function runHealthCheck() {
|
async function runHealthCheck() {
|
||||||
if (healthCheckRunning || uploading) return;
|
if (healthCheckRunning || uploading) return;
|
||||||
const hosters = getSelectedHosters().filter(n => n === 'doodstream.com' || n === 'vidmoly.me' || n === 'voe.sx');
|
const hosters = getSelectedHosters().filter(n => n === 'doodstream.com' || n === 'vidmoly.me' || n === 'voe.sx' || n === 'byse.sx');
|
||||||
if (hosters.length === 0) {
|
if (hosters.length === 0) {
|
||||||
const allHosters = ['doodstream.com', 'vidmoly.me', 'voe.sx'].filter(n => hosterHasCredentials(n, config.hosters[n] || {}));
|
const allHosters = ['doodstream.com', 'vidmoly.me', 'voe.sx', 'byse.sx'].filter(n => hosterHasCredentials(n, config.hosters[n] || {}));
|
||||||
if (allHosters.length === 0) { alert('Keine Hoster mit Zugangsdaten fuer Health-Check.'); return; }
|
if (allHosters.length === 0) { alert('Keine Hoster mit Zugangsdaten fuer Health-Check.'); return; }
|
||||||
hosters.push(...allHosters);
|
hosters.push(...allHosters);
|
||||||
}
|
}
|
||||||
@ -1443,6 +1529,10 @@ function setupListeners() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Virtual scroll for large queues
|
||||||
|
const queueContainer = document.getElementById('queueContainer');
|
||||||
|
if (queueContainer) queueContainer.addEventListener('scroll', _onQueueScroll, { passive: true });
|
||||||
|
|
||||||
// Queue table sorting
|
// Queue table sorting
|
||||||
document.querySelectorAll('#queueTable th.sortable').forEach(th => {
|
document.querySelectorAll('#queueTable th.sortable').forEach(th => {
|
||||||
th.addEventListener('click', () => {
|
th.addEventListener('click', () => {
|
||||||
|
|||||||
@ -164,8 +164,10 @@
|
|||||||
<h2>Upload Einstellungen</h2>
|
<h2>Upload Einstellungen</h2>
|
||||||
<p class="settings-hint">Upload-Einstellungen pro Hoster. Zugangsdaten werden im Accounts-Tab verwaltet.</p>
|
<p class="settings-hint">Upload-Einstellungen pro Hoster. Zugangsdaten werden im Accounts-Tab verwaltet.</p>
|
||||||
<div class="settings-hosters" id="settingsHosters"></div>
|
<div class="settings-hosters" id="settingsHosters"></div>
|
||||||
<button class="btn btn-primary" id="saveSettingsBtn">Alles Speichern</button>
|
<div class="settings-save-row">
|
||||||
<span class="save-feedback" id="saveFeedback"></span>
|
<span class="save-feedback" id="saveFeedback"></span>
|
||||||
|
<button class="btn btn-primary" id="saveSettingsBtn">Alles Speichern</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -227,6 +227,8 @@ body {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.virtual-spacer td { padding: 0 !important; border: none !important; }
|
||||||
|
|
||||||
/* Queue Row States */
|
/* Queue Row States */
|
||||||
.queue-row { transition: background 0.15s; cursor: pointer; }
|
.queue-row { transition: background 0.15s; cursor: pointer; }
|
||||||
.queue-row:hover { background: rgba(255, 255, 255, 0.04); }
|
.queue-row:hover { background: rgba(255, 255, 255, 0.04); }
|
||||||
@ -569,7 +571,8 @@ body {
|
|||||||
}
|
}
|
||||||
.toggle-vis:hover { border-color: var(--border-hover); }
|
.toggle-vis:hover { border-color: var(--border-hover); }
|
||||||
|
|
||||||
.save-feedback { font-size: 12px; color: var(--success); margin-left: 8px; }
|
.settings-save-row { display: flex; justify-content: flex-end; align-items: center; gap: 8px; margin-top: 12px; }
|
||||||
|
.save-feedback { font-size: 12px; color: var(--success); }
|
||||||
|
|
||||||
.settings-grid-mini {
|
.settings-grid-mini {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user