// Dashcam page: scan for a USB dashcam, then trigger importing its videos. // Uses plain fetch() (not socket.io / yaris.js) since this is a one-shot // request/response flow, not a live data stream, and yaris.js assumes DOM // elements (#map, #speedGraph) that don't exist on this page. const deviceStatus = document.getElementById('dash-device-status'); const importStatusBar = document.getElementById('dash-import-status-bar'); const importStatus = document.getElementById('dash-import-status'); const importEta = document.getElementById('dash-import-eta'); const importResultBar = document.getElementById('dash-import-result-bar'); const importResult = document.getElementById('dash-import-result'); const scanBtn = document.getElementById('dash-scan-btn'); const importBtn = document.getElementById('dash-import-btn'); const fileSummary = document.getElementById('dash-file-summary'); const fileListBody = document.querySelector('#dashcam-file-list [data-data-list-body]'); const filesRefreshBtn = document.getElementById('dash-files-refresh-btn'); const selectAllBtn = document.getElementById('dash-select-all-btn'); const importSelectedBtn = document.getElementById('dash-import-selected-btn'); const deleteAllBtn = document.getElementById('dash-delete-all-btn'); let importInProgress = false; let importStartTime = null; let importTotal = 0; let importEtaInterval = null; function showImportProgress(msg, total) { importTotal = total || 0; importStartTime = Date.now(); importStatus.textContent = msg; importEta.textContent = ''; importStatusBar.hidden = false; importResultBar.hidden = true; // Update ETA every second based on elapsed time vs total clearInterval(importEtaInterval); if (importTotal > 0) { importEtaInterval = setInterval(() => { const elapsed = (Date.now() - importStartTime) / 1000; // rough heuristic: ~3s per video on average const estTotal = importTotal * 3; const remaining = Math.max(0, Math.round(estTotal - elapsed)); importEta.textContent = remaining > 0 ? `~${remaining}s remaining` : ''; }, 1000); } } function showImportResult(msg) { clearInterval(importEtaInterval); importStatusBar.hidden = true; importResult.textContent = msg; importResultBar.hidden = false; // Auto-hide result after 10s setTimeout(() => { importResultBar.hidden = true; }, 10000); } function fetchJson(url, options) { return fetch(url, options).then(response => response.json().then(data => { if (!response.ok) { const details = data.errors && data.errors.length ? data.errors.join('; ') : null; throw new Error(data.message || details || data.status || `HTTP ${response.status}`); } return data; })); } function createCell(text) { const cell = document.createElement('td'); cell.textContent = text; return cell; } function selectedPaths() { return Array.from(fileListBody.querySelectorAll('[data-select-path]:checked')) .map(checkbox => checkbox.dataset.selectPath); } function updateSelectionControls() { const checkboxes = Array.from(fileListBody.querySelectorAll('[data-select-path]')); const selectedCount = selectedPaths().length; importSelectedBtn.disabled = importInProgress || selectedCount === 0; selectAllBtn.disabled = checkboxes.length === 0; selectAllBtn.textContent = checkboxes.length > 0 && selectedCount === checkboxes.length ? 'Clear selection' : 'Select all'; importSelectedBtn.innerHTML = selectedCount ? `Import selected (${selectedCount})` : 'Import selected'; } function setImportBusy(busy) { importInProgress = busy; importBtn.disabled = busy; fileListBody.querySelectorAll('[data-import-path]').forEach(button => { button.disabled = busy; }); updateSelectionControls(); } function renderDashcamFiles(summary) { fileListBody.replaceChildren(); if (summary.status !== 'ok') { fileSummary.textContent = summary.message || `Unable to list files: ${summary.status}`; deleteAllBtn.disabled = true; return; } const files = summary.files || []; files.forEach(file => { const row = document.createElement('tr'); row.dataset.path = file.path; row.dataset.size = file.size; row.dataset.modified = file.modified_at; row.className = 'dash-row'; const selection = document.createElement('td'); selection.className = 'dash-col-check'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.className = 'form-check-input'; checkbox.dataset.selectPath = file.path; checkbox.setAttribute('aria-label', `Select ${file.path}`); selection.appendChild(checkbox); row.appendChild(selection); const pathCell = createCell(file.path); pathCell.className = 'dash-col-file'; row.appendChild(pathCell); const sizeCell = createCell(YarisUI.formatBytes(file.size)); sizeCell.className = 'dash-col-size'; row.appendChild(sizeCell); const dateCell = createCell(new Date(file.modified_at * 1000).toLocaleString()); dateCell.className = 'dash-col-date'; row.appendChild(dateCell); const actionsTd = document.createElement('td'); actionsTd.className = 'dash-actions-td'; const actionsDiv = document.createElement('div'); actionsDiv.className = 'data-list-actions'; const importButton = document.createElement('button'); importButton.type = 'button'; importButton.className = 'btn btn-sm btn-outline-primary btn-icon-mobile'; importButton.dataset.importPath = file.path; importButton.innerHTML = ' Import'; actionsDiv.appendChild(importButton); const deleteButton = document.createElement('button'); deleteButton.type = 'button'; deleteButton.className = 'btn btn-sm btn-outline-danger btn-icon-mobile'; deleteButton.dataset.deletePath = file.path; deleteButton.dataset.confirm = `Permanently delete "${file.path}" from the dashcam?`; deleteButton.innerHTML = ' Delete'; actionsDiv.appendChild(deleteButton); actionsTd.appendChild(actionsDiv); row.appendChild(actionsTd); fileListBody.appendChild(row); }); fileSummary.textContent = `${files.length} video(s), ${YarisUI.formatBytes(summary.total_size || 0)} total`; if (summary.errors && summary.errors.length) { fileSummary.textContent += ` (${summary.errors.length} file error(s))`; } deleteAllBtn.disabled = files.length === 0; updateSelectionControls(); } function refreshDashcamFiles() { fileSummary.textContent = 'Scanning...'; filesRefreshBtn.disabled = true; return fetchJson('/api/dash/files') .then(renderDashcamFiles) .catch(err => { fileListBody.replaceChildren(); fileSummary.textContent = `Unable to list dashcam files: ${err.message}`; deleteAllBtn.disabled = true; updateSelectionControls(); }) .finally(() => { filesRefreshBtn.disabled = false; }); } function deleteDashcamFile(path) { return fetchJson('/api/dash/file', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path }), }).then(result => { fileSummary.textContent = `Deleted ${result.deleted_count} video(s). Refreshing...`; return refreshDashcamFiles(); }).catch(err => { fileSummary.textContent = `Delete failed: ${err.message}`; }); } function describeImportResult(summary) { let text = `Selected ${summary.found} video(s), copied ${summary.copied}, skipped ${summary.skipped} (already imported).`; if (summary.errors && summary.errors.length) { text += ` Errors: ${summary.errors.join('; ')}`; } return text; } function importDashcamFiles(paths) { showImportProgress(`Importing ${paths.length} video(s)…`, paths.length); setImportBusy(true); return fetchJson('/api/dash/import', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ paths }), }).then(summary => { showImportResult(describeImportResult(summary)); paths.forEach(path => { const checkbox = Array.from( fileListBody.querySelectorAll('[data-select-path]') ).find(candidate => candidate.dataset.selectPath === path); if (checkbox) checkbox.checked = false; }); return summary; }).catch(err => { showImportResult(`Selected import failed: ${err.message}`); return null; }).finally(() => setImportBusy(false)); } function describeDevice(device) { const label = device.label ? ` "${device.label}"` : ''; return `${device.path}${label} (${device.size}, ${device.fstype || 'unknown fs'})`; } function scanForDashcam() { deviceStatus.textContent = 'Scanning...'; importBtn.disabled = true; fetch('/api/dash/devices') .then(response => response.json()) .then(data => { const devices = data.devices || []; if (devices.length === 0) { deviceStatus.textContent = 'No USB dashcam/storage device detected. Plug it in and scan again.'; return; } deviceStatus.textContent = 'Detected: ' + devices.map(describeDevice).join(', '); importBtn.disabled = false; }) .catch(err => { deviceStatus.textContent = 'Error while scanning: ' + err; }); } function importDashcam() { showImportProgress('Importing all videos… this can take a while.', 0); setImportBusy(true); fetch('/api/dash/import', { method: 'POST' }) .then(response => response.json()) .then(summary => { if (summary.status !== 'ok') { showImportResult(summary.message || ('Import failed: ' + summary.status)); return; } let text = `Found ${summary.found} video(s), copied ${summary.copied}, skipped ${summary.skipped}.`; if (summary.errors && summary.errors.length > 0) { text += ` Errors: ${summary.errors.join('; ')}`; } showImportResult(text); refreshDashcamFiles(); }) .catch(err => { showImportResult('Error while importing: ' + err); }) .finally(() => setImportBusy(false)); } scanBtn.addEventListener('click', scanForDashcam); importBtn.addEventListener('click', importDashcam); filesRefreshBtn.addEventListener('click', refreshDashcamFiles); fileListBody.addEventListener('click', event => { const importButton = event.target.closest('[data-import-path]'); if (importButton) { importDashcamFiles([importButton.dataset.importPath]); return; } const deleteButton = event.target.closest('[data-delete-path]'); if (deleteButton) { deleteButton.disabled = true; deleteDashcamFile(deleteButton.dataset.deletePath).finally(() => { deleteButton.disabled = false; }); } }); fileListBody.addEventListener('change', event => { if (event.target.matches('[data-select-path]')) { updateSelectionControls(); } }); importSelectedBtn.addEventListener('click', () => { const paths = selectedPaths(); if (paths.length) { importDashcamFiles(paths); } }); selectAllBtn.addEventListener('click', () => { const checkboxes = Array.from(fileListBody.querySelectorAll('[data-select-path]')); const shouldSelect = checkboxes.some(checkbox => !checkbox.checked); checkboxes.forEach(checkbox => { checkbox.checked = shouldSelect; }); updateSelectionControls(); }); deleteAllBtn.addEventListener('click', () => { if (!window.confirm(deleteAllBtn.dataset.confirm)) { return; } deleteAllBtn.disabled = true; fetchJson('/api/dash/files', { method: 'DELETE' }) .then(result => { fileSummary.textContent = `Deleted ${result.deleted_count} video(s). Refreshing...`; return refreshDashcamFiles(); }) .catch(err => { fileSummary.textContent = `Delete all failed: ${err.message}`; deleteAllBtn.disabled = false; }); }); // Auto-scan once when the page loads. scanForDashcam(); refreshDashcamFiles(); // --- Remote server upload status/toggle ---