// 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 ---
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);