feat(ui): per-job log modal + account label in status

Two related visibility improvements.

1. Status cell now shows which account the job is running on:
   "Upload · Primär", "Retry 2/3 · Fallback #1: <error>", etc.
   - _emitProgress passes task.accountId in every progress event
   - renderer maps accountId → position in config.hosters[hoster] and
     renders "Primär" for index 0 and "Fallback #N" for the rest
   - Applies to uploading/getting-server/retrying (static states like
     done/error already tell their own story)

2. Right-click on a job → "Log anzeigen" opens a modal with the full
   per-job trail: every rot-log entry tagged with that job's jobId
   plus every non-uploading progress transition. Replaces the need to
   grep account-rotation.log for a single filename.
   - UploadManager: all 13 job-scoped _rotLog calls now carry jobId
   - main.js: _jobLogCollector Map<jobId, Array<entry>> with 200-entry
     ring buffer per job; cleared on each new start-upload (fresh
     batch = fresh log). addJobs mid-batch keeps history.
   - New IPC 'get-job-log' returns the array; preload.js exposes
     window.api.getJobLog(jobId)
   - renderer: modal card + context-menu item "Log anzeigen";
     entries formatted as "[HH:MM:SS.mmm] [event] k=v k=v"; copy-to-
     clipboard button
This commit is contained in:
Administrator 2026-04-22 18:13:53 +02:00
parent 329f501a6e
commit b96ccf851a
5 changed files with 158 additions and 26 deletions

View File

@ -342,7 +342,7 @@ class UploadManager extends EventEmitter {
}; };
const emitFinalStatus = (status, payload = {}) => { const emitFinalStatus = (status, payload = {}) => {
this._emitProgress(uploadId, fileName, task.hoster, { this._emitProgress(uploadId, fileName, task.hoster, { accountId: task.accountId,
jobId, jobId,
status, status,
progress: status === 'done' ? 1 : 0, progress: status === 'done' ? 1 : 0,
@ -378,7 +378,7 @@ class UploadManager extends EventEmitter {
return; return;
} }
this._emitProgress(uploadId, fileName, task.hoster, { this._emitProgress(uploadId, fileName, task.hoster, { accountId: task.accountId,
jobId, jobId,
status: 'queued', status: 'queued',
progress: 0, progress: 0,
@ -416,7 +416,7 @@ class UploadManager extends EventEmitter {
const override = this._accountOverrides.get(task.hoster); const override = this._accountOverrides.get(task.hoster);
if (override && !this._failedAccounts.has(task.hoster + ':' + override.id)) { if (override && !this._failedAccounts.has(task.hoster + ':' + override.id)) {
this._rotLog('pre-job-swap', { this._rotLog('pre-job-swap', {
hoster: task.hoster, fileName, fromAccountId: task.accountId, toAccountId: override.id jobId, hoster: task.hoster, fileName, fromAccountId: task.accountId, toAccountId: override.id
}); });
task.accountId = override.id; task.accountId = override.id;
task.username = override.username; task.username = override.username;
@ -424,7 +424,7 @@ class UploadManager extends EventEmitter {
task.apiKey = override.apiKey; task.apiKey = override.apiKey;
} else { } else {
this._rotLog('pre-job-swap-blocked', { this._rotLog('pre-job-swap-blocked', {
hoster: task.hoster, fileName, accountId: task.accountId, jobId, hoster: task.hoster, fileName, accountId: task.accountId,
hasOverride: !!override, hasOverride: !!override,
overrideAlsoFailed: override ? this._failedAccounts.has(task.hoster + ':' + override.id) : false overrideAlsoFailed: override ? this._failedAccounts.has(task.hoster + ':' + override.id) : false
}); });
@ -435,7 +435,7 @@ class UploadManager extends EventEmitter {
if (signal.aborted || this.stopAfterActive) break; if (signal.aborted || this.stopAfterActive) break;
if (attempt > 1) { if (attempt > 1) {
this._emitProgress(uploadId, fileName, task.hoster, { this._emitProgress(uploadId, fileName, task.hoster, { accountId: task.accountId,
jobId, jobId,
status: 'retrying', status: 'retrying',
progress: 0, progress: 0,
@ -462,7 +462,7 @@ class UploadManager extends EventEmitter {
let uploadSignalBundle = { signal, cleanup() {} }; let uploadSignalBundle = { signal, cleanup() {} };
try { try {
this._emitProgress(uploadId, fileName, task.hoster, { this._emitProgress(uploadId, fileName, task.hoster, { accountId: task.accountId,
jobId, jobId,
status: 'getting-server', status: 'getting-server',
progress: 0, progress: 0,
@ -531,7 +531,7 @@ class UploadManager extends EventEmitter {
? Math.round((bytesTotal - bytesUploaded) / (currentSpeedKbs * 1024)) ? Math.round((bytesTotal - bytesUploaded) / (currentSpeedKbs * 1024))
: 0; : 0;
this._emitProgress(uploadId, fileName, task.hoster, { this._emitProgress(uploadId, fileName, task.hoster, { accountId: task.accountId,
jobId, jobId,
status: 'uploading', status: 'uploading',
progress: bytesTotal > 0 ? Math.min(1, bytesUploaded / bytesTotal) : 0, progress: bytesTotal > 0 ? Math.min(1, bytesUploaded / bytesTotal) : 0,
@ -590,7 +590,7 @@ class UploadManager extends EventEmitter {
// jump straight to rotation. // jump straight to rotation.
if (this._shouldSkipRetryOnAccountError(err)) { if (this._shouldSkipRetryOnAccountError(err)) {
this._rotLog('fast-fail', { this._rotLog('fast-fail', {
hoster: task.hoster, fileName, accountId: task.accountId, jobId, hoster: task.hoster, fileName, accountId: task.accountId,
attempt, error: err && err.message ? err.message : String(err) attempt, error: err && err.message ? err.message : String(err)
}); });
break; break;
@ -622,7 +622,7 @@ class UploadManager extends EventEmitter {
// already marked it failed). Otherwise the second job falls straight // already marked it failed). Otherwise the second job falls straight
// through to final-error instead of using the already-resolved fallback. // through to final-error instead of using the already-resolved fallback.
this._rotLog('retries-exhausted', { this._rotLog('retries-exhausted', {
hoster: task.hoster, fileName, accountId: task.accountId, jobId, hoster: task.hoster, fileName, accountId: task.accountId,
lastError: lastError ? lastError.message : null lastError: lastError ? lastError.message : null
}); });
// File-specific rejection → same file will get the same verdict on // File-specific rejection → same file will get the same verdict on
@ -630,7 +630,7 @@ class UploadManager extends EventEmitter {
// retry siblings, just fail this file cleanly. // retry siblings, just fail this file cleanly.
if (this._isFileRejectedError(lastError)) { if (this._isFileRejectedError(lastError)) {
this._rotLog('skip-rotation-file-rejected', { this._rotLog('skip-rotation-file-rejected', {
hoster: task.hoster, fileName, accountId: task.accountId, jobId, hoster: task.hoster, fileName, accountId: task.accountId,
lastError: lastError ? lastError.message : null lastError: lastError ? lastError.message : null
}); });
const error = lastError.message || 'Datei abgelehnt'; const error = lastError.message || 'Datei abgelehnt';
@ -643,7 +643,7 @@ class UploadManager extends EventEmitter {
// can still try fresh. This file just errors out for now. // can still try fresh. This file just errors out for now.
if (this._isTransientNetworkError(lastError)) { if (this._isTransientNetworkError(lastError)) {
this._rotLog('skip-rotation-transient', { this._rotLog('skip-rotation-transient', {
hoster: task.hoster, fileName, accountId: task.accountId, jobId, hoster: task.hoster, fileName, accountId: task.accountId,
lastError: lastError ? lastError.message : null lastError: lastError ? lastError.message : null
}); });
const error = lastError.message || 'Netzwerkfehler'; const error = lastError.message || 'Netzwerkfehler';
@ -657,48 +657,48 @@ class UploadManager extends EventEmitter {
if (!alreadyMarked) { if (!alreadyMarked) {
this._failedAccounts.set(task.hoster + ':' + task.accountId, true); this._failedAccounts.set(task.hoster + ':' + task.accountId, true);
this._rotLog('mark-failed', { this._rotLog('mark-failed', {
hoster: task.hoster, fileName, accountId: task.accountId, jobId, hoster: task.hoster, fileName, accountId: task.accountId,
lastError: lastError ? lastError.message : null lastError: lastError ? lastError.message : null
}); });
this.emit('account-failed', { hoster: task.hoster, accountId: task.accountId }); this.emit('account-failed', { hoster: task.hoster, accountId: task.accountId });
await this._sleep(800, signal); await this._sleep(800, signal);
} else { } else {
this._rotLog('already-marked', { this._rotLog('already-marked', {
hoster: task.hoster, fileName, accountId: task.accountId jobId, hoster: task.hoster, fileName, accountId: task.accountId
}); });
} }
const override = this._accountOverrides.get(task.hoster); const override = this._accountOverrides.get(task.hoster);
if (!override) { if (!override) {
this._rotLog('rotation-end', { this._rotLog('rotation-end', {
hoster: task.hoster, fileName, reason: 'no-override-set', jobId, hoster: task.hoster, fileName, reason: 'no-override-set',
lastFailedAccountId: task.accountId lastFailedAccountId: task.accountId
}); });
break; break;
} }
if (this._failedAccounts.has(task.hoster + ':' + override.id)) { if (this._failedAccounts.has(task.hoster + ':' + override.id)) {
this._rotLog('rotation-end', { this._rotLog('rotation-end', {
hoster: task.hoster, fileName, reason: 'override-already-failed', jobId, hoster: task.hoster, fileName, reason: 'override-already-failed',
overrideId: override.id, lastFailedAccountId: task.accountId overrideId: override.id, lastFailedAccountId: task.accountId
}); });
break; break;
} }
if (override.id === task.accountId) { if (override.id === task.accountId) {
this._rotLog('rotation-end', { this._rotLog('rotation-end', {
hoster: task.hoster, fileName, reason: 'override-same-as-current', jobId, hoster: task.hoster, fileName, reason: 'override-same-as-current',
lastFailedAccountId: task.accountId lastFailedAccountId: task.accountId
}); });
break; break;
} }
// Switch to fallback account and retry this file // Switch to fallback account and retry this file
this._rotLog('rotate', { this._rotLog('rotate', {
hoster: task.hoster, fileName, jobId, hoster: task.hoster, fileName,
fromAccountId: task.accountId, toAccountId: override.id fromAccountId: task.accountId, toAccountId: override.id
}); });
task.accountId = override.id; task.accountId = override.id;
task.username = override.username; task.username = override.username;
task.password = override.password; task.password = override.password;
task.apiKey = override.apiKey; task.apiKey = override.apiKey;
this._emitProgress(uploadId, fileName, task.hoster, { this._emitProgress(uploadId, fileName, task.hoster, { accountId: task.accountId,
jobId, status: 'retrying', progress: 0, bytesUploaded: 0, bytesTotal: fileSize, jobId, status: 'retrying', progress: 0, bytesUploaded: 0, bytesTotal: fileSize,
speedKbs: 0, elapsed: 0, remaining: 0, speedKbs: 0, elapsed: 0, remaining: 0,
error: 'Account-Wechsel zu Fallback', result: null, attempt: 1, maxAttempts error: 'Account-Wechsel zu Fallback', result: null, attempt: 1, maxAttempts
@ -709,7 +709,7 @@ class UploadManager extends EventEmitter {
for (let attempt = 1; attempt <= maxAttempts; attempt++) { for (let attempt = 1; attempt <= maxAttempts; attempt++) {
if (signal.aborted || this.stopAfterActive) break; if (signal.aborted || this.stopAfterActive) break;
if (attempt > 1) { if (attempt > 1) {
this._emitProgress(uploadId, fileName, task.hoster, { this._emitProgress(uploadId, fileName, task.hoster, { accountId: task.accountId,
jobId, status: 'retrying', progress: 0, bytesUploaded: 0, bytesTotal: fileSize, jobId, status: 'retrying', progress: 0, bytesUploaded: 0, bytesTotal: fileSize,
speedKbs: 0, elapsed: 0, remaining: 0, speedKbs: 0, elapsed: 0, remaining: 0,
error: lastError ? lastError.message : '', result: null, attempt, maxAttempts error: lastError ? lastError.message : '', result: null, attempt, maxAttempts
@ -736,7 +736,7 @@ class UploadManager extends EventEmitter {
activeEntry.bytesUploaded = bytesUploaded; activeEntry.bytesUploaded = bytesUploaded;
const elapsed = Math.round((now - jobStart) / 1000); const elapsed = Math.round((now - jobStart) / 1000);
const remaining = currentSpeedKbs > 0 ? Math.round((bytesTotal - bytesUploaded) / (currentSpeedKbs * 1024)) : 0; const remaining = currentSpeedKbs > 0 ? Math.round((bytesTotal - bytesUploaded) / (currentSpeedKbs * 1024)) : 0;
this._emitProgress(uploadId, fileName, task.hoster, { this._emitProgress(uploadId, fileName, task.hoster, { accountId: task.accountId,
jobId, status: 'uploading', jobId, status: 'uploading',
progress: bytesTotal > 0 ? Math.min(1, bytesUploaded / bytesTotal) : 0, progress: bytesTotal > 0 ? Math.min(1, bytesUploaded / bytesTotal) : 0,
bytesUploaded, bytesTotal, speedKbs: currentSpeedKbs, bytesUploaded, bytesTotal, speedKbs: currentSpeedKbs,
@ -767,7 +767,7 @@ class UploadManager extends EventEmitter {
const error = lastError && lastError.message ? lastError.message : 'Unbekannter Fehler'; const error = lastError && lastError.message ? lastError.message : 'Unbekannter Fehler';
this._rotLog('final-error', { this._rotLog('final-error', {
hoster: task.hoster, fileName, lastFailedAccountId: task.accountId, error jobId, hoster: task.hoster, fileName, lastFailedAccountId: task.accountId, error
}); });
emitFinalStatus('error', { error }); emitFinalStatus('error', { error });
recordFinalResult('error', { error }); recordFinalResult('error', { error });

32
main.js
View File

@ -26,6 +26,19 @@ let uploadManager = null;
// dead. Cleared on app restart (which is the user's signal for "try fresh"). // dead. Cleared on app restart (which is the user's signal for "try fresh").
const _sessionFailedAccounts = new Map(); // "hoster:accountId" -> true const _sessionFailedAccounts = new Map(); // "hoster:accountId" -> true
const _sessionAccountOverrides = new Map(); // hoster -> account object const _sessionAccountOverrides = new Map(); // hoster -> account object
// Per-job log collector: backs the right-click "Log anzeigen" modal so the
// user can see the full rot-log + status trail for a single file without
// grepping account-rotation.log. Ring buffer per job keeps memory bounded.
const _jobLogCollector = new Map(); // jobId -> Array<entry>
const _MAX_LOG_ENTRIES_PER_JOB = 200;
function _appendJobLog(jobId, entry) {
if (!jobId) return;
let arr = _jobLogCollector.get(jobId);
if (!arr) { arr = []; _jobLogCollector.set(jobId, arr); }
if (arr.length >= _MAX_LOG_ENTRIES_PER_JOB) arr.shift();
arr.push(entry);
}
const folderMonitor = new FolderMonitor(); const folderMonitor = new FolderMonitor();
let remoteServer = null; let remoteServer = null;
let captureWindow = null; let captureWindow = null;
@ -1120,6 +1133,11 @@ ipcMain.handle('start-upload', (_event, payload) => {
if (tasks.length === 0) return { error: 'Keine gültigen Zugangsdaten für die gewählten Hoster.', skippedJobs }; if (tasks.length === 0) return { error: 'Keine gültigen Zugangsdaten für die gewählten Hoster.', skippedJobs };
// Fresh collector for this new batch — old entries from the previous
// batch's jobs are dropped (user's signal for "fresh log" is starting a
// new upload; addJobs during a running batch keeps them).
_jobLogCollector.clear();
// Pass hoster settings to the upload manager // Pass hoster settings to the upload manager
uploadManager = new UploadManager(config.hosterSettings || {}, config.globalSettings || {}); uploadManager = new UploadManager(config.hosterSettings || {}, config.globalSettings || {});
@ -1127,6 +1145,11 @@ ipcMain.handle('start-upload', (_event, payload) => {
// Only log state changes, not continuous progress updates // Only log state changes, not continuous progress updates
if (data.status !== 'uploading') { if (data.status !== 'uploading') {
debugLog(`progress: ${data.fileName} ${data.hoster} ${data.status} ${data.error || ''}`); debugLog(`progress: ${data.fileName} ${data.hoster} ${data.status} ${data.error || ''}`);
_appendJobLog(data.jobId, {
ts: Date.now(), kind: 'progress', status: data.status,
hoster: data.hoster, accountId: data.accountId || null,
error: data.error || null, attempt: data.attempt || 0, maxAttempts: data.maxAttempts || 0
});
} }
// Write to fileuploader.log immediately when a single upload finishes // Write to fileuploader.log immediately when a single upload finishes
if (data.status === 'done' && data.result) { if (data.status === 'done' && data.result) {
@ -1179,6 +1202,9 @@ ipcMain.handle('start-upload', (_event, payload) => {
.map(([k, v]) => `${k}=${typeof v === 'string' ? v : JSON.stringify(v)}`) .map(([k, v]) => `${k}=${typeof v === 'string' ? v : JSON.stringify(v)}`)
.join(' '); .join(' ');
rotLog(`[${event}] ${pairs}`, ts); rotLog(`[${event}] ${pairs}`, ts);
if (entry.jobId) {
_appendJobLog(entry.jobId, { ts: ts || Date.now(), kind: 'rot', event, ...rest });
}
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('account-rotation-log', entry); mainWindow.webContents.send('account-rotation-log', entry);
} }
@ -1291,6 +1317,12 @@ ipcMain.handle('finish-after-active', () => {
return true; return true;
}); });
ipcMain.handle('get-job-log', (_event, jobId) => {
if (!jobId || typeof jobId !== 'string') return [];
const arr = _jobLogCollector.get(jobId);
return Array.isArray(arr) ? arr.slice() : [];
});
ipcMain.handle('open-log-folder', async () => { ipcMain.handle('open-log-folder', async () => {
// Reveal the active log file (or its directory) in the OS file manager. // Reveal the active log file (or its directory) in the OS file manager.
// Prefers the configured log path, then the rotation log, then just the // Prefers the configured log path, then the rotation log, then just the

View File

@ -108,6 +108,7 @@ contextBridge.exposeInMainWorld('api', {
ipcRenderer.on('account-rotation-log', (_event, data) => callback(data)); ipcRenderer.on('account-rotation-log', (_event, data) => callback(data));
}, },
openLogFolder: () => ipcRenderer.invoke('open-log-folder'), openLogFolder: () => ipcRenderer.invoke('open-log-folder'),
getJobLog: (jobId) => ipcRenderer.invoke('get-job-log', jobId),
onLogPathAutoUpdated: (callback) => { onLogPathAutoUpdated: (callback) => {
ipcRenderer.on('log-path-auto-updated', (_event, data) => callback(data)); ipcRenderer.on('log-path-auto-updated', (_event, data) => callback(data));
}, },

View File

@ -1162,16 +1162,31 @@ function getStatusOrder(status) {
return order[status] ?? 4; return order[status] ?? 4;
} }
// "Primär" / "Fallback #1" / "Fallback #2"… derived from the job's current
// accountId position in the configured hoster account list. Returns '' if we
// can't resolve it (e.g. account was removed mid-session).
function getAccountLabel(job) {
if (!job || !job.accountId || !job.hoster) return '';
const accounts = config && config.hosters && config.hosters[job.hoster];
if (!Array.isArray(accounts)) return '';
const idx = accounts.findIndex(a => a && a.id === job.accountId);
if (idx < 0) return '';
return idx === 0 ? 'Primär' : `Fallback #${idx}`;
}
function getStatusText(job) { function getStatusText(job) {
const shortErr = job.error ? String(job.error).replace(/\s+/g, ' ').slice(0, 100) : ''; const shortErr = job.error ? String(job.error).replace(/\s+/g, ' ').slice(0, 100) : '';
const acc = getAccountLabel(job);
const accSuffix = acc ? ` · ${acc}` : '';
switch (job.status) { switch (job.status) {
case 'preview': return 'Bereit'; case 'preview': return 'Bereit';
case 'queued': return 'Wartet'; case 'queued': return 'Wartet';
case 'getting-server': return 'Server...'; case 'getting-server': return `Server...${accSuffix}`;
case 'uploading': return 'Upload'; case 'uploading': return `Upload${accSuffix}`;
case 'retrying': return shortErr case 'retrying': {
? `Retry ${job.attempt}/${job.maxAttempts}: ${shortErr}` const base = `Retry ${job.attempt}/${job.maxAttempts}${accSuffix}`;
: `Retry ${job.attempt}/${job.maxAttempts}`; return shortErr ? `${base}: ${shortErr}` : base;
}
case 'done': return 'Fertig'; case 'done': return 'Fertig';
case 'aborted': return 'Abgebrochen'; case 'aborted': return 'Abgebrochen';
case 'error': return shortErr ? `Fehlgeschlagen: ${shortErr}` : 'Fehlgeschlagen'; case 'error': return shortErr ? `Fehlgeschlagen: ${shortErr}` : 'Fehlgeschlagen';
@ -1534,6 +1549,8 @@ async function handleContextAction(action) {
if (links.length) { window.api.copyToClipboard(links.join('\n')); showCopyToast(`${links.length} Links kopiert`); } if (links.length) { window.api.copyToClipboard(links.join('\n')); showCopyToast(`${links.length} Links kopiert`); }
} else if (action === 'retry-selected') { } else if (action === 'retry-selected') {
retrySelectedJobs(); retrySelectedJobs();
} else if (action === 'show-log') {
showJobLogModal();
} else if (action === 'delete-selected') { } else if (action === 'delete-selected') {
// Cancel active uploads for deleted jobs // Cancel active uploads for deleted jobs
const activeIds = [...selectedJobIds].filter(id => { const activeIds = [...selectedJobIds].filter(id => {
@ -1854,6 +1871,9 @@ function handleProgress(data) {
job.attempt = data.attempt || 0; job.attempt = data.attempt || 0;
job.maxAttempts = data.maxAttempts || 0; job.maxAttempts = data.maxAttempts || 0;
job.progress = data.progress || 0; job.progress = data.progress || 0;
// Track which account the backend is currently using so the status cell
// can display "Primär" vs "Fallback #N" during rotation.
if (data.accountId) job.accountId = data.accountId;
if (data.uploadId) { if (data.uploadId) {
job.uploadId = data.uploadId; job.uploadId = data.uploadId;
_jobIndexByUploadId.set(data.uploadId, job); _jobIndexByUploadId.set(data.uploadId, job);
@ -1967,6 +1987,60 @@ function handleStats(data) {
} }
} }
// --- Per-job log modal ---
async function showJobLogModal() {
if (selectedJobIds.size === 0) return;
// Use the first selected job — log view is per-file, multi-select doesn't
// make sense here.
const jobId = [...selectedJobIds][0];
const job = _jobIndexById.get(jobId);
const modal = document.getElementById('jobLogModal');
const titleEl = document.getElementById('jobLogTitle');
const bodyEl = document.getElementById('jobLogBody');
if (!modal || !titleEl || !bodyEl) return;
titleEl.textContent = job && job.fileName ? `Log · ${job.fileName}` : 'Upload-Log';
bodyEl.textContent = 'Lade…';
modal.style.display = 'flex';
let entries = [];
try { entries = await window.api.getJobLog(jobId); } catch {}
if (!Array.isArray(entries) || entries.length === 0) {
bodyEl.textContent = 'Keine Log-Einträge für diesen Job (entweder noch nichts passiert oder aus vorherigem Batch und schon geräumt).';
return;
}
const fmt = (e) => {
const t = new Date(e.ts || Date.now()).toLocaleTimeString('de-DE', { hour12: false }) + '.' +
String((e.ts || 0) % 1000).padStart(3, '0');
if (e.kind === 'progress') {
const attempt = e.attempt ? ` (${e.attempt}/${e.maxAttempts || '?'})` : '';
const acc = e.accountId ? ` acc=${e.accountId.slice(0, 32)}` : '';
const err = e.error ? `\n${e.error}` : '';
return `[${t}] status=${e.status}${attempt}${acc}${err}`;
}
// rot-log
const rest = Object.entries(e)
.filter(([k]) => !['ts', 'kind', 'event', 'jobId'].includes(k))
.map(([k, v]) => `${k}=${typeof v === 'string' ? v : JSON.stringify(v)}`)
.join(' ');
return `[${t}] [${e.event}] ${rest}`;
};
bodyEl.textContent = entries.map(fmt).join('\n');
}
function hideJobLogModal() {
const m = document.getElementById('jobLogModal');
if (m) m.style.display = 'none';
}
async function copyJobLogToClipboard() {
const body = document.getElementById('jobLogBody');
if (!body || !body.textContent) return;
try { await window.api.copyToClipboard(body.textContent); showCopyToast('Log in Zwischenablage'); } catch {}
}
// --- Retry --- // --- Retry ---
async function retrySelectedJobs() { async function retrySelectedJobs() {
const retryJobs = []; const retryJobs = [];
@ -3715,6 +3789,14 @@ function setupListeners() {
document.getElementById('deleteAccountModal').addEventListener('click', (e) => { document.getElementById('deleteAccountModal').addEventListener('click', (e) => {
if (e.target.id === 'deleteAccountModal') closeDeleteModal(); if (e.target.id === 'deleteAccountModal') closeDeleteModal();
}); });
// Job log modal
document.getElementById('closeJobLogBtn')?.addEventListener('click', hideJobLogModal);
document.getElementById('closeJobLogBtn2')?.addEventListener('click', hideJobLogModal);
document.getElementById('copyJobLogBtn')?.addEventListener('click', copyJobLogToClipboard);
document.getElementById('jobLogModal')?.addEventListener('click', (e) => {
if (e.target.id === 'jobLogModal') hideJobLogModal();
});
} }
// --- Update UI --- // --- Update UI ---

View File

@ -197,6 +197,22 @@
</div> </div>
</div> </div>
<div class="modal-overlay" id="jobLogModal" style="display:none">
<div class="modal-card" style="width:min(820px,96%);max-height:80vh;display:flex;flex-direction:column">
<div class="modal-header">
<div><h3 id="jobLogTitle">Upload-Log</h3></div>
<button class="icon-btn" id="closeJobLogBtn" aria-label="Schließen">&times;</button>
</div>
<div class="modal-body" style="flex:1 1 auto;overflow:auto">
<pre id="jobLogBody" style="white-space:pre-wrap;font-family:ui-monospace,Consolas,Menlo,monospace;font-size:12px;line-height:1.4;margin:0">Keine Einträge.</pre>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="copyJobLogBtn">In Zwischenablage</button>
<button class="btn btn-primary" id="closeJobLogBtn2">Schließen</button>
</div>
</div>
</div>
<div class="modal-overlay" id="deleteAccountModal" style="display:none"> <div class="modal-overlay" id="deleteAccountModal" style="display:none">
<div class="modal-card" style="width:min(400px,100%)"> <div class="modal-card" style="width:min(400px,100%)">
<div class="modal-header"> <div class="modal-header">
@ -241,6 +257,7 @@
<div class="context-menu" id="contextMenu" style="display:none"> <div class="context-menu" id="contextMenu" style="display:none">
<div class="ctx-item" data-action="start-selected">Ausgewählte starten</div> <div class="ctx-item" data-action="start-selected">Ausgewählte starten</div>
<div class="ctx-item" data-action="retry-selected">Erneut versuchen</div> <div class="ctx-item" data-action="retry-selected">Erneut versuchen</div>
<div class="ctx-item" data-action="show-log">Log anzeigen</div>
<div class="ctx-separator"></div> <div class="ctx-separator"></div>
<div class="ctx-item" data-action="copy-links">Links kopieren</div> <div class="ctx-item" data-action="copy-links">Links kopieren</div>
<div class="ctx-item" data-action="copy-all-links">Alle Links kopieren</div> <div class="ctx-item" data-action="copy-all-links">Alle Links kopieren</div>