Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>SafetyMaster Pro - AI Safety Monitoring</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.js"></script> | |
| <style> | |
| :root { | |
| --primary-color: #2563eb; | |
| --primary-dark: #1d4ed8; | |
| --secondary-color: #64748b; | |
| --success-color: #10b981; | |
| --warning-color: #f59e0b; | |
| --danger-color: #ef4444; | |
| --bg-primary: #0f172a; | |
| --bg-secondary: #1e293b; | |
| --bg-tertiary: #334155; | |
| --text-primary: #f8fafc; | |
| --text-secondary: #cbd5e1; | |
| --text-muted: #94a3b8; | |
| --border-color: #334155; | |
| --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); | |
| --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); | |
| --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1); | |
| --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1); | |
| --border-radius: 12px; | |
| --border-radius-lg: 16px; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; | |
| background: var(--bg-primary); | |
| color: var(--text-primary); | |
| height: 100vh; | |
| overflow: hidden; | |
| line-height: 1.6; | |
| } | |
| .main-container { | |
| height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| position: relative; | |
| } | |
| /* Fullscreen Video Container */ | |
| .video-main { | |
| flex: 1; | |
| position: relative; | |
| background: #000; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| overflow: hidden; | |
| } | |
| .video-feed { | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .video-feed img { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| } | |
| .no-feed { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 1.5rem; | |
| color: var(--text-muted); | |
| text-align: center; | |
| } | |
| .no-feed i { | |
| font-size: 4rem; | |
| color: var(--text-muted); | |
| opacity: 0.5; | |
| } | |
| .no-feed h3 { | |
| font-size: 1.5rem; | |
| font-weight: 500; | |
| } | |
| .no-feed p { | |
| opacity: 0.7; | |
| } | |
| /* Floating Header */ | |
| .floating-header { | |
| position: absolute; | |
| top: 1.5rem; | |
| left: 1.5rem; | |
| right: 1.5rem; | |
| background: rgba(15, 23, 42, 0.8); | |
| backdrop-filter: blur(20px); | |
| border: 1px solid rgba(51, 65, 85, 0.3); | |
| border-radius: 16px; | |
| padding: 1rem 1.5rem; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| z-index: 100; | |
| transition: all 0.3s ease; | |
| } | |
| .floating-header:hover { | |
| background: rgba(15, 23, 42, 0.9); | |
| border-color: rgba(51, 65, 85, 0.5); | |
| } | |
| .header-left { | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| } | |
| .logo { | |
| width: 36px; | |
| height: 36px; | |
| background: linear-gradient(135deg, var(--primary-color), var(--primary-dark)); | |
| border-radius: 10px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: white; | |
| font-size: 1.25rem; | |
| } | |
| .header-title { | |
| font-size: 1.25rem; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| } | |
| .header-right { | |
| display: flex; | |
| align-items: center; | |
| gap: 1.5rem; | |
| } | |
| .status-badge { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| padding: 0.5rem 1rem; | |
| border-radius: 50px; | |
| font-size: 0.875rem; | |
| font-weight: 500; | |
| transition: all 0.3s ease; | |
| } | |
| .status-badge.connected { | |
| background: rgba(16, 185, 129, 0.15); | |
| color: var(--success-color); | |
| border: 1px solid rgba(16, 185, 129, 0.3); | |
| } | |
| .status-badge.disconnected { | |
| background: rgba(239, 68, 68, 0.15); | |
| color: var(--danger-color); | |
| border: 1px solid rgba(239, 68, 68, 0.3); | |
| } | |
| .status-indicator { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| background: currentColor; | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.5; } | |
| } | |
| /* Floating Stats */ | |
| .floating-stats { | |
| position: absolute; | |
| top: 6rem; | |
| left: 1.5rem; | |
| display: grid; | |
| grid-template-columns: repeat(4, 1fr); | |
| gap: 1rem; | |
| z-index: 90; | |
| } | |
| .mini-stat { | |
| background: rgba(15, 23, 42, 0.8); | |
| backdrop-filter: blur(20px); | |
| border: 1px solid rgba(51, 65, 85, 0.3); | |
| border-radius: 12px; | |
| padding: 1rem; | |
| min-width: 120px; | |
| text-align: center; | |
| transition: all 0.3s ease; | |
| } | |
| .mini-stat:hover { | |
| background: rgba(15, 23, 42, 0.9); | |
| transform: translateY(-2px); | |
| } | |
| .mini-stat-value { | |
| font-size: 1.75rem; | |
| font-weight: 700; | |
| margin-bottom: 0.25rem; | |
| } | |
| .mini-stat-label { | |
| font-size: 0.75rem; | |
| color: var(--text-muted); | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| } | |
| .mini-stat.success .mini-stat-value { | |
| color: var(--success-color); | |
| } | |
| .mini-stat.warning .mini-stat-value { | |
| color: var(--warning-color); | |
| } | |
| .mini-stat.danger .mini-stat-value { | |
| color: var(--danger-color); | |
| } | |
| /* Floating Controls */ | |
| .floating-controls { | |
| position: absolute; | |
| bottom: 1.5rem; | |
| left: 1.5rem; | |
| background: rgba(15, 23, 42, 0.8); | |
| backdrop-filter: blur(20px); | |
| border: 1px solid rgba(51, 65, 85, 0.3); | |
| border-radius: 16px; | |
| padding: 1.5rem; | |
| z-index: 100; | |
| transition: all 0.3s ease; | |
| transform: translateY(0); | |
| } | |
| .floating-controls.collapsed { | |
| transform: translateY(calc(100% - 60px)); | |
| } | |
| .controls-toggle { | |
| position: absolute; | |
| top: -15px; | |
| right: 20px; | |
| background: rgba(37, 99, 235, 0.9); | |
| border: none; | |
| border-radius: 50px; | |
| width: 30px; | |
| height: 30px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: white; | |
| cursor: pointer; | |
| font-size: 0.875rem; | |
| transition: all 0.3s ease; | |
| } | |
| .controls-toggle:hover { | |
| background: var(--primary-dark); | |
| transform: scale(1.1); | |
| } | |
| .controls-content { | |
| display: flex; | |
| align-items: center; | |
| gap: 1.5rem; | |
| } | |
| .control-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.5rem; | |
| } | |
| .control-label { | |
| font-size: 0.75rem; | |
| color: var(--text-muted); | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| } | |
| .control-input { | |
| padding: 0.5rem 0.75rem; | |
| background: rgba(51, 65, 85, 0.5); | |
| border: 1px solid rgba(51, 65, 85, 0.5); | |
| border-radius: 8px; | |
| color: var(--text-primary); | |
| font-size: 0.875rem; | |
| width: 120px; | |
| } | |
| .control-input:focus { | |
| outline: none; | |
| border-color: var(--primary-color); | |
| box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2); | |
| } | |
| .range-input { | |
| -webkit-appearance: none; | |
| height: 4px; | |
| background: var(--bg-tertiary); | |
| border-radius: 2px; | |
| outline: none; | |
| width: 120px; | |
| } | |
| .range-input::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 16px; | |
| height: 16px; | |
| background: var(--primary-color); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| } | |
| .btn { | |
| padding: 0.75rem 1.5rem; | |
| border: none; | |
| border-radius: 10px; | |
| font-size: 0.875rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, var(--primary-color), var(--primary-dark)); | |
| color: white; | |
| } | |
| .btn-primary:hover:not(:disabled) { | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 20px rgba(37, 99, 235, 0.3); | |
| } | |
| .btn-danger { | |
| background: linear-gradient(135deg, var(--danger-color), #dc2626); | |
| color: white; | |
| } | |
| .btn-danger:hover:not(:disabled) { | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 20px rgba(239, 68, 68, 0.3); | |
| } | |
| .btn:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| transform: none ; | |
| } | |
| /* Floating FPS */ | |
| .floating-fps { | |
| position: absolute; | |
| top: 1.5rem; | |
| right: 1.5rem; | |
| background: rgba(15, 23, 42, 0.8); | |
| backdrop-filter: blur(20px); | |
| border: 1px solid rgba(51, 65, 85, 0.3); | |
| border-radius: 12px; | |
| padding: 0.75rem 1rem; | |
| font-size: 0.875rem; | |
| font-weight: 600; | |
| color: var(--primary-color); | |
| z-index: 100; | |
| } | |
| /* Floating Violations */ | |
| .floating-violations { | |
| position: absolute; | |
| bottom: 1.5rem; | |
| right: 1.5rem; | |
| width: 300px; | |
| max-height: 400px; | |
| background: rgba(15, 23, 42, 0.8); | |
| backdrop-filter: blur(20px); | |
| border: 1px solid rgba(51, 65, 85, 0.3); | |
| border-radius: 16px; | |
| overflow: hidden; | |
| z-index: 100; | |
| transition: all 0.3s ease; | |
| transform: translateX(0); | |
| } | |
| .floating-violations.collapsed { | |
| transform: translateX(calc(100% - 60px)); | |
| } | |
| .violations-header { | |
| padding: 1rem 1.5rem; | |
| border-bottom: 1px solid rgba(51, 65, 85, 0.3); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| .violations-title { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| font-size: 1rem; | |
| font-weight: 600; | |
| } | |
| .violations-badge { | |
| background: rgba(239, 68, 68, 0.2); | |
| color: var(--danger-color); | |
| padding: 0.25rem 0.5rem; | |
| border-radius: 6px; | |
| font-size: 0.75rem; | |
| font-weight: 600; | |
| } | |
| .violations-toggle { | |
| background: none; | |
| border: none; | |
| color: var(--text-muted); | |
| cursor: pointer; | |
| padding: 0.25rem; | |
| border-radius: 4px; | |
| transition: all 0.3s ease; | |
| } | |
| .violations-toggle:hover { | |
| background: rgba(51, 65, 85, 0.5); | |
| color: var(--text-primary); | |
| } | |
| .violations-content { | |
| max-height: 300px; | |
| overflow-y: auto; | |
| padding: 1rem; | |
| } | |
| .violation-item { | |
| background: rgba(239, 68, 68, 0.1); | |
| border: 1px solid rgba(239, 68, 68, 0.2); | |
| border-radius: 10px; | |
| padding: 1rem; | |
| margin-bottom: 0.75rem; | |
| opacity: 0; | |
| animation: violationSlideIn 0.5s ease-out forwards; | |
| transition: all 0.3s ease; | |
| } | |
| .violation-item:hover { | |
| background: rgba(239, 68, 68, 0.15); | |
| transform: translateX(-4px); | |
| } | |
| .violation-item:last-child { | |
| margin-bottom: 0; | |
| } | |
| @keyframes violationSlideIn { | |
| from { | |
| opacity: 0; | |
| transform: translateY(20px) scale(0.95); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0) scale(1); | |
| } | |
| } | |
| .violation-header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| margin-bottom: 0.5rem; | |
| } | |
| .violation-time { | |
| font-size: 0.75rem; | |
| color: var(--text-muted); | |
| font-weight: 500; | |
| } | |
| .violation-severity { | |
| padding: 0.25rem 0.5rem; | |
| border-radius: 4px; | |
| font-size: 0.75rem; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| } | |
| .violation-severity.high { | |
| background: rgba(239, 68, 68, 0.2); | |
| color: var(--danger-color); | |
| } | |
| .violation-description { | |
| font-size: 0.875rem; | |
| color: var(--text-secondary); | |
| line-height: 1.4; | |
| } | |
| .no-violations { | |
| text-align: center; | |
| color: var(--text-muted); | |
| padding: 2rem 1rem; | |
| } | |
| .no-violations i { | |
| font-size: 2rem; | |
| margin-bottom: 1rem; | |
| color: var(--success-color); | |
| opacity: 0.5; | |
| } | |
| /* Loading Animation */ | |
| .loading { | |
| display: inline-block; | |
| width: 16px; | |
| height: 16px; | |
| border: 2px solid rgba(255, 255, 255, 0.3); | |
| border-radius: 50%; | |
| border-top-color: currentColor; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| to { | |
| transform: rotate(360deg); | |
| } | |
| } | |
| /* Scrollbar Styling */ | |
| ::-webkit-scrollbar { | |
| width: 4px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: rgba(100, 116, 139, 0.5); | |
| border-radius: 2px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: rgba(100, 116, 139, 0.7); | |
| } | |
| /* Responsive Design */ | |
| @media (max-width: 1024px) { | |
| .floating-stats { | |
| grid-template-columns: repeat(2, 1fr); | |
| top: 5rem; | |
| } | |
| .floating-violations { | |
| width: 280px; | |
| } | |
| } | |
| @media (max-width: 768px) { | |
| .floating-header { | |
| top: 1rem; | |
| left: 1rem; | |
| right: 1rem; | |
| padding: 0.75rem 1rem; | |
| } | |
| .floating-stats { | |
| top: 4.5rem; | |
| left: 1rem; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 0.75rem; | |
| } | |
| .mini-stat { | |
| padding: 0.75rem; | |
| min-width: 100px; | |
| } | |
| .floating-controls { | |
| bottom: 1rem; | |
| left: 1rem; | |
| right: 1rem; | |
| padding: 1rem; | |
| } | |
| .controls-content { | |
| flex-wrap: wrap; | |
| gap: 1rem; | |
| } | |
| .floating-violations { | |
| bottom: 1rem; | |
| right: 1rem; | |
| width: 260px; | |
| } | |
| } | |
| /* Fade in animation for page load */ | |
| .main-container { | |
| animation: fadeIn 0.5s ease-out; | |
| } | |
| @keyframes fadeIn { | |
| from { | |
| opacity: 0; | |
| } | |
| to { | |
| opacity: 1; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="main-container"> | |
| <!-- Fullscreen Video --> | |
| <div class="video-main"> | |
| <div class="video-feed" id="videoFeed"> | |
| <div class="no-feed"> | |
| <i class="fas fa-video-slash"></i> | |
| <h3>SafetyMaster Pro</h3> | |
| <p>Click "Start" to begin AI safety monitoring</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Floating Header --> | |
| <div class="floating-header"> | |
| <div class="header-left"> | |
| <div class="logo"> | |
| <i class="fas fa-shield-alt"></i> | |
| </div> | |
| <div class="header-title">SafetyMaster Pro</div> | |
| </div> | |
| <div class="header-right"> | |
| <div class="status-badge" id="statusBadge"> | |
| <div class="status-indicator"></div> | |
| <span id="statusText">Disconnected</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Floating FPS Counter --> | |
| <div class="floating-fps" id="fpsCounter">FPS: 0</div> | |
| <!-- Floating Statistics --> | |
| <div class="floating-stats"> | |
| <div class="mini-stat"> | |
| <div class="mini-stat-value" id="totalPeople">0</div> | |
| <div class="mini-stat-label">People</div> | |
| </div> | |
| <div class="mini-stat success"> | |
| <div class="mini-stat-value" id="compliantPeople">0</div> | |
| <div class="mini-stat-label">Compliant</div> | |
| </div> | |
| <div class="mini-stat danger"> | |
| <div class="mini-stat-value" id="violationCount">0</div> | |
| <div class="mini-stat-label">Violations</div> | |
| </div> | |
| <div class="mini-stat warning"> | |
| <div class="mini-stat-value" id="complianceRate">0%</div> | |
| <div class="mini-stat-label">Compliance</div> | |
| </div> | |
| </div> | |
| <!-- Floating Controls --> | |
| <div class="floating-controls" id="floatingControls"> | |
| <button class="controls-toggle" id="controlsToggle"> | |
| <i class="fas fa-chevron-down"></i> | |
| </button> | |
| <div class="controls-content"> | |
| <div class="control-group"> | |
| <label class="control-label">Camera</label> | |
| <input type="number" class="control-input" id="cameraSource" value="0" min="0"> | |
| </div> | |
| <div class="control-group"> | |
| <label class="control-label">Confidence: <span id="confidenceValue">0.5</span></label> | |
| <input type="range" class="control-input range-input" id="confidenceSlider" min="0.1" max="1" step="0.1" value="0.5"> | |
| </div> | |
| <div class="control-group"> | |
| <button class="btn btn-primary" id="startBtn"> | |
| <i class="fas fa-play"></i> | |
| Start | |
| </button> | |
| </div> | |
| <div class="control-group"> | |
| <button class="btn btn-danger" id="stopBtn" disabled> | |
| <i class="fas fa-stop"></i> | |
| Stop | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Floating Violations --> | |
| <div class="floating-violations" id="floatingViolations"> | |
| <div class="violations-header"> | |
| <div class="violations-title"> | |
| <i class="fas fa-exclamation-triangle"></i> | |
| Violations | |
| <span class="violations-badge" id="violationBadge">0</span> | |
| </div> | |
| <button class="violations-toggle" id="violationsToggle"> | |
| <i class="fas fa-chevron-right"></i> | |
| </button> | |
| </div> | |
| <div class="violations-content" id="violationsList"> | |
| <div class="no-violations"> | |
| <i class="fas fa-shield-check"></i> | |
| <div>All Clear</div> | |
| <small>No safety violations detected</small> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Initialize Socket.IO connection | |
| const socket = io(); | |
| // DOM elements | |
| const statusBadge = document.getElementById('statusBadge'); | |
| const statusText = document.getElementById('statusText'); | |
| const videoFeed = document.getElementById('videoFeed'); | |
| const fpsCounter = document.getElementById('fpsCounter'); | |
| const startBtn = document.getElementById('startBtn'); | |
| const stopBtn = document.getElementById('stopBtn'); | |
| const cameraSource = document.getElementById('cameraSource'); | |
| const confidenceSlider = document.getElementById('confidenceSlider'); | |
| const confidenceValue = document.getElementById('confidenceValue'); | |
| const violationsList = document.getElementById('violationsList'); | |
| const violationBadge = document.getElementById('violationBadge'); | |
| // Floating panel elements | |
| const controlsToggle = document.getElementById('controlsToggle'); | |
| const floatingControls = document.getElementById('floatingControls'); | |
| const violationsToggle = document.getElementById('violationsToggle'); | |
| const floatingViolations = document.getElementById('floatingViolations'); | |
| // Statistics elements | |
| const totalPeople = document.getElementById('totalPeople'); | |
| const compliantPeople = document.getElementById('compliantPeople'); | |
| const violationCount = document.getElementById('violationCount'); | |
| const complianceRate = document.getElementById('complianceRate'); | |
| // State variables | |
| let isMonitoring = false; | |
| let frameCount = 0; | |
| let lastFpsUpdate = Date.now(); | |
| let violationsData = []; | |
| let violationIds = new Set(); // Track violation IDs to prevent duplicates | |
| // Socket event handlers | |
| socket.on('connect', function() { | |
| console.log('Connected to server'); | |
| updateConnectionStatus(true); | |
| }); | |
| socket.on('disconnect', function() { | |
| console.log('Disconnected from server'); | |
| updateConnectionStatus(false); | |
| }); | |
| socket.on('video_frame', function(data) { | |
| updateVideoFeed(data); | |
| updateStatistics(data); | |
| updateFPS(); | |
| }); | |
| socket.on('violation_alert', function(data) { | |
| addViolationAlert(data); | |
| }); | |
| socket.on('status', function(data) { | |
| console.log('Status update:', data); | |
| }); | |
| // UI event handlers | |
| startBtn.addEventListener('click', startMonitoring); | |
| stopBtn.addEventListener('click', stopMonitoring); | |
| confidenceSlider.addEventListener('input', function() { | |
| confidenceValue.textContent = this.value; | |
| }); | |
| // Floating panel toggles | |
| controlsToggle.addEventListener('click', function() { | |
| floatingControls.classList.toggle('collapsed'); | |
| const icon = this.querySelector('i'); | |
| if (floatingControls.classList.contains('collapsed')) { | |
| icon.classList.replace('fa-chevron-down', 'fa-chevron-up'); | |
| } else { | |
| icon.classList.replace('fa-chevron-up', 'fa-chevron-down'); | |
| } | |
| }); | |
| violationsToggle.addEventListener('click', function() { | |
| floatingViolations.classList.toggle('collapsed'); | |
| const icon = this.querySelector('i'); | |
| if (floatingViolations.classList.contains('collapsed')) { | |
| icon.classList.replace('fa-chevron-right', 'fa-chevron-left'); | |
| } else { | |
| icon.classList.replace('fa-chevron-left', 'fa-chevron-right'); | |
| } | |
| }); | |
| // Functions | |
| function updateConnectionStatus(connected) { | |
| if (connected) { | |
| statusBadge.classList.remove('disconnected'); | |
| statusBadge.classList.add('connected'); | |
| statusText.textContent = 'Connected'; | |
| } else { | |
| statusBadge.classList.remove('connected'); | |
| statusBadge.classList.add('disconnected'); | |
| statusText.textContent = 'Disconnected'; | |
| } | |
| } | |
| function updateVideoFeed(data) { | |
| const img = new Image(); | |
| img.onload = function() { | |
| videoFeed.innerHTML = ''; | |
| videoFeed.appendChild(img); | |
| }; | |
| img.onerror = function() { | |
| showNoFeed(); | |
| }; | |
| img.src = 'data:image/jpeg;base64,' + data.frame; | |
| } | |
| function showNoFeed() { | |
| videoFeed.innerHTML = ` | |
| <div class="no-feed"> | |
| <i class="fas fa-video-slash"></i> | |
| <h3>Camera Disconnected</h3> | |
| <p>Check camera connection and try again</p> | |
| </div> | |
| `; | |
| } | |
| function updateStatistics(data) { | |
| totalPeople.textContent = data.people_count || 0; | |
| // Calculate compliant people (people - violations) | |
| const violationsLength = (data.violations || []).length; | |
| const compliantCount = Math.max(0, (data.people_count || 0) - violationsLength); | |
| compliantPeople.textContent = compliantCount; | |
| violationCount.textContent = violationsLength; | |
| // Calculate compliance rate | |
| const totalPeopleCount = data.people_count || 0; | |
| const compliancePercentage = totalPeopleCount > 0 ? | |
| (compliantCount / totalPeopleCount * 100) : 100; | |
| complianceRate.textContent = compliancePercentage.toFixed(0) + '%'; | |
| // Update violations if present (with duplicate prevention) | |
| if (data.violations && data.violations.length > 0) { | |
| data.violations.forEach(violation => { | |
| const violationId = `${violation.type}_${violation.description}_${Math.floor(Date.now() / 5000)}`; // Group by 5-second intervals | |
| if (!violationIds.has(violationId)) { | |
| violationIds.add(violationId); | |
| addViolationAlert({ | |
| id: violationId, | |
| timestamp: new Date().toISOString(), | |
| type: violation.type, | |
| description: violation.description, | |
| severity: violation.severity || 'high' | |
| }); | |
| // Clean up old IDs after 30 seconds | |
| setTimeout(() => { | |
| violationIds.delete(violationId); | |
| }, 30000); | |
| } | |
| }); | |
| } | |
| } | |
| function updateFPS() { | |
| frameCount++; | |
| const now = Date.now(); | |
| if (now - lastFpsUpdate >= 1000) { | |
| const fps = Math.round(frameCount * 1000 / (now - lastFpsUpdate)); | |
| fpsCounter.textContent = `FPS: ${fps}`; | |
| frameCount = 0; | |
| lastFpsUpdate = now; | |
| } | |
| } | |
| function addViolationAlert(violation) { | |
| violationsData.unshift(violation); | |
| if (violationsData.length > 5) { | |
| violationsData = violationsData.slice(0, 5); | |
| } | |
| renderViolations(); | |
| updateViolationBadge(); | |
| } | |
| function renderViolations() { | |
| if (violationsData.length === 0) { | |
| violationsList.innerHTML = ` | |
| <div class="no-violations"> | |
| <i class="fas fa-shield-check"></i> | |
| <div>All Clear</div> | |
| <small>No safety violations detected</small> | |
| </div> | |
| `; | |
| return; | |
| } | |
| violationsList.innerHTML = violationsData.map((violation, index) => ` | |
| <div class="violation-item" style="animation-delay: ${index * 0.1}s"> | |
| <div class="violation-header"> | |
| <div class="violation-time">${formatTime(violation.timestamp)}</div> | |
| <div class="violation-severity ${violation.severity || 'high'}">${violation.severity || 'HIGH'}</div> | |
| </div> | |
| <div class="violation-description"> | |
| <strong>${violation.type || 'Safety Violation'}</strong><br> | |
| ${violation.description || 'Missing safety equipment detected'} | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| function formatTime(timestamp) { | |
| return new Date(timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); | |
| } | |
| function updateViolationBadge() { | |
| violationBadge.textContent = violationsData.length; | |
| } | |
| function setLoadingState(button, loading) { | |
| if (loading) { | |
| button.innerHTML = button.innerHTML.replace(/<i[^>]*><\/i>/, '<div class="loading"></div>'); | |
| button.disabled = true; | |
| } else { | |
| // Restore original icon based on button | |
| if (button === startBtn) { | |
| button.innerHTML = '<i class="fas fa-play"></i> Start'; | |
| } else if (button === stopBtn) { | |
| button.innerHTML = '<i class="fas fa-stop"></i> Stop'; | |
| } | |
| } | |
| } | |
| function startMonitoring() { | |
| const source = parseInt(cameraSource.value) || 0; | |
| const confidence = parseFloat(confidenceSlider.value); | |
| setLoadingState(startBtn, true); | |
| fetch('/api/start_monitoring', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| camera_source: source, | |
| confidence: confidence | |
| }) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success) { | |
| isMonitoring = true; | |
| updateUI(); | |
| console.log('Monitoring started:', data); | |
| } else { | |
| alert('Failed to start monitoring: ' + data.message); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Error:', error); | |
| alert('Failed to start monitoring'); | |
| }) | |
| .finally(() => { | |
| setLoadingState(startBtn, false); | |
| updateUI(); | |
| }); | |
| } | |
| function stopMonitoring() { | |
| setLoadingState(stopBtn, true); | |
| fetch('/api/stop_monitoring', { | |
| method: 'POST' | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success) { | |
| isMonitoring = false; | |
| updateUI(); | |
| showNoFeed(); | |
| fpsCounter.textContent = 'FPS: 0'; | |
| // Reset statistics | |
| totalPeople.textContent = '0'; | |
| compliantPeople.textContent = '0'; | |
| violationCount.textContent = '0'; | |
| complianceRate.textContent = '0%'; | |
| // Clear violations | |
| violationsData = []; | |
| violationIds.clear(); | |
| renderViolations(); | |
| updateViolationBadge(); | |
| console.log('Monitoring stopped:', data); | |
| } else { | |
| alert('Failed to stop monitoring: ' + data.message); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Error:', error); | |
| alert('Failed to stop monitoring'); | |
| }) | |
| .finally(() => { | |
| setLoadingState(stopBtn, false); | |
| updateUI(); | |
| }); | |
| } | |
| function updateUI() { | |
| startBtn.disabled = isMonitoring; | |
| stopBtn.disabled = !isMonitoring; | |
| } | |
| // Load initial violations | |
| fetch('/api/violations') | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success) { | |
| violationsData = data.violations || []; | |
| renderViolations(); | |
| updateViolationBadge(); | |
| } | |
| }) | |
| .catch(error => console.error('Error loading violations:', error)); | |
| // Initial UI update | |
| updateUI(); | |
| </script> | |
| </body> | |
| </html> |