diff --git a/backend/static/event-modal.js b/backend/static/event-modal.js
index ec210cf..162bced 100644
--- a/backend/static/event-modal.js
+++ b/backend/static/event-modal.js
@@ -245,6 +245,47 @@
`;
}
+ function _renderReview(s, eventId) {
+ const rev = s.review || {};
+ const ft = !!rev.false_trigger;
+ const reviewer = rev.reviewer || '';
+ const notes = rev.notes || '';
+ const reviewedAt = rev.reviewed_at
+ ? rev.reviewed_at.replace('T', ' ').slice(0, 19)
+ : null;
+ return `
+
+
+
+
+
+
+
+ ${reviewedAt ? `Last reviewed ${reviewedAt}` : 'Not yet reviewed.'}
+
+
+
+
`;
+ }
+
// ── Waveform / histogram chart helpers ──────────────────────────
async function _loadMicUnitPref() {
@@ -527,27 +568,47 @@
const src = s.source || {};
const sizeKb = bw.filesize ? (bw.filesize / 1024).toFixed(1) : null;
const canDownloadBinary = !!(bw.available && bw.filename && eventId);
+ const txtFilename = src && src.txt_filename;
+ const reportPdfUrl = `/api/sfm/db/events/${encodeURIComponent(eventId)}/report.pdf`;
+ const reportTxtUrl = `/api/sfm/db/events/${encodeURIComponent(eventId)}/ascii_report.txt`;
const downloadButtons = `
Sidecar JSON
@@ -660,6 +726,9 @@
${_renderDeviceMetadata(s)}
` : ''}
+ ${_sectionHeader('Review')}
+ ${_renderReview(s, eventId)}
+
${_sectionHeader('Source File')}
${_renderFileInfo(s, eventId)}
`;
@@ -704,6 +773,71 @@
if (label) label.textContent = isHidden ? 'View JSON' : 'Hide JSON';
};
+ window.toggleEventPdfPreview = function () {
+ const preview = document.getElementById('event-pdf-preview');
+ const iframe = document.getElementById('event-pdf-iframe');
+ const label = document.getElementById('event-pdf-toggle-label');
+ if (!preview || !iframe) return;
+ const isHidden = preview.classList.toggle('hidden');
+ // Lazy-load the PDF: only set the iframe src on first reveal, so
+ // closing the event modal without opening the PDF never spends
+ // bandwidth on it.
+ if (!isHidden && !iframe.src) {
+ iframe.src = iframe.dataset.pdfUrl || '';
+ }
+ if (label) label.textContent = isHidden ? 'Show Event Report PDF' : 'Hide Event Report PDF';
+ // Scroll the iframe into view on first reveal so the operator
+ // doesn't have to hunt for it after clicking.
+ if (!isHidden) {
+ preview.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ }
+ };
+
+ window.saveEventReview = async function (eventId) {
+ const ft = document.getElementById('event-review-ft');
+ const reviewer = document.getElementById('event-review-reviewer');
+ const notes = document.getElementById('event-review-notes');
+ const status = document.getElementById('event-review-status');
+ if (!ft || !reviewer || !notes) return;
+
+ const payload = {
+ review: {
+ false_trigger: ft.checked,
+ reviewer: reviewer.value.trim() || null,
+ notes: notes.value.trim() || null,
+ }
+ };
+ if (status) {
+ status.textContent = 'Saving…';
+ status.className = 'text-xs text-gray-500 dark:text-gray-400';
+ }
+ try {
+ const r = await fetch(`/api/sfm/db/events/${encodeURIComponent(eventId)}/sidecar`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload),
+ });
+ if (!r.ok) {
+ const t = await r.text().catch(() => '');
+ throw new Error('HTTP ' + r.status + (t ? ` — ${t.slice(0, 120)}` : ''));
+ }
+ if (status) {
+ status.textContent = 'Saved.';
+ status.className = 'text-xs text-green-600 dark:text-green-400';
+ }
+ // Notify the host page so its event-list FT badge / row state
+ // can refresh. Pages opt in by listening for this event.
+ window.dispatchEvent(new CustomEvent('sfm-event-review-saved', {
+ detail: { eventId, review: payload.review },
+ }));
+ } catch (e) {
+ if (status) {
+ status.textContent = 'Save failed: ' + e.message;
+ status.className = 'text-xs text-red-600 dark:text-red-400';
+ }
+ }
+ };
+
window.copyEventJson = function () {
const pre = document.getElementById('event-json-pre');
const label = document.getElementById('event-json-copy-label');