itsluckysharma01's picture
Clean deployment
4ed7d03
// Live Camera JavaScript
let detectionCount = 0;
let alertCount = 0;
let personDetectedCount = 0;
let personPresent = false;
let availableModels = {};
let selectedModels = [];
let cameraRunning = false;
let sessionStartTime = null;
let sessionData = {
startTime: null,
endTime: null,
duration: 0,
selectedModels: [],
detections: [],
alerts: [],
totalDetections: 0,
totalAlerts: 0,
personDetected: 0,
personPresent: false,
};
// Initialize model selection on page load
document.addEventListener("DOMContentLoaded", async () => {
await loadAvailableModels();
await loadSelectedModels();
await loadAvailableCameras();
startDetectionHistoryPolling(); // Start loading detection history
// Do NOT auto-start camera - wait for user to click Start
// initializeCameraFeed();
});
// Load available cameras and populate dropdown
async function loadAvailableCameras() {
try {
const response = await fetch("/api/cameras");
const result = await response.json();
if (!result.success) {
console.error("Failed to load cameras:", result.error);
document.getElementById("camera-select").innerHTML =
'<option value="">❌ Error loading cameras</option>';
return;
}
const cameras = result.cameras || [];
const selected = result.selected || 0;
const cameraSelect = document.getElementById("camera-select");
if (cameras.length === 0) {
cameraSelect.innerHTML =
'<option value="">❌ No cameras detected - please connect a USB camera</option>';
return;
}
// Populate dropdown with available cameras
cameraSelect.innerHTML = cameras
.map(
(cam) =>
`<option value="${cam.index}" ${cam.index === selected ? "selected" : ""}>
πŸ“· ${cam.name} (${cam.resolution} @ ${cam.fps} FPS)
</option>`,
)
.join("");
// Add change listener to camera select
cameraSelect.addEventListener("change", handleCameraChange);
} catch (error) {
console.error("Error loading cameras:", error);
document.getElementById("camera-select").innerHTML =
'<option value="">❌ Error loading cameras</option>';
}
}
// Handle camera selection change
async function handleCameraChange(event) {
const cameraIndex = parseInt(event.target.value);
if (isNaN(cameraIndex)) return;
// Stop current camera if running
if (cameraRunning) {
await stopCamera();
}
try {
const response = await fetch("/api/select-camera", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ camera_index: cameraIndex }),
});
const result = await response.json();
if (result.success) {
showToast(`πŸ“· Camera ${cameraIndex} selected`, "success");
console.log("Camera selected:", result.message);
} else {
showToast("Failed to select camera: " + result.error, "error");
// Reload cameras list
await loadAvailableCameras();
}
} catch (error) {
console.error("Error selecting camera:", error);
showToast("Error selecting camera", "error");
}
}
// Start camera stream with selected models
async function startCamera() {
if (cameraRunning) {
showToast("Camera is already running", "warning");
return;
}
// Check if models are selected
const checkboxes = document.querySelectorAll(".model-checkbox:checked");
const selected = Array.from(checkboxes).map((cb) => cb.dataset.modelId);
if (selected.length === 0) {
showToast(
"Please select at least one model before starting the camera",
"warning",
);
return;
}
try {
// Load previous person detection data
await initializePersonDetection();
// Apply selected models
const response = await fetch("/api/set-models", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ models: selected }),
});
const data = await response.json();
if (!data.success) {
showToast("Failed to apply model selection", "error");
return;
}
// Start monitoring session
const sessionResponse = await fetch("/api/start-camera", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ selectedModels: selected }),
});
const sessionData = await sessionResponse.json();
if (!sessionData.success) {
showToast("Failed to start camera session", "error");
return;
}
// Update UI
cameraRunning = true;
sessionStartTime = new Date();
sessionData.startTime = sessionStartTime;
sessionData.selectedModels = selected;
// Show camera feed
const cameraFeed = document.getElementById("camera-feed");
const cameraPlaceholder = document.getElementById("camera-placeholder");
const videoOverlay = document.getElementById("video-overlay");
cameraFeed.style.display = "block";
cameraFeed.src = "/camera_feed?" + new Date().getTime();
cameraPlaceholder.style.display = "none";
videoOverlay.style.display = "block";
// Update controls
document.getElementById("start-btn").style.display = "none";
document.getElementById("stop-btn").style.display = "block";
// Show session info
document.getElementById("camera-status-info").style.display = "block";
document.getElementById("session-models").textContent = selected.join(", ");
// Initialize camera feed monitoring
initializeCameraFeed();
// Start recording the session automatically
try {
const recordResponse = await fetch("/api/start-recording", {
method: "POST",
});
const recordData = await recordResponse.json();
if (recordData.success) {
isRecording = true;
showToast("Recording live session...", "info");
}
} catch (error) {
console.error("Error starting recording:", error);
}
// Update session duration every second
setInterval(() => {
if (cameraRunning) {
const elapsed = Math.floor((new Date() - sessionStartTime) / 1000);
const minutes = Math.floor(elapsed / 60);
const seconds = elapsed % 60;
document.getElementById("session-duration").textContent =
`${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
}
}, 1000);
showToast("Camera started with selected models", "success");
} catch (error) {
console.error("Error starting camera:", error);
showToast("Failed to start camera", "error");
}
}
// Initialize and monitor camera feed
function initializeCameraFeed() {
const cameraFeed = document.getElementById("camera-feed");
if (!cameraFeed) return;
// Add error handling
cameraFeed.addEventListener("error", function () {
console.error("Camera feed error - reloading...");
if (cameraRunning) {
document.getElementById("status-badge").innerHTML = `
<span class="status-dot"></span>
<span>RECONNECTING...</span>
`;
// Reload feed after 2 seconds
setTimeout(() => {
cameraFeed.src = "/camera_feed?" + new Date().getTime();
}, 2000);
}
});
// Refresh camera feed periodically to ensure stream stays active
const feedRefreshInterval = setInterval(() => {
if (!cameraRunning) {
clearInterval(feedRefreshInterval);
return;
}
const isStreaming =
cameraFeed.src && cameraFeed.src.includes("/camera_feed");
if (!isStreaming && cameraRunning) {
console.log("Restarting camera feed...");
cameraFeed.src = "/camera_feed?" + new Date().getTime();
}
}, 30000); // Every 30 seconds
// Start polling for stats
updateLiveStats(); // Initial call
const statsInterval = setInterval(() => {
if (cameraRunning) {
updateLiveStats();
} else {
clearInterval(statsInterval);
}
}, 1000);
}
async function loadAvailableModels() {
try {
const response = await fetch("/api/available-models");
const data = await response.json();
availableModels = data.models || {};
renderModelSelection();
} catch (error) {
console.error("Error loading models:", error);
showToast("Failed to load available models", "error");
}
}
async function loadSelectedModels() {
try {
const response = await fetch("/api/get-selected-models");
const data = await response.json();
selectedModels = data.selected_models || [];
updateModelCheckboxes();
} catch (error) {
console.error("Error loading selected models:", error);
}
}
function renderModelSelection() {
const grid = document.getElementById("models-grid");
if (!grid) return;
const modelCards = Object.entries(availableModels).map(
([modelKey, modelInfo]) => {
const isSelected = selectedModels.includes(modelKey);
return `
<div class="model-card">
<label class="model-checkbox-label">
<input
type="checkbox"
class="model-checkbox"
data-model-id="${modelKey}"
${isSelected ? "checked" : ""}
>
<div class="checkbox-content">
<strong>${modelInfo.name}</strong>
<p>${modelInfo.description}</p>
<span class="model-badge">${modelKey}</span>
</div>
</label>
</div>
`;
},
);
grid.innerHTML =
modelCards.length > 0 ? modelCards.join("") : "<p>No models available</p>";
}
function updateModelCheckboxes() {
const checkboxes = document.querySelectorAll(".model-checkbox");
checkboxes.forEach((checkbox) => {
checkbox.checked = selectedModels.includes(checkbox.dataset.modelId);
});
}
function toggleModelPanel() {
const content = document.getElementById("model-panel-content");
const icon = document.getElementById("model-toggle-icon");
if (!content) return;
content.classList.toggle("open");
icon.textContent = content.classList.contains("open") ? "β–²" : "β–Ό";
}
function selectAllModels() {
const checkboxes = document.querySelectorAll(".model-checkbox");
checkboxes.forEach((checkbox) => {
checkbox.checked = true;
});
}
function deselectAllModels() {
const checkboxes = document.querySelectorAll(".model-checkbox");
checkboxes.forEach((checkbox) => {
checkbox.checked = false;
});
}
async function applyModelSelection() {
const checkboxes = document.querySelectorAll(".model-checkbox:checked");
const selected = Array.from(checkboxes).map((cb) => cb.dataset.modelId);
try {
const response = await fetch("/api/set-models", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ models: selected }),
});
const data = await response.json();
if (data.success) {
selectedModels = selected;
showToast(
`Applied ${selected.length > 0 ? selected.length + " model(s)" : "all models"}`,
"success",
);
} else {
showToast("Failed to apply model selection", "error");
}
} catch (error) {
console.error("Error applying model selection:", error);
showToast("Error applying model selection", "error");
}
}
function showToast(message, type = "info") {
const container = document.getElementById("toast-container");
if (!container) return;
const toast = document.createElement("div");
toast.className = `toast ${type}`;
toast.textContent = message;
container.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 2600);
}
// ===================== Person Detection Tracking =====================
// Load previous person detection data from database
async function loadPersonDetectionHistory() {
try {
const response = await fetch("/api/person-detection-history");
const data = await response.json();
if (data.success && data.data) {
personDetectedCount = data.data.total_detections || 0;
personPresent = data.data.currently_present || false;
updatePersonDetectionUI();
}
} catch (error) {
console.error("Error loading person detection history:", error);
}
}
// Save person detection data to database
async function savePersonDetection(
count,
isPresent,
confidence = 0.0,
detailsObj = null,
) {
try {
const detailsData = detailsObj || {
timestamp: new Date().toISOString(),
session_active: cameraRunning,
};
const response = await fetch("/api/save-person-detection", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
person_count: count,
is_present: isPresent,
confidence: confidence,
detection_details: JSON.stringify(detailsData),
}),
});
const data = await response.json();
if (!data.success) {
console.error("Failed to save person detection:", data.message);
}
} catch (error) {
console.error("Error saving person detection:", error);
}
}
// Update person detection UI elements
function updatePersonDetectionUI() {
const detectedEl = document.getElementById("person-detected");
const presentEl = document.getElementById("person-present");
if (detectedEl) {
detectedEl.textContent = personDetectedCount;
}
if (presentEl) {
presentEl.textContent = personPresent ? "βœ… Yes" : "❌ No";
presentEl.style.color = personPresent ? "var(--ok)" : "var(--danger)";
}
}
// Track person detected in detection results (call this when processing detections)
// Supports detections from YOLO with class='person' and bounding boxes
function recordPersonDetection(detectionData) {
let hasPersonDetection = false;
let personCount = 0;
let boundingBoxes = [];
let avgConfidence = 0.0;
if (detectionData && typeof detectionData === "object") {
// Check for detections array (from YOLO/frame processing)
if (Array.isArray(detectionData)) {
for (const detection of detectionData) {
if (
detection.class &&
(detection.class.toLowerCase() === "person" ||
detection.class.toLowerCase() === "human")
) {
hasPersonDetection = true;
personCount++;
if (detection.bbox) {
boundingBoxes.push({
bbox: detection.bbox,
confidence: detection.confidence || 0.0,
type: detection.type || "object",
});
}
if (detection.confidence) {
avgConfidence += detection.confidence;
}
}
}
if (personCount > 0) {
avgConfidence = avgConfidence / personCount;
}
}
// Legacy support for object detection structure
else if (detectionData.objects) {
for (const obj of detectionData.objects) {
if (
obj.label &&
(obj.label.toLowerCase() === "person" ||
obj.label.toLowerCase() === "human")
) {
hasPersonDetection = true;
personCount++;
if (obj.bbox) {
boundingBoxes.push({
bbox: obj.bbox,
confidence: obj.confidence || 0.0,
label: obj.label,
});
}
if (obj.confidence) {
avgConfidence += obj.confidence;
}
}
}
if (personCount > 0) {
avgConfidence = avgConfidence / personCount;
}
}
// Update person detected count if person detected
if (hasPersonDetection) {
personDetectedCount++;
personPresent = true;
updatePersonDetectionUI();
// Save with bounding box information
savePersonDetection(personDetectedCount, true, avgConfidence, {
person_count: personCount,
bounding_boxes: boundingBoxes,
timestamp: new Date().toISOString(),
session_active: cameraRunning,
});
}
}
}
// Call this on camera start to load previous data
async function initializePersonDetection() {
await loadPersonDetectionHistory();
}
async function stopCamera() {
if (!cameraRunning) {
showToast("Camera is not running", "warning");
return;
}
if (!confirm("Stop camera and save the monitoring session?")) {
return;
}
// Stop recording if active and get video filename
let recordingFilename = null;
if (isRecording) {
isRecording = false;
clearInterval(recordingInterval);
try {
const stopResponse = await fetch("/api/stop-recording", {
method: "POST",
});
const stopData = await stopResponse.json();
if (stopData.success) {
recordingFilename = stopData.filename;
sessionData.recordingFilename = recordingFilename;
showToast("Recording saved", "success");
}
} catch (error) {
console.error("Error stopping recording:", error);
}
}
// Prepare session data
sessionData.endTime = new Date();
sessionData.duration = Math.floor(
(sessionData.endTime - sessionData.startTime) / 1000,
);
sessionData.totalDetections = detectionCount;
sessionData.totalAlerts = alertCount;
// Stop camera
cameraRunning = false;
const cameraFeed = document.getElementById("camera-feed");
cameraFeed.src = "";
cameraFeed.style.display = "none";
// Hide controls
document.getElementById("stop-btn").style.display = "none";
document.getElementById("record-btn").style.display = "none";
document.getElementById("start-btn").style.display = "block";
document.getElementById("camera-status-info").style.display = "none";
// Show camera placeholder
document.getElementById("camera-placeholder").style.display = "block";
document.getElementById("video-overlay").style.display = "none";
// Save session to database
saveMonitoringSession(sessionData);
}
async function saveMonitoringSession(session) {
try {
const response = await fetch("/api/save-monitoring-session", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(session),
});
const data = await response.json();
if (data.success) {
showToast("Monitoring session saved successfully", "success");
// Show session summary
const summary = `
<h3>πŸ“Š Session Summary</h3>
<p><strong>Duration:</strong> ${Math.floor(session.duration / 60)}:${String(session.duration % 60).padStart(2, "0")}</p>
<p><strong>Total Detections:</strong> ${session.totalDetections}</p>
<p><strong>Total Alerts:</strong> ${session.totalAlerts}</p>
<p><strong>Models Used:</strong> ${session.selectedModels.join(", ")}</p>
`;
// You could show this in a modal or in the alerts panel
setTimeout(() => {
alert(
`Session saved!\n\nDuration: ${Math.floor(session.duration / 60)}:${String(session.duration % 60).padStart(2, "0")}\nDetections: ${session.totalDetections}\nAlerts: ${session.totalAlerts}`,
);
}, 500);
// Reset counters
detectionCount = 0;
alertCount = 0;
document.getElementById("total-detections").textContent = "0";
document.getElementById("total-alerts").textContent = "0";
} else {
showToast("Failed to save monitoring session: " + data.error, "error");
}
} catch (error) {
console.error("Error saving monitoring session:", error);
showToast("Error saving session to database", "error");
}
}
function toggleFullscreen() {
const videoContainer = document.querySelector(".video-container");
if (!document.fullscreenElement) {
if (videoContainer.requestFullscreen) {
videoContainer.requestFullscreen();
} else if (videoContainer.webkitRequestFullscreen) {
videoContainer.webkitRequestFullscreen();
} else if (videoContainer.msRequestFullscreen) {
videoContainer.msRequestFullscreen();
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
}
}
}
function addAlert(alertData) {
const alertsContainer = document.getElementById("alerts-container");
// Remove placeholder if exists
const placeholder = alertsContainer.querySelector(".alert-placeholder");
if (placeholder) {
placeholder.remove();
}
const alertDiv = document.createElement("div");
alertDiv.className = "alert-item";
if (alertData.severity === "HIGH" || alertData.severity === "HIGH RISK") {
alertDiv.classList.add("high");
}
const timestamp = new Date().toLocaleTimeString();
alertDiv.innerHTML = `
<div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
<strong>${alertData.type || "Alert"}</strong>
<span style="font-size: 0.9em; color: #666;">${timestamp}</span>
</div>
<div>${alertData.message}</div>
${alertData.confidence ? `<div style="margin-top: 5px; font-size: 0.9em;">Confidence: ${(alertData.confidence * 100).toFixed(1)}%</div>` : ""}
`;
// Add to top of list
alertsContainer.insertBefore(alertDiv, alertsContainer.firstChild);
// Update counter
alertCount++;
document.getElementById("total-alerts").textContent = alertCount;
// Keep only last 10 alerts visible
while (alertsContainer.children.length > 10) {
alertsContainer.removeChild(alertsContainer.lastChild);
}
}
// Monitor camera feed for errors (when element exists)
document.addEventListener("DOMContentLoaded", function () {
const cameraFeedElement = document.getElementById("camera-feed");
if (cameraFeedElement) {
cameraFeedElement.addEventListener("error", function () {
console.error("Camera feed error");
const statusBadge = document.getElementById("status-badge");
if (statusBadge) {
statusBadge.innerHTML = `
<span class="status-dot"></span>
<span>DISCONNECTED</span>
`;
}
});
}
});
// Poll for live statistics updates
async function updateLiveStats() {
try {
const response = await fetch("/api/live-stats");
const data = await response.json();
if (data.error) {
console.error("Stats error:", data.error);
return;
}
// Update detection count based on actual detections
detectionCount = data.person_detections + data.weapon_detections;
document.getElementById("total-detections").textContent = detectionCount;
// Update alert count (only if alerts have been triggered)
if (data.alert_count > 0) {
document.getElementById("total-alerts").textContent = data.alert_count;
alertCount = data.alert_count;
}
// Update pose analysis information
if (data.pose_analysis && Object.keys(data.pose_analysis).length > 0) {
const pose = data.pose_analysis;
document.getElementById("pose-risk-level").textContent =
pose.risk_level || "SAFE";
document.getElementById("pose-action").textContent = pose.action || "β€”";
document.getElementById("pose-score").textContent = (
pose.risk_score || 0
).toFixed(2);
// Update color based on risk level
const riskElement = document.getElementById("pose-risk-level");
if (pose.risk_level === "HIGH_RISK") {
riskElement.style.color = "var(--danger)";
} else if (pose.risk_level === "LOW_RISK") {
riskElement.style.color = "var(--orange-600)";
} else {
riskElement.style.color = "var(--ok)";
}
}
// Show visual indicators for active detections
updateDetectionIndicators(data);
} catch (error) {
console.error("Error fetching live stats:", error);
}
}
function updateDetectionIndicators(data) {
// Visual feedback when person or weapon is visible (3+ frames)
const cameraFeed = document.getElementById("camera-feed");
if (data.person_visible || data.weapon_visible) {
if (!cameraFeed.classList.contains("detection-active")) {
cameraFeed.classList.add("detection-active");
}
} else {
cameraFeed.classList.remove("detection-active");
}
}
// Update detection count periodically (now fetches from backend)
// Moved to initializeCameraFeed function
// Load and display detection history
async function loadDetectionHistory() {
try {
const response = await fetch("/api/detection-history?limit=12");
const result = await response.json();
if (!result.success) {
console.error("Failed to load detection history");
return;
}
const detections = result.data || [];
const gallery = document.getElementById("detection-gallery");
// Update badge
document.getElementById("detection-count-badge").textContent =
detections.length;
if (detections.length === 0) {
gallery.innerHTML = `
<div style="grid-column: 1 / -1; text-align: center; color: var(--slate-500); padding: 2rem;">
<p style="margin: 0;">πŸ“Έ No detections yet</p>
<small>Detected threats will appear here</small>
</div>
`;
return;
}
// Build gallery
gallery.innerHTML = detections
.map((det) => {
const typeEmoji =
{
weapon: "πŸ”«",
risk: "⚠️",
unusual: "❓",
}[det.detection_type] || "πŸ“Έ";
const levelColor =
{
LOW: "var(--blue-600)",
MEDIUM: "var(--orange-600)",
HIGH: "var(--danger)",
CRITICAL: "var(--danger)",
}[det.alert_level] || "var(--slate-600)";
const timestamp = new Date(det.detected_at).toLocaleTimeString();
return `
<div style="position: relative; cursor: pointer; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1); transition: transform 0.2s ease;" onclick="openDetectionDetail(${det.id}, '${det.image_filename}', '${det.detection_type}', '${det.alert_level}', '${timestamp}')">
<img src="/api/detection-image/${det.image_filename}" alt="Detection" style="width: 100%; height: 100px; object-fit: cover; display: block;">
<div style="position: absolute; bottom: 0; left: 0; right: 0; background: linear-gradient(to top, rgba(0,0,0,0.7), transparent); padding: 0.5rem; color: white; font-size: 0.75rem;">
<div style="font-weight: 600;">${typeEmoji} ${det.detection_type.toUpperCase()}</div>
<div style="color: #ccc; font-size: 0.7rem;">${timestamp}</div>
</div>
<div style="position: absolute; top: 4px; right: 4px; background: ${levelColor}; color: white; padding: 2px 6px; border-radius: 4px; font-weight: 600; font-size: 0.7rem;">${det.alert_level}</div>
</div>
`;
})
.join("");
} catch (error) {
console.error("Error loading detection history:", error);
}
}
function openDetectionDetail(id, filename, type, level, timestamp) {
// Create modal with detection details
const modal = `
<div style="text-align: center;">
<img src="/api/detection-image/${filename}" alt="Detection" style="max-width: 100%; max-height: 400px; border-radius: 8px; margin-bottom: 1rem;">
<div style="background: var(--slate-100); padding: 1rem; border-radius: 8px; margin-bottom: 1rem;">
<p><strong>Type:</strong> ${type.toUpperCase()}</p>
<p><strong>Alert Level:</strong> <span style="color: var(--${level === "CRITICAL" || level === "HIGH" ? "danger" : "orange-600"});">${level}</span></p>
<p><strong>Detected:</strong> ${timestamp}</p>
</div>
</div>
`;
new Modal(`Detection #${id}`, modal, { footer: false }).show();
}
// Refresh detection history periodically
function startDetectionHistoryPolling() {
// Load immediately
loadDetectionHistory();
// Then every 5 seconds
setInterval(loadDetectionHistory, 5000);
}
// Video Recording Functions
let isRecording = false;
let recordingStartTime = null;
let recordingInterval = null;
async function toggleRecording() {
if (!cameraRunning) {
showToast("❌ Camera is not running. Start the camera first.", "error");
return;
}
if (isRecording) {
// Stop recording
try {
const response = await fetch("/api/stop-recording", { method: "POST" });
const result = await response.json();
if (result.success) {
isRecording = false;
clearInterval(recordingInterval);
document.getElementById("record-btn").classList.remove("btn-danger");
document.getElementById("record-btn").textContent = "πŸ”΄ Record";
document.getElementById("record-btn").classList.add("btn-secondary");
const duration = result.duration ? ` (${result.duration}s)` : "";
showToast(`βœ… Recording stopped${duration}`, "success");
} else {
showToast("❌ Error stopping recording: " + result.error, "error");
}
} catch (error) {
console.error("Error stopping recording:", error);
showToast("❌ Error stopping recording", "error");
}
} else {
// Start recording
try {
const response = await fetch("/api/start-recording", { method: "POST" });
const result = await response.json();
if (result.success) {
isRecording = true;
recordingStartTime = new Date();
document.getElementById("record-btn").classList.remove("btn-secondary");
document.getElementById("record-btn").classList.add("btn-danger");
document.getElementById("record-btn").innerHTML = "⏹️ Stop Recording";
showToast("πŸ”΄ Recording started", "success");
// Update recording duration every second
recordingInterval = setInterval(() => {
if (isRecording && recordingStartTime) {
const elapsed = Math.round(
(new Date() - recordingStartTime) / 1000,
);
document.getElementById("record-btn").innerHTML =
`⏹️ Stop (${elapsed}s)`;
}
}, 1000);
} else {
showToast("❌ Error starting recording: " + result.error, "error");
}
} catch (error) {
console.error("Error starting recording:", error);
showToast("❌ Error starting recording", "error");
}
}
}
// ─── Storage Drawer ──────────────────────────────────────────────────────────
let _storageData = { sessions: [], videos: [], images: [] };
let _activeStorageTab = "sessions";
async function openStorageManager() {
document.getElementById("storage-drawer").classList.add("open");
document.getElementById("storage-backdrop").classList.add("open");
await refreshStorageDrawer();
}
function closeStorageDrawer() {
document.getElementById("storage-drawer").classList.remove("open");
document.getElementById("storage-backdrop").classList.remove("open");
closeInlinePlayer();
}
async function refreshStorageDrawer() {
document.getElementById("sd-content").innerHTML =
'<div class="sd-empty"><div class="sd-empty-icon">⏳</div><p>Loading…</p></div>';
try {
const [sessRes, vidRes, imgRes] = await Promise.all([
fetch("/api/detection-history?type=session&limit=50").then((r) => r.json()),
fetch("/api/videos").then((r) => r.json()),
fetch("/api/images").then((r) => r.json()),
]);
_storageData.sessions = (sessRes.data || []).filter((d) => d.type === "session");
_storageData.videos = vidRes.videos || [];
_storageData.images = imgRes.images || [];
// Update tab labels with counts
document.getElementById("sd-tab-sessions").textContent = `πŸŽ₯ Sessions (${_storageData.sessions.length})`;
document.getElementById("sd-tab-recordings").textContent = `🎬 Recordings (${_storageData.videos.length})`;
document.getElementById("sd-tab-images").textContent = `πŸ“· Images (${_storageData.images.length})`;
// Stats row
const totalSecs = _storageData.sessions.reduce((a, s) => a + (s.duration || 0), 0);
const totalMins = Math.floor(totalSecs / 60);
document.getElementById("sd-stats-row").innerHTML = `
<div class="sd-stat"><strong>${_storageData.sessions.length}</strong> sessions</div>
<div class="sd-stat"><strong>${totalMins}m</strong> recorded</div>
<div class="sd-stat"><strong>${_storageData.images.length}</strong> detection shots</div>
`;
renderStorageTab(_activeStorageTab);
} catch (e) {
document.getElementById("sd-content").innerHTML =
'<div class="sd-empty"><div class="sd-empty-icon">⚠️</div><p>Failed to load storage data.</p></div>';
}
}
function switchStorageTab(tab) {
_activeStorageTab = tab;
["sessions", "recordings", "images"].forEach((t) => {
document.getElementById("sd-tab-" + t).classList.toggle("active", t === tab);
});
closeInlinePlayer();
renderStorageTab(tab);
}
function renderStorageTab(tab) {
const el = document.getElementById("sd-content");
if (tab === "sessions") el.innerHTML = buildSessionsHTML(_storageData.sessions);
if (tab === "recordings") el.innerHTML = buildRecordingsHTML(_storageData.videos);
if (tab === "images") el.innerHTML = buildImagesHTML(_storageData.images);
}
// ── Sessions ─────────────────────────────────────────────────────────────────
function buildSessionsHTML(sessions) {
if (sessions.length === 0) return `
<div class="sd-empty">
<div class="sd-empty-icon">πŸŽ₯</div>
<p>No sessions saved yet.<br><small>Sessions are recorded automatically when you stop the camera.</small></p>
</div>`;
return `<div class="sd-sessions-grid">${sessions.map(renderSessionCard).join("")}</div>`;
}
function renderSessionCard(s) {
const dur = s.duration || 0;
const durStr = `${Math.floor(dur / 60)}m ${String(dur % 60).padStart(2, "0")}s`;
const ts = new Date(s.created_at).toLocaleString();
const videoUrl = s.video_filename ? `/processed/${s.video_filename}` : null;
const previewUrl = s.preview_image ? `/processed/${s.preview_image}` : null;
const criticalBadge = s.is_critical ? `<span class="sd-badge critical">⚑ CRITICAL</span>` : "";
return `
<div class="sd-session-card" id="sess-card-${s.id}">
${previewUrl
? `<img class="sd-session-thumb" src="${previewUrl}" onerror="this.style.display='none'">`
: `<div class="sd-session-thumb-placeholder">πŸŽ₯</div>`}
<div class="sd-session-body">
<div class="sd-session-name" title="${s.session_name || "Session"}">${s.session_name || "Monitoring Session"}</div>
<div class="sd-session-meta">⏱ ${durStr} &nbsp;β€’&nbsp; ${ts}</div>
<div class="sd-session-badges">
<span class="sd-badge blue">πŸ” ${s.detection_count} detections</span>
${s.alert_count > 0 ? `<span class="sd-badge danger">🚨 ${s.alert_count} alerts</span>` : `<span class="sd-badge">βœ“ No alerts</span>`}
${criticalBadge}
</div>
<div class="sd-session-actions">
${videoUrl
? `<button class="sd-btn primary" onclick="playInline('${videoUrl}')">β–Ά Play</button>
<a class="sd-btn" href="${videoUrl}" download>⬇ Download</a>`
: `<span class="sd-btn" style="opacity:.4;cursor:default;">No video</span>`}
<button class="sd-btn danger" onclick="deleteSession(${s.id})">πŸ—‘</button>
</div>
</div>
</div>`;
}
async function deleteSession(id) {
if (!confirm("Delete this session and its video file?")) return;
const card = document.getElementById("sess-card-" + id);
if (card) card.style.opacity = "0.4";
try {
const r = await fetch(`/api/sessions/${id}`, { method: "DELETE" });
const d = await r.json();
if (d.success) {
showToast("Session deleted", "success");
_storageData.sessions = _storageData.sessions.filter((s) => s.id !== id);
document.getElementById("sd-tab-sessions").textContent = `πŸŽ₯ Sessions (${_storageData.sessions.length})`;
document.getElementById("sd-content").innerHTML = buildSessionsHTML(_storageData.sessions);
} else {
showToast("Delete failed: " + (d.error || "error"), "error");
if (card) card.style.opacity = "1";
}
} catch (e) {
showToast("Delete error", "error");
if (card) card.style.opacity = "1";
}
}
// ── Recordings ────────────────────────────────────────────────────────────────
function buildRecordingsHTML(videos) {
if (videos.length === 0) return `
<div class="sd-empty">
<div class="sd-empty-icon">🎬</div>
<p>No raw recordings yet.<br><small>Use the Record button during live monitoring.</small></p>
</div>`;
return videos.map((v) => {
const ts = new Date(v.created).toLocaleString();
return `
<div class="sd-recording-item" id="rec-${v.filename.replace(/\./g, '_')}">
<div class="sd-rec-icon">🎬</div>
<div style="flex:1;min-width:0;">
<div class="sd-rec-name">${v.filename}</div>
<div class="sd-rec-meta">${v.size} MB &nbsp;β€’&nbsp; ${ts}</div>
</div>
<div class="sd-rec-actions">
<button class="sd-icon-btn" title="Play" onclick="playInline('/api/download-video/${v.filename}')">β–Ά</button>
<a class="sd-icon-btn" href="/api/download-video/${v.filename}" download title="Download">⬇</a>
<button class="sd-icon-btn danger" title="Delete" onclick="deleteVideo('${v.filename}')">πŸ—‘</button>
</div>
</div>`;
}).join("");
}
async function deleteVideo(filename) {
if (!confirm(`Delete "${filename}"?`)) return;
try {
const r = await fetch(`/api/videos/${encodeURIComponent(filename)}`, { method: "DELETE" });
const d = await r.json();
if (d.success) {
showToast("Recording deleted", "success");
_storageData.videos = _storageData.videos.filter((v) => v.filename !== filename);
document.getElementById("sd-tab-recordings").textContent = `🎬 Recordings (${_storageData.videos.length})`;
document.getElementById("sd-content").innerHTML = buildRecordingsHTML(_storageData.videos);
} else {
showToast("Delete failed", "error");
}
} catch (e) {
showToast("Delete error", "error");
}
}
// ── Images ────────────────────────────────────────────────────────────────────
function buildImagesHTML(images) {
if (images.length === 0) return `
<div class="sd-empty">
<div class="sd-empty-icon">πŸ“·</div>
<p>No detection images yet.<br><small>High-risk detections are automatically captured.</small></p>
</div>`;
const tiles = images.map((img) => {
const levelColor = img.filename.includes('weapon') ? 'var(--danger)' :
img.filename.includes('violence') ? '#f97316' : 'var(--blue-600)';
return `
<div class="sd-img-tile" id="img-${img.filename.replace(/\./g,'_')}">
<img src="${img.path}" alt="Detection" loading="lazy" onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 1 1%22><rect fill=%22%23e2e8f0%22/></svg>'">
<div class="sd-img-overlay">
<button class="sd-img-overlay-btn" title="View" onclick="viewStorageImage('${img.filename}')">πŸ”</button>
<a class="sd-img-overlay-btn" href="/api/download-image/${img.filename}" download title="Download">⬇</a>
<button class="sd-img-overlay-btn" title="Delete" onclick="deleteImage('${img.filename}')">πŸ—‘</button>
</div>
<div class="sd-img-level" style="background:${levelColor};">${img.size}KB</div>
</div>`;
}).join("");
return `<div class="sd-img-grid">${tiles}</div>`;
}
function viewStorageImage(filename) {
const url = `/api/detection-image/${filename}`;
new Modal("πŸ“· Detection Image", `
<img src="${url}" alt="Detection" style="max-width:100%;max-height:500px;border-radius:8px;display:block;margin:0 auto;">
<div style="text-align:center;margin-top:1rem;">
<a href="/api/download-image/${filename}" class="btn btn-primary" download>⬇️ Download</a>
</div>`, { width: "580px" }).show();
}
async function deleteImage(filename) {
if (!confirm("Delete this detection image?")) return;
try {
const r = await fetch(`/api/images/${encodeURIComponent(filename)}`, { method: "DELETE" });
const d = await r.json();
if (d.success) {
showToast("Image deleted", "success");
_storageData.images = _storageData.images.filter((i) => i.filename !== filename);
document.getElementById("sd-tab-images").textContent = `πŸ“· Images (${_storageData.images.length})`;
document.getElementById("sd-content").innerHTML = buildImagesHTML(_storageData.images);
} else {
showToast("Delete failed", "error");
}
} catch (e) {
showToast("Delete error", "error");
}
}
// ── Inline player ─────────────────────────────────────────────────────────────
function playInline(url) {
const player = document.getElementById("sd-inline-player");
const vid = document.getElementById("sd-video-el");
vid.src = url;
player.style.display = "block";
vid.play().catch(() => {});
// Scroll player into view
player.scrollIntoView({ behavior: "smooth", block: "start" });
}
function closeInlinePlayer() {
const player = document.getElementById("sd-inline-player");
const vid = document.getElementById("sd-video-el");
vid.pause();
vid.src = "";
player.style.display = "none";
}
function viewImage(filename) {
viewStorageImage(filename);
}
function downloadAllImages() {
showToast("πŸ’‘ Download images individually from the Images tab", "info");
}
// Example: Add test alert (remove in production)
// setTimeout(() => {
// addAlert({
// type: 'OBJECT DETECTED',
// message: 'Person detected in frame',
// severity: 'MEDIUM',
// confidence: 0.95
// });
// }, 5000);