`;
container.appendChild(row);
});
}
function updateDominance(classIn, classOut) {
const labels = [], values = [];
for (const [group, ids] of Object.entries(BUSINESS_MAP)) {
let total = 0;
ids.forEach(id => { total += (classIn[String(id)] || 0) + (classOut[String(id)] || 0); });
labels.push(group);
values.push(total);
}
domChart.data.labels = labels;
domChart.data.datasets[0].data = values;
domChart.update();
}
function buildFlowHistogram(flowTimes, videoDuration) {
const binCount = Math.max(1, Math.ceil(videoDuration));
const bins = new Array(binCount).fill(0);
const labels = [];
for (let i = 0; i < binCount; i++) labels.push(i);
flowTimes.forEach(t => { bins[Math.min(Math.floor(t), binCount - 1)]++; });
flowChart.data.labels = labels;
flowChart.data.datasets[0].data = bins;
flowChart.update();
}
let _alpha = 0.25;
function updateCongestion(congestion, stride) {
let data = congestion;
// Apply EMA smoothing if alpha is less than 1 (1 = no smoothing)
if (_alpha < 0.99) {
data = [];
let s = congestion[0] || 0;
for (let v of congestion) {
s = _alpha * v + (1 - _alpha) * s;
data.push(s);
}
}
const len = data.length;
if (len <= 200) {
congChart.data.labels = data.map((_, i) => i * stride);
congChart.data.datasets[0].data = data;
} else {
// Dynamic sampling to keep chart performance high
const step = Math.ceil(len / 200);
const sampled = [], labels = [];
for (let i = 0; i < len; i += step) {
labels.push(i * stride);
sampled.push(data[i]);
}
congChart.data.labels = labels;
congChart.data.datasets[0].data = sampled;
}
congChart.update();
}
// =========== Main ===========
let _params = null;
function populateAndInit(params) {
populateRunDetails(params.config);
populateSettingsTab(params.config, params.settings || {});
}
function startProcessingFromSettings() {
if (!_params) return;
// Read current stepper/control values
const imgsz = parseInt(document.getElementById('sv-imgsz').textContent);
const conf = parseFloat(document.getElementById('sv-conf').textContent);
const iou = parseFloat(document.getElementById('sv-iou').textContent);
const stride = parseInt(document.getElementById('sv-stride').textContent);
const reportFmt = document.getElementById('sv-report').value;
const annotated = document.getElementById('sv-annotated').classList.contains('active');
// Annotation Options
const annotated_options = {
bbox: true, // Always true if export is enabled
spatial: document.getElementById('chip-spatial').classList.contains('active'),
class_name: document.getElementById('chip-class_name').classList.contains('active'),
class_id: document.getElementById('chip-class_id').classList.contains('active'),
track_id: document.getElementById('chip-track_id').classList.contains('active')
};
const exportJson = document.getElementById('sv-export-json').classList.contains('active');
const exportCsv = document.getElementById('sv-export-csv').classList.contains('active');
// Apply to config
_params.config.imgsz = imgsz;
_params.config.conf = conf;
_params.config.iou = iou;
_params.config.detect_stride = stride;
// Reflect final resolved params in Run tab
populateInferPanel(_params.config);
// Lock settings
lockSettings();
// Freeze annotation chips during processing
const chipSelector = document.getElementById('chip-selector');
if (chipSelector) {
chipSelector.style.pointerEvents = 'none';
chipSelector.style.opacity = '0.5';
}
// Switch to overview
switchTab('overview');
document.getElementById('proc-label').innerText = 'Connecting...';
// Analytics Funnel
if (typeof trackFunnel === 'function') {
trackFunnel('PROCESS_STARTED');
}
// Reset Run Tab Results to Awaiting
document.getElementById('run-results-content').innerHTML = `
Executing inference pipeline... results pending
`;
// Update Results tab pending message
const repIcon = document.getElementById('reports-pending-icon');
if (repIcon) {
repIcon.className = 'fa-solid fa-satellite-dish animate-pulse text-5xl mb-2';
repIcon.style.color = '#c89a6c';
}
const repText = document.getElementById('reports-pending-text');
if (repText) repText.innerText = 'Transmission in progress...';
// Start WebSocket
const videoDuration = _params.config.duration || 10;
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
// Get the directory path (e.g., /app/ or /) rather than the full filename
const dirPath = location.pathname.substring(0, location.pathname.lastIndexOf('/') + 1);
const ws = new WebSocket(`${proto}://${location.host}${dirPath}ws/run`);
ws.onopen = () => {
ws.send(JSON.stringify({
video_id: _params.video_id,
line: _params.line,
config: _params.config,
annotated_video: annotated,
annotated_options: annotated_options,
export_json: exportJson,
export_csv: exportCsv,
report_format: reportFmt
}));
};
ws.onerror = e => {
console.error('WS Error:', e);
document.getElementById('proc-label').innerText = 'Connection Error';
showToast('Connection error — server may be busy', 'error');
};
let processingDone = false;
let firstMessageReceived = false;
ws.onclose = () => {
console.log('WS Closed');
if (!processingDone) {
// Closed before done=True received — show error state
document.getElementById('proc-label').innerText = 'Disconnected';
document.getElementById('run-results-content').innerHTML = `
Processing connection was lost.The server may have timed out or restarted. Please try again.
`;
}
};
let lastUIUpdate = 0;
let liveCongestion = [];
let liveFlowTimes = [];
ws.onmessage = e => {
const d = JSON.parse(e.data);
if (!firstMessageReceived) {
firstMessageReceived = true;
document.getElementById('proc-label').innerText = 'Processing';
}
// Hide empty state on first data
const emptyState = document.getElementById('stats-empty-state');
if (emptyState) emptyState.style.display = 'none';
if (d.error) {
processingDone = true;
document.getElementById('proc-label').innerText = 'Engine Error';
console.error('[UrbanFlow] Engine error:', d.detail || d.error);
document.getElementById('run-results-content').innerHTML = `
Inference pipeline failed.${d.error}
`;
return;
}
if (d.done) {
processingDone = true;
// Stats Tracking (Scoped by email)
const session = (typeof getAuthSession === 'function') ? getAuthSession() : null;
const emailKey = session ? `_${session.email}` : '';
let currentRuns = parseInt(localStorage.getItem(`uf_total_runs${emailKey}`) || '0');
localStorage.setItem(`uf_total_runs${emailKey}`, currentRuns + 1);
localStorage.setItem(`uf_last_active${emailKey}`, new Date().toLocaleString());
document.getElementById('proc-label').innerText = 'Complete';
document.getElementById('proc-bar').style.width = '100%';
document.getElementById('proc-pct').innerText = '100%';
// Force frame counter to n/n
const framesEl = document.getElementById('proc-frames');
if (framesEl) {
const parts = framesEl.innerText.split('/');
if (parts.length === 2) {
const total = parts[1].trim().replace(' Frames', '');
framesEl.innerText = `${total} / ${total} Frames`;
}
}
// GLOW NOTIFICATION: Let the user know artifacts are ready
const resultsMob = document.getElementById('mob-nav-results');
if (resultsMob) {
resultsMob.classList.add('notify-glow');
}
// Show results content immediately (telemetry first, reports load async)
const rPendingMsg = document.getElementById('reports-pending-message');
if (rPendingMsg) rPendingMsg.classList.add('hidden');
const rContentWrap = document.getElementById('results-content-wrap');
if (rContentWrap) rContentWrap.classList.remove('hidden');
document.getElementById('run-results-content').innerHTML =
detailRow('Inference Time', (d.processing_time || 0).toFixed(2) + ' sec') +
infoRow('Throughput (FPS)', (d.actual_fps || 0).toFixed(2), 'Measured frame throughput during processing.', ' fps') +
infoRow('Real-time Ratio', (d.speed_vs_realtime || 0).toFixed(2) + 'x', 'Processing speed relative to video playback rate.');
if (d.video_id) {
loadReports(d.video_id).then(data => {
if (!data) return;
// Auto-Download Logic (Respects live toggle state)
if (document.getElementById('sv-auto-download').classList.contains('active')) {
// Download the full bundle ZIP via direct navigation
setTimeout(() => {
console.log('[UrbanFlow] Fetching ZIP bundle for:', d.video_id);
window.open(`bundle/${d.video_id}`, '_blank');
}, 1000);
}
});
}
// Disable Auto-Download toggle after completion
const adToggle = document.getElementById('sv-auto-download');
if (adToggle) {
adToggle.closest('.s-row').classList.add('disabled');
}
const jsonToggle = document.getElementById('sv-export-json');
if (jsonToggle) {
jsonToggle.closest('.s-row').classList.add('disabled');
}
// NOTIFY USER: Glow the results icon in mobile nav
const resultsNav = document.getElementById('mob-nav-results');
if (resultsNav) {
resultsNav.classList.add('notify-glow');
}
const csvToggle = document.getElementById('sv-export-csv');
if (csvToggle) {
csvToggle.closest('.s-row').classList.add('disabled');
}
// Show New Analysis button in Settings
const newWrap = document.getElementById('new-analysis-wrap');
if (newWrap) newWrap.classList.remove('hidden');
// Toast + Insights
showToast('Processing complete — artifacts ready', 'success');
renderInsights(d);
showRetryBubble();
// Store video_id for keyboard shortcut download
document.body.setAttribute('data-last-video-id', d.video_id);
return;
}
let pct = ((d.frame_index / d.total_iters) * 100).toFixed(1);
if (d.frame_index >= d.total_iters - 1) pct = '100.0';
document.getElementById('proc-bar').style.width = pct + '%';
document.getElementById('proc-frames').innerText = `${d.frame_index} / ${d.total_iters} Frames`;
const procPctEl = document.getElementById('proc-pct');
const currPct = parseFloat(procPctEl.innerText) || 0;
animateValue(procPctEl, currPct, pct + '%', 300);
const totalIn = sumValues(d.class_in);
const totalOut = sumValues(d.class_out);
const cntTotalEl = document.getElementById('cnt-total');
const currTotal = parseInt(cntTotalEl.innerText) || 0;
animateValue(cntTotalEl, currTotal, totalIn + totalOut, 300);
// Update PCU display
const pcuVal = calcPCU(d.class_in, d.class_out);
const pcuEl = document.getElementById('cnt-pcu');
if (pcuEl) pcuEl.innerText = pcuVal;
// Update doughnut
doughChart.data.datasets[0].data = [totalIn, totalOut];
doughChart.update();
// Live history management (Backend sends incremental updates now)
if (d.congestion_last !== undefined) {
liveCongestion.push(d.congestion_last);
}
if (d.flow_count !== undefined) {
// Approximate timestamps for new crossings based on current frame
const videoFps = _params.config.video_fps || 30;
while (liveFlowTimes.length < d.flow_count) {
liveFlowTimes.push(d.frame_index * stride / videoFps);
}
}
const now = performance.now();
if (now - lastUIUpdate < 300) return;
lastUIUpdate = now;
updateCongestion(liveCongestion, stride);
updateBreakdown(d.class_in, d.class_out);
updateDominance(d.class_in, d.class_out);
buildFlowHistogram(liveFlowTimes, videoDuration);
};
}
const REPORT_LABELS = {
'direction_pie.png': { title: 'Directional Split', desc: 'Incoming vs outgoing vehicle ratio' },
'direction_pie.pdf': { title: 'Directional Split', desc: 'Incoming vs outgoing vehicle ratio' },
'flow_over_time.png': { title: 'Traffic Flow', desc: 'Vehicles crossing the line over time' },
'flow_over_time.pdf': { title: 'Traffic Flow', desc: 'Vehicles crossing the line over time' },
'congestion_index.png': { title: 'Congestion Index', desc: 'Active vehicles per frame with smoothing' },
'congestion_index.pdf': { title: 'Congestion Index', desc: 'Active vehicles per frame with smoothing' },
'class_dominance.png': { title: 'Class Dominance', desc: 'Vehicle count by classification type' },
'class_dominance.pdf': { title: 'Class Dominance', desc: 'Vehicle count by classification type' },
'confidence_dist.png': { title: 'Confidence Curve', desc: 'Distribution of detection confidences' },
'confidence_dist.pdf': { title: 'Confidence Curve', desc: 'Distribution of detection confidences' },
'annotated.mp4': { title: 'Annotated Video Export', desc: 'Rendered video with tracking overlays' },
'heatmap.png': { title: 'Detection Confidence Density Map', desc: 'xAI spatial explanation — confidence-weighted Gaussian kernel density over all detections' },
'heatmap.pdf': { title: 'Detection Confidence Density Map', desc: 'xAI spatial explanation — confidence-weighted Gaussian kernel density over all detections' },
'raw_data.csv': { title: 'Raw Analytics Export', desc: 'Comma-separated values of all crossings' },
'analysis.json': { title: 'Structured JSON Export', desc: 'Complete analysis data with metadata for API consumption' }
};
async function loadReports(videoId) {
const res = await fetch(`reports/${videoId}`, { method: 'POST' });
const data = await res.json();
if (!data.files || !data.files.length) return null;
const rPending = document.getElementById('reports-pending');
if (rPending) rPending.classList.add('hidden');
const rMsg = document.getElementById('reports-pending-message');
if (rMsg) rMsg.classList.add('hidden');
document.getElementById('post-process-cards').classList.remove('hidden');
const grid = document.getElementById('reports-grid');
grid.classList.remove('hidden');
const mobileBtn = document.getElementById('mobile-download-wrap');
if (mobileBtn) mobileBtn.classList.remove('hidden');
grid.innerHTML = '';
data.files.forEach(name => {
const info = REPORT_LABELS[name] || { title: name, desc: '' };
const url = `reports/${videoId}/${name}`;
const isVideo = name.endsWith('.mp4');
const isPDF = name.endsWith('.pdf');
const isCSV = name.endsWith('.csv');
const card = document.createElement('div');
card.className = 'bg-black rounded-xl border border-slate-800 shadow-sm flex flex-col overflow-hidden';
let previewHTML = '';
if (isVideo) {
previewHTML = `
Video Ready for Local Analysis
`;
} else if (isPDF) {
previewHTML = `
PDF Document
`;
} else if (isCSV) {
previewHTML = `
Raw Analytics Export
`;
} else if (name.endsWith('.json')) {
previewHTML = `
Structured JSON
`;
} else {
previewHTML = ``;
}
const isHeatmap = name.includes('heatmap');
const tooltipHTML = isHeatmap ? `
Confidence-weighted spatial density map — a faithful xAI explanation of WHERE the detector is most certain vehicles exist. Each detection stamps a Gaussian kernel scaled by its confidence score. Brighter regions = higher accumulated detection confidence.` : '';
card.innerHTML = `