feat: add import preflight summaries

Inspect every manual file, folder, and drag-and-drop import before queue admission. Report exact duplicate, filename-filter, filesystem, destination, configured size-limit, and resulting job counts in the host selection dialog with live bilingual updates.
This commit is contained in:
Sucukdeluxe
2026-08-16 21:31:56 +02:00
parent 16f6f93a72
commit 88a3ee0937
9 changed files with 475 additions and 78 deletions
+112 -73
View File
@@ -673,7 +673,7 @@ async function init() {
}
} else {
// No pre-selected hosters: open modal
addPathsToQueue(files);
await addPathsToQueue(files);
}
});
@@ -1226,6 +1226,7 @@ function renderHosterModal() {
if (available.length === 0) {
list.innerHTML = '';
hint.textContent = 'Keine Hoster mit Zugangsdaten vorhanden. Bitte zuerst in den Accounts einen Login oder API-Key hinterlegen.';
renderImportPlanSummary();
return;
}
@@ -1257,8 +1258,11 @@ function renderHosterModal() {
list.querySelectorAll('input[data-hoster-modal]').forEach(input => {
input.addEventListener('change', () => {
input.closest('.hoster-option')?.classList.toggle('selected', input.checked);
renderImportPlanSummary();
});
});
renderImportPlanSummary();
}
function openHosterModal() {
@@ -1266,9 +1270,7 @@ function openHosterModal() {
renderHosterModal();
const description = document.getElementById('hosterModalDescription');
if (description) {
description.textContent = _pendingImportSummary
? formatFilenameFilterResult(_pendingImportSummary)
: localizeUiText('Dateien wurden hinzugefügt. Wähle jetzt die Hoster für den Upload.');
description.textContent = localizeUiText('Vorabprüfung abgeschlossen. Wähle jetzt die Hoster für den Upload.');
}
modalController.open('hosterModal', {
initialFocus: '#cancelHosterModalBtn',
@@ -1308,12 +1310,12 @@ async function applyHosterSelection() {
updateUploadView();
persistQueueStateSoon(true); // immediate persist after adding files
closeHosterModal();
_pendingImportSummary = null;
_pendingImportInspection = null;
}
function cancelHosterModal() {
_pendingFiles = [];
_pendingImportSummary = null;
_pendingImportInspection = null;
closeHosterModal();
}
@@ -1550,7 +1552,8 @@ function setupDragDrop() {
}
let _pendingFiles = []; // Files waiting for hoster modal confirmation
let _pendingImportSummary = null;
let _pendingImportInspection = null;
let _importCoordination = Promise.resolve();
let _addingDropped = false;
@@ -1558,19 +1561,6 @@ function admitFilenameFilter(files) {
return window.FilenameFilter.applyFilenameFilter(files, config?.globalSettings?.filenameFilter);
}
function mergePendingImportSummary(result) {
if (!result.active) {
if (!_pendingImportSummary) _pendingImportSummary = null;
return;
}
const current = _pendingImportSummary || { total: 0, accepted: 0, excluded: 0 };
_pendingImportSummary = {
total: current.total + result.total,
accepted: current.accepted + result.accepted.length,
excluded: current.excluded + result.excluded.length
};
}
function formatFilenameFilterResult(result) {
const accepted = Array.isArray(result.accepted) ? result.accepted.length : Number(result.accepted) || 0;
const excluded = Array.isArray(result.excluded) ? result.excluded.length : Number(result.excluded) || 0;
@@ -1581,6 +1571,99 @@ function showFilenameFilterResult(result) {
showCopyToast(formatFilenameFilterResult(result), 6500);
}
function mergePendingImportInspection(result) {
const current = _pendingImportInspection || {
candidateCount: 0,
duplicateCount: 0,
filteredCount: 0,
unavailableCount: 0,
accepted: []
};
_pendingImportInspection = {
candidateCount: current.candidateCount + result.candidateCount,
duplicateCount: current.duplicateCount + result.duplicateCount,
filteredCount: current.filteredCount + result.filteredCount,
unavailableCount: current.unavailableCount + result.unavailableCount,
accepted: current.accepted.concat(result.accepted)
};
}
function getImportPlanHosters() {
const inputs = Array.from(document.querySelectorAll('input[data-hoster-modal]:checked'));
return inputs.length > 0 || document.getElementById('hosterModal')?.style.display === 'flex'
? inputs.map(input => input.dataset.hosterModal)
: getSelectedHosters();
}
function renderImportPlanSummary() {
if (!_pendingImportInspection || !window.ImportPreflight) return;
const summary = window.ImportPreflight.summarizeImportPlan({
inspection: _pendingImportInspection,
selectedHosters: getImportPlanHosters(),
hosterSettings
});
const values = {
importPlanCandidates: summary.candidateCount,
importPlanDuplicates: summary.duplicateCount,
importPlanFiltered: summary.filteredCount,
importPlanUnavailable: summary.unavailableCount,
importPlanAccepted: summary.acceptedCount,
importPlanTargets: summary.targetCount,
importPlanJobs: summary.jobCount,
importPlanSizeLimited: summary.sizeLimitedJobCount
};
for (const [id, value] of Object.entries(values)) {
const element = document.getElementById(id);
if (element) element.textContent = String(value);
}
}
function existingImportPaths() {
return [...selectedFiles.map(file => file.path), ..._pendingFiles.map(file => file.path), ...queueJobs.map(job => job.file)];
}
function coordinateImportEntries(entries) {
const run = async () => {
const candidates = Array.isArray(entries) ? entries : [];
if (candidates.length === 0) return null;
let inspection;
try {
inspection = await window.api.inspectImportFiles(candidates, existingImportPaths());
} catch {
showCopyToast('Vorabprüfung fehlgeschlagen.', 6500);
return null;
}
mergePendingImportInspection(inspection);
if (inspection.accepted.length > 0) {
if (document.getElementById('hosterModal')?.style.display === 'flex') {
selectedUploadHosters = Array.from(document.querySelectorAll('input[data-hoster-modal]:checked'))
.map(input => input.dataset.hosterModal);
}
const acceptedPaths = new Set(inspection.accepted.map(file => file.path));
clearDedupKeysForPaths(acceptedPaths);
_pendingFiles.push(...inspection.accepted);
if (document.getElementById('hosterModal')?.style.display === 'flex') {
syncSelectedUploadHosters();
renderHosterModal();
} else {
openHosterModal();
}
} else if (_pendingFiles.length > 0) {
renderImportPlanSummary();
} else if (inspection.candidateCount > 0 && inspection.duplicateCount === inspection.candidateCount) {
showCopyToast('Auswahl ist bereits in den Upload-Aufträgen.');
_pendingImportInspection = null;
} else {
showCopyToast('Keine Dateien wurden akzeptiert.', 6500);
_pendingImportInspection = null;
}
return inspection;
};
const pending = _importCoordination.then(run, run);
_importCoordination = pending.catch(() => {});
return pending;
}
async function addDropTargetEntries(entries) {
const files = [];
for (const entry of Array.isArray(entries) ? entries : []) {
@@ -1595,7 +1678,7 @@ async function addDropTargetEntries(entries) {
}
files.push(entry);
}
addPathsToQueue(files);
await addPathsToQueue(files);
}
async function addDroppedFiles(fileList) {
@@ -1631,7 +1714,7 @@ async function addDroppedFiles(fileList) {
const fileName = file.name || '';
entries.push({ path: filePath, name: fileName, size: file.size });
}
addPathsToQueue(entries);
await addPathsToQueue(entries);
} finally {
_addingDropped = false;
}
@@ -1640,65 +1723,19 @@ async function addDroppedFiles(fileList) {
async function pickFiles() {
const paths = await window.api.selectFiles();
if (!paths) return;
addPathsToQueue(paths);
await addPathsToQueue(paths);
}
async function pickFolder() {
const richFiles = window.api.selectFolderWithSizes ? await window.api.selectFolderWithSizes() : null;
if (richFiles && Array.isArray(richFiles)) { addPathsToQueue(richFiles); return; }
if (richFiles && Array.isArray(richFiles)) { await addPathsToQueue(richFiles); return; }
const paths = await window.api.selectFolder();
if (!paths) return;
addPathsToQueue(paths);
await addPathsToQueue(paths);
}
function addPathsToQueue(paths) {
const existing = new Set();
for (const f of selectedFiles) existing.add(f.path);
for (const f of _pendingFiles) existing.add(f.path);
const newFiles = [];
const pendingSizeFetch = [];
for (const entry of paths) {
const p = typeof entry === 'string' ? entry : (entry && entry.path);
if (!p || existing.has(p)) continue;
existing.add(p);
const name = typeof entry === 'string' ? p.split('\\').pop().split('/').pop() : (entry.name || p.split('\\').pop().split('/').pop());
const size = typeof entry === 'string' ? null : (entry.size || 0);
newFiles.push({ path: p, name, size });
if (size === null || size === undefined || size === 0) pendingSizeFetch.push(p);
}
const admitted = admitFilenameFilter(newFiles);
if (admitted.accepted.length > 0) {
const acceptedPaths = new Set(admitted.accepted.map(file => file.path));
const acceptedSizeFetch = pendingSizeFetch.filter(filePath => acceptedPaths.has(filePath));
_pendingFiles.push(...admitted.accepted);
mergePendingImportSummary(admitted);
openHosterModal();
if (acceptedSizeFetch.length > 0 && window.api.getFileSizes) {
window.api.getFileSizes(acceptedSizeFetch).then((sizeMap) => {
if (!sizeMap || typeof sizeMap !== 'object') return;
let changed = false;
for (const f of _pendingFiles) {
if (sizeMap[f.path] && (!f.size || f.size === 0)) { f.size = sizeMap[f.path]; changed = true; }
}
for (const f of selectedFiles) {
if (sizeMap[f.path] && (!f.size || f.size === 0)) { f.size = sizeMap[f.path]; changed = true; }
}
for (const j of queueJobs) {
if (sizeMap[j.file] && (!j.bytesTotal || j.bytesTotal === 0)) { j.bytesTotal = sizeMap[j.file]; changed = true; }
}
if (changed) {
_queueStatsCache = null;
if (typeof renderQueueTable === 'function') renderQueueTable();
if (typeof updateStatusBar === 'function') updateStatusBar();
}
}).catch(() => {});
}
} else if (admitted.active && admitted.total > 0) {
showFilenameFilterResult(admitted);
} else if (Array.isArray(paths) && paths.length > 0) {
showCopyToast('Auswahl ist bereits in den Upload-Aufträgen.');
}
async function addPathsToQueue(paths) {
return coordinateImportEntries(paths);
}
function updateUploadView() {
@@ -7500,12 +7537,14 @@ function setupListeners() {
input.checked = true;
input.closest('.hoster-option')?.classList.add('selected');
});
renderImportPlanSummary();
});
document.getElementById('clearHostersBtn').addEventListener('click', () => {
document.querySelectorAll('input[data-hoster-modal]').forEach(input => {
input.checked = false;
input.closest('.hoster-option')?.classList.remove('selected');
});
renderImportPlanSummary();
});
document.getElementById('saveSettingsBtn').addEventListener('click', saveSettings);
+12
View File
@@ -339,6 +339,18 @@
['Ausgewählte starten', 'Start selected'],
['Upload-Ziele auswählen', 'Choose upload destinations'],
['Dateien wurden hinzugefügt. Wähle jetzt die Hoster für den Upload.', 'Files were added. Now choose the hosts for the upload.'],
['Vorabprüfung abgeschlossen. Wähle jetzt die Hoster für den Upload.', 'Preflight complete. Now choose the hosts for the upload.'],
['Import-Vorabprüfung', 'Import preflight'],
['Kandidaten', 'Candidates'],
['Bereits vorhanden / dupliziert', 'Already present / duplicated'],
['Durch Dateinamenfilter ausgeschlossen', 'Excluded by filename filter'],
['Fehlend / unlesbar / leer', 'Missing / unreadable / empty'],
['Akzeptierte Dateien', 'Accepted files'],
['Ausgewählte Ziele', 'Selected destinations'],
['Entstehende Jobs', 'Resulting jobs'],
['Durch konfigurierte Größenlimits entfallene Jobs', 'Jobs omitted by configured size limits'],
['Keine Dateien wurden akzeptiert.', 'No files were accepted.'],
['Vorabprüfung fehlgeschlagen.', 'Preflight failed.'],
['Alle', 'All'],
['Keine', 'None'],
['Keine Hoster mit Zugangsdaten vorhanden. Bitte zuerst in den Accounts einen Login oder API-Key hinterlegen.', 'No hosts with credentials are available. Add a login or API key under Accounts first.'],
+11
View File
@@ -663,6 +663,16 @@
<button class="btn btn-xs btn-secondary" id="clearHostersBtn">Keine</button>
</div>
<div class="hoster-modal-list" id="hosterModalList"></div>
<dl class="import-plan-summary" id="importPlanSummary" aria-label="Import-Vorabprüfung">
<div><dt>Kandidaten</dt><dd id="importPlanCandidates">0</dd></div>
<div><dt>Bereits vorhanden / dupliziert</dt><dd id="importPlanDuplicates">0</dd></div>
<div><dt>Durch Dateinamenfilter ausgeschlossen</dt><dd id="importPlanFiltered">0</dd></div>
<div><dt>Fehlend / unlesbar / leer</dt><dd id="importPlanUnavailable">0</dd></div>
<div><dt>Akzeptierte Dateien</dt><dd id="importPlanAccepted">0</dd></div>
<div><dt>Ausgewählte Ziele</dt><dd id="importPlanTargets">0</dd></div>
<div><dt>Entstehende Jobs</dt><dd id="importPlanJobs">0</dd></div>
<div><dt>Durch konfigurierte Größenlimits entfallene Jobs</dt><dd id="importPlanSizeLimited">0</dd></div>
</dl>
<p class="modal-hint" id="hosterModalHint"></p>
</div>
<div class="modal-footer">
@@ -684,6 +694,7 @@
<script src="../lib/speed-history.js"></script>
<script src="../lib/upload-recovery.js"></script>
<script src="../lib/filename-filter.js"></script>
<script src="../lib/import-preflight.js"></script>
<script src="account-submit.js"></script>
<script src="account-status.js"></script>
<script src="history-status.js"></script>
+30
View File
@@ -739,6 +739,36 @@ body.col-resizing, body.col-resizing * { cursor: col-resize !important; user-sel
display: grid;
gap: 10px;
}
.import-plan-summary {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
margin: 12px 0 0;
}
.import-plan-summary > div {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 8px;
min-height: 38px;
padding: 8px 10px;
border: 1px solid var(--border);
border-radius: 8px;
background: rgba(255, 255, 255, 0.025);
}
.import-plan-summary dt {
min-width: 0;
font-size: 11px;
line-height: 1.3;
color: var(--text-muted);
}
.import-plan-summary dd {
margin: 0;
font-size: 15px;
font-weight: 700;
color: var(--text);
font-variant-numeric: tabular-nums;
}
.hoster-option {
display: flex;
align-items: center;