// 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 deleteAllBtn = document.getElementById('dash-delete-all-btn'); function fetchJson(url, options) { return fetch(url, options).then(response => response.json().then(data => { if (!response.ok) { throw new Error(data.message || data.status || `HTTP ${response.status}`); } return data; })); } function formatBytes(bytes) { if (bytes === 0) { return '0 B'; } const units = ['B', 'KB', 'MB', 'GB', 'TB']; const unit = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1); return `${(bytes / (1024 ** unit)).toFixed(unit === 0 ? 0 : 1)} ${units[unit]}`; } function createCell(text) { const cell = document.createElement('td'); cell.textContent = text; return cell; } 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.appendChild(createCell(file.path)); row.appendChild(createCell(formatBytes(file.size))); row.appendChild(createCell(new Date(file.modified_at * 1000).toLocaleString())); const actions = document.createElement('td'); actions.className = 'data-list-actions'; 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), ${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; } 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; }) .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 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.'; importBtn.disabled = 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(() => { importBtn.disabled = false; }); } scanBtn.addEventListener('click', scanForDashcam); importBtn.addEventListener('click', importDashcam); filesRefreshBtn.addEventListener('click', refreshDashcamFiles); fileListBody.addEventListener('click', event => { const deleteButton = event.target.closest('[data-delete-path]'); if (deleteButton) { deleteButton.disabled = true; deleteDashcamFile(deleteButton.dataset.deletePath).finally(() => { deleteButton.disabled = false; }); } }); 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 --- const uploadStatusEl = document.getElementById('upload-status'); const dashcamToggle = document.getElementById('dashcam-upload-toggle'); function describeUploadStatus(status) { if (!status.configured) { return 'Remote server not configured on this device (see db/upload_creds.env).'; } let text = `Pending trip-log rows to upload: ${status.pending_tracking_rows}.`; if (status.last_success_at) { text += ` Last successful upload: ${status.last_success_at}`; if (status.last_network_path) { text += ` (via ${status.last_network_path === 'cellular' ? 'cellular fallback' : 'WiFi/Ethernet'})`; } text += '.'; } if (status.cellular_fallback_configured) { text += ' Cellular (SIM868) fallback is enabled for trip logs if WiFi/Ethernet is unreachable.'; } if (status.last_error) { text += ` Last error: ${status.last_error}.`; } return text; } function refreshUploadStatus() { fetch('/api/upload/status') .then(response => response.json()) .then(status => { uploadStatusEl.textContent = describeUploadStatus(status); dashcamToggle.checked = status.dashcam_upload_enabled; dashcamToggle.disabled = !status.configured; }) .catch(err => { uploadStatusEl.textContent = 'Error while checking upload status: ' + err; }); } dashcamToggle.addEventListener('change', () => { fetch('/api/upload/dashcam_toggle', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: dashcamToggle.checked }), }) .then(response => response.json()) .then(status => { uploadStatusEl.textContent = describeUploadStatus(status); dashcamToggle.checked = status.dashcam_upload_enabled; }) .catch(err => { uploadStatusEl.textContent = 'Error while updating toggle: ' + err; }); }); refreshUploadStatus(); setInterval(refreshUploadStatus, 15000);