fix: selector overflow for 10+ items, drag-drop status guard, filename claim set for parallel safety

- Queue selector uses min-width instead of fixed width for double-digit numbers
- Drag-start handler validates item is still pending before allowing drag
- ensureUniqueFilename uses in-memory claim set to prevent TOCTOU race

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
xRangerDE 2026-03-21 15:15:25 +01:00
parent 6c47c63fa8
commit cf9d7b8334
3 changed files with 22 additions and 6 deletions

View File

@ -685,20 +685,26 @@ function formatDurationDashed(seconds: number): string {
return `${h.toString().padStart(2, '0')}-${m.toString().padStart(2, '0')}-${s.toString().padStart(2, '0')}`; return `${h.toString().padStart(2, '0')}-${m.toString().padStart(2, '0')}-${s.toString().padStart(2, '0')}`;
} }
const claimedFilenames = new Set<string>();
function ensureUniqueFilename(filePath: string): string { function ensureUniqueFilename(filePath: string): string {
if (!fs.existsSync(filePath)) return filePath;
const dir = path.dirname(filePath); const dir = path.dirname(filePath);
const ext = path.extname(filePath); const ext = path.extname(filePath);
const base = path.basename(filePath, ext); const base = path.basename(filePath, ext);
let counter = 1;
let candidate = filePath; let candidate = filePath;
while (fs.existsSync(candidate)) { let counter = 0;
candidate = path.join(dir, `${base}_${counter}${ext}`); while (fs.existsSync(candidate) || claimedFilenames.has(candidate)) {
counter++; counter++;
candidate = path.join(dir, `${base}_${counter}${ext}`);
} }
claimedFilenames.add(candidate);
return candidate; return candidate;
} }
function releaseClaimedFilename(filePath: string): void {
claimedFilenames.delete(filePath);
}
function sanitizeFilenamePart(input: string, fallback = 'unnamed'): string { function sanitizeFilenamePart(input: string, fallback = 'unnamed'): string {
const cleaned = (input || '') const cleaned = (input || '')
.replace(/[<>:"|?*\x00-\x1f]/g, '_') .replace(/[<>:"|?*\x00-\x1f]/g, '_')

View File

@ -216,6 +216,15 @@ function initQueueDragDrop(): void {
list.addEventListener('dragstart', (e: DragEvent) => { list.addEventListener('dragstart', (e: DragEvent) => {
const el = (e.target as HTMLElement).closest('.queue-item') as HTMLElement; const el = (e.target as HTMLElement).closest('.queue-item') as HTMLElement;
if (!el) return; if (!el) return;
// Prevent dragging items that are no longer pending (race window between status change and re-render)
const itemId = el.dataset.id;
if (itemId) {
const item = queue.find(i => i.id === itemId);
if (!item || item.status !== 'pending') {
e.preventDefault();
return;
}
}
draggedQueueItemId = el.dataset.id || null; draggedQueueItemId = el.dataset.id || null;
el.classList.add('dragging'); el.classList.add('dragging');
if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move'; if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move';

View File

@ -358,8 +358,9 @@ body {
} }
.queue-selector { .queue-selector {
width: 22px; min-width: 22px;
height: 22px; height: 22px;
padding: 0 3px;
border: 2px solid var(--text-secondary); border: 2px solid var(--text-secondary);
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
@ -367,7 +368,7 @@ body {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 12px; font-size: 11px;
font-weight: 700; font-weight: 700;
color: var(--bg-primary); color: var(--bg-primary);
user-select: none; user-select: none;