// 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 importStatus = document.getElementById('dash-import-status'); 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; 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; const selection = document.createElement('td'); 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); row.appendChild(createCell(file.path)); row.appendChild(createCell(YarisUI.formatBytes(file.size))); row.appendChild(createCell(new Date(file.modified_at * 1000).toLocaleString())); const actions = document.createElement('td'); actions.className = 'data-list-actions'; const importButton = document.createElement('button'); importButton.type = 'button'; importButton.className = 'btn btn-sm btn-outline-primary'; importButton.dataset.importPath = file.path; importButton.innerHTML = 'Import'; actions.appendChild(importButton); const deleteButton = document.createElement('button'); deleteButton.type = 'button'; deleteButton.className = 'btn btn-sm btn-outline-danger'; deleteButton.dataset.deletePath = file.path; deleteButton.dataset.confirm = `Permanently delete "${file.path}" from the dashcam?`; deleteButton.innerHTML = 'Delete'; actions.appendChild(deleteButton); row.appendChild(actions); 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) { importStatus.textContent = `Importing ${paths.length} selected video(s)...`; setImportBusy(true); return fetchJson('/api/dash/import', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ paths }), }).then(summary => { importStatus.textContent = 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 => { importStatus.textContent = `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() { importStatus.textContent = 'Importing... this can take a while for large videos.'; setImportBusy(true); fetch('/api/dash/import', { method: 'POST' }) .then(response => response.json()) .then(summary => { if (summary.status !== 'ok') { importStatus.textContent = summary.message || ('Import failed: ' + summary.status); return; } let text = `Found ${summary.found} video(s), copied ${summary.copied}, skipped ${summary.skipped} (already imported).`; if (summary.errors && summary.errors.length > 0) { text += ` Errors: ${summary.errors.join('; ')}`; } importStatus.textContent = text; refreshDashcamFiles(); }) .catch(err => { importStatus.textContent = '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 ---