Files
yaris-obd/server/templates/dashcam.html
Mathieu MoranandCopilot 7aa9ffda16 Add delete with confirmation for trips and dashcam
- Confirmation modal in server base.html (window.confirmDelete promise helper)
- Trip delete: trash button in Replay column (server-only via data-deletable on
  trip-list div), DELETE /api/v1/portal/trip/<id>, removes computed trip only
  (raw tracking_points kept)
- Dashcam delete: trash button next to download, DELETE /api/v1/portal/dashcam/<id>,
  removes from DB + disk (best-effort file removal)
- history.js: delete button gated on data-deletable='1' attribute; click handler
  intercepts before replay logic
- db.py: delete_trip(), delete_dashcam_file() helpers

Co-authored-by: Copilot <[email protected]>
2026-08-10 04:40:22 -04:00

184 lines
8.0 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Dashcam archive – Yaris OBD Remote{% endblock %}
{% block content %}
<h3 class="text-dark mb-3 fw-bold">Dashcam archive</h3>
<div class="row g-3">
{# ── File list ────────────────────────────────────────────────────── #}
<div class="col-12 col-xl-5">
<div class="card shadow-sm">
<div class="card-header py-2 d-flex justify-content-between align-items-center">
<h6 class="text-primary fw-bold m-0">Uploaded clips</h6>
<span id="file-count" class="badge bg-secondary small">…</span>
</div>
<div class="card-body p-0">
<div id="files-loading" class="text-center text-muted py-4">
<span class="spinner-border spinner-border-sm me-2" aria-hidden="true"></span>
Loading…
</div>
<div id="files-error" class="alert alert-danger m-3" role="alert" hidden></div>
<div class="table-responsive">
<table class="table table-hover mb-0 small" id="files-table" hidden>
<thead class="table-light">
<tr>
<th>File</th>
<th class="text-nowrap">Size</th>
<th class="text-nowrap d-none d-sm-table-cell">Uploaded</th>
<th></th>
</tr>
</thead>
<tbody id="files-body"></tbody>
</table>
</div>
<div id="files-empty" class="text-muted text-center py-4 small" hidden>
<i class="fas fa-inbox fa-2x mb-2 d-block text-muted"></i>
No dashcam files uploaded yet.
</div>
</div>
</div>
</div>
{# ── Video player ─────────────────────────────────────────────────── #}
<div class="col-12 col-xl-7">
<div class="card shadow-sm">
<div class="card-header py-2 d-flex justify-content-between align-items-center">
<h6 class="text-primary fw-bold m-0">Preview</h6>
<a id="btn-download" class="btn btn-outline-secondary btn-sm" href="#" download hidden>
<i class="fas fa-download fa-fw me-1"></i> Download
</a>
</div>
<div class="card-body p-0">
<div id="player-placeholder" class="text-muted text-center py-5 small">
<i class="fas fa-film fa-2x mb-2 d-block"></i>
Select a clip to preview
</div>
<video id="video-player" class="w-100" controls preload="metadata" hidden
style="background:#000;max-height:420px;"></video>
<div id="player-meta" class="px-3 py-2 border-top small text-muted" hidden>
<span id="player-filename"></span>
&nbsp;·&nbsp;
<span id="player-size"></span>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function fmtSize(b) {
if (b >= 1e9) return (b / 1e9).toFixed(2) + ' GB';
if (b >= 1e6) return (b / 1e6).toFixed(1) + ' MB';
return (b / 1e3).toFixed(0) + ' KB';
}
function fmtDate(iso) {
return new Date(iso).toLocaleString('fr-CA', {
year: 'numeric', month: 'short', day: 'numeric',
hour: '2-digit', minute: '2-digit'
});
}
let currentFileId = null;
function playFile(id, path, sizeBytes) {
const player = document.getElementById('video-player');
const placeholder = document.getElementById('player-placeholder');
const meta = document.getElementById('player-meta');
const btnDownload = document.getElementById('btn-download');
const name = path.split('/').pop();
currentFileId = id;
const streamUrl = `/api/v1/portal/dashcam/stream/${id}`;
player.src = streamUrl;
player.hidden = false;
placeholder.hidden = true;
meta.hidden = false;
document.getElementById('player-filename').textContent = name;
document.getElementById('player-size').textContent = fmtSize(sizeBytes);
btnDownload.href = streamUrl;
btnDownload.download = name;
btnDownload.hidden = false;
player.play().catch(() => {});
// Highlight selected row
document.querySelectorAll('#files-body tr').forEach(r => r.classList.remove('table-active'));
const row = document.querySelector(`#files-body tr[data-file-id="${id}"]`);
if (row) row.classList.add('table-active');
}
async function loadFiles() {
const loading = document.getElementById('files-loading');
const table = document.getElementById('files-table');
const empty = document.getElementById('files-empty');
const errEl = document.getElementById('files-error');
const countEl = document.getElementById('file-count');
try {
const res = await fetch('/api/v1/portal/dashcam');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const files = await res.json();
loading.hidden = true;
countEl.textContent = files.length;
if (!files.length) { empty.hidden = false; return; }
const tbody = document.getElementById('files-body');
tbody.innerHTML = files.map(f => {
const name = f.relative_path.split('/').pop();
const isVid = /\.(mp4|mov|avi|mkv|ts|h264)$/i.test(name);
return `
<tr data-file-id="${f.id}" onclick="playFile(${f.id}, '${f.relative_path.replace(/'/g,"\\'")}', ${f.size_bytes})"
style="cursor:pointer;">
<td class="text-truncate" style="max-width:200px;" title="${f.relative_path}">${name}</td>
<td class="text-nowrap">${fmtSize(f.size_bytes)}</td>
<td class="text-nowrap d-none d-sm-table-cell">${fmtDate(f.uploaded_at)}</td>
<td class="text-end text-nowrap">
<a href="/api/v1/portal/dashcam/stream/${f.id}" download="${name}"
class="btn btn-outline-secondary btn-sm py-0 px-1" title="Download"
onclick="event.stopPropagation()">
<i class="fas fa-download fa-fw"></i>
</a>
<button class="btn btn-outline-danger btn-sm py-0 px-1 ms-1" title="Delete"
onclick="event.stopPropagation(); deleteFile(${f.id}, '${name.replace(/'/g,"\\'")}')">
<i class="fas fa-trash fa-fw"></i>
</button>
</td>
</tr>
`;
}).join('');
table.hidden = false;
} catch (e) {
loading.hidden = true;
errEl.textContent = 'Failed to load files: ' + e.message;
errEl.hidden = false;
}
}
loadFiles();
async function deleteFile(id, name) {
const confirmed = await window.confirmDelete(`Delete "${name}"? This removes the file from disk permanently.`);
if (!confirmed) return;
const res = await fetch(`/api/v1/portal/dashcam/${id}`, { method: 'DELETE' });
if (!res.ok) { alert('Delete failed.'); return; }
// Clear player if this file was playing
const player = document.getElementById('video-player');
if (currentFileId === id) {
player.pause();
player.src = '';
player.hidden = true;
document.getElementById('player-placeholder').hidden = false;
document.getElementById('player-meta').hidden = true;
document.getElementById('btn-download').hidden = true;
currentFileId = null;
}
// Remove row
const row = document.querySelector(`#files-body tr[data-file-id="${id}"]`);
if (row) row.remove();
const countEl = document.getElementById('file-count');
countEl.textContent = parseInt(countEl.textContent) - 1;
}
</script>
{% endblock %}