Spaces:
Paused
Paused
docker deployment
Browse files- All_weapon.pt +3 -0
- Dockerfile +35 -0
- app.py +6 -0
- face_mask.pt +3 -0
- packages.txt +1 -0
- requirements.txt +11 -0
- sentinel_app_v13.py +1337 -0
- sort.py +330 -0
- templates/gun_index.html +581 -0
- templates/index.html +428 -0
- templates/movement_index.html +429 -0
- templates/sentinel_dashboard.html +403 -0
- templates/sentinel_dashboard_v10.html +1385 -0
- templates/sentinel_dashboard_v11.html +1413 -0
- templates/sentinel_dashboard_v11_partial.html +1382 -0
- templates/sentinel_dashboard_v12.html +1454 -0
- templates/sentinel_dashboard_v13.html +2154 -0
- templates/sentinel_dashboard_v15.html +2253 -0
- templates/sentinel_dashboard_v2.html +624 -0
- templates/sentinel_dashboard_v3.html +573 -0
- templates/sentinel_dashboard_v4.html +573 -0
- templates/sentinel_dashboard_v7.html +1386 -0
- templates/sentinel_dashboard_v9.html +1385 -0
- yolov8n.pt +3 -0
All_weapon.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:47836d3c3a614948197aa4bc8cf95b1e572ba07f10486498d14c67bcab4bb7cf
|
| 3 |
+
size 155664349
|
Dockerfile
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use Python 3.9
|
| 2 |
+
FROM python:3.9
|
| 3 |
+
|
| 4 |
+
# Install system dependencies for OpenCV and GLib
|
| 5 |
+
USER root
|
| 6 |
+
RUN apt-get update && apt-get install -y \
|
| 7 |
+
libgl1 \
|
| 8 |
+
libglib2.0-0 \
|
| 9 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
+
|
| 11 |
+
# Create a non-root user matching HF Spaces UID (1000)
|
| 12 |
+
RUN useradd -m -u 1000 user
|
| 13 |
+
ENV HOME=/home/user \
|
| 14 |
+
PATH=/home/user/.local/bin:$PATH \
|
| 15 |
+
YOLO_CONFIG_DIR=/tmp/Ultralytics \
|
| 16 |
+
MPLCONFIGDIR=/tmp/matplotlib
|
| 17 |
+
|
| 18 |
+
# Set working directory to user's home/app (Standard for spaces)
|
| 19 |
+
WORKDIR $HOME/app
|
| 20 |
+
|
| 21 |
+
# Copy only requirements first to cache dependencies
|
| 22 |
+
COPY --chown=user requirements.txt $HOME/app/requirements.txt
|
| 23 |
+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
| 24 |
+
|
| 25 |
+
# Copy the rest of the application with correct ownership
|
| 26 |
+
COPY --chown=user . $HOME/app
|
| 27 |
+
|
| 28 |
+
# ensure uploads directory exists and is writable
|
| 29 |
+
RUN mkdir -p uploads && chmod 777 uploads
|
| 30 |
+
|
| 31 |
+
# Run as the user
|
| 32 |
+
USER user
|
| 33 |
+
|
| 34 |
+
# Command to run the application
|
| 35 |
+
CMD ["python", "app.py"]
|
app.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sentinel_app_v13 import app
|
| 2 |
+
|
| 3 |
+
if __name__ == '__main__':
|
| 4 |
+
# Hugging Face Spaces Docker (and our requirements) expects the app to run on port 7860
|
| 5 |
+
# Port 8080 is reserved for local, so we won't conflict.
|
| 6 |
+
app.run(host='0.0.0.0', port=7860, debug=False)
|
face_mask.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a9e7e8b18cfa8a154eb522f2b68b9ec7234ac48107f3089c71980f3172f0ada7
|
| 3 |
+
size 6240056
|
packages.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
libgl1
|
requirements.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flask
|
| 2 |
+
opencv-python-headless
|
| 3 |
+
numpy
|
| 4 |
+
ultralytics
|
| 5 |
+
google-generativeai
|
| 6 |
+
filterpy
|
| 7 |
+
scikit-image
|
| 8 |
+
matplotlib
|
| 9 |
+
scipy
|
| 10 |
+
lap
|
| 11 |
+
requests
|
sentinel_app_v13.py
ADDED
|
@@ -0,0 +1,1337 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os, math, time, datetime, threading, json, uuid
|
| 2 |
+
import requests as http_requests
|
| 3 |
+
from collections import deque
|
| 4 |
+
from flask import Flask, render_template, Response, jsonify, request
|
| 5 |
+
from werkzeug.utils import secure_filename
|
| 6 |
+
import cv2
|
| 7 |
+
import numpy as np
|
| 8 |
+
from ultralytics import YOLO
|
| 9 |
+
from sort import Sort
|
| 10 |
+
import google.generativeai as genai
|
| 11 |
+
from PIL import Image
|
| 12 |
+
import torch
|
| 13 |
+
import torchvision.transforms as T
|
| 14 |
+
from torchvision.models import resnet18, ResNet18_Weights
|
| 15 |
+
|
| 16 |
+
# Configure Gemini
|
| 17 |
+
GENAI_API_KEY = ''
|
| 18 |
+
genai.configure(api_key=GENAI_API_KEY)
|
| 19 |
+
model_gemini = genai.GenerativeModel('gemini-2.5-flash')
|
| 20 |
+
|
| 21 |
+
app = Flask(__name__)
|
| 22 |
+
app.config['UPLOAD_FOLDER'] = 'uploads'
|
| 23 |
+
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
| 24 |
+
|
| 25 |
+
# Load models
|
| 26 |
+
model_movement = YOLO('yolov8n.pt')
|
| 27 |
+
model_facemask = YOLO('face_mask.pt')
|
| 28 |
+
model_weapon = YOLO('All_weapon.pt')
|
| 29 |
+
|
| 30 |
+
# Re-ID Embedding Extractor (ResNet18)
|
| 31 |
+
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| 32 |
+
reid_model = resnet18(weights=ResNet18_Weights.DEFAULT)
|
| 33 |
+
reid_model.fc = torch.nn.Identity() # Remove classification layer to get embeddings
|
| 34 |
+
reid_model.to(device)
|
| 35 |
+
reid_model.eval()
|
| 36 |
+
|
| 37 |
+
reid_transform = T.Compose([
|
| 38 |
+
T.ToPILImage(),
|
| 39 |
+
T.Resize((128, 64)),
|
| 40 |
+
T.ToTensor(),
|
| 41 |
+
T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
|
| 42 |
+
])
|
| 43 |
+
|
| 44 |
+
print(f"All Models Loaded (V13 - Journey Tracker). Device: {device}")
|
| 45 |
+
|
| 46 |
+
# ════════════════════════════════════════════════════════════════
|
| 47 |
+
# GLOBAL STATE
|
| 48 |
+
# ════════════════════════════════════════════════════════════════
|
| 49 |
+
# Multi-module system: set of active detection modules
|
| 50 |
+
active_modules = {'movement'} # Default: movement active
|
| 51 |
+
red_alert = False
|
| 52 |
+
|
| 53 |
+
# Per-feed video source management
|
| 54 |
+
# feed_id -> filepath (None means no source / webcam for feed 0)
|
| 55 |
+
feed_sources = {} # e.g. {0: '/path/to/clip1.mp4', 1: '/path/to/clip2.mp4'}
|
| 56 |
+
|
| 57 |
+
# Multi-camera feed management
|
| 58 |
+
camera_feeds = {}
|
| 59 |
+
feed_versions = {}
|
| 60 |
+
MAX_FEEDS = 4
|
| 61 |
+
|
| 62 |
+
# Multi-camera tracking
|
| 63 |
+
feed_trackers = {i: Sort() for i in range(MAX_FEEDS)}
|
| 64 |
+
person_history = {}
|
| 65 |
+
object_history = {}
|
| 66 |
+
journey_log = deque(maxlen=500)
|
| 67 |
+
|
| 68 |
+
class SubjectIdentityManager:
|
| 69 |
+
def __init__(self, threshold=0.7):
|
| 70 |
+
self.threshold = threshold
|
| 71 |
+
self.global_subjects = {} # global_id -> {'embedding': tensor, 'first_seen': timestamp}
|
| 72 |
+
self.local_to_global = {} # (feed_id, local_id) -> {'global_id': int, 'last_seen': float}
|
| 73 |
+
self.next_global_id = 100
|
| 74 |
+
self.lock = threading.Lock()
|
| 75 |
+
|
| 76 |
+
def get_embedding(self, face_img):
|
| 77 |
+
img_t = reid_transform(face_img).unsqueeze(0).to(device)
|
| 78 |
+
with torch.no_grad():
|
| 79 |
+
emb = reid_model(img_t)
|
| 80 |
+
return emb / emb.norm() # Normalize
|
| 81 |
+
|
| 82 |
+
def match_or_register(self, feed_id, local_id, frame, bbox):
|
| 83 |
+
x1, y1, x2, y2 = map(int, bbox)
|
| 84 |
+
h, w = frame.shape[:2]
|
| 85 |
+
x1, y1 = max(0, x1), max(0, y1)
|
| 86 |
+
x2, y2 = min(w, x2), min(h, y2)
|
| 87 |
+
|
| 88 |
+
if x2 <= x1 or y2 <= y1:
|
| 89 |
+
return None
|
| 90 |
+
|
| 91 |
+
crop = frame[y1:y2, x1:x2]
|
| 92 |
+
if crop.size == 0:
|
| 93 |
+
return None
|
| 94 |
+
|
| 95 |
+
key = (feed_id, local_id)
|
| 96 |
+
now = time.time()
|
| 97 |
+
with self.lock:
|
| 98 |
+
# Use cached mapping if fresh (< 3 seconds), otherwise re-evaluate
|
| 99 |
+
if key in self.local_to_global:
|
| 100 |
+
entry = self.local_to_global[key]
|
| 101 |
+
if now - entry['last_seen'] < 3.0:
|
| 102 |
+
entry['last_seen'] = now
|
| 103 |
+
return entry['global_id']
|
| 104 |
+
# Stale — re-evaluate embedding
|
| 105 |
+
|
| 106 |
+
new_emb = self.get_embedding(crop)
|
| 107 |
+
|
| 108 |
+
best_id = None
|
| 109 |
+
best_sim = -1
|
| 110 |
+
|
| 111 |
+
for gid, data in self.global_subjects.items():
|
| 112 |
+
sim = torch.mm(new_emb, data['embedding'].t()).item()
|
| 113 |
+
if sim > best_sim:
|
| 114 |
+
best_sim = sim
|
| 115 |
+
best_id = gid
|
| 116 |
+
|
| 117 |
+
if best_sim > self.threshold:
|
| 118 |
+
match_id = best_id
|
| 119 |
+
# Update embedding (moving average)
|
| 120 |
+
self.global_subjects[match_id]['embedding'] = (
|
| 121 |
+
0.9 * self.global_subjects[match_id]['embedding'] + 0.1 * new_emb
|
| 122 |
+
)
|
| 123 |
+
else:
|
| 124 |
+
match_id = self.next_global_id
|
| 125 |
+
self.next_global_id += 1
|
| 126 |
+
self.global_subjects[match_id] = {
|
| 127 |
+
'embedding': new_emb,
|
| 128 |
+
'first_seen': now
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
self.local_to_global[key] = {'global_id': match_id, 'last_seen': now}
|
| 132 |
+
return match_id
|
| 133 |
+
|
| 134 |
+
def cleanup_feed(self, feed_id, active_local_ids):
|
| 135 |
+
"""Remove stale local-to-global mappings for tracks no longer active."""
|
| 136 |
+
with self.lock:
|
| 137 |
+
stale = [k for k in self.local_to_global if k[0] == feed_id and k[1] not in active_local_ids]
|
| 138 |
+
for k in stale:
|
| 139 |
+
del self.local_to_global[k]
|
| 140 |
+
|
| 141 |
+
def get_global_id_cached(self, feed_id, local_id):
|
| 142 |
+
"""Get cached global_id without re-evaluating (for re-id check)."""
|
| 143 |
+
key = (feed_id, local_id)
|
| 144 |
+
entry = self.local_to_global.get(key)
|
| 145 |
+
return entry['global_id'] if entry else None
|
| 146 |
+
|
| 147 |
+
identity_manager = SubjectIdentityManager()
|
| 148 |
+
previous_score = 0.0
|
| 149 |
+
ALPHA = 0.2
|
| 150 |
+
|
| 151 |
+
# ════════════════════════════════════════════════════════════════
|
| 152 |
+
# OPERATOR AUDIT LOG
|
| 153 |
+
# ════════════════════════════════════════════════════════════════
|
| 154 |
+
audit_log = deque(maxlen=200)
|
| 155 |
+
audit_lock = threading.Lock()
|
| 156 |
+
|
| 157 |
+
def log_audit(action, details="", severity="INFO"):
|
| 158 |
+
"""Append an event to the operator audit log."""
|
| 159 |
+
entry = {
|
| 160 |
+
'timestamp': datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
| 161 |
+
'action': action,
|
| 162 |
+
'details': details,
|
| 163 |
+
'severity': severity # INFO, WARNING, CRITICAL
|
| 164 |
+
}
|
| 165 |
+
with audit_lock:
|
| 166 |
+
audit_log.appendleft(entry)
|
| 167 |
+
if severity == "CRITICAL":
|
| 168 |
+
print(f"[AUDIT-CRITICAL] {entry['timestamp']} | {action} | {details}")
|
| 169 |
+
|
| 170 |
+
log_audit("SYSTEM_BOOT", "Sentinel V14 initializing — Dispatch Integration active.", "INFO")
|
| 171 |
+
|
| 172 |
+
# ════════════════════════════════════════════════════════════════
|
| 173 |
+
# TELEGRAM DISPATCH ENGINE
|
| 174 |
+
# ════════════════════════════════════════════════════════════════
|
| 175 |
+
|
| 176 |
+
class TelegramDispatcher:
|
| 177 |
+
"""Sends alert messages via Telegram Bot API using simple HTTP requests."""
|
| 178 |
+
|
| 179 |
+
def __init__(self, bot_token='', chat_id=''):
|
| 180 |
+
self.bot_token = bot_token
|
| 181 |
+
self.chat_id = chat_id
|
| 182 |
+
self.base_url = ''
|
| 183 |
+
if bot_token:
|
| 184 |
+
self.base_url = f'https://api.telegram.org/bot{bot_token}'
|
| 185 |
+
|
| 186 |
+
def configure(self, bot_token, chat_id=''):
|
| 187 |
+
self.bot_token = bot_token
|
| 188 |
+
self.chat_id = chat_id
|
| 189 |
+
self.base_url = f'https://api.telegram.org/bot{bot_token}'
|
| 190 |
+
|
| 191 |
+
def is_configured(self):
|
| 192 |
+
return bool(self.bot_token and self.chat_id)
|
| 193 |
+
|
| 194 |
+
def send_message(self, text, parse_mode='HTML'):
|
| 195 |
+
"""Send a text message to the configured chat."""
|
| 196 |
+
if not self.is_configured():
|
| 197 |
+
return {'ok': False, 'error': 'Telegram not configured (missing token or chat_id)'}
|
| 198 |
+
try:
|
| 199 |
+
resp = http_requests.post(
|
| 200 |
+
f'{self.base_url}/sendMessage',
|
| 201 |
+
json={
|
| 202 |
+
'chat_id': self.chat_id,
|
| 203 |
+
'text': text,
|
| 204 |
+
'parse_mode': parse_mode
|
| 205 |
+
},
|
| 206 |
+
timeout=10
|
| 207 |
+
)
|
| 208 |
+
result = resp.json()
|
| 209 |
+
if result.get('ok'):
|
| 210 |
+
log_audit("DISPATCH_SENT", f"Telegram message delivered to chat {self.chat_id}", "INFO")
|
| 211 |
+
else:
|
| 212 |
+
log_audit("DISPATCH_FAILED", f"Telegram API error: {result.get('description', 'Unknown')}", "WARNING")
|
| 213 |
+
return result
|
| 214 |
+
except Exception as e:
|
| 215 |
+
log_audit("DISPATCH_ERROR", f"Network error: {str(e)}", "WARNING")
|
| 216 |
+
return {'ok': False, 'error': str(e)}
|
| 217 |
+
|
| 218 |
+
def test_connection(self):
|
| 219 |
+
"""Send a test message to verify the bot is working."""
|
| 220 |
+
test_msg = (
|
| 221 |
+
"🛡️ <b>SENTINEL DISPATCH — TEST</b>\n\n"
|
| 222 |
+
"✅ Connection verified.\n"
|
| 223 |
+
"This bot is now linked to Project SENTINEL.\n\n"
|
| 224 |
+
f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
|
| 225 |
+
"<i>Automated threat alerts will be delivered here.</i>"
|
| 226 |
+
)
|
| 227 |
+
return self.send_message(test_msg)
|
| 228 |
+
|
| 229 |
+
def auto_detect_chat_id(self):
|
| 230 |
+
"""Try to detect chat_id from recent /start messages."""
|
| 231 |
+
if not self.bot_token:
|
| 232 |
+
return None
|
| 233 |
+
try:
|
| 234 |
+
resp = http_requests.get(f'{self.base_url}/getUpdates', timeout=10)
|
| 235 |
+
data = resp.json()
|
| 236 |
+
if data.get('ok') and data.get('result'):
|
| 237 |
+
for update in reversed(data['result']):
|
| 238 |
+
msg = update.get('message', {})
|
| 239 |
+
chat = msg.get('chat', {})
|
| 240 |
+
if chat.get('id'):
|
| 241 |
+
self.chat_id = str(chat['id'])
|
| 242 |
+
log_audit("DISPATCH_CONFIG", f"Auto-detected chat ID: {self.chat_id}", "INFO")
|
| 243 |
+
return self.chat_id
|
| 244 |
+
except:
|
| 245 |
+
pass
|
| 246 |
+
return None
|
| 247 |
+
|
| 248 |
+
def send_threat_alert(self, threat_score, details, active_modules):
|
| 249 |
+
"""Format and send a threat alert message."""
|
| 250 |
+
modules_str = ', '.join([m.upper() for m in active_modules]) if active_modules else 'NONE'
|
| 251 |
+
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
| 252 |
+
|
| 253 |
+
# Build detail lines
|
| 254 |
+
detail_lines = []
|
| 255 |
+
for module, data in details.items():
|
| 256 |
+
if isinstance(data, dict):
|
| 257 |
+
for k, v in data.items():
|
| 258 |
+
if v and v != 0 and v != False:
|
| 259 |
+
label = k.replace('_', ' ').title()
|
| 260 |
+
if isinstance(v, bool):
|
| 261 |
+
v = '⚠️ YES'
|
| 262 |
+
detail_lines.append(f" • {label}: {v}")
|
| 263 |
+
|
| 264 |
+
details_text = '\n'.join(detail_lines) if detail_lines else ' No specific details'
|
| 265 |
+
|
| 266 |
+
severity = '🔴 CRITICAL' if threat_score >= 80 else '🟠 ELEVATED' if threat_score >= 50 else '🟡 GUARDED'
|
| 267 |
+
|
| 268 |
+
msg = (
|
| 269 |
+
f"🚨 <b>SENTINEL THREAT ALERT</b>\n\n"
|
| 270 |
+
f"<b>Threat Level:</b> {severity} ({threat_score}/100)\n"
|
| 271 |
+
f"<b>Active Modules:</b> {modules_str}\n"
|
| 272 |
+
f"<b>Time:</b> {timestamp}\n\n"
|
| 273 |
+
f"<b>Detection Details:</b>\n{details_text}\n\n"
|
| 274 |
+
f"🏛️ <i>Project SENTINEL — Automated Dispatch</i>"
|
| 275 |
+
)
|
| 276 |
+
return self.send_message(msg)
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
# Initialize dispatcher with the bot token
|
| 280 |
+
telegram_dispatcher = TelegramDispatcher(
|
| 281 |
+
bot_token='8659917680:AAFHai-uliSdnX2zNhKL_-a5fZV_x0DcJ2E',
|
| 282 |
+
chat_id='8521681859' # Paulo's Telegram chat ID
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
# ════════════════════════════════════════════════════════════════
|
| 286 |
+
# DISPATCH STATE MANAGEMENT
|
| 287 |
+
# ════════════════════════════════════════════════════════════════
|
| 288 |
+
|
| 289 |
+
dispatch_log = deque(maxlen=100) # History of all dispatch events
|
| 290 |
+
pending_approvals = {} # id -> dispatch event awaiting approval
|
| 291 |
+
dispatch_lock = threading.Lock()
|
| 292 |
+
|
| 293 |
+
# Dispatch settings
|
| 294 |
+
dispatch_settings = {
|
| 295 |
+
'auto_dispatch': False, # If True, send alerts immediately; if False, queue for approval
|
| 296 |
+
'cooldown_seconds': 60, # Minimum seconds between auto-dispatches
|
| 297 |
+
'enabled': True, # Master switch
|
| 298 |
+
'last_dispatch_time': 0, # Timestamp of last sent alert
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
def create_dispatch_event(threat_score, details, active_modules):
|
| 302 |
+
"""Create a dispatch event and either auto-send or queue for approval."""
|
| 303 |
+
event_id = str(uuid.uuid4())[:8]
|
| 304 |
+
now = time.time()
|
| 305 |
+
event = {
|
| 306 |
+
'id': event_id,
|
| 307 |
+
'timestamp': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
| 308 |
+
'threat_score': threat_score,
|
| 309 |
+
'details': dict(details) if details else {},
|
| 310 |
+
'active_modules': list(active_modules),
|
| 311 |
+
'status': 'pending', # pending, sent, rejected, failed
|
| 312 |
+
'created_at': now,
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
if not dispatch_settings['enabled']:
|
| 316 |
+
return
|
| 317 |
+
|
| 318 |
+
if not telegram_dispatcher.is_configured():
|
| 319 |
+
event['status'] = 'failed'
|
| 320 |
+
event['error'] = 'Telegram not configured'
|
| 321 |
+
with dispatch_lock:
|
| 322 |
+
dispatch_log.appendleft(event)
|
| 323 |
+
log_audit("DISPATCH_SKIPPED", "Telegram not configured — alert not sent", "WARNING")
|
| 324 |
+
return
|
| 325 |
+
|
| 326 |
+
# Check cooldown
|
| 327 |
+
if now - dispatch_settings['last_dispatch_time'] < dispatch_settings['cooldown_seconds']:
|
| 328 |
+
remaining = int(dispatch_settings['cooldown_seconds'] - (now - dispatch_settings['last_dispatch_time']))
|
| 329 |
+
log_audit("DISPATCH_COOLDOWN", f"Alert suppressed — cooldown active ({remaining}s remaining)", "INFO")
|
| 330 |
+
return
|
| 331 |
+
|
| 332 |
+
if dispatch_settings['auto_dispatch']:
|
| 333 |
+
# Send immediately
|
| 334 |
+
result = telegram_dispatcher.send_threat_alert(threat_score, details, active_modules)
|
| 335 |
+
event['status'] = 'sent' if result.get('ok') else 'failed'
|
| 336 |
+
if not result.get('ok'):
|
| 337 |
+
event['error'] = result.get('error', result.get('description', 'Unknown'))
|
| 338 |
+
dispatch_settings['last_dispatch_time'] = now
|
| 339 |
+
with dispatch_lock:
|
| 340 |
+
dispatch_log.appendleft(event)
|
| 341 |
+
log_audit("AUTO_DISPATCH", f"Threat alert auto-dispatched (score: {threat_score})", "CRITICAL")
|
| 342 |
+
else:
|
| 343 |
+
# Queue for human approval
|
| 344 |
+
with dispatch_lock:
|
| 345 |
+
pending_approvals[event_id] = event
|
| 346 |
+
log_audit("DISPATCH_QUEUED", f"Alert queued for operator approval (score: {threat_score})", "WARNING")
|
| 347 |
+
|
| 348 |
+
def approve_dispatch_event(event_id):
|
| 349 |
+
"""Operator approves a pending dispatch."""
|
| 350 |
+
with dispatch_lock:
|
| 351 |
+
event = pending_approvals.pop(event_id, None)
|
| 352 |
+
if not event:
|
| 353 |
+
return {'error': 'Event not found or already processed'}
|
| 354 |
+
|
| 355 |
+
result = telegram_dispatcher.send_threat_alert(
|
| 356 |
+
event['threat_score'], event['details'], event['active_modules']
|
| 357 |
+
)
|
| 358 |
+
event['status'] = 'sent' if result.get('ok') else 'failed'
|
| 359 |
+
if not result.get('ok'):
|
| 360 |
+
event['error'] = result.get('error', result.get('description', 'Unknown'))
|
| 361 |
+
event['approved_at'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
| 362 |
+
dispatch_settings['last_dispatch_time'] = time.time()
|
| 363 |
+
|
| 364 |
+
with dispatch_lock:
|
| 365 |
+
dispatch_log.appendleft(event)
|
| 366 |
+
log_audit("DISPATCH_APPROVED", f"Operator approved alert {event_id} (score: {event['threat_score']})", "CRITICAL")
|
| 367 |
+
return {'success': True, 'status': event['status']}
|
| 368 |
+
|
| 369 |
+
def reject_dispatch_event(event_id):
|
| 370 |
+
"""Operator rejects a pending dispatch."""
|
| 371 |
+
with dispatch_lock:
|
| 372 |
+
event = pending_approvals.pop(event_id, None)
|
| 373 |
+
if not event:
|
| 374 |
+
return {'error': 'Event not found or already processed'}
|
| 375 |
+
|
| 376 |
+
event['status'] = 'rejected'
|
| 377 |
+
event['rejected_at'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
| 378 |
+
with dispatch_lock:
|
| 379 |
+
dispatch_log.appendleft(event)
|
| 380 |
+
log_audit("DISPATCH_REJECTED", f"Operator rejected alert {event_id}", "INFO")
|
| 381 |
+
return {'success': True}
|
| 382 |
+
|
| 383 |
+
# ════════════════════════════════════════════════════════════════
|
| 384 |
+
# GIF HANDLER
|
| 385 |
+
# ════════════════════════════════════════════════════════════════
|
| 386 |
+
class GIFReader:
|
| 387 |
+
"""Handles GIF file reading and frame extraction."""
|
| 388 |
+
def __init__(self, filepath):
|
| 389 |
+
self.filepath = filepath
|
| 390 |
+
self.gif = Image.open(filepath)
|
| 391 |
+
self.frame_count = 0
|
| 392 |
+
try:
|
| 393 |
+
while True:
|
| 394 |
+
self.gif.seek(self.frame_count)
|
| 395 |
+
self.frame_count += 1
|
| 396 |
+
except EOFError:
|
| 397 |
+
pass
|
| 398 |
+
|
| 399 |
+
self.current_frame_idx = 0
|
| 400 |
+
# Get duration for each frame (in milliseconds)
|
| 401 |
+
try:
|
| 402 |
+
self.gif.seek(0)
|
| 403 |
+
self.frame_duration = self.gif.info.get('duration', 100) / 1000.0 # Convert to seconds
|
| 404 |
+
except:
|
| 405 |
+
self.frame_duration = 0.04 # Default ~25 FPS
|
| 406 |
+
|
| 407 |
+
def get_next_frame(self):
|
| 408 |
+
"""Get the next frame from GIF."""
|
| 409 |
+
try:
|
| 410 |
+
self.gif.seek(self.current_frame_idx)
|
| 411 |
+
frame = self.gif.convert('RGB')
|
| 412 |
+
frame_np = np.array(frame)
|
| 413 |
+
# Convert RGB to BGR for OpenCV
|
| 414 |
+
frame_bgr = cv2.cvtColor(frame_np, cv2.COLOR_RGB2BGR)
|
| 415 |
+
|
| 416 |
+
self.current_frame_idx = (self.current_frame_idx + 1) % self.frame_count
|
| 417 |
+
return True, frame_bgr, self.frame_duration
|
| 418 |
+
except Exception as e:
|
| 419 |
+
print(f"[GIF] Error reading frame: {e}")
|
| 420 |
+
return False, None, 0
|
| 421 |
+
|
| 422 |
+
def close(self):
|
| 423 |
+
if self.gif:
|
| 424 |
+
self.gif.close()
|
| 425 |
+
|
| 426 |
+
# ════════════════════════════════════════════════════════════════
|
| 427 |
+
# THREADED VIDEO CAPTURE (OPTIMIZED FOR LARGE FILES)
|
| 428 |
+
# ════════════════════════════════════════════════════════════════
|
| 429 |
+
class VideoCamera:
|
| 430 |
+
def __init__(self, src=0, feed_id=0):
|
| 431 |
+
self.feed_id = feed_id
|
| 432 |
+
self.src = src
|
| 433 |
+
self.source_is_file = isinstance(src, str)
|
| 434 |
+
self.is_gif = False
|
| 435 |
+
self.gif_reader = None
|
| 436 |
+
self.stream = None
|
| 437 |
+
self.lock = threading.Lock()
|
| 438 |
+
self.frame_buffer = deque(maxlen=2) # Keep only last 2 frames to reduce memory
|
| 439 |
+
|
| 440 |
+
# Check if source is a GIF
|
| 441 |
+
if self.source_is_file and src.lower().endswith('.gif'):
|
| 442 |
+
self.is_gif = True
|
| 443 |
+
try:
|
| 444 |
+
self.gif_reader = GIFReader(src)
|
| 445 |
+
self.grabbed = True
|
| 446 |
+
success, frame, duration = self.gif_reader.get_next_frame()
|
| 447 |
+
self.frame = frame
|
| 448 |
+
self.target_delay = duration
|
| 449 |
+
self.stopped = False
|
| 450 |
+
print(f"[V8] GIF loaded: {src} ({self.gif_reader.frame_count} frames)")
|
| 451 |
+
except Exception as e:
|
| 452 |
+
print(f"[WARNING] Could not open GIF: {src} - {e}")
|
| 453 |
+
self.grabbed = False
|
| 454 |
+
self.frame = None
|
| 455 |
+
self.stopped = True
|
| 456 |
+
return
|
| 457 |
+
else:
|
| 458 |
+
# Regular video file or camera
|
| 459 |
+
self.stream = cv2.VideoCapture(src)
|
| 460 |
+
|
| 461 |
+
if not self.stream.isOpened():
|
| 462 |
+
print(f"[WARNING] Could not open video source: {src}")
|
| 463 |
+
self.grabbed = False
|
| 464 |
+
self.frame = None
|
| 465 |
+
self.stopped = True
|
| 466 |
+
return
|
| 467 |
+
|
| 468 |
+
(self.grabbed, self.frame) = self.stream.read()
|
| 469 |
+
self.stopped = False
|
| 470 |
+
|
| 471 |
+
# For file sources: read at the file's native FPS to simulate live playback
|
| 472 |
+
self.target_delay = 0
|
| 473 |
+
if self.source_is_file:
|
| 474 |
+
fps = self.stream.get(cv2.CAP_PROP_FPS)
|
| 475 |
+
if fps and fps > 0:
|
| 476 |
+
self.target_delay = 1.0 / fps
|
| 477 |
+
print(f"[V8] Video loaded: {src} (FPS: {fps:.2f})")
|
| 478 |
+
else:
|
| 479 |
+
self.target_delay = 1.0 / 25 # fallback 25fps
|
| 480 |
+
|
| 481 |
+
self.t = threading.Thread(target=self.update, args=())
|
| 482 |
+
self.t.daemon = True
|
| 483 |
+
self.t.start()
|
| 484 |
+
|
| 485 |
+
def update(self):
|
| 486 |
+
while True:
|
| 487 |
+
if self.stopped:
|
| 488 |
+
return
|
| 489 |
+
|
| 490 |
+
frame_start = time.time()
|
| 491 |
+
|
| 492 |
+
if self.is_gif:
|
| 493 |
+
# Handle GIF frames
|
| 494 |
+
grabbed, frame, delay = self.gif_reader.get_next_frame()
|
| 495 |
+
else:
|
| 496 |
+
# Handle video/camera frames
|
| 497 |
+
(grabbed, frame) = self.stream.read()
|
| 498 |
+
|
| 499 |
+
if not grabbed and self.source_is_file:
|
| 500 |
+
# End of video file — loop it seamlessly like CCTV
|
| 501 |
+
self.stream.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
| 502 |
+
(grabbed, frame) = self.stream.read()
|
| 503 |
+
|
| 504 |
+
with self.lock:
|
| 505 |
+
self.grabbed = grabbed
|
| 506 |
+
if grabbed and frame is not None:
|
| 507 |
+
self.frame = frame
|
| 508 |
+
# Clear buffer and add only current frame to minimize memory
|
| 509 |
+
self.frame_buffer.clear()
|
| 510 |
+
self.frame_buffer.append(frame)
|
| 511 |
+
|
| 512 |
+
# Throttle to native FPS
|
| 513 |
+
if self.target_delay > 0:
|
| 514 |
+
elapsed = time.time() - frame_start
|
| 515 |
+
sleep_time = max(0, self.target_delay - elapsed)
|
| 516 |
+
if sleep_time > 0:
|
| 517 |
+
time.sleep(sleep_time)
|
| 518 |
+
|
| 519 |
+
def get_frame(self):
|
| 520 |
+
# Handle case where thread wasn't started
|
| 521 |
+
if self.stopped and self.frame is None:
|
| 522 |
+
return False, None
|
| 523 |
+
|
| 524 |
+
with self.lock:
|
| 525 |
+
return self.grabbed, self.frame.copy() if self.frame is not None else (False, None)
|
| 526 |
+
|
| 527 |
+
def stop(self):
|
| 528 |
+
self.stopped = True
|
| 529 |
+
if hasattr(self, 't') and self.t.is_alive():
|
| 530 |
+
self.t.join(timeout=2)
|
| 531 |
+
|
| 532 |
+
if self.is_gif and self.gif_reader:
|
| 533 |
+
self.gif_reader.close()
|
| 534 |
+
elif self.stream:
|
| 535 |
+
self.stream.release()
|
| 536 |
+
|
| 537 |
+
def set_source(self, src):
|
| 538 |
+
self.stop()
|
| 539 |
+
self.__init__(src, self.feed_id)
|
| 540 |
+
# Note: __init__ starts the new thread if successful
|
| 541 |
+
|
| 542 |
+
def is_opened(self):
|
| 543 |
+
if self.is_gif:
|
| 544 |
+
return self.gif_reader is not None
|
| 545 |
+
return self.stream is not None and self.stream.isOpened()
|
| 546 |
+
|
| 547 |
+
def get(self, prop):
|
| 548 |
+
if self.is_gif:
|
| 549 |
+
if prop == cv2.CAP_PROP_FPS:
|
| 550 |
+
return 1.0 / self.target_delay if self.target_delay > 0 else 25
|
| 551 |
+
return 0
|
| 552 |
+
return self.stream.get(prop) if self.stream else 0
|
| 553 |
+
|
| 554 |
+
def set(self, prop, val):
|
| 555 |
+
if not self.is_gif and self.stream:
|
| 556 |
+
self.stream.set(prop, val)
|
| 557 |
+
|
| 558 |
+
|
| 559 |
+
def get_or_create_feed(feed_id=0, src=None):
|
| 560 |
+
"""Get existing feed or create a new one."""
|
| 561 |
+
global camera_feeds
|
| 562 |
+
if feed_id in camera_feeds and camera_feeds[feed_id].is_opened():
|
| 563 |
+
return camera_feeds[feed_id]
|
| 564 |
+
|
| 565 |
+
if src is None:
|
| 566 |
+
# Check per-feed source first
|
| 567 |
+
if feed_id in feed_sources and os.path.exists(feed_sources[feed_id]):
|
| 568 |
+
src = feed_sources[feed_id]
|
| 569 |
+
elif feed_id == 0:
|
| 570 |
+
src = 0 # Default webcam for feed 0 only
|
| 571 |
+
else:
|
| 572 |
+
return None # No source for this feed
|
| 573 |
+
|
| 574 |
+
cam = VideoCamera(src, feed_id)
|
| 575 |
+
camera_feeds[feed_id] = cam
|
| 576 |
+
log_audit("FEED_CREATED", f"Feed #{feed_id} initialized (source: {src})", "INFO")
|
| 577 |
+
return cam
|
| 578 |
+
|
| 579 |
+
|
| 580 |
+
def restart_feed(feed_id=0):
|
| 581 |
+
"""Restart a specific feed and bump the version so generators re-acquire."""
|
| 582 |
+
global camera_feeds, feed_versions
|
| 583 |
+
if feed_id in camera_feeds:
|
| 584 |
+
camera_feeds[feed_id].stop()
|
| 585 |
+
del camera_feeds[feed_id]
|
| 586 |
+
|
| 587 |
+
src = feed_sources.get(feed_id)
|
| 588 |
+
if src and os.path.exists(src):
|
| 589 |
+
camera_feeds[feed_id] = VideoCamera(src, feed_id)
|
| 590 |
+
elif feed_id == 0:
|
| 591 |
+
camera_feeds[feed_id] = VideoCamera(0, feed_id)
|
| 592 |
+
# else: no source, feed stays empty
|
| 593 |
+
|
| 594 |
+
feed_versions[feed_id] = feed_versions.get(feed_id, 0) + 1
|
| 595 |
+
log_audit("FEED_RESTART", f"Feed #{feed_id} restarted (v{feed_versions[feed_id]}).", "INFO")
|
| 596 |
+
|
| 597 |
+
|
| 598 |
+
# ════════════════════════════════════════════════════════════════
|
| 599 |
+
# SCORING & DETECTION LOGIC
|
| 600 |
+
# ════════════════════════════════════════════════════════════════
|
| 601 |
+
def smooth_score(new_score):
|
| 602 |
+
global previous_score
|
| 603 |
+
smoothed = ALPHA * new_score + (1 - ALPHA) * previous_score
|
| 604 |
+
previous_score = smoothed
|
| 605 |
+
return int(smoothed)
|
| 606 |
+
|
| 607 |
+
|
| 608 |
+
def calculate_threat_score(mode, details):
|
| 609 |
+
score = 0
|
| 610 |
+
if mode == 'weapon':
|
| 611 |
+
score += details.get('guns', 0) * 100
|
| 612 |
+
score += details.get('knives', 0) * 80
|
| 613 |
+
elif mode == 'facemask':
|
| 614 |
+
score += details.get('with_mask', 0) * 60
|
| 615 |
+
score += details.get('obscured_faces', 0) * 90
|
| 616 |
+
elif mode == 'movement':
|
| 617 |
+
score += details.get('long_stays', 0) * 40
|
| 618 |
+
elif mode == 'public_safety':
|
| 619 |
+
score += details.get('abandoned_objects', 0) * 70
|
| 620 |
+
if details.get('crowd_panic', False):
|
| 621 |
+
score += 85
|
| 622 |
+
return min(score, 100)
|
| 623 |
+
|
| 624 |
+
|
| 625 |
+
# ════════════════════════════════════════════════════════════════
|
| 626 |
+
# FRAME GENERATOR (per-feed) - OPTIMIZED
|
| 627 |
+
# ════════════════════════════════════════════════════════════════
|
| 628 |
+
def generate_frames(feed_id=0):
|
| 629 |
+
global active_modules, person_history, object_history, previous_score, red_alert
|
| 630 |
+
|
| 631 |
+
camera = get_or_create_feed(feed_id)
|
| 632 |
+
my_version = feed_versions.get(feed_id, 0)
|
| 633 |
+
|
| 634 |
+
while True:
|
| 635 |
+
# Detect if the feed was restarted (e.g. user uploaded a video or switched source)
|
| 636 |
+
current_version = feed_versions.get(feed_id, 0)
|
| 637 |
+
if current_version != my_version:
|
| 638 |
+
# Feed was swapped — re-acquire the new camera
|
| 639 |
+
my_version = current_version
|
| 640 |
+
camera = camera_feeds.get(feed_id)
|
| 641 |
+
if camera is None:
|
| 642 |
+
time.sleep(0.1)
|
| 643 |
+
continue
|
| 644 |
+
|
| 645 |
+
if not camera.is_opened():
|
| 646 |
+
time.sleep(0.1)
|
| 647 |
+
camera = get_or_create_feed(feed_id)
|
| 648 |
+
my_version = feed_versions.get(feed_id, 0)
|
| 649 |
+
continue
|
| 650 |
+
|
| 651 |
+
success, img = camera.get_frame()
|
| 652 |
+
|
| 653 |
+
if not success or img is None:
|
| 654 |
+
# Generate a "NO SIGNAL" placeholder frame
|
| 655 |
+
blank_frame = np.zeros((480, 640, 3), dtype=np.uint8)
|
| 656 |
+
cv2.putText(blank_frame, "NO SIGNAL / CAMERA UNAVAILABLE", (100, 240),
|
| 657 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2)
|
| 658 |
+
cv2.putText(blank_frame, "Please use 'Upload Video' on Server", (120, 280),
|
| 659 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1)
|
| 660 |
+
|
| 661 |
+
# Encode and yield
|
| 662 |
+
ret, buffer = cv2.imencode('.jpg', blank_frame)
|
| 663 |
+
frame_bytes = buffer.tobytes()
|
| 664 |
+
yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n' b'X-Score: 0\r\n\r\n' + frame_bytes + b'\r\n')
|
| 665 |
+
time.sleep(1.0) # Slow update for static error frame
|
| 666 |
+
continue
|
| 667 |
+
|
| 668 |
+
# Resize for performance
|
| 669 |
+
h, w = img.shape[:2]
|
| 670 |
+
new_w = 640
|
| 671 |
+
new_h = int(h * (new_w / w))
|
| 672 |
+
frame = cv2.resize(img, (new_w, new_h))
|
| 673 |
+
frame = cv2.GaussianBlur(frame, (3, 3), 0)
|
| 674 |
+
|
| 675 |
+
current_time = time.time()
|
| 676 |
+
current_details = {}
|
| 677 |
+
|
| 678 |
+
if 'movement' in active_modules:
|
| 679 |
+
results = model_movement(frame, stream=True, verbose=False)
|
| 680 |
+
detections = np.empty((0, 5))
|
| 681 |
+
for r in results:
|
| 682 |
+
for box in r.boxes:
|
| 683 |
+
if int(box.cls[0]) == 0:
|
| 684 |
+
x1, y1, x2, y2 = map(int, box.xyxy[0])
|
| 685 |
+
conf = math.ceil(box.conf[0] * 100) / 100
|
| 686 |
+
detections = np.vstack((detections, [x1, y1, x2, y2, conf]))
|
| 687 |
+
# Use feed-specific tracker
|
| 688 |
+
tracks = feed_trackers[feed_id].update(detections)
|
| 689 |
+
current_used_ids = []
|
| 690 |
+
journey_active = 'suspect_journey' in active_modules
|
| 691 |
+
|
| 692 |
+
for tr in tracks:
|
| 693 |
+
x1, y1, x2, y2, local_id = tr
|
| 694 |
+
|
| 695 |
+
is_reidentified = False
|
| 696 |
+
display_id = f"L{int(local_id)}"
|
| 697 |
+
|
| 698 |
+
# Only run Re-ID when suspect_journey module is active
|
| 699 |
+
if journey_active:
|
| 700 |
+
global_id = identity_manager.match_or_register(feed_id, local_id, frame, (x1, y1, x2, y2))
|
| 701 |
+
if global_id is not None:
|
| 702 |
+
# Check if this subject exists on ANY other feed
|
| 703 |
+
for key, entry in identity_manager.local_to_global.items():
|
| 704 |
+
gid = entry['global_id'] if isinstance(entry, dict) else entry
|
| 705 |
+
if gid == global_id and key[0] != feed_id:
|
| 706 |
+
is_reidentified = True
|
| 707 |
+
break
|
| 708 |
+
display_id = global_id
|
| 709 |
+
else:
|
| 710 |
+
display_id = int(local_id)
|
| 711 |
+
|
| 712 |
+
midX = int((x1 + x2) / 2)
|
| 713 |
+
headX, headY = midX, int(y1)
|
| 714 |
+
|
| 715 |
+
# Track journey (only when journey module is active)
|
| 716 |
+
if journey_active:
|
| 717 |
+
journey_log.append({
|
| 718 |
+
'global_id': display_id,
|
| 719 |
+
'feed_id': feed_id,
|
| 720 |
+
'timestamp': datetime.datetime.now().strftime("%H:%M:%S"),
|
| 721 |
+
'local_id': int(local_id)
|
| 722 |
+
})
|
| 723 |
+
|
| 724 |
+
pid = display_id
|
| 725 |
+
if pid not in person_history:
|
| 726 |
+
person_history[pid] = []
|
| 727 |
+
person_history[pid].append((headX, headY, current_time))
|
| 728 |
+
person_history[pid] = [p for p in person_history[pid] if current_time - p[2] < 5.0]
|
| 729 |
+
|
| 730 |
+
if len(person_history[pid]) > 1:
|
| 731 |
+
pts = np.array([(p[0], p[1]) for p in person_history[pid]], np.int32).reshape((-1, 1, 2))
|
| 732 |
+
cv2.polylines(frame, [pts], False, (0, 255, 0), 2)
|
| 733 |
+
|
| 734 |
+
target_prev = current_time - 1.0
|
| 735 |
+
prev_pt = None
|
| 736 |
+
if len(person_history[pid]) > 2:
|
| 737 |
+
diffs = [abs(p[2] - target_prev) for p in person_history[pid]]
|
| 738 |
+
min_idx = int(np.argmin(diffs))
|
| 739 |
+
if diffs[min_idx] < 0.5:
|
| 740 |
+
prev_pt = person_history[pid][min_idx]
|
| 741 |
+
|
| 742 |
+
if prev_pt:
|
| 743 |
+
dt = current_time - prev_pt[2]
|
| 744 |
+
if dt > 0.1:
|
| 745 |
+
vx = (headX - prev_pt[0]) / dt
|
| 746 |
+
vy = (headY - prev_pt[1]) / dt
|
| 747 |
+
predX = int(headX + vx * 4)
|
| 748 |
+
predY = int(headY + vy * 4)
|
| 749 |
+
cv2.arrowedLine(frame, (headX, headY), (predX, predY), (0, 255, 255), 2, tipLength=0.3)
|
| 750 |
+
|
| 751 |
+
# ── Visual markers ──
|
| 752 |
+
if is_reidentified:
|
| 753 |
+
# TRACKED subject: cyan reticle + thick border
|
| 754 |
+
cx, cy = int((x1 + x2) / 2), int((y1 + y2) / 2)
|
| 755 |
+
r = min(int(x2 - x1), int(y2 - y1)) // 3
|
| 756 |
+
cv2.circle(frame, (cx, cy), r, (255, 255, 0), 2) # Cyan circle
|
| 757 |
+
cv2.line(frame, (cx - r - 5, cy), (cx + r + 5, cy), (255, 255, 0), 1) # H crosshair
|
| 758 |
+
cv2.line(frame, (cx, cy - r - 5), (cx, cy + r + 5), (255, 255, 0), 1) # V crosshair
|
| 759 |
+
cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (255, 255, 0), 3)
|
| 760 |
+
cv2.putText(frame, f"TRACKED #{display_id}", (int(x1), int(y1) - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 0), 2)
|
| 761 |
+
else:
|
| 762 |
+
# Normal detection: magenta border
|
| 763 |
+
cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (255, 0, 255), 2)
|
| 764 |
+
cv2.putText(frame, f"ID:{display_id}", (int(x1), int(y1) - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 255), 2)
|
| 765 |
+
current_used_ids.append(display_id)
|
| 766 |
+
|
| 767 |
+
# Cleanup stale Re-ID mappings for tracks no longer in this frame
|
| 768 |
+
if journey_active:
|
| 769 |
+
active_local_ids = set(int(tr[4]) for tr in tracks)
|
| 770 |
+
identity_manager.cleanup_feed(feed_id, active_local_ids)
|
| 771 |
+
|
| 772 |
+
long_stays = sum(1 for hist in person_history.values() if len(hist) > 30)
|
| 773 |
+
current_details['movement'] = {'total_people': len(person_history), 'current_people': len(current_used_ids), 'long_stays': long_stays}
|
| 774 |
+
|
| 775 |
+
if 'facemask' in active_modules:
|
| 776 |
+
results_mask = model_facemask(frame, stream=True, verbose=False)
|
| 777 |
+
with_mask = 0
|
| 778 |
+
without_mask = 0
|
| 779 |
+
mask_boxes = []
|
| 780 |
+
|
| 781 |
+
for r in results_mask:
|
| 782 |
+
for box in r.boxes:
|
| 783 |
+
cls = int(box.cls[0])
|
| 784 |
+
x1, y1, x2, y2 = map(int, box.xyxy[0])
|
| 785 |
+
mask_boxes.append((x1, y1, x2, y2))
|
| 786 |
+
|
| 787 |
+
if cls == 0:
|
| 788 |
+
with_mask += 1
|
| 789 |
+
color = (0, 0, 255)
|
| 790 |
+
label = "Mask (THREAT)"
|
| 791 |
+
else:
|
| 792 |
+
without_mask += 1
|
| 793 |
+
color = (0, 255, 0)
|
| 794 |
+
label = "No Mask"
|
| 795 |
+
cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)
|
| 796 |
+
cv2.putText(frame, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
|
| 797 |
+
|
| 798 |
+
obscured_faces = 0
|
| 799 |
+
people_res = model_movement(frame, stream=True, verbose=False, classes=[0])
|
| 800 |
+
people_boxes = []
|
| 801 |
+
for r in people_res:
|
| 802 |
+
for box in r.boxes:
|
| 803 |
+
people_boxes.append(tuple(map(int, box.xyxy[0])))
|
| 804 |
+
|
| 805 |
+
def overlap(person, face):
|
| 806 |
+
px1, py1, px2, py2 = person
|
| 807 |
+
fx1, fy1, fx2, fy2 = face
|
| 808 |
+
upper = py1 + (py2 - py1) // 1.5
|
| 809 |
+
return fx1 >= px1 and fx2 <= px2 and fy1 >= py1 and fy2 <= upper
|
| 810 |
+
|
| 811 |
+
for pbox in people_boxes:
|
| 812 |
+
if not any(overlap(pbox, mbox) for mbox in mask_boxes):
|
| 813 |
+
obscured_faces += 1
|
| 814 |
+
cv2.rectangle(frame, (pbox[0], pbox[1]), (pbox[2], pbox[3]), (0, 0, 255), 3)
|
| 815 |
+
cv2.putText(frame, "Face Obscured (THREAT)", (pbox[0], pbox[1] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
|
| 816 |
+
|
| 817 |
+
current_details['facemask'] = {'with_mask': with_mask, 'without_mask': without_mask, 'obscured_faces': obscured_faces}
|
| 818 |
+
|
| 819 |
+
if 'weapon' in active_modules:
|
| 820 |
+
h_w, w_w = frame.shape[:2]
|
| 821 |
+
weapon_w = 416
|
| 822 |
+
weapon_h = int(h_w * (weapon_w / w_w))
|
| 823 |
+
weapon_frame = cv2.resize(frame, (weapon_w, weapon_h))
|
| 824 |
+
|
| 825 |
+
results = model_weapon(weapon_frame, stream=True, verbose=False, conf=0.5)
|
| 826 |
+
guns = 0
|
| 827 |
+
melee = 0
|
| 828 |
+
|
| 829 |
+
scale_x = w_w / weapon_w
|
| 830 |
+
scale_y = h_w / weapon_h
|
| 831 |
+
|
| 832 |
+
firearm_keywords = ['gun', 'pistol', 'rifle', 'shotgun', 'sniper', 'machine', 'glock', 'ak47', 'm4', 'awp', 'famas', 'galil', 'mp5', 'p90']
|
| 833 |
+
melee_keywords = ['knife', 'sword', 'dagger', 'axe', 'bat', 'stick', 'machete', 'blade']
|
| 834 |
+
|
| 835 |
+
for r in results:
|
| 836 |
+
for box in r.boxes:
|
| 837 |
+
cls = int(box.cls[0])
|
| 838 |
+
conf = float(box.conf[0])
|
| 839 |
+
x1, y1, x2, y2 = box.xyxy[0]
|
| 840 |
+
x1 = int(x1 * scale_x)
|
| 841 |
+
y1 = int(y1 * scale_y)
|
| 842 |
+
x2 = int(x2 * scale_x)
|
| 843 |
+
y2 = int(y2 * scale_y)
|
| 844 |
+
|
| 845 |
+
class_name = model_weapon.names[cls].lower()
|
| 846 |
+
is_firearm = any(k in class_name for k in firearm_keywords)
|
| 847 |
+
is_melee = any(k in class_name for k in melee_keywords)
|
| 848 |
+
|
| 849 |
+
if is_firearm:
|
| 850 |
+
guns += 1
|
| 851 |
+
color = (0, 0, 255)
|
| 852 |
+
label = f"{class_name.upper()} {conf:.2f}"
|
| 853 |
+
elif is_melee:
|
| 854 |
+
melee += 1
|
| 855 |
+
color = (0, 165, 255)
|
| 856 |
+
label = f"{class_name.upper()} {conf:.2f}"
|
| 857 |
+
else:
|
| 858 |
+
guns += 1
|
| 859 |
+
color = (0, 0, 255)
|
| 860 |
+
label = f"{class_name.upper()} {conf:.2f}"
|
| 861 |
+
|
| 862 |
+
cv2.rectangle(frame, (x1, y1), (x2, y2), color, 3)
|
| 863 |
+
cv2.putText(frame, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
|
| 864 |
+
cv2.rectangle(frame, (0, 0), (new_w, new_h), (0, 255, 0), 5)
|
| 865 |
+
|
| 866 |
+
current_details['weapon'] = {'guns': guns, 'knives': melee}
|
| 867 |
+
|
| 868 |
+
if 'public_safety' in active_modules:
|
| 869 |
+
results = model_movement(frame, stream=True, verbose=False, classes=[0, 24, 26, 28])
|
| 870 |
+
detections = np.empty((0, 5))
|
| 871 |
+
object_detections = []
|
| 872 |
+
|
| 873 |
+
for r in results:
|
| 874 |
+
for box in r.boxes:
|
| 875 |
+
cls = int(box.cls[0])
|
| 876 |
+
x1, y1, x2, y2 = map(int, box.xyxy[0])
|
| 877 |
+
conf = float(box.conf[0])
|
| 878 |
+
if cls == 0:
|
| 879 |
+
detections = np.vstack((detections, [x1, y1, x2, y2, conf]))
|
| 880 |
+
else:
|
| 881 |
+
object_detections.append({'box': [x1, y1, x2, y2], 'cls': cls, 'conf': conf})
|
| 882 |
+
cv2.rectangle(frame, (x1, y1), (x2, y2), (255, 200, 0), 1)
|
| 883 |
+
|
| 884 |
+
tracks = feed_trackers[feed_id].update(detections)
|
| 885 |
+
velocities = []
|
| 886 |
+
for tr in tracks:
|
| 887 |
+
x1, y1, x2, y2, Id = tr
|
| 888 |
+
midX, midY = int((x1 + x2) / 2), int((y1 + y2) / 2)
|
| 889 |
+
if Id not in person_history:
|
| 890 |
+
person_history[Id] = []
|
| 891 |
+
person_history[Id].append((midX, midY, current_time))
|
| 892 |
+
person_history[Id] = [p for p in person_history[Id] if current_time - p[2] < 2.0]
|
| 893 |
+
|
| 894 |
+
if len(person_history[Id]) > 2:
|
| 895 |
+
p_start = person_history[Id][0]
|
| 896 |
+
p_end = person_history[Id][-1]
|
| 897 |
+
dt = p_end[2] - p_start[2]
|
| 898 |
+
if dt > 0.5:
|
| 899 |
+
dist = math.sqrt((p_end[0] - p_start[0])**2 + (p_end[1] - p_start[1])**2)
|
| 900 |
+
speed = dist / dt
|
| 901 |
+
velocities.append(speed)
|
| 902 |
+
|
| 903 |
+
cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 1)
|
| 904 |
+
|
| 905 |
+
crowd_panic = False
|
| 906 |
+
avg_speed = 0
|
| 907 |
+
if len(velocities) > 3:
|
| 908 |
+
avg_speed = sum(velocities) / len(velocities)
|
| 909 |
+
if avg_speed > 150:
|
| 910 |
+
crowd_panic = True
|
| 911 |
+
cv2.putText(frame, f"CROWD ANOMALY: PANIC ({int(avg_speed)} px/s)", (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 3)
|
| 912 |
+
|
| 913 |
+
active_objects = []
|
| 914 |
+
for obj in object_detections:
|
| 915 |
+
box = obj['box']
|
| 916 |
+
cx, cy = (box[0] + box[2]) // 2, (box[1] + box[3]) // 2
|
| 917 |
+
matched_id = None
|
| 918 |
+
for obj_id, history in object_history.items():
|
| 919 |
+
last_pos = history['last_pos']
|
| 920 |
+
dist = math.sqrt((cx - last_pos[0])**2 + (cy - last_pos[1])**2)
|
| 921 |
+
if dist < 50:
|
| 922 |
+
matched_id = obj_id
|
| 923 |
+
break
|
| 924 |
+
if matched_id is None:
|
| 925 |
+
new_id = str(time.time())
|
| 926 |
+
object_history[new_id] = {'start_time': current_time, 'last_seen': current_time, 'last_pos': (cx, cy), 'stationary': True}
|
| 927 |
+
matched_id = new_id
|
| 928 |
+
else:
|
| 929 |
+
object_history[matched_id]['last_seen'] = current_time
|
| 930 |
+
object_history[matched_id]['last_pos'] = (cx, cy)
|
| 931 |
+
active_objects.append(matched_id)
|
| 932 |
+
|
| 933 |
+
abandoned_count = 0
|
| 934 |
+
keys_to_remove = []
|
| 935 |
+
for obj_id, history in object_history.items():
|
| 936 |
+
if current_time - history['last_seen'] > 2.0:
|
| 937 |
+
keys_to_remove.append(obj_id)
|
| 938 |
+
continue
|
| 939 |
+
duration = current_time - history['start_time']
|
| 940 |
+
if duration > 10.0:
|
| 941 |
+
abandoned_count += 1
|
| 942 |
+
pos = history['last_pos']
|
| 943 |
+
cv2.circle(frame, pos, 30, (0, 0, 255), 2)
|
| 944 |
+
cv2.putText(frame, f"ABANDONED {int(duration)}s", (pos[0]-40, pos[1]-40), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
|
| 945 |
+
|
| 946 |
+
for k in keys_to_remove:
|
| 947 |
+
del object_history[k]
|
| 948 |
+
|
| 949 |
+
current_details['public_safety'] = {'people_count': len(tracks), 'avg_crowd_speed': int(avg_speed), 'crowd_panic': crowd_panic, 'abandoned_objects': abandoned_count}
|
| 950 |
+
|
| 951 |
+
# If no modules active, show idle state
|
| 952 |
+
if not active_modules:
|
| 953 |
+
cv2.putText(frame, "NO DETECTION ACTIVE", (new_w // 2 - 140, new_h // 2),
|
| 954 |
+
cv2.FONT_HERSHEY_SIMPLEX, 1.2, (100, 100, 100), 2)
|
| 955 |
+
current_details = {'status': 'idle', 'active_modules': 0}
|
| 956 |
+
|
| 957 |
+
# Store details globally
|
| 958 |
+
global latest_details
|
| 959 |
+
latest_details = current_details
|
| 960 |
+
|
| 961 |
+
# Aggregated threat scoring from ALL active modules
|
| 962 |
+
total_score = 0
|
| 963 |
+
for module, details in current_details.items():
|
| 964 |
+
if module == 'weapon':
|
| 965 |
+
total_score += details.get('guns', 0) * 100
|
| 966 |
+
total_score += details.get('knives', 0) * 80
|
| 967 |
+
elif module == 'facemask':
|
| 968 |
+
total_score += details.get('with_mask', 0) * 60
|
| 969 |
+
total_score += details.get('obscured_faces', 0) * 90
|
| 970 |
+
elif module == 'movement':
|
| 971 |
+
total_score += details.get('long_stays', 0) * 40
|
| 972 |
+
elif module == 'public_safety':
|
| 973 |
+
total_score += details.get('abandoned_objects', 0) * 70
|
| 974 |
+
if details.get('crowd_panic', False):
|
| 975 |
+
total_score += 85
|
| 976 |
+
|
| 977 |
+
raw_score = min(total_score, 100)
|
| 978 |
+
smooth = smooth_score(raw_score)
|
| 979 |
+
|
| 980 |
+
# Red Alert escalation
|
| 981 |
+
was_red = red_alert
|
| 982 |
+
red_alert = smooth >= 80
|
| 983 |
+
if red_alert and not was_red:
|
| 984 |
+
active_str = ', '.join([m.upper() for m in active_modules]) if active_modules else 'NONE'
|
| 985 |
+
log_audit("RED_ALERT_TRIGGERED", f"Threat score escalated to {smooth}. Active: {active_str}", "CRITICAL")
|
| 986 |
+
# ── Dispatch Integration: trigger alert on RED_ALERT ──
|
| 987 |
+
threading.Thread(
|
| 988 |
+
target=create_dispatch_event,
|
| 989 |
+
args=(smooth, dict(current_details), set(active_modules)),
|
| 990 |
+
daemon=True
|
| 991 |
+
).start()
|
| 992 |
+
elif not red_alert and was_red:
|
| 993 |
+
log_audit("RED_ALERT_CLEARED", f"Threat score de-escalated to {smooth}.", "WARNING")
|
| 994 |
+
|
| 995 |
+
# Feed label overlay
|
| 996 |
+
cv2.putText(frame, f"FEED {feed_id} | LIVE", (10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 255), 1)
|
| 997 |
+
|
| 998 |
+
ret, buffer = cv2.imencode('.jpg', frame)
|
| 999 |
+
frame_bytes = buffer.tobytes()
|
| 1000 |
+
yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n' b'X-Score: ' + str(smooth).encode() + b'\r\n\r\n' + frame_bytes + b'\r\n')
|
| 1001 |
+
|
| 1002 |
+
|
| 1003 |
+
# Global to store latest details for stats
|
| 1004 |
+
latest_details = {}
|
| 1005 |
+
|
| 1006 |
+
|
| 1007 |
+
# ════════════════════════════════════════════════════════════════
|
| 1008 |
+
# ROUTES
|
| 1009 |
+
# ════════════════════════════════════════════════════════════════
|
| 1010 |
+
@app.route('/')
|
| 1011 |
+
def index():
|
| 1012 |
+
return render_template('sentinel_dashboard_v13.html')
|
| 1013 |
+
|
| 1014 |
+
|
| 1015 |
+
@app.route('/journey_data')
|
| 1016 |
+
def get_journey_data():
|
| 1017 |
+
"""Return recent subject journey logs for the dashboard."""
|
| 1018 |
+
# Group by global_id and get the last seen location for each
|
| 1019 |
+
unique_journeys = {}
|
| 1020 |
+
for entry in list(journey_log):
|
| 1021 |
+
gid = entry['global_id']
|
| 1022 |
+
if gid not in unique_journeys:
|
| 1023 |
+
unique_journeys[gid] = {
|
| 1024 |
+
'id': gid,
|
| 1025 |
+
'last_feed': entry['feed_id'],
|
| 1026 |
+
'last_seen': entry['timestamp'],
|
| 1027 |
+
'history': []
|
| 1028 |
+
}
|
| 1029 |
+
unique_journeys[gid]['history'].append({
|
| 1030 |
+
'feed': entry['feed_id'],
|
| 1031 |
+
'time': entry['timestamp']
|
| 1032 |
+
})
|
| 1033 |
+
|
| 1034 |
+
# Sort history and limit to last 5 entries per ID
|
| 1035 |
+
for gid in unique_journeys:
|
| 1036 |
+
unique_journeys[gid]['history'] = unique_journeys[gid]['history'][-5:]
|
| 1037 |
+
unique_journeys[gid]['last_feed'] = unique_journeys[gid]['history'][-1]['feed']
|
| 1038 |
+
unique_journeys[gid]['last_seen'] = unique_journeys[gid]['history'][-1]['time']
|
| 1039 |
+
|
| 1040 |
+
return jsonify(list(unique_journeys.values()))
|
| 1041 |
+
|
| 1042 |
+
|
| 1043 |
+
@app.route('/video_feed')
|
| 1044 |
+
@app.route('/video_feed/<int:feed_id>')
|
| 1045 |
+
def video_feed(feed_id=0):
|
| 1046 |
+
return Response(generate_frames(feed_id), mimetype='multipart/x-mixed-replace; boundary=frame')
|
| 1047 |
+
|
| 1048 |
+
|
| 1049 |
+
@app.route('/toggle_module', methods=['POST'])
|
| 1050 |
+
def toggle_module():
|
| 1051 |
+
global active_modules
|
| 1052 |
+
data = request.get_json()
|
| 1053 |
+
module = data.get('module')
|
| 1054 |
+
|
| 1055 |
+
valid_modules = ['movement', 'facemask', 'weapon', 'public_safety', 'suspect_journey']
|
| 1056 |
+
if module not in valid_modules:
|
| 1057 |
+
return jsonify({'error': 'Invalid module'}), 400
|
| 1058 |
+
|
| 1059 |
+
if module in active_modules:
|
| 1060 |
+
active_modules.remove(module)
|
| 1061 |
+
log_audit("MODULE_DEACTIVATED", f"{module.upper()} detection disabled", "INFO")
|
| 1062 |
+
else:
|
| 1063 |
+
active_modules.add(module)
|
| 1064 |
+
log_audit("MODULE_ACTIVATED", f"{module.upper()} detection enabled", "INFO")
|
| 1065 |
+
|
| 1066 |
+
# When suspect_journey is activated, ensure movement is also active
|
| 1067 |
+
if module == 'suspect_journey':
|
| 1068 |
+
active_modules.add('movement')
|
| 1069 |
+
log_audit("MODULE_ACTIVATED", "MOVEMENT auto-enabled for journey tracking", "INFO")
|
| 1070 |
+
|
| 1071 |
+
return jsonify({
|
| 1072 |
+
'success': True,
|
| 1073 |
+
'active_modules': list(active_modules)
|
| 1074 |
+
})
|
| 1075 |
+
|
| 1076 |
+
|
| 1077 |
+
@app.route('/set_source', methods=['POST'])
|
| 1078 |
+
def set_source():
|
| 1079 |
+
data = request.get_json()
|
| 1080 |
+
source = data.get('source')
|
| 1081 |
+
feed_id = data.get('feed_id', 0)
|
| 1082 |
+
if source == 'camera' and feed_id == 0:
|
| 1083 |
+
feed_sources.pop(feed_id, None)
|
| 1084 |
+
restart_feed(feed_id)
|
| 1085 |
+
log_audit("SOURCE_CHANGE", f"Feed #{feed_id} set to live camera", "INFO")
|
| 1086 |
+
return jsonify({'success': True})
|
| 1087 |
+
return jsonify({'error': 'Invalid source'}), 400
|
| 1088 |
+
|
| 1089 |
+
|
| 1090 |
+
@app.route('/upload_video', methods=['POST'])
|
| 1091 |
+
@app.route('/upload_video/<int:feed_id>', methods=['POST'])
|
| 1092 |
+
def upload_video(feed_id=0):
|
| 1093 |
+
"""Upload a video to a specific feed. Default: feed 0."""
|
| 1094 |
+
if 'file' not in request.files:
|
| 1095 |
+
return jsonify({'error': 'No file part'}), 400
|
| 1096 |
+
file = request.files['file']
|
| 1097 |
+
if file.filename == '':
|
| 1098 |
+
return jsonify({'error': 'No selected file'}), 400
|
| 1099 |
+
|
| 1100 |
+
if feed_id < 0 or feed_id >= MAX_FEEDS:
|
| 1101 |
+
return jsonify({'error': f'Invalid feed_id. Must be 0-{MAX_FEEDS-1}'}), 400
|
| 1102 |
+
|
| 1103 |
+
filename = secure_filename(file.filename)
|
| 1104 |
+
|
| 1105 |
+
allowed_extensions = {'.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm', '.gif'}
|
| 1106 |
+
file_ext = os.path.splitext(filename)[1].lower()
|
| 1107 |
+
if file_ext not in allowed_extensions:
|
| 1108 |
+
return jsonify({'error': f'Unsupported file format. Allowed: {", ".join(allowed_extensions)}'}), 400
|
| 1109 |
+
|
| 1110 |
+
# Save with feed-specific prefix to avoid collisions
|
| 1111 |
+
save_name = f"feed{feed_id}_{filename}"
|
| 1112 |
+
filepath = os.path.join(app.config['UPLOAD_FOLDER'], save_name)
|
| 1113 |
+
filepath = os.path.abspath(filepath)
|
| 1114 |
+
file.save(filepath)
|
| 1115 |
+
|
| 1116 |
+
# Register source for this feed and restart it
|
| 1117 |
+
feed_sources[feed_id] = filepath
|
| 1118 |
+
restart_feed(feed_id)
|
| 1119 |
+
|
| 1120 |
+
log_audit("FILE_UPLOAD", f"Feed #{feed_id}: {filename} uploaded", "INFO")
|
| 1121 |
+
return jsonify({
|
| 1122 |
+
'success': True,
|
| 1123 |
+
'message': f'Feed {feed_id}: {filename} loaded',
|
| 1124 |
+
'feed_id': feed_id,
|
| 1125 |
+
'feed_url': f'/video_feed/{feed_id}'
|
| 1126 |
+
})
|
| 1127 |
+
|
| 1128 |
+
|
| 1129 |
+
@app.route('/stats')
|
| 1130 |
+
def get_stats():
|
| 1131 |
+
return jsonify({
|
| 1132 |
+
'threat_score': int(previous_score),
|
| 1133 |
+
'details': latest_details,
|
| 1134 |
+
'red_alert': red_alert,
|
| 1135 |
+
'active_modules': list(active_modules)
|
| 1136 |
+
})
|
| 1137 |
+
|
| 1138 |
+
|
| 1139 |
+
@app.route('/audit_log')
|
| 1140 |
+
def get_audit_log():
|
| 1141 |
+
with audit_lock:
|
| 1142 |
+
entries = list(audit_log)
|
| 1143 |
+
return jsonify({'log': entries})
|
| 1144 |
+
|
| 1145 |
+
|
| 1146 |
+
@app.route('/generate_report', methods=['POST'])
|
| 1147 |
+
def generate_report():
|
| 1148 |
+
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 1149 |
+
log_audit("REPORT_GENERATED", f"Operator requested AI incident report at threat score {int(previous_score)}", "INFO")
|
| 1150 |
+
|
| 1151 |
+
prompt = f"""
|
| 1152 |
+
You are the AI Agent for Project SENTINEL, a national security surveillance system.
|
| 1153 |
+
Generate a concise, professional security incident report based on the following real-time data.
|
| 1154 |
+
|
| 1155 |
+
TIMESTAMP: {timestamp}
|
| 1156 |
+
THREAT SCORE: {int(previous_score)}/100
|
| 1157 |
+
ACTIVE MODULES: {', '.join([m.upper() for m in active_modules])}
|
| 1158 |
+
DETECTION DETAILS: {latest_details}
|
| 1159 |
+
|
| 1160 |
+
Format the report with these sections:
|
| 1161 |
+
1. INCIDENT SUMMARY (1-2 sentences)
|
| 1162 |
+
2. THREAT ANALYSIS (Bullet points of specific risks)
|
| 1163 |
+
3. RECOMMENDED ACTION (Clear directive for security personnel)
|
| 1164 |
+
|
| 1165 |
+
Keep it brief and authoritative.
|
| 1166 |
+
"""
|
| 1167 |
+
|
| 1168 |
+
try:
|
| 1169 |
+
response = model_gemini.generate_content(prompt)
|
| 1170 |
+
report_text = response.text
|
| 1171 |
+
except Exception as e:
|
| 1172 |
+
print(f"Gemini Error: {e}")
|
| 1173 |
+
import traceback
|
| 1174 |
+
traceback.print_exc()
|
| 1175 |
+
report_text = f"ERROR: Could not generate AI report. System fallback.\n\nDetails: {latest_details}"
|
| 1176 |
+
|
| 1177 |
+
return jsonify({'report': report_text})
|
| 1178 |
+
|
| 1179 |
+
|
| 1180 |
+
@app.route('/reset_system', methods=['POST'])
|
| 1181 |
+
def reset_system():
|
| 1182 |
+
global active_modules, red_alert, previous_score, feed_sources
|
| 1183 |
+
global person_history, object_history, latest_details
|
| 1184 |
+
|
| 1185 |
+
active_modules = {'movement'}
|
| 1186 |
+
red_alert = False
|
| 1187 |
+
previous_score = 0.0
|
| 1188 |
+
feed_sources.clear()
|
| 1189 |
+
|
| 1190 |
+
person_history.clear()
|
| 1191 |
+
object_history.clear()
|
| 1192 |
+
latest_details.clear()
|
| 1193 |
+
journey_log.clear()
|
| 1194 |
+
identity_manager.global_subjects.clear()
|
| 1195 |
+
identity_manager.local_to_global.clear()
|
| 1196 |
+
identity_manager.next_global_id = 100
|
| 1197 |
+
|
| 1198 |
+
for fid in list(camera_feeds.keys()):
|
| 1199 |
+
restart_feed(fid)
|
| 1200 |
+
|
| 1201 |
+
log_audit("SYSTEM_RESET", "Operator initiated full system reset. All states cleared.", "WARNING")
|
| 1202 |
+
|
| 1203 |
+
return jsonify({'success': True})
|
| 1204 |
+
|
| 1205 |
+
# ════════════════════════════════════════════════════════════════
|
| 1206 |
+
# DISPATCH ROUTES
|
| 1207 |
+
# ════════════════════════════════════════════════════════════════
|
| 1208 |
+
|
| 1209 |
+
@app.route('/dispatch_log')
|
| 1210 |
+
def get_dispatch_log():
|
| 1211 |
+
"""Return dispatch history and pending approvals."""
|
| 1212 |
+
with dispatch_lock:
|
| 1213 |
+
log_entries = list(dispatch_log)
|
| 1214 |
+
pending = list(pending_approvals.values())
|
| 1215 |
+
return jsonify({
|
| 1216 |
+
'log': log_entries,
|
| 1217 |
+
'pending': pending,
|
| 1218 |
+
'settings': {
|
| 1219 |
+
'auto_dispatch': dispatch_settings['auto_dispatch'],
|
| 1220 |
+
'cooldown_seconds': dispatch_settings['cooldown_seconds'],
|
| 1221 |
+
'enabled': dispatch_settings['enabled'],
|
| 1222 |
+
'telegram_configured': telegram_dispatcher.is_configured(),
|
| 1223 |
+
'chat_id': telegram_dispatcher.chat_id or '',
|
| 1224 |
+
}
|
| 1225 |
+
})
|
| 1226 |
+
|
| 1227 |
+
|
| 1228 |
+
@app.route('/dispatch_settings', methods=['GET', 'POST'])
|
| 1229 |
+
def handle_dispatch_settings():
|
| 1230 |
+
if request.method == 'GET':
|
| 1231 |
+
return jsonify({
|
| 1232 |
+
'auto_dispatch': dispatch_settings['auto_dispatch'],
|
| 1233 |
+
'cooldown_seconds': dispatch_settings['cooldown_seconds'],
|
| 1234 |
+
'enabled': dispatch_settings['enabled'],
|
| 1235 |
+
'bot_token': telegram_dispatcher.bot_token[:10] + '...' if telegram_dispatcher.bot_token else '',
|
| 1236 |
+
'chat_id': telegram_dispatcher.chat_id or '',
|
| 1237 |
+
'telegram_configured': telegram_dispatcher.is_configured(),
|
| 1238 |
+
})
|
| 1239 |
+
|
| 1240 |
+
data = request.get_json()
|
| 1241 |
+
if 'auto_dispatch' in data:
|
| 1242 |
+
dispatch_settings['auto_dispatch'] = bool(data['auto_dispatch'])
|
| 1243 |
+
mode = 'AUTO' if dispatch_settings['auto_dispatch'] else 'MANUAL APPROVAL'
|
| 1244 |
+
log_audit("DISPATCH_MODE", f"Dispatch mode set to {mode}", "INFO")
|
| 1245 |
+
if 'cooldown_seconds' in data:
|
| 1246 |
+
dispatch_settings['cooldown_seconds'] = max(10, min(600, int(data['cooldown_seconds'])))
|
| 1247 |
+
if 'enabled' in data:
|
| 1248 |
+
dispatch_settings['enabled'] = bool(data['enabled'])
|
| 1249 |
+
log_audit("DISPATCH_TOGGLE", f"Dispatch {'enabled' if dispatch_settings['enabled'] else 'disabled'}", "INFO")
|
| 1250 |
+
if 'bot_token' in data and data['bot_token']:
|
| 1251 |
+
telegram_dispatcher.configure(data['bot_token'], data.get('chat_id', telegram_dispatcher.chat_id))
|
| 1252 |
+
log_audit("DISPATCH_CONFIG", "Telegram bot token updated", "INFO")
|
| 1253 |
+
if 'chat_id' in data and data['chat_id']:
|
| 1254 |
+
telegram_dispatcher.chat_id = str(data['chat_id'])
|
| 1255 |
+
log_audit("DISPATCH_CONFIG", f"Chat ID set to {telegram_dispatcher.chat_id}", "INFO")
|
| 1256 |
+
|
| 1257 |
+
return jsonify({'success': True})
|
| 1258 |
+
|
| 1259 |
+
|
| 1260 |
+
@app.route('/approve_dispatch/<event_id>', methods=['POST'])
|
| 1261 |
+
def approve_dispatch(event_id):
|
| 1262 |
+
result = approve_dispatch_event(event_id)
|
| 1263 |
+
return jsonify(result)
|
| 1264 |
+
|
| 1265 |
+
|
| 1266 |
+
@app.route('/reject_dispatch/<event_id>', methods=['POST'])
|
| 1267 |
+
def reject_dispatch(event_id):
|
| 1268 |
+
result = reject_dispatch_event(event_id)
|
| 1269 |
+
return jsonify(result)
|
| 1270 |
+
|
| 1271 |
+
|
| 1272 |
+
@app.route('/test_dispatch', methods=['POST'])
|
| 1273 |
+
def test_dispatch():
|
| 1274 |
+
"""Send a test message to verify Telegram connection."""
|
| 1275 |
+
# Try auto-detect chat_id if not set
|
| 1276 |
+
if not telegram_dispatcher.chat_id:
|
| 1277 |
+
detected = telegram_dispatcher.auto_detect_chat_id()
|
| 1278 |
+
if detected:
|
| 1279 |
+
log_audit("DISPATCH_CONFIG", f"Auto-detected chat ID: {detected}", "INFO")
|
| 1280 |
+
|
| 1281 |
+
if not telegram_dispatcher.is_configured():
|
| 1282 |
+
return jsonify({
|
| 1283 |
+
'ok': False,
|
| 1284 |
+
'error': 'Telegram not fully configured. Please set bot token and chat ID.'
|
| 1285 |
+
}), 400
|
| 1286 |
+
|
| 1287 |
+
result = telegram_dispatcher.test_connection()
|
| 1288 |
+
return jsonify(result)
|
| 1289 |
+
|
| 1290 |
+
|
| 1291 |
+
@app.route('/dispatch_alert', methods=['POST'])
|
| 1292 |
+
def manual_dispatch_alert():
|
| 1293 |
+
"""Manually trigger a dispatch alert with current threat data."""
|
| 1294 |
+
event_id = str(uuid.uuid4())[:8]
|
| 1295 |
+
now = time.time()
|
| 1296 |
+
event = {
|
| 1297 |
+
'id': event_id,
|
| 1298 |
+
'timestamp': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
| 1299 |
+
'threat_score': int(previous_score),
|
| 1300 |
+
'details': dict(latest_details) if latest_details else {},
|
| 1301 |
+
'active_modules': list(active_modules),
|
| 1302 |
+
'status': 'pending',
|
| 1303 |
+
'created_at': now,
|
| 1304 |
+
}
|
| 1305 |
+
|
| 1306 |
+
if not telegram_dispatcher.is_configured():
|
| 1307 |
+
return jsonify({'ok': False, 'error': 'Telegram not configured'}), 400
|
| 1308 |
+
|
| 1309 |
+
result = telegram_dispatcher.send_threat_alert(
|
| 1310 |
+
event['threat_score'], event['details'], event['active_modules']
|
| 1311 |
+
)
|
| 1312 |
+
event['status'] = 'sent' if result.get('ok') else 'failed'
|
| 1313 |
+
dispatch_settings['last_dispatch_time'] = now
|
| 1314 |
+
|
| 1315 |
+
with dispatch_lock:
|
| 1316 |
+
dispatch_log.appendleft(event)
|
| 1317 |
+
log_audit("MANUAL_DISPATCH", f"Operator manually dispatched alert (score: {event['threat_score']})", "CRITICAL")
|
| 1318 |
+
return jsonify({'success': True, 'telegram_result': result})
|
| 1319 |
+
|
| 1320 |
+
|
| 1321 |
+
|
| 1322 |
+
|
| 1323 |
+
# ════════════════════════════════════════════════════════════════
|
| 1324 |
+
# ENTRY POINT
|
| 1325 |
+
# ════════════════════════════════════════════════════════════════
|
| 1326 |
+
if __name__ == '__main__':
|
| 1327 |
+
print("\n" + "═"*60)
|
| 1328 |
+
print(" PROJECT SENTINEL V14 — DISPATCH INTEGRATION")
|
| 1329 |
+
print("═"*60)
|
| 1330 |
+
print("\n ✨ V14 FEATURES:")
|
| 1331 |
+
print(" • Automated Telegram alert dispatch on RED_ALERT")
|
| 1332 |
+
print(" • Human-in-the-loop approval/rejection queue")
|
| 1333 |
+
print(" • Dispatch Center dashboard with alert history")
|
| 1334 |
+
print(" • Configurable auto-dispatch / manual mode")
|
| 1335 |
+
print(" • All V13 features (Journey Tracker, Multi-Camera, Re-ID)")
|
| 1336 |
+
print("\n 👉 Open your browser: http://localhost:8080\n")
|
| 1337 |
+
app.run(host='0.0.0.0', port=8080, debug=False, threaded=True)
|
sort.py
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SORT: A Simple, Online and Realtime Tracker
|
| 3 |
+
Copyright (C) 2016-2020 Alex Bewley alex@bewley.ai
|
| 4 |
+
|
| 5 |
+
This program is free software: you can redistribute it and/or modify
|
| 6 |
+
it under the terms of the GNU General Public License as published by
|
| 7 |
+
the Free Software Foundation, either version 3 of the License, or
|
| 8 |
+
(at your option) any later version.
|
| 9 |
+
|
| 10 |
+
This program is distributed in the hope that it will be useful,
|
| 11 |
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
+
GNU General Public License for more details.
|
| 14 |
+
|
| 15 |
+
You should have received a copy of the GNU General Public License
|
| 16 |
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 17 |
+
"""
|
| 18 |
+
from __future__ import print_function
|
| 19 |
+
|
| 20 |
+
import os
|
| 21 |
+
import numpy as np
|
| 22 |
+
import matplotlib
|
| 23 |
+
matplotlib.use('Agg')
|
| 24 |
+
import matplotlib.pyplot as plt
|
| 25 |
+
import matplotlib.patches as patches
|
| 26 |
+
from skimage import io
|
| 27 |
+
|
| 28 |
+
import glob
|
| 29 |
+
import time
|
| 30 |
+
import argparse
|
| 31 |
+
from filterpy.kalman import KalmanFilter
|
| 32 |
+
|
| 33 |
+
np.random.seed(0)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def linear_assignment(cost_matrix):
|
| 37 |
+
try:
|
| 38 |
+
import lap
|
| 39 |
+
_, x, y = lap.lapjv(cost_matrix, extend_cost=True)
|
| 40 |
+
return np.array([[y[i],i] for i in x if i >= 0]) #
|
| 41 |
+
except ImportError:
|
| 42 |
+
from scipy.optimize import linear_sum_assignment
|
| 43 |
+
x, y = linear_sum_assignment(cost_matrix)
|
| 44 |
+
return np.array(list(zip(x, y)))
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def iou_batch(bb_test, bb_gt):
|
| 48 |
+
"""
|
| 49 |
+
From SORT: Computes IOU between two bboxes in the form [x1,y1,x2,y2]
|
| 50 |
+
"""
|
| 51 |
+
bb_gt = np.expand_dims(bb_gt, 0)
|
| 52 |
+
bb_test = np.expand_dims(bb_test, 1)
|
| 53 |
+
|
| 54 |
+
xx1 = np.maximum(bb_test[..., 0], bb_gt[..., 0])
|
| 55 |
+
yy1 = np.maximum(bb_test[..., 1], bb_gt[..., 1])
|
| 56 |
+
xx2 = np.minimum(bb_test[..., 2], bb_gt[..., 2])
|
| 57 |
+
yy2 = np.minimum(bb_test[..., 3], bb_gt[..., 3])
|
| 58 |
+
w = np.maximum(0., xx2 - xx1)
|
| 59 |
+
h = np.maximum(0., yy2 - yy1)
|
| 60 |
+
wh = w * h
|
| 61 |
+
o = wh / ((bb_test[..., 2] - bb_test[..., 0]) * (bb_test[..., 3] - bb_test[..., 1])
|
| 62 |
+
+ (bb_gt[..., 2] - bb_gt[..., 0]) * (bb_gt[..., 3] - bb_gt[..., 1]) - wh)
|
| 63 |
+
return(o)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def convert_bbox_to_z(bbox):
|
| 67 |
+
"""
|
| 68 |
+
Takes a bounding box in the form [x1,y1,x2,y2] and returns z in the form
|
| 69 |
+
[x,y,s,r] where x,y is the centre of the box and s is the scale/area and r is
|
| 70 |
+
the aspect ratio
|
| 71 |
+
"""
|
| 72 |
+
w = bbox[2] - bbox[0]
|
| 73 |
+
h = bbox[3] - bbox[1]
|
| 74 |
+
x = bbox[0] + w/2.
|
| 75 |
+
y = bbox[1] + h/2.
|
| 76 |
+
s = w * h #scale is just area
|
| 77 |
+
r = w / float(h)
|
| 78 |
+
return np.array([x, y, s, r]).reshape((4, 1))
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def convert_x_to_bbox(x,score=None):
|
| 82 |
+
"""
|
| 83 |
+
Takes a bounding box in the centre form [x,y,s,r] and returns it in the form
|
| 84 |
+
[x1,y1,x2,y2] where x1,y1 is the top left and x2,y2 is the bottom right
|
| 85 |
+
"""
|
| 86 |
+
w = np.sqrt(x[2] * x[3])
|
| 87 |
+
h = x[2] / w
|
| 88 |
+
if(score==None):
|
| 89 |
+
return np.array([x[0]-w/2.,x[1]-h/2.,x[0]+w/2.,x[1]+h/2.]).reshape((1,4))
|
| 90 |
+
else:
|
| 91 |
+
return np.array([x[0]-w/2.,x[1]-h/2.,x[0]+w/2.,x[1]+h/2.,score]).reshape((1,5))
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
class KalmanBoxTracker(object):
|
| 95 |
+
"""
|
| 96 |
+
This class represents the internal state of individual tracked objects observed as bbox.
|
| 97 |
+
"""
|
| 98 |
+
count = 0
|
| 99 |
+
def __init__(self,bbox):
|
| 100 |
+
"""
|
| 101 |
+
Initialises a tracker using initial bounding box.
|
| 102 |
+
"""
|
| 103 |
+
#define constant velocity model
|
| 104 |
+
self.kf = KalmanFilter(dim_x=7, dim_z=4)
|
| 105 |
+
self.kf.F = np.array([[1,0,0,0,1,0,0],[0,1,0,0,0,1,0],[0,0,1,0,0,0,1],[0,0,0,1,0,0,0], [0,0,0,0,1,0,0],[0,0,0,0,0,1,0],[0,0,0,0,0,0,1]])
|
| 106 |
+
self.kf.H = np.array([[1,0,0,0,0,0,0],[0,1,0,0,0,0,0],[0,0,1,0,0,0,0],[0,0,0,1,0,0,0]])
|
| 107 |
+
|
| 108 |
+
self.kf.R[2:,2:] *= 10.
|
| 109 |
+
self.kf.P[4:,4:] *= 1000. #give high uncertainty to the unobservable initial velocities
|
| 110 |
+
self.kf.P *= 10.
|
| 111 |
+
self.kf.Q[-1,-1] *= 0.01
|
| 112 |
+
self.kf.Q[4:,4:] *= 0.01
|
| 113 |
+
|
| 114 |
+
self.kf.x[:4] = convert_bbox_to_z(bbox)
|
| 115 |
+
self.time_since_update = 0
|
| 116 |
+
self.id = KalmanBoxTracker.count
|
| 117 |
+
KalmanBoxTracker.count += 1
|
| 118 |
+
self.history = []
|
| 119 |
+
self.hits = 0
|
| 120 |
+
self.hit_streak = 0
|
| 121 |
+
self.age = 0
|
| 122 |
+
|
| 123 |
+
def update(self,bbox):
|
| 124 |
+
"""
|
| 125 |
+
Updates the state vector with observed bbox.
|
| 126 |
+
"""
|
| 127 |
+
self.time_since_update = 0
|
| 128 |
+
self.history = []
|
| 129 |
+
self.hits += 1
|
| 130 |
+
self.hit_streak += 1
|
| 131 |
+
self.kf.update(convert_bbox_to_z(bbox))
|
| 132 |
+
|
| 133 |
+
def predict(self):
|
| 134 |
+
"""
|
| 135 |
+
Advances the state vector and returns the predicted bounding box estimate.
|
| 136 |
+
"""
|
| 137 |
+
if((self.kf.x[6]+self.kf.x[2])<=0):
|
| 138 |
+
self.kf.x[6] *= 0.0
|
| 139 |
+
self.kf.predict()
|
| 140 |
+
self.age += 1
|
| 141 |
+
if(self.time_since_update>0):
|
| 142 |
+
self.hit_streak = 0
|
| 143 |
+
self.time_since_update += 1
|
| 144 |
+
self.history.append(convert_x_to_bbox(self.kf.x))
|
| 145 |
+
return self.history[-1]
|
| 146 |
+
|
| 147 |
+
def get_state(self):
|
| 148 |
+
"""
|
| 149 |
+
Returns the current bounding box estimate.
|
| 150 |
+
"""
|
| 151 |
+
return convert_x_to_bbox(self.kf.x)
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def associate_detections_to_trackers(detections,trackers,iou_threshold = 0.3):
|
| 155 |
+
"""
|
| 156 |
+
Assigns detections to tracked object (both represented as bounding boxes)
|
| 157 |
+
|
| 158 |
+
Returns 3 lists of matches, unmatched_detections and unmatched_trackers
|
| 159 |
+
"""
|
| 160 |
+
if(len(trackers)==0):
|
| 161 |
+
return np.empty((0,2),dtype=int), np.arange(len(detections)), np.empty((0,5),dtype=int)
|
| 162 |
+
|
| 163 |
+
iou_matrix = iou_batch(detections, trackers)
|
| 164 |
+
|
| 165 |
+
if min(iou_matrix.shape) > 0:
|
| 166 |
+
a = (iou_matrix > iou_threshold).astype(np.int32)
|
| 167 |
+
if a.sum(1).max() == 1 and a.sum(0).max() == 1:
|
| 168 |
+
matched_indices = np.stack(np.where(a), axis=1)
|
| 169 |
+
else:
|
| 170 |
+
matched_indices = linear_assignment(-iou_matrix)
|
| 171 |
+
else:
|
| 172 |
+
matched_indices = np.empty(shape=(0,2))
|
| 173 |
+
|
| 174 |
+
unmatched_detections = []
|
| 175 |
+
for d, det in enumerate(detections):
|
| 176 |
+
if(d not in matched_indices[:,0]):
|
| 177 |
+
unmatched_detections.append(d)
|
| 178 |
+
unmatched_trackers = []
|
| 179 |
+
for t, trk in enumerate(trackers):
|
| 180 |
+
if(t not in matched_indices[:,1]):
|
| 181 |
+
unmatched_trackers.append(t)
|
| 182 |
+
|
| 183 |
+
#filter out matched with low IOU
|
| 184 |
+
matches = []
|
| 185 |
+
for m in matched_indices:
|
| 186 |
+
if(iou_matrix[m[0], m[1]]<iou_threshold):
|
| 187 |
+
unmatched_detections.append(m[0])
|
| 188 |
+
unmatched_trackers.append(m[1])
|
| 189 |
+
else:
|
| 190 |
+
matches.append(m.reshape(1,2))
|
| 191 |
+
if(len(matches)==0):
|
| 192 |
+
matches = np.empty((0,2),dtype=int)
|
| 193 |
+
else:
|
| 194 |
+
matches = np.concatenate(matches,axis=0)
|
| 195 |
+
|
| 196 |
+
return matches, np.array(unmatched_detections), np.array(unmatched_trackers)
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
class Sort(object):
|
| 200 |
+
def __init__(self, max_age=1, min_hits=3, iou_threshold=0.3):
|
| 201 |
+
"""
|
| 202 |
+
Sets key parameters for SORT
|
| 203 |
+
"""
|
| 204 |
+
self.max_age = max_age
|
| 205 |
+
self.min_hits = min_hits
|
| 206 |
+
self.iou_threshold = iou_threshold
|
| 207 |
+
self.trackers = []
|
| 208 |
+
self.frame_count = 0
|
| 209 |
+
|
| 210 |
+
def update(self, dets=np.empty((0, 5))):
|
| 211 |
+
"""
|
| 212 |
+
Params:
|
| 213 |
+
dets - a numpy array of detections in the format [[x1,y1,x2,y2,score],[x1,y1,x2,y2,score],...]
|
| 214 |
+
Requires: this method must be called once for each frame even with empty detections (use np.empty((0, 5)) for frames without detections).
|
| 215 |
+
Returns the a similar array, where the last column is the object ID.
|
| 216 |
+
|
| 217 |
+
NOTE: The number of objects returned may differ from the number of detections provided.
|
| 218 |
+
"""
|
| 219 |
+
self.frame_count += 1
|
| 220 |
+
# get predicted locations from existing trackers.
|
| 221 |
+
trks = np.zeros((len(self.trackers), 5))
|
| 222 |
+
to_del = []
|
| 223 |
+
ret = []
|
| 224 |
+
for t, trk in enumerate(trks):
|
| 225 |
+
pos = self.trackers[t].predict()[0]
|
| 226 |
+
trk[:] = [pos[0], pos[1], pos[2], pos[3], 0]
|
| 227 |
+
if np.any(np.isnan(pos)):
|
| 228 |
+
to_del.append(t)
|
| 229 |
+
trks = np.ma.compress_rows(np.ma.masked_invalid(trks))
|
| 230 |
+
for t in reversed(to_del):
|
| 231 |
+
self.trackers.pop(t)
|
| 232 |
+
matched, unmatched_dets, unmatched_trks = associate_detections_to_trackers(dets,trks, self.iou_threshold)
|
| 233 |
+
|
| 234 |
+
# update matched trackers with assigned detections
|
| 235 |
+
for m in matched:
|
| 236 |
+
self.trackers[m[1]].update(dets[m[0], :])
|
| 237 |
+
|
| 238 |
+
# create and initialise new trackers for unmatched detections
|
| 239 |
+
for i in unmatched_dets:
|
| 240 |
+
trk = KalmanBoxTracker(dets[i,:])
|
| 241 |
+
self.trackers.append(trk)
|
| 242 |
+
i = len(self.trackers)
|
| 243 |
+
for trk in reversed(self.trackers):
|
| 244 |
+
d = trk.get_state()[0]
|
| 245 |
+
if (trk.time_since_update < 1) and (trk.hit_streak >= self.min_hits or self.frame_count <= self.min_hits):
|
| 246 |
+
ret.append(np.concatenate((d,[trk.id+1])).reshape(1,-1)) # +1 as MOT benchmark requires positive
|
| 247 |
+
i -= 1
|
| 248 |
+
# remove dead tracklet
|
| 249 |
+
if(trk.time_since_update > self.max_age):
|
| 250 |
+
self.trackers.pop(i)
|
| 251 |
+
if(len(ret)>0):
|
| 252 |
+
return np.concatenate(ret)
|
| 253 |
+
return np.empty((0,5))
|
| 254 |
+
|
| 255 |
+
def parse_args():
|
| 256 |
+
"""Parse input arguments."""
|
| 257 |
+
parser = argparse.ArgumentParser(description='SORT demo')
|
| 258 |
+
parser.add_argument('--display', dest='display', help='Display online tracker output (slow) [False]',action='store_true')
|
| 259 |
+
parser.add_argument("--seq_path", help="Path to detections.", type=str, default='data')
|
| 260 |
+
parser.add_argument("--phase", help="Subdirectory in seq_path.", type=str, default='train')
|
| 261 |
+
parser.add_argument("--max_age",
|
| 262 |
+
help="Maximum number of frames to keep alive a track without associated detections.",
|
| 263 |
+
type=int, default=1)
|
| 264 |
+
parser.add_argument("--min_hits",
|
| 265 |
+
help="Minimum number of associated detections before track is initialised.",
|
| 266 |
+
type=int, default=3)
|
| 267 |
+
parser.add_argument("--iou_threshold", help="Minimum IOU for match.", type=float, default=0.3)
|
| 268 |
+
args = parser.parse_args()
|
| 269 |
+
return args
|
| 270 |
+
|
| 271 |
+
if __name__ == '__main__':
|
| 272 |
+
# all train
|
| 273 |
+
args = parse_args()
|
| 274 |
+
display = args.display
|
| 275 |
+
phase = args.phase
|
| 276 |
+
total_time = 0.0
|
| 277 |
+
total_frames = 0
|
| 278 |
+
colours = np.random.rand(32, 3) #used only for display
|
| 279 |
+
if(display):
|
| 280 |
+
if not os.path.exists('mot_benchmark'):
|
| 281 |
+
print('\n\tERROR: mot_benchmark link not found!\n\n Create a symbolic link to the MOT benchmark\n (https://motchallenge.net/data/2D_MOT_2015/#download). E.g.:\n\n $ ln -s /path/to/MOT2015_challenge/2DMOT2015 mot_benchmark\n\n')
|
| 282 |
+
exit()
|
| 283 |
+
plt.ion()
|
| 284 |
+
fig = plt.figure()
|
| 285 |
+
ax1 = fig.add_subplot(111, aspect='equal')
|
| 286 |
+
|
| 287 |
+
if not os.path.exists('output'):
|
| 288 |
+
os.makedirs('output')
|
| 289 |
+
pattern = os.path.join(args.seq_path, phase, '*', 'det', 'det.txt')
|
| 290 |
+
for seq_dets_fn in glob.glob(pattern):
|
| 291 |
+
mot_tracker = Sort(max_age=args.max_age,
|
| 292 |
+
min_hits=args.min_hits,
|
| 293 |
+
iou_threshold=args.iou_threshold) #create instance of the SORT tracker
|
| 294 |
+
seq_dets = np.loadtxt(seq_dets_fn, delimiter=',')
|
| 295 |
+
seq = seq_dets_fn[pattern.find('*'):].split(os.path.sep)[0]
|
| 296 |
+
|
| 297 |
+
with open(os.path.join('output', '%s.txt'%(seq)),'w') as out_file:
|
| 298 |
+
print("Processing %s."%(seq))
|
| 299 |
+
for frame in range(int(seq_dets[:,0].max())):
|
| 300 |
+
frame += 1 #detection and frame numbers begin at 1
|
| 301 |
+
dets = seq_dets[seq_dets[:, 0]==frame, 2:7]
|
| 302 |
+
dets[:, 2:4] += dets[:, 0:2] #convert to [x1,y1,w,h] to [x1,y1,x2,y2]
|
| 303 |
+
total_frames += 1
|
| 304 |
+
|
| 305 |
+
if(display):
|
| 306 |
+
fn = os.path.join('mot_benchmark', phase, seq, 'img1', '%06d.jpg'%(frame))
|
| 307 |
+
im =io.imread(fn)
|
| 308 |
+
ax1.imshow(im)
|
| 309 |
+
plt.title(seq + ' Tracked Targets')
|
| 310 |
+
|
| 311 |
+
start_time = time.time()
|
| 312 |
+
trackers = mot_tracker.update(dets)
|
| 313 |
+
cycle_time = time.time() - start_time
|
| 314 |
+
total_time += cycle_time
|
| 315 |
+
|
| 316 |
+
for d in trackers:
|
| 317 |
+
print('%d,%d,%.2f,%.2f,%.2f,%.2f,1,-1,-1,-1'%(frame,d[4],d[0],d[1],d[2]-d[0],d[3]-d[1]),file=out_file)
|
| 318 |
+
if(display):
|
| 319 |
+
d = d.astype(np.int32)
|
| 320 |
+
ax1.add_patch(patches.Rectangle((d[0],d[1]),d[2]-d[0],d[3]-d[1],fill=False,lw=3,ec=colours[d[4]%32,:]))
|
| 321 |
+
|
| 322 |
+
if(display):
|
| 323 |
+
fig.canvas.flush_events()
|
| 324 |
+
plt.draw()
|
| 325 |
+
ax1.cla()
|
| 326 |
+
|
| 327 |
+
print("Total Tracking took: %.3f seconds for %d frames or %.1f FPS" % (total_time, total_frames, total_frames / total_time))
|
| 328 |
+
|
| 329 |
+
if(display):
|
| 330 |
+
print("Note: to get real runtime results run without the option: --display")
|
templates/gun_index.html
ADDED
|
@@ -0,0 +1,581 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>ATM ThreatVision - Gun Detection</title>
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
| 9 |
+
<style>
|
| 10 |
+
:root {
|
| 11 |
+
--ios-bg: #F2F2F7;
|
| 12 |
+
--ios-card: rgba(255, 255, 255, 0.8);
|
| 13 |
+
--ios-text: #000000;
|
| 14 |
+
--ios-secondary: #8E8E93;
|
| 15 |
+
--ios-orange: #FF6B00;
|
| 16 |
+
--ios-red: #FF3B30;
|
| 17 |
+
--ios-green: #34C759;
|
| 18 |
+
--ios-blue: #007AFF;
|
| 19 |
+
--ios-blur: blur(20px);
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
* {
|
| 23 |
+
margin: 0;
|
| 24 |
+
padding: 0;
|
| 25 |
+
box-sizing: border-box;
|
| 26 |
+
-webkit-font-smoothing: antialiased;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
body {
|
| 30 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 31 |
+
background-color: var(--ios-bg);
|
| 32 |
+
color: var(--ios-text);
|
| 33 |
+
min-height: 100vh;
|
| 34 |
+
padding: 40px 20px;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
.container {
|
| 38 |
+
max-width: 1200px;
|
| 39 |
+
margin: 0 auto;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/* Header */
|
| 43 |
+
.header {
|
| 44 |
+
display: flex;
|
| 45 |
+
justify-content: space-between;
|
| 46 |
+
align-items: center;
|
| 47 |
+
margin-bottom: 40px;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.logo h1 {
|
| 51 |
+
font-size: 28px;
|
| 52 |
+
font-weight: 700;
|
| 53 |
+
letter-spacing: -0.5px;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
.logo h1 span {
|
| 57 |
+
color: var(--ios-orange);
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.logo p {
|
| 61 |
+
color: var(--ios-secondary);
|
| 62 |
+
font-size: 14px;
|
| 63 |
+
margin-top: 4px;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
/* Controls */
|
| 67 |
+
.controls {
|
| 68 |
+
background: rgba(255, 255, 255, 0.6);
|
| 69 |
+
backdrop-filter: var(--ios-blur);
|
| 70 |
+
padding: 6px;
|
| 71 |
+
border-radius: 999px;
|
| 72 |
+
display: flex;
|
| 73 |
+
gap: 5px;
|
| 74 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.btn {
|
| 78 |
+
padding: 10px 20px;
|
| 79 |
+
border-radius: 999px;
|
| 80 |
+
border: none;
|
| 81 |
+
font-size: 14px;
|
| 82 |
+
font-weight: 600;
|
| 83 |
+
cursor: pointer;
|
| 84 |
+
transition: all 0.3s ease;
|
| 85 |
+
background: transparent;
|
| 86 |
+
color: var(--ios-secondary);
|
| 87 |
+
position: relative;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.btn.active {
|
| 91 |
+
background: #fff;
|
| 92 |
+
color: #000;
|
| 93 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.btn:hover:not(.active) {
|
| 97 |
+
color: #000;
|
| 98 |
+
transform: scale(1.02);
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.btn:active {
|
| 102 |
+
transform: scale(0.98);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
#upload-input {
|
| 106 |
+
display: none;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
/* Loading Spinner */
|
| 110 |
+
.spinner {
|
| 111 |
+
display: none;
|
| 112 |
+
width: 16px;
|
| 113 |
+
height: 16px;
|
| 114 |
+
border: 2px solid var(--ios-secondary);
|
| 115 |
+
border-top-color: transparent;
|
| 116 |
+
border-radius: 50%;
|
| 117 |
+
animation: spin 0.8s linear infinite;
|
| 118 |
+
margin-left: 8px;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
.btn.loading .spinner {
|
| 122 |
+
display: inline-block;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
@keyframes spin {
|
| 126 |
+
to {
|
| 127 |
+
transform: rotate(360deg);
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
/* Main Grid */
|
| 132 |
+
.main-grid {
|
| 133 |
+
display: grid;
|
| 134 |
+
grid-template-columns: 1fr 320px;
|
| 135 |
+
gap: 30px;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
/* Video Feed */
|
| 139 |
+
.video-card {
|
| 140 |
+
background: #fff;
|
| 141 |
+
border-radius: 30px;
|
| 142 |
+
overflow: hidden;
|
| 143 |
+
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.08);
|
| 144 |
+
position: relative;
|
| 145 |
+
aspect-ratio: 16/9;
|
| 146 |
+
transition: box-shadow 0.3s ease;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.video-card:hover {
|
| 150 |
+
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.12);
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
#video-stream {
|
| 154 |
+
width: 100%;
|
| 155 |
+
height: 100%;
|
| 156 |
+
object-fit: cover;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.live-badge {
|
| 160 |
+
position: absolute;
|
| 161 |
+
top: 20px;
|
| 162 |
+
left: 20px;
|
| 163 |
+
background: rgba(255, 255, 255, 0.9);
|
| 164 |
+
backdrop-filter: blur(10px);
|
| 165 |
+
padding: 6px 12px;
|
| 166 |
+
border-radius: 999px;
|
| 167 |
+
font-size: 12px;
|
| 168 |
+
font-weight: 600;
|
| 169 |
+
display: flex;
|
| 170 |
+
align-items: center;
|
| 171 |
+
gap: 6px;
|
| 172 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
| 173 |
+
z-index: 10;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
.dot {
|
| 177 |
+
width: 8px;
|
| 178 |
+
height: 8px;
|
| 179 |
+
background: var(--ios-red);
|
| 180 |
+
border-radius: 50%;
|
| 181 |
+
animation: pulse 2s infinite;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
@keyframes pulse {
|
| 185 |
+
0% {
|
| 186 |
+
opacity: 1;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
50% {
|
| 190 |
+
opacity: 0.5;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
100% {
|
| 194 |
+
opacity: 1;
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
/* FPS Badge */
|
| 199 |
+
.fps-badge {
|
| 200 |
+
position: absolute;
|
| 201 |
+
top: 20px;
|
| 202 |
+
right: 20px;
|
| 203 |
+
background: rgba(0, 0, 0, 0.7);
|
| 204 |
+
backdrop-filter: blur(10px);
|
| 205 |
+
padding: 6px 12px;
|
| 206 |
+
border-radius: 999px;
|
| 207 |
+
font-size: 12px;
|
| 208 |
+
font-weight: 600;
|
| 209 |
+
color: #fff;
|
| 210 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
| 211 |
+
z-index: 10;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
/* Stats Panel */
|
| 215 |
+
.stats-panel {
|
| 216 |
+
display: flex;
|
| 217 |
+
flex-direction: column;
|
| 218 |
+
gap: 20px;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
.stat-card {
|
| 222 |
+
background: rgba(255, 255, 255, 0.7);
|
| 223 |
+
backdrop-filter: var(--ios-blur);
|
| 224 |
+
border-radius: 24px;
|
| 225 |
+
padding: 24px;
|
| 226 |
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05);
|
| 227 |
+
transition: all 0.3s ease;
|
| 228 |
+
position: relative;
|
| 229 |
+
overflow: hidden;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
.stat-card::before {
|
| 233 |
+
content: '';
|
| 234 |
+
position: absolute;
|
| 235 |
+
top: 0;
|
| 236 |
+
left: 0;
|
| 237 |
+
right: 0;
|
| 238 |
+
height: 3px;
|
| 239 |
+
background: linear-gradient(90deg, transparent, var(--accent-color), transparent);
|
| 240 |
+
opacity: 0;
|
| 241 |
+
transition: opacity 0.3s ease;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.stat-card:hover {
|
| 245 |
+
transform: translateY(-4px);
|
| 246 |
+
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.08);
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
.stat-card:hover::before {
|
| 250 |
+
opacity: 1;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
.stat-card h3 {
|
| 254 |
+
font-size: 13px;
|
| 255 |
+
text-transform: uppercase;
|
| 256 |
+
letter-spacing: 0.5px;
|
| 257 |
+
color: var(--ios-secondary);
|
| 258 |
+
margin-bottom: 12px;
|
| 259 |
+
font-weight: 600;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.stat-value {
|
| 263 |
+
font-size: 42px;
|
| 264 |
+
font-weight: 700;
|
| 265 |
+
letter-spacing: -1px;
|
| 266 |
+
line-height: 1;
|
| 267 |
+
transition: transform 0.3s ease;
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
.stat-card:hover .stat-value {
|
| 271 |
+
transform: scale(1.05);
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
.stat-card.threat {
|
| 275 |
+
--accent-color: var(--ios-red);
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
.stat-card.threat .stat-value {
|
| 279 |
+
color: var(--ios-red);
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
.stat-card.warning {
|
| 283 |
+
--accent-color: var(--ios-orange);
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
.stat-card.warning .stat-value {
|
| 287 |
+
color: var(--ios-orange);
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
.stat-card.total {
|
| 291 |
+
--accent-color: var(--ios-text);
|
| 292 |
+
background: linear-gradient(135deg, rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0.7));
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
.stat-card.total .stat-value {
|
| 296 |
+
color: var(--ios-text);
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
.legend {
|
| 300 |
+
margin-top: auto;
|
| 301 |
+
background: #fff;
|
| 302 |
+
border-radius: 24px;
|
| 303 |
+
padding: 24px;
|
| 304 |
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05);
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
.legend-item {
|
| 308 |
+
display: flex;
|
| 309 |
+
align-items: center;
|
| 310 |
+
gap: 12px;
|
| 311 |
+
margin-bottom: 12px;
|
| 312 |
+
transition: transform 0.2s ease;
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
.legend-item:hover {
|
| 316 |
+
transform: translateX(4px);
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
.legend-item:last-child {
|
| 320 |
+
margin-bottom: 0;
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
.legend-color {
|
| 324 |
+
width: 12px;
|
| 325 |
+
height: 12px;
|
| 326 |
+
border-radius: 4px;
|
| 327 |
+
transition: transform 0.2s ease;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
.legend-item:hover .legend-color {
|
| 331 |
+
transform: scale(1.2);
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
.legend-text {
|
| 335 |
+
font-size: 14px;
|
| 336 |
+
font-weight: 500;
|
| 337 |
+
color: var(--ios-secondary);
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
/* Toast */
|
| 341 |
+
.toast {
|
| 342 |
+
position: fixed;
|
| 343 |
+
top: 20px;
|
| 344 |
+
left: 50%;
|
| 345 |
+
transform: translateX(-50%) translateY(-100px);
|
| 346 |
+
background: rgba(0, 0, 0, 0.8);
|
| 347 |
+
color: #fff;
|
| 348 |
+
padding: 12px 24px;
|
| 349 |
+
border-radius: 999px;
|
| 350 |
+
font-size: 14px;
|
| 351 |
+
font-weight: 500;
|
| 352 |
+
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
| 353 |
+
z-index: 1000;
|
| 354 |
+
backdrop-filter: blur(10px);
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
.toast.show {
|
| 358 |
+
transform: translateX(-50%) translateY(0);
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
.toast.success {
|
| 362 |
+
background: rgba(52, 199, 89, 0.9);
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
.toast.error {
|
| 366 |
+
background: rgba(255, 59, 48, 0.9);
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
/* Upload Progress */
|
| 370 |
+
.upload-progress {
|
| 371 |
+
position: fixed;
|
| 372 |
+
top: 50%;
|
| 373 |
+
left: 50%;
|
| 374 |
+
transform: translate(-50%, -50%);
|
| 375 |
+
background: rgba(0, 0, 0, 0.9);
|
| 376 |
+
color: #fff;
|
| 377 |
+
padding: 32px;
|
| 378 |
+
border-radius: 24px;
|
| 379 |
+
font-size: 16px;
|
| 380 |
+
font-weight: 500;
|
| 381 |
+
z-index: 2000;
|
| 382 |
+
backdrop-filter: blur(20px);
|
| 383 |
+
display: none;
|
| 384 |
+
flex-direction: column;
|
| 385 |
+
align-items: center;
|
| 386 |
+
gap: 16px;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
.upload-progress.show {
|
| 390 |
+
display: flex;
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
.upload-spinner {
|
| 394 |
+
width: 40px;
|
| 395 |
+
height: 40px;
|
| 396 |
+
border: 3px solid rgba(255, 255, 255, 0.3);
|
| 397 |
+
border-top-color: var(--ios-orange);
|
| 398 |
+
border-radius: 50%;
|
| 399 |
+
animation: spin 0.8s linear infinite;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
@media (max-width: 900px) {
|
| 403 |
+
.main-grid {
|
| 404 |
+
grid-template-columns: 1fr;
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
.stats-panel {
|
| 408 |
+
flex-direction: row;
|
| 409 |
+
flex-wrap: wrap;
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
.stat-card {
|
| 413 |
+
flex: 1;
|
| 414 |
+
min-width: 140px;
|
| 415 |
+
}
|
| 416 |
+
}
|
| 417 |
+
</style>
|
| 418 |
+
</head>
|
| 419 |
+
|
| 420 |
+
<body>
|
| 421 |
+
<div class="container">
|
| 422 |
+
<header class="header">
|
| 423 |
+
<div class="logo">
|
| 424 |
+
<h1>ATM <span>ThreatVision</span></h1>
|
| 425 |
+
<p>Weapon Detection System</p>
|
| 426 |
+
</div>
|
| 427 |
+
<div class="controls">
|
| 428 |
+
<button class="btn active" id="btn-camera" onclick="setSource('camera')">Camera</button>
|
| 429 |
+
<button class="btn" id="btn-upload"
|
| 430 |
+
onclick="document.getElementById('upload-input').click()">Upload</button>
|
| 431 |
+
<input type="file" id="upload-input" accept="video/*" onchange="handleFileUpload(this)">
|
| 432 |
+
</div>
|
| 433 |
+
</header>
|
| 434 |
+
|
| 435 |
+
<div class="main-grid">
|
| 436 |
+
<div class="video-card">
|
| 437 |
+
<div class="live-badge">
|
| 438 |
+
<span class="dot"></span>
|
| 439 |
+
<span id="source-label">LIVE</span>
|
| 440 |
+
</div>
|
| 441 |
+
<div class="fps-badge" id="fps-display">FPS: --</div>
|
| 442 |
+
<img id="video-stream" src="{{ url_for('video_feed') }}" alt="Video Stream">
|
| 443 |
+
</div>
|
| 444 |
+
|
| 445 |
+
<div class="stats-panel">
|
| 446 |
+
<div class="stat-card threat">
|
| 447 |
+
<h3>Guns Detected</h3>
|
| 448 |
+
<div class="stat-value" id="guns-count">0</div>
|
| 449 |
+
</div>
|
| 450 |
+
|
| 451 |
+
<div class="stat-card warning">
|
| 452 |
+
<h3>Knives Detected</h3>
|
| 453 |
+
<div class="stat-value" id="knives-count">0</div>
|
| 454 |
+
</div>
|
| 455 |
+
|
| 456 |
+
<div class="stat-card total">
|
| 457 |
+
<h3>Total Threats</h3>
|
| 458 |
+
<div class="stat-value" id="total-threats">0</div>
|
| 459 |
+
</div>
|
| 460 |
+
|
| 461 |
+
<div class="legend">
|
| 462 |
+
<div class="legend-item">
|
| 463 |
+
<div class="legend-color" style="background: var(--ios-red)"></div>
|
| 464 |
+
<div class="legend-text">Gun (High Threat)</div>
|
| 465 |
+
</div>
|
| 466 |
+
<div class="legend-item">
|
| 467 |
+
<div class="legend-color" style="background: var(--ios-orange)"></div>
|
| 468 |
+
<div class="legend-text">Knife (Medium Threat)</div>
|
| 469 |
+
</div>
|
| 470 |
+
</div>
|
| 471 |
+
</div>
|
| 472 |
+
</div>
|
| 473 |
+
</div>
|
| 474 |
+
|
| 475 |
+
<div id="toast" class="toast">Action Successful</div>
|
| 476 |
+
<div id="upload-progress" class="upload-progress">
|
| 477 |
+
<div class="upload-spinner"></div>
|
| 478 |
+
<div>Uploading video...</div>
|
| 479 |
+
</div>
|
| 480 |
+
|
| 481 |
+
<script>
|
| 482 |
+
let lastUpdateTime = Date.now();
|
| 483 |
+
let frameCount = 0;
|
| 484 |
+
|
| 485 |
+
function showToast(message, type = 'success') {
|
| 486 |
+
const toast = document.getElementById('toast');
|
| 487 |
+
toast.textContent = message;
|
| 488 |
+
toast.className = 'toast show ' + type;
|
| 489 |
+
setTimeout(() => toast.classList.remove('show'), 3000);
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
function showUploadProgress() {
|
| 493 |
+
document.getElementById('upload-progress').classList.add('show');
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
function hideUploadProgress() {
|
| 497 |
+
document.getElementById('upload-progress').classList.remove('show');
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
function setActiveButton(type) {
|
| 501 |
+
document.querySelectorAll('.btn').forEach(btn => btn.classList.remove('active'));
|
| 502 |
+
if (type === 'camera') document.getElementById('btn-camera').classList.add('active');
|
| 503 |
+
else document.getElementById('btn-upload').classList.add('active');
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
function setSource(source) {
|
| 507 |
+
fetch('/set_source', {
|
| 508 |
+
method: 'POST',
|
| 509 |
+
headers: { 'Content-Type': 'application/json' },
|
| 510 |
+
body: JSON.stringify({ source: source }),
|
| 511 |
+
})
|
| 512 |
+
.then(response => response.json())
|
| 513 |
+
.then(data => {
|
| 514 |
+
if (data.success) {
|
| 515 |
+
document.getElementById('source-label').textContent = source === 'camera' ? 'LIVE' : 'PLAYBACK';
|
| 516 |
+
setActiveButton(source === 'camera' ? 'camera' : 'upload');
|
| 517 |
+
showToast('Switched to ' + source, 'success');
|
| 518 |
+
}
|
| 519 |
+
})
|
| 520 |
+
.catch(error => {
|
| 521 |
+
console.error('Error:', error);
|
| 522 |
+
showToast('Failed to switch source', 'error');
|
| 523 |
+
});
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
function handleFileUpload(input) {
|
| 527 |
+
if (input.files && input.files[0]) {
|
| 528 |
+
const formData = new FormData();
|
| 529 |
+
formData.append('file', input.files[0]);
|
| 530 |
+
|
| 531 |
+
showUploadProgress();
|
| 532 |
+
|
| 533 |
+
fetch('/upload_video', {
|
| 534 |
+
method: 'POST',
|
| 535 |
+
body: formData
|
| 536 |
+
})
|
| 537 |
+
.then(response => response.json())
|
| 538 |
+
.then(data => {
|
| 539 |
+
hideUploadProgress();
|
| 540 |
+
if (data.success) {
|
| 541 |
+
document.getElementById('source-label').textContent = 'PLAYBACK';
|
| 542 |
+
setActiveButton('upload');
|
| 543 |
+
showToast('Video uploaded successfully', 'success');
|
| 544 |
+
} else {
|
| 545 |
+
showToast(data.error || 'Upload failed', 'error');
|
| 546 |
+
}
|
| 547 |
+
})
|
| 548 |
+
.catch(error => {
|
| 549 |
+
hideUploadProgress();
|
| 550 |
+
console.error('Error:', error);
|
| 551 |
+
showToast('Upload failed', 'error');
|
| 552 |
+
});
|
| 553 |
+
}
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
function updateStats() {
|
| 557 |
+
fetch('/stats')
|
| 558 |
+
.then(response => response.json())
|
| 559 |
+
.then(data => {
|
| 560 |
+
document.getElementById('guns-count').textContent = data.guns;
|
| 561 |
+
document.getElementById('knives-count').textContent = data.knives;
|
| 562 |
+
document.getElementById('total-threats').textContent = data.total_threats;
|
| 563 |
+
|
| 564 |
+
// Calculate approximate FPS from update frequency
|
| 565 |
+
frameCount++;
|
| 566 |
+
const now = Date.now();
|
| 567 |
+
if (now - lastUpdateTime >= 1000) {
|
| 568 |
+
const fps = Math.round((frameCount * 1000) / (now - lastUpdateTime));
|
| 569 |
+
document.getElementById('fps-display').textContent = `FPS: ${fps}`;
|
| 570 |
+
frameCount = 0;
|
| 571 |
+
lastUpdateTime = now;
|
| 572 |
+
}
|
| 573 |
+
})
|
| 574 |
+
.catch(error => console.error('Error fetching stats:', error));
|
| 575 |
+
}
|
| 576 |
+
|
| 577 |
+
setInterval(updateStats, 100); // Update more frequently for smoother FPS display
|
| 578 |
+
</script>
|
| 579 |
+
</body>
|
| 580 |
+
|
| 581 |
+
</html>
|
templates/index.html
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>ATM ThreatVision - Facemask Detection</title>
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&display=swap"
|
| 9 |
+
rel="stylesheet">
|
| 10 |
+
<style>
|
| 11 |
+
:root {
|
| 12 |
+
--primary-orange: #FF6B00;
|
| 13 |
+
--dark-bg: #000000;
|
| 14 |
+
--card-bg: #111111;
|
| 15 |
+
--text-white: #FFFFFF;
|
| 16 |
+
--text-gray: #888888;
|
| 17 |
+
--border-color: #333333;
|
| 18 |
+
--success-green: #00FF88;
|
| 19 |
+
--danger-red: #FF4757;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
* {
|
| 23 |
+
margin: 0;
|
| 24 |
+
padding: 0;
|
| 25 |
+
box-sizing: border-box;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
body {
|
| 29 |
+
font-family: 'Outfit', sans-serif;
|
| 30 |
+
background-color: var(--dark-bg);
|
| 31 |
+
color: var(--text-white);
|
| 32 |
+
min-height: 100vh;
|
| 33 |
+
padding: 20px;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.container {
|
| 37 |
+
max-width: 1400px;
|
| 38 |
+
margin: 0 auto;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.header {
|
| 42 |
+
display: flex;
|
| 43 |
+
justify-content: space-between;
|
| 44 |
+
align-items: center;
|
| 45 |
+
padding: 20px 0;
|
| 46 |
+
margin-bottom: 40px;
|
| 47 |
+
border-bottom: 1px solid var(--border-color);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.logo-area h1 {
|
| 51 |
+
font-size: 2rem;
|
| 52 |
+
font-weight: 800;
|
| 53 |
+
letter-spacing: -1px;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
.logo-area h1 span {
|
| 57 |
+
color: var(--primary-orange);
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.logo-area p {
|
| 61 |
+
color: var(--text-gray);
|
| 62 |
+
font-size: 0.9rem;
|
| 63 |
+
margin-top: 5px;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.controls {
|
| 67 |
+
display: flex;
|
| 68 |
+
gap: 15px;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.btn {
|
| 72 |
+
padding: 12px 24px;
|
| 73 |
+
border-radius: 8px;
|
| 74 |
+
border: none;
|
| 75 |
+
font-weight: 600;
|
| 76 |
+
cursor: pointer;
|
| 77 |
+
transition: all 0.3s ease;
|
| 78 |
+
font-family: 'Outfit', sans-serif;
|
| 79 |
+
display: flex;
|
| 80 |
+
align-items: center;
|
| 81 |
+
gap: 8px;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.btn-primary {
|
| 85 |
+
background-color: var(--primary-orange);
|
| 86 |
+
color: var(--text-white);
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.btn-primary:hover {
|
| 90 |
+
background-color: #e65a00;
|
| 91 |
+
transform: translateY(-2px);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
.btn-outline {
|
| 95 |
+
background-color: transparent;
|
| 96 |
+
border: 1px solid var(--border-color);
|
| 97 |
+
color: var(--text-white);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.btn-outline:hover {
|
| 101 |
+
border-color: var(--primary-orange);
|
| 102 |
+
color: var(--primary-orange);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.main-grid {
|
| 106 |
+
display: grid;
|
| 107 |
+
grid-template-columns: 1fr 350px;
|
| 108 |
+
gap: 30px;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.video-container {
|
| 112 |
+
background-color: var(--card-bg);
|
| 113 |
+
border-radius: 20px;
|
| 114 |
+
padding: 20px;
|
| 115 |
+
border: 1px solid var(--border-color);
|
| 116 |
+
position: relative;
|
| 117 |
+
overflow: hidden;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.video-header {
|
| 121 |
+
display: flex;
|
| 122 |
+
justify-content: space-between;
|
| 123 |
+
align-items: center;
|
| 124 |
+
margin-bottom: 20px;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.video-header h2 {
|
| 128 |
+
font-size: 1.2rem;
|
| 129 |
+
font-weight: 600;
|
| 130 |
+
display: flex;
|
| 131 |
+
align-items: center;
|
| 132 |
+
gap: 10px;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.live-indicator {
|
| 136 |
+
width: 8px;
|
| 137 |
+
height: 8px;
|
| 138 |
+
background-color: var(--primary-orange);
|
| 139 |
+
border-radius: 50%;
|
| 140 |
+
box-shadow: 0 0 10px var(--primary-orange);
|
| 141 |
+
animation: pulse 2s infinite;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
@keyframes pulse {
|
| 145 |
+
0% {
|
| 146 |
+
opacity: 1;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
50% {
|
| 150 |
+
opacity: 0.5;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
100% {
|
| 154 |
+
opacity: 1;
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
#video-stream {
|
| 159 |
+
width: 100%;
|
| 160 |
+
border-radius: 12px;
|
| 161 |
+
background-color: #000;
|
| 162 |
+
min-height: 480px;
|
| 163 |
+
object-fit: contain;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
.stats-panel {
|
| 167 |
+
display: flex;
|
| 168 |
+
flex-direction: column;
|
| 169 |
+
gap: 20px;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
.stat-card {
|
| 173 |
+
background-color: var(--card-bg);
|
| 174 |
+
border-radius: 16px;
|
| 175 |
+
padding: 25px;
|
| 176 |
+
border: 1px solid var(--border-color);
|
| 177 |
+
position: relative;
|
| 178 |
+
overflow: hidden;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
.stat-card::before {
|
| 182 |
+
content: '';
|
| 183 |
+
position: absolute;
|
| 184 |
+
top: 0;
|
| 185 |
+
left: 0;
|
| 186 |
+
width: 4px;
|
| 187 |
+
height: 100%;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
.stat-card.orange::before {
|
| 191 |
+
background-color: var(--primary-orange);
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.stat-card.green::before {
|
| 195 |
+
background-color: var(--success-green);
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.stat-card.red::before {
|
| 199 |
+
background-color: var(--danger-red);
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
.stat-card h3 {
|
| 203 |
+
font-size: 0.8rem;
|
| 204 |
+
text-transform: uppercase;
|
| 205 |
+
letter-spacing: 1.5px;
|
| 206 |
+
color: var(--text-gray);
|
| 207 |
+
margin-bottom: 15px;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.stat-value {
|
| 211 |
+
font-size: 3rem;
|
| 212 |
+
font-weight: 700;
|
| 213 |
+
line-height: 1;
|
| 214 |
+
margin-bottom: 5px;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
.stat-label {
|
| 218 |
+
font-size: 0.9rem;
|
| 219 |
+
color: var(--text-gray);
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.legend-panel {
|
| 223 |
+
background-color: var(--card-bg);
|
| 224 |
+
border-radius: 16px;
|
| 225 |
+
padding: 25px;
|
| 226 |
+
border: 1px solid var(--border-color);
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.legend-item {
|
| 230 |
+
display: flex;
|
| 231 |
+
align-items: center;
|
| 232 |
+
gap: 12px;
|
| 233 |
+
margin-bottom: 15px;
|
| 234 |
+
padding: 10px;
|
| 235 |
+
background-color: rgba(255, 255, 255, 0.03);
|
| 236 |
+
border-radius: 8px;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.legend-color {
|
| 240 |
+
width: 16px;
|
| 241 |
+
height: 16px;
|
| 242 |
+
border-radius: 4px;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
.legend-text {
|
| 246 |
+
font-size: 0.95rem;
|
| 247 |
+
color: var(--text-white);
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
#upload-input {
|
| 251 |
+
display: none;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
/* Toast Notification */
|
| 255 |
+
.toast {
|
| 256 |
+
position: fixed;
|
| 257 |
+
bottom: 30px;
|
| 258 |
+
right: 30px;
|
| 259 |
+
background-color: var(--card-bg);
|
| 260 |
+
border: 1px solid var(--primary-orange);
|
| 261 |
+
color: var(--text-white);
|
| 262 |
+
padding: 15px 25px;
|
| 263 |
+
border-radius: 8px;
|
| 264 |
+
transform: translateY(100px);
|
| 265 |
+
opacity: 0;
|
| 266 |
+
transition: all 0.3s ease;
|
| 267 |
+
z-index: 1000;
|
| 268 |
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
.toast.show {
|
| 272 |
+
transform: translateY(0);
|
| 273 |
+
opacity: 1;
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
@media (max-width: 1024px) {
|
| 277 |
+
.main-grid {
|
| 278 |
+
grid-template-columns: 1fr;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
.stats-panel {
|
| 282 |
+
flex-direction: row;
|
| 283 |
+
flex-wrap: wrap;
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
.stat-card {
|
| 287 |
+
flex: 1;
|
| 288 |
+
min-width: 200px;
|
| 289 |
+
}
|
| 290 |
+
}
|
| 291 |
+
</style>
|
| 292 |
+
</head>
|
| 293 |
+
|
| 294 |
+
<body>
|
| 295 |
+
<div class="container">
|
| 296 |
+
<header class="header">
|
| 297 |
+
<div class="logo-area">
|
| 298 |
+
<h1>ATM <span>ThreatVision</span></h1>
|
| 299 |
+
<p>Advanced Facemask Detection System</p>
|
| 300 |
+
</div>
|
| 301 |
+
<div class="controls">
|
| 302 |
+
<button class="btn btn-primary" onclick="setSource('camera')">
|
| 303 |
+
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 304 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
| 305 |
+
d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z">
|
| 306 |
+
</path>
|
| 307 |
+
</svg>
|
| 308 |
+
Live Camera
|
| 309 |
+
</button>
|
| 310 |
+
<button class="btn btn-outline" onclick="document.getElementById('upload-input').click()">
|
| 311 |
+
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 312 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
| 313 |
+
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path>
|
| 314 |
+
</svg>
|
| 315 |
+
Upload Video
|
| 316 |
+
</button>
|
| 317 |
+
<input type="file" id="upload-input" accept="video/*" onchange="handleFileUpload(this)">
|
| 318 |
+
</div>
|
| 319 |
+
</header>
|
| 320 |
+
|
| 321 |
+
<div class="main-grid">
|
| 322 |
+
<div class="video-container">
|
| 323 |
+
<div class="video-header">
|
| 324 |
+
<h2><span class="live-indicator"></span> Live Feed</h2>
|
| 325 |
+
<span style="color: var(--text-gray); font-size: 0.9rem;" id="source-label">Source: Camera</span>
|
| 326 |
+
</div>
|
| 327 |
+
<img id="video-stream" src="{{ url_for('video_feed') }}" alt="Video Stream">
|
| 328 |
+
</div>
|
| 329 |
+
|
| 330 |
+
<div class="stats-panel">
|
| 331 |
+
<div class="stat-card red">
|
| 332 |
+
<h3>With Mask</h3>
|
| 333 |
+
<div class="stat-value" id="with-mask" style="color: var(--danger-red)">0</div>
|
| 334 |
+
<div class="stat-label">Threat</div>
|
| 335 |
+
</div>
|
| 336 |
+
|
| 337 |
+
<div class="stat-card green">
|
| 338 |
+
<h3>Without Mask</h3>
|
| 339 |
+
<div class="stat-value" id="without-mask" style="color: var(--success-green)">0</div>
|
| 340 |
+
<div class="stat-label">Safe</div>
|
| 341 |
+
</div>
|
| 342 |
+
|
| 343 |
+
<div class="stat-card orange">
|
| 344 |
+
<h3>Total Detections</h3>
|
| 345 |
+
<div class="stat-value" id="total-detections">0</div>
|
| 346 |
+
<div class="stat-label">In Current Frame</div>
|
| 347 |
+
</div>
|
| 348 |
+
|
| 349 |
+
<div class="legend-panel">
|
| 350 |
+
<div class="legend-item">
|
| 351 |
+
<div class="legend-color" style="background: #FF0000"></div>
|
| 352 |
+
<div class="legend-text">Mask Detected (Threat)</div>
|
| 353 |
+
</div>
|
| 354 |
+
<div class="legend-item">
|
| 355 |
+
<div class="legend-color" style="background: #00FF88"></div>
|
| 356 |
+
<div class="legend-text">No Mask (Safe)</div>
|
| 357 |
+
</div>
|
| 358 |
+
</div>
|
| 359 |
+
</div>
|
| 360 |
+
</div>
|
| 361 |
+
</div>
|
| 362 |
+
|
| 363 |
+
<div id="toast" class="toast">Action Successful</div>
|
| 364 |
+
|
| 365 |
+
<script>
|
| 366 |
+
function showToast(message) {
|
| 367 |
+
const toast = document.getElementById('toast');
|
| 368 |
+
toast.textContent = message;
|
| 369 |
+
toast.classList.add('show');
|
| 370 |
+
setTimeout(() => toast.classList.remove('show'), 3000);
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
function setSource(source) {
|
| 374 |
+
fetch('/set_source', {
|
| 375 |
+
method: 'POST',
|
| 376 |
+
headers: {
|
| 377 |
+
'Content-Type': 'application/json',
|
| 378 |
+
},
|
| 379 |
+
body: JSON.stringify({ source: source }),
|
| 380 |
+
})
|
| 381 |
+
.then(response => response.json())
|
| 382 |
+
.then(data => {
|
| 383 |
+
if (data.success) {
|
| 384 |
+
document.getElementById('source-label').textContent = 'Source: ' + (source === 'camera' ? 'Live Camera' : 'Uploaded Video');
|
| 385 |
+
showToast('Switched to ' + source);
|
| 386 |
+
}
|
| 387 |
+
});
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
function handleFileUpload(input) {
|
| 391 |
+
if (input.files && input.files[0]) {
|
| 392 |
+
const formData = new FormData();
|
| 393 |
+
formData.append('file', input.files[0]);
|
| 394 |
+
|
| 395 |
+
fetch('/upload_video', {
|
| 396 |
+
method: 'POST',
|
| 397 |
+
body: formData
|
| 398 |
+
})
|
| 399 |
+
.then(response => response.json())
|
| 400 |
+
.then(data => {
|
| 401 |
+
if (data.success) {
|
| 402 |
+
document.getElementById('source-label').textContent = 'Source: Uploaded Video';
|
| 403 |
+
showToast('Video uploaded successfully');
|
| 404 |
+
}
|
| 405 |
+
})
|
| 406 |
+
.catch(error => {
|
| 407 |
+
console.error('Error:', error);
|
| 408 |
+
showToast('Upload failed');
|
| 409 |
+
});
|
| 410 |
+
}
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
function updateStats() {
|
| 414 |
+
fetch('/stats')
|
| 415 |
+
.then(response => response.json())
|
| 416 |
+
.then(data => {
|
| 417 |
+
document.getElementById('with-mask').textContent = data.with_mask;
|
| 418 |
+
document.getElementById('without-mask').textContent = data.without_mask;
|
| 419 |
+
document.getElementById('total-detections').textContent = data.total_detections;
|
| 420 |
+
})
|
| 421 |
+
.catch(error => console.error('Error fetching stats:', error));
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
setInterval(updateStats, 500);
|
| 425 |
+
</script>
|
| 426 |
+
</body>
|
| 427 |
+
|
| 428 |
+
</html>
|
templates/movement_index.html
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>ATM ThreatVision - Movement Analysis</title>
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&display=swap"
|
| 9 |
+
rel="stylesheet">
|
| 10 |
+
<style>
|
| 11 |
+
:root {
|
| 12 |
+
--primary-orange: #FF6B00;
|
| 13 |
+
--dark-bg: #000000;
|
| 14 |
+
--card-bg: #111111;
|
| 15 |
+
--text-white: #FFFFFF;
|
| 16 |
+
--text-gray: #888888;
|
| 17 |
+
--border-color: #333333;
|
| 18 |
+
--success-green: #00FF88;
|
| 19 |
+
--danger-red: #FF4757;
|
| 20 |
+
--info-blue: #0088FF;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
* {
|
| 24 |
+
margin: 0;
|
| 25 |
+
padding: 0;
|
| 26 |
+
box-sizing: border-box;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
body {
|
| 30 |
+
font-family: 'Outfit', sans-serif;
|
| 31 |
+
background-color: var(--dark-bg);
|
| 32 |
+
color: var(--text-white);
|
| 33 |
+
min-height: 100vh;
|
| 34 |
+
padding: 20px;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
.container {
|
| 38 |
+
max-width: 1400px;
|
| 39 |
+
margin: 0 auto;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
.header {
|
| 43 |
+
display: flex;
|
| 44 |
+
justify-content: space-between;
|
| 45 |
+
align-items: center;
|
| 46 |
+
padding: 20px 0;
|
| 47 |
+
margin-bottom: 40px;
|
| 48 |
+
border-bottom: 1px solid var(--border-color);
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
.logo-area h1 {
|
| 52 |
+
font-size: 2rem;
|
| 53 |
+
font-weight: 800;
|
| 54 |
+
letter-spacing: -1px;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
.logo-area h1 span {
|
| 58 |
+
color: var(--primary-orange);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.logo-area p {
|
| 62 |
+
color: var(--text-gray);
|
| 63 |
+
font-size: 0.9rem;
|
| 64 |
+
margin-top: 5px;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.controls {
|
| 68 |
+
display: flex;
|
| 69 |
+
gap: 15px;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.btn {
|
| 73 |
+
padding: 12px 24px;
|
| 74 |
+
border-radius: 8px;
|
| 75 |
+
border: none;
|
| 76 |
+
font-weight: 600;
|
| 77 |
+
cursor: pointer;
|
| 78 |
+
transition: all 0.3s ease;
|
| 79 |
+
font-family: 'Outfit', sans-serif;
|
| 80 |
+
display: flex;
|
| 81 |
+
align-items: center;
|
| 82 |
+
gap: 8px;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.btn-primary {
|
| 86 |
+
background-color: var(--primary-orange);
|
| 87 |
+
color: var(--text-white);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.btn-primary:hover {
|
| 91 |
+
background-color: #e65a00;
|
| 92 |
+
transform: translateY(-2px);
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.btn-outline {
|
| 96 |
+
background-color: transparent;
|
| 97 |
+
border: 1px solid var(--border-color);
|
| 98 |
+
color: var(--text-white);
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.btn-outline:hover {
|
| 102 |
+
border-color: var(--primary-orange);
|
| 103 |
+
color: var(--primary-orange);
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.main-grid {
|
| 107 |
+
display: grid;
|
| 108 |
+
grid-template-columns: 1fr 350px;
|
| 109 |
+
gap: 30px;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.video-container {
|
| 113 |
+
background-color: var(--card-bg);
|
| 114 |
+
border-radius: 20px;
|
| 115 |
+
padding: 20px;
|
| 116 |
+
border: 1px solid var(--border-color);
|
| 117 |
+
position: relative;
|
| 118 |
+
overflow: hidden;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
.video-header {
|
| 122 |
+
display: flex;
|
| 123 |
+
justify-content: space-between;
|
| 124 |
+
align-items: center;
|
| 125 |
+
margin-bottom: 20px;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
.video-header h2 {
|
| 129 |
+
font-size: 1.2rem;
|
| 130 |
+
font-weight: 600;
|
| 131 |
+
display: flex;
|
| 132 |
+
align-items: center;
|
| 133 |
+
gap: 10px;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.live-indicator {
|
| 137 |
+
width: 8px;
|
| 138 |
+
height: 8px;
|
| 139 |
+
background-color: var(--primary-orange);
|
| 140 |
+
border-radius: 50%;
|
| 141 |
+
box-shadow: 0 0 10px var(--primary-orange);
|
| 142 |
+
animation: pulse 2s infinite;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
@keyframes pulse {
|
| 146 |
+
0% {
|
| 147 |
+
opacity: 1;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
50% {
|
| 151 |
+
opacity: 0.5;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
100% {
|
| 155 |
+
opacity: 1;
|
| 156 |
+
}
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
#video-stream {
|
| 160 |
+
width: 100%;
|
| 161 |
+
border-radius: 12px;
|
| 162 |
+
background-color: #000;
|
| 163 |
+
min-height: 480px;
|
| 164 |
+
object-fit: contain;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
.stats-panel {
|
| 168 |
+
display: flex;
|
| 169 |
+
flex-direction: column;
|
| 170 |
+
gap: 20px;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
.stat-card {
|
| 174 |
+
background-color: var(--card-bg);
|
| 175 |
+
border-radius: 16px;
|
| 176 |
+
padding: 25px;
|
| 177 |
+
border: 1px solid var(--border-color);
|
| 178 |
+
position: relative;
|
| 179 |
+
overflow: hidden;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.stat-card::before {
|
| 183 |
+
content: '';
|
| 184 |
+
position: absolute;
|
| 185 |
+
top: 0;
|
| 186 |
+
left: 0;
|
| 187 |
+
width: 4px;
|
| 188 |
+
height: 100%;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
.stat-card.orange::before {
|
| 192 |
+
background-color: var(--primary-orange);
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
.stat-card.blue::before {
|
| 196 |
+
background-color: var(--info-blue);
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
.stat-card.red::before {
|
| 200 |
+
background-color: var(--danger-red);
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
.stat-card h3 {
|
| 204 |
+
font-size: 0.8rem;
|
| 205 |
+
text-transform: uppercase;
|
| 206 |
+
letter-spacing: 1.5px;
|
| 207 |
+
color: var(--text-gray);
|
| 208 |
+
margin-bottom: 15px;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
.stat-value {
|
| 212 |
+
font-size: 3rem;
|
| 213 |
+
font-weight: 700;
|
| 214 |
+
line-height: 1;
|
| 215 |
+
margin-bottom: 5px;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
.stat-label {
|
| 219 |
+
font-size: 0.9rem;
|
| 220 |
+
color: var(--text-gray);
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.legend-panel {
|
| 224 |
+
background-color: var(--card-bg);
|
| 225 |
+
border-radius: 16px;
|
| 226 |
+
padding: 25px;
|
| 227 |
+
border: 1px solid var(--border-color);
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
.legend-item {
|
| 231 |
+
display: flex;
|
| 232 |
+
align-items: center;
|
| 233 |
+
gap: 12px;
|
| 234 |
+
margin-bottom: 15px;
|
| 235 |
+
padding: 10px;
|
| 236 |
+
background-color: rgba(255, 255, 255, 0.03);
|
| 237 |
+
border-radius: 8px;
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
.legend-color {
|
| 241 |
+
width: 16px;
|
| 242 |
+
height: 16px;
|
| 243 |
+
border-radius: 4px;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.legend-text {
|
| 247 |
+
font-size: 0.95rem;
|
| 248 |
+
color: var(--text-white);
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
#upload-input {
|
| 252 |
+
display: none;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
/* Toast Notification */
|
| 256 |
+
.toast {
|
| 257 |
+
position: fixed;
|
| 258 |
+
bottom: 30px;
|
| 259 |
+
right: 30px;
|
| 260 |
+
background-color: var(--card-bg);
|
| 261 |
+
border: 1px solid var(--primary-orange);
|
| 262 |
+
color: var(--text-white);
|
| 263 |
+
padding: 15px 25px;
|
| 264 |
+
border-radius: 8px;
|
| 265 |
+
transform: translateY(100px);
|
| 266 |
+
opacity: 0;
|
| 267 |
+
transition: all 0.3s ease;
|
| 268 |
+
z-index: 1000;
|
| 269 |
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
.toast.show {
|
| 273 |
+
transform: translateY(0);
|
| 274 |
+
opacity: 1;
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
@media (max-width: 1024px) {
|
| 278 |
+
.main-grid {
|
| 279 |
+
grid-template-columns: 1fr;
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
.stats-panel {
|
| 283 |
+
flex-direction: row;
|
| 284 |
+
flex-wrap: wrap;
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
.stat-card {
|
| 288 |
+
flex: 1;
|
| 289 |
+
min-width: 200px;
|
| 290 |
+
}
|
| 291 |
+
}
|
| 292 |
+
</style>
|
| 293 |
+
</head>
|
| 294 |
+
|
| 295 |
+
<body>
|
| 296 |
+
<div class="container">
|
| 297 |
+
<header class="header">
|
| 298 |
+
<div class="logo-area">
|
| 299 |
+
<h1>ATM <span>ThreatVision</span></h1>
|
| 300 |
+
<p>Advanced Movement Analysis System</p>
|
| 301 |
+
</div>
|
| 302 |
+
<div class="controls">
|
| 303 |
+
<button class="btn btn-primary" onclick="setSource('camera')">
|
| 304 |
+
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 305 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
| 306 |
+
d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z">
|
| 307 |
+
</path>
|
| 308 |
+
</svg>
|
| 309 |
+
Live Camera
|
| 310 |
+
</button>
|
| 311 |
+
<button class="btn btn-outline" onclick="document.getElementById('upload-input').click()">
|
| 312 |
+
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 313 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
| 314 |
+
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path>
|
| 315 |
+
</svg>
|
| 316 |
+
Upload Video
|
| 317 |
+
</button>
|
| 318 |
+
<input type="file" id="upload-input" accept="video/*" onchange="handleFileUpload(this)">
|
| 319 |
+
</div>
|
| 320 |
+
</header>
|
| 321 |
+
|
| 322 |
+
<div class="main-grid">
|
| 323 |
+
<div class="video-container">
|
| 324 |
+
<div class="video-header">
|
| 325 |
+
<h2><span class="live-indicator"></span> Live Feed</h2>
|
| 326 |
+
<span style="color: var(--text-gray); font-size: 0.9rem;" id="source-label">Source: Camera</span>
|
| 327 |
+
</div>
|
| 328 |
+
<img id="video-stream" src="{{ url_for('video_feed') }}" alt="Video Stream">
|
| 329 |
+
</div>
|
| 330 |
+
|
| 331 |
+
<div class="stats-panel">
|
| 332 |
+
<div class="stat-card orange">
|
| 333 |
+
<h3>Current People</h3>
|
| 334 |
+
<div class="stat-value" id="current-people">0</div>
|
| 335 |
+
<div class="stat-label">In Frame</div>
|
| 336 |
+
</div>
|
| 337 |
+
|
| 338 |
+
<div class="stat-card blue">
|
| 339 |
+
<h3>Total People</h3>
|
| 340 |
+
<div class="stat-value" id="total-people" style="color: var(--info-blue)">0</div>
|
| 341 |
+
<div class="stat-label">Detected Session</div>
|
| 342 |
+
</div>
|
| 343 |
+
|
| 344 |
+
<div class="stat-card red">
|
| 345 |
+
<h3>Long Stays (>10s)</h3>
|
| 346 |
+
<div class="stat-value" id="long-stays" style="color: var(--danger-red)">0</div>
|
| 347 |
+
<div class="stat-label">Potential Loitering</div>
|
| 348 |
+
</div>
|
| 349 |
+
|
| 350 |
+
<div class="legend-panel">
|
| 351 |
+
<div class="legend-item">
|
| 352 |
+
<div class="legend-color" style="background: #00FF00"></div>
|
| 353 |
+
<div class="legend-text">Past Path (5s)</div>
|
| 354 |
+
</div>
|
| 355 |
+
<div class="legend-item">
|
| 356 |
+
<div class="legend-color" style="background: #FFFF00"></div>
|
| 357 |
+
<div class="legend-text">Predicted Path (4s)</div>
|
| 358 |
+
</div>
|
| 359 |
+
</div>
|
| 360 |
+
</div>
|
| 361 |
+
</div>
|
| 362 |
+
</div>
|
| 363 |
+
|
| 364 |
+
<div id="toast" class="toast">Action Successful</div>
|
| 365 |
+
|
| 366 |
+
<script>
|
| 367 |
+
function showToast(message) {
|
| 368 |
+
const toast = document.getElementById('toast');
|
| 369 |
+
toast.textContent = message;
|
| 370 |
+
toast.classList.add('show');
|
| 371 |
+
setTimeout(() => toast.classList.remove('show'), 3000);
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
function setSource(source) {
|
| 375 |
+
fetch('/set_source', {
|
| 376 |
+
method: 'POST',
|
| 377 |
+
headers: {
|
| 378 |
+
'Content-Type': 'application/json',
|
| 379 |
+
},
|
| 380 |
+
body: JSON.stringify({ source: source }),
|
| 381 |
+
})
|
| 382 |
+
.then(response => response.json())
|
| 383 |
+
.then(data => {
|
| 384 |
+
if (data.success) {
|
| 385 |
+
document.getElementById('source-label').textContent = 'Source: ' + (source === 'camera' ? 'Live Camera' : 'Uploaded Video');
|
| 386 |
+
showToast('Switched to ' + source);
|
| 387 |
+
}
|
| 388 |
+
});
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
function handleFileUpload(input) {
|
| 392 |
+
if (input.files && input.files[0]) {
|
| 393 |
+
const formData = new FormData();
|
| 394 |
+
formData.append('file', input.files[0]);
|
| 395 |
+
|
| 396 |
+
fetch('/upload_video', {
|
| 397 |
+
method: 'POST',
|
| 398 |
+
body: formData
|
| 399 |
+
})
|
| 400 |
+
.then(response => response.json())
|
| 401 |
+
.then(data => {
|
| 402 |
+
if (data.success) {
|
| 403 |
+
document.getElementById('source-label').textContent = 'Source: Uploaded Video';
|
| 404 |
+
showToast('Video uploaded successfully');
|
| 405 |
+
}
|
| 406 |
+
})
|
| 407 |
+
.catch(error => {
|
| 408 |
+
console.error('Error:', error);
|
| 409 |
+
showToast('Upload failed');
|
| 410 |
+
});
|
| 411 |
+
}
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
function updateStats() {
|
| 415 |
+
fetch('/stats')
|
| 416 |
+
.then(response => response.json())
|
| 417 |
+
.then(data => {
|
| 418 |
+
document.getElementById('current-people').textContent = data.current_people;
|
| 419 |
+
document.getElementById('total-people').textContent = data.total_people;
|
| 420 |
+
document.getElementById('long-stays').textContent = data.long_stays;
|
| 421 |
+
})
|
| 422 |
+
.catch(error => console.error('Error fetching stats:', error));
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
setInterval(updateStats, 500);
|
| 426 |
+
</script>
|
| 427 |
+
</body>
|
| 428 |
+
|
| 429 |
+
</html>
|
templates/sentinel_dashboard.html
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>Project SENTINEL - National Security Grid</title>
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&display=swap"
|
| 9 |
+
rel="stylesheet">
|
| 10 |
+
<style>
|
| 11 |
+
:root {
|
| 12 |
+
--primary-orange: #FF6B00;
|
| 13 |
+
--dark-bg: #050505;
|
| 14 |
+
--panel-bg: #111111;
|
| 15 |
+
--text-white: #FFFFFF;
|
| 16 |
+
--text-gray: #888888;
|
| 17 |
+
--border-color: #333333;
|
| 18 |
+
--danger-red: #FF4757;
|
| 19 |
+
--success-green: #00FF88;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
* {
|
| 23 |
+
margin: 0;
|
| 24 |
+
padding: 0;
|
| 25 |
+
box-sizing: border-box;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
body {
|
| 29 |
+
font-family: 'Outfit', sans-serif;
|
| 30 |
+
background-color: var(--dark-bg);
|
| 31 |
+
color: var(--text-white);
|
| 32 |
+
height: 100vh;
|
| 33 |
+
overflow: hidden;
|
| 34 |
+
display: flex;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/* Sidebar */
|
| 38 |
+
.sidebar {
|
| 39 |
+
width: 250px;
|
| 40 |
+
background-color: var(--panel-bg);
|
| 41 |
+
border-right: 1px solid var(--border-color);
|
| 42 |
+
display: flex;
|
| 43 |
+
flex-direction: column;
|
| 44 |
+
padding: 20px;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
.logo {
|
| 48 |
+
font-size: 1.5rem;
|
| 49 |
+
font-weight: 800;
|
| 50 |
+
margin-bottom: 40px;
|
| 51 |
+
letter-spacing: -1px;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
.logo span {
|
| 55 |
+
color: var(--primary-orange);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.nav-item {
|
| 59 |
+
padding: 15px;
|
| 60 |
+
margin-bottom: 10px;
|
| 61 |
+
border-radius: 8px;
|
| 62 |
+
cursor: pointer;
|
| 63 |
+
transition: all 0.3s;
|
| 64 |
+
display: flex;
|
| 65 |
+
align-items: center;
|
| 66 |
+
gap: 10px;
|
| 67 |
+
color: var(--text-gray);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.nav-item:hover,
|
| 71 |
+
.nav-item.active {
|
| 72 |
+
background-color: rgba(255, 107, 0, 0.1);
|
| 73 |
+
color: var(--primary-orange);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.nav-item.active {
|
| 77 |
+
border-left: 3px solid var(--primary-orange);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
/* Main Content */
|
| 81 |
+
.main-content {
|
| 82 |
+
flex: 1;
|
| 83 |
+
padding: 20px;
|
| 84 |
+
display: grid;
|
| 85 |
+
grid-template-columns: 1fr 350px;
|
| 86 |
+
gap: 20px;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
/* Video Feed */
|
| 90 |
+
.video-panel {
|
| 91 |
+
background-color: var(--panel-bg);
|
| 92 |
+
border-radius: 16px;
|
| 93 |
+
border: 1px solid var(--border-color);
|
| 94 |
+
overflow: hidden;
|
| 95 |
+
display: flex;
|
| 96 |
+
flex-direction: column;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.panel-header {
|
| 100 |
+
padding: 15px 20px;
|
| 101 |
+
border-bottom: 1px solid var(--border-color);
|
| 102 |
+
display: flex;
|
| 103 |
+
justify-content: space-between;
|
| 104 |
+
align-items: center;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
.live-badge {
|
| 108 |
+
background-color: var(--danger-red);
|
| 109 |
+
color: white;
|
| 110 |
+
padding: 4px 8px;
|
| 111 |
+
border-radius: 4px;
|
| 112 |
+
font-size: 0.7rem;
|
| 113 |
+
font-weight: 700;
|
| 114 |
+
animation: pulse 2s infinite;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
#video-stream {
|
| 118 |
+
width: 100%;
|
| 119 |
+
height: 100%;
|
| 120 |
+
object-fit: contain;
|
| 121 |
+
background: black;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
/* Intelligence Panel */
|
| 125 |
+
.intel-panel {
|
| 126 |
+
display: flex;
|
| 127 |
+
flex-direction: column;
|
| 128 |
+
gap: 20px;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.card {
|
| 132 |
+
background-color: var(--panel-bg);
|
| 133 |
+
border-radius: 16px;
|
| 134 |
+
padding: 20px;
|
| 135 |
+
border: 1px solid var(--border-color);
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.card h3 {
|
| 139 |
+
font-size: 0.9rem;
|
| 140 |
+
color: var(--text-gray);
|
| 141 |
+
text-transform: uppercase;
|
| 142 |
+
letter-spacing: 1px;
|
| 143 |
+
margin-bottom: 15px;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.threat-score {
|
| 147 |
+
text-align: center;
|
| 148 |
+
padding: 20px 0;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.score-value {
|
| 152 |
+
font-size: 4rem;
|
| 153 |
+
font-weight: 800;
|
| 154 |
+
line-height: 1;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
.score-label {
|
| 158 |
+
color: var(--text-gray);
|
| 159 |
+
font-size: 0.9rem;
|
| 160 |
+
margin-top: 5px;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.stats-grid {
|
| 164 |
+
display: grid;
|
| 165 |
+
grid-template-columns: 1fr 1fr;
|
| 166 |
+
gap: 10px;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
.stat-item {
|
| 170 |
+
background: rgba(255, 255, 255, 0.05);
|
| 171 |
+
padding: 10px;
|
| 172 |
+
border-radius: 8px;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.stat-num {
|
| 176 |
+
font-size: 1.5rem;
|
| 177 |
+
font-weight: 700;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.stat-desc {
|
| 181 |
+
font-size: 0.8rem;
|
| 182 |
+
color: var(--text-gray);
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.btn {
|
| 186 |
+
width: 100%;
|
| 187 |
+
padding: 12px;
|
| 188 |
+
border: none;
|
| 189 |
+
border-radius: 8px;
|
| 190 |
+
font-weight: 600;
|
| 191 |
+
cursor: pointer;
|
| 192 |
+
margin-top: 10px;
|
| 193 |
+
font-family: 'Outfit', sans-serif;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
.btn-primary {
|
| 197 |
+
background-color: var(--primary-orange);
|
| 198 |
+
color: white;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
.btn-outline {
|
| 202 |
+
background-color: transparent;
|
| 203 |
+
border: 1px solid var(--border-color);
|
| 204 |
+
color: var(--text-gray);
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
.btn:hover {
|
| 208 |
+
opacity: 0.9;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
/* Report Modal */
|
| 212 |
+
.modal {
|
| 213 |
+
display: none;
|
| 214 |
+
position: fixed;
|
| 215 |
+
top: 0;
|
| 216 |
+
left: 0;
|
| 217 |
+
width: 100%;
|
| 218 |
+
height: 100%;
|
| 219 |
+
background: rgba(0, 0, 0, 0.8);
|
| 220 |
+
z-index: 1000;
|
| 221 |
+
justify-content: center;
|
| 222 |
+
align-items: center;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
.modal-content {
|
| 226 |
+
background: var(--panel-bg);
|
| 227 |
+
width: 500px;
|
| 228 |
+
padding: 30px;
|
| 229 |
+
border-radius: 16px;
|
| 230 |
+
border: 1px solid var(--primary-orange);
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
.report-text {
|
| 234 |
+
white-space: pre-wrap;
|
| 235 |
+
font-family: monospace;
|
| 236 |
+
color: #00FF88;
|
| 237 |
+
background: rgba(0, 0, 0, 0.5);
|
| 238 |
+
padding: 15px;
|
| 239 |
+
border-radius: 8px;
|
| 240 |
+
margin: 20px 0;
|
| 241 |
+
max-height: 300px;
|
| 242 |
+
overflow-y: auto;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
@keyframes pulse {
|
| 246 |
+
0% {
|
| 247 |
+
opacity: 1;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
50% {
|
| 251 |
+
opacity: 0.5;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
100% {
|
| 255 |
+
opacity: 1;
|
| 256 |
+
}
|
| 257 |
+
}
|
| 258 |
+
</style>
|
| 259 |
+
</head>
|
| 260 |
+
|
| 261 |
+
<body>
|
| 262 |
+
<div class="sidebar">
|
| 263 |
+
<div class="logo">PROJECT <span>SENTINEL</span></div>
|
| 264 |
+
<div class="nav-item active" onclick="setMode('movement', this)">
|
| 265 |
+
<span>🏃</span> Movement Analysis
|
| 266 |
+
</div>
|
| 267 |
+
<div class="nav-item" onclick="setMode('facemask', this)">
|
| 268 |
+
<span>😷</span> Facemask Detection
|
| 269 |
+
</div>
|
| 270 |
+
<div class="nav-item" onclick="setMode('weapon', this)">
|
| 271 |
+
<span>🔫</span> Weapon Detection
|
| 272 |
+
</div>
|
| 273 |
+
|
| 274 |
+
<div style="margin-top: auto;">
|
| 275 |
+
<div class="nav-item" onclick="document.getElementById('upload-input').click()">
|
| 276 |
+
<span>📤</span> Upload Video
|
| 277 |
+
</div>
|
| 278 |
+
<input type="file" id="upload-input" style="display: none" accept="video/*"
|
| 279 |
+
onchange="handleFileUpload(this)">
|
| 280 |
+
<div class="nav-item" onclick="setSource('camera')">
|
| 281 |
+
<span>📷</span> Live Camera
|
| 282 |
+
</div>
|
| 283 |
+
</div>
|
| 284 |
+
</div>
|
| 285 |
+
|
| 286 |
+
<div class="main-content">
|
| 287 |
+
<div class="video-panel">
|
| 288 |
+
<div class="panel-header">
|
| 289 |
+
<span id="mode-title">Movement Analysis Feed</span>
|
| 290 |
+
<span class="live-badge">LIVE</span>
|
| 291 |
+
</div>
|
| 292 |
+
<img id="video-stream" src="{{ url_for('video_feed') }}">
|
| 293 |
+
</div>
|
| 294 |
+
|
| 295 |
+
<div class="intel-panel">
|
| 296 |
+
<div class="card">
|
| 297 |
+
<h3>Dynamic Threat Score</h3>
|
| 298 |
+
<div class="threat-score">
|
| 299 |
+
<div class="score-value" id="threat-score" style="color: var(--success-green)">0</div>
|
| 300 |
+
<div class="score-label">Low Risk</div>
|
| 301 |
+
</div>
|
| 302 |
+
</div>
|
| 303 |
+
|
| 304 |
+
<div class="card">
|
| 305 |
+
<h3>Live Intelligence</h3>
|
| 306 |
+
<div class="stats-grid" id="stats-container">
|
| 307 |
+
<!-- Populated by JS -->
|
| 308 |
+
</div>
|
| 309 |
+
</div>
|
| 310 |
+
|
| 311 |
+
<div class="card">
|
| 312 |
+
<h3>AI Agent</h3>
|
| 313 |
+
<p style="font-size: 0.9rem; color: var(--text-gray); margin-bottom: 15px;">
|
| 314 |
+
Generate automated incident report based on current threat assessment.
|
| 315 |
+
</p>
|
| 316 |
+
<button class="btn btn-primary" onclick="generateReport()">Generate Report</button>
|
| 317 |
+
</div>
|
| 318 |
+
</div>
|
| 319 |
+
</div>
|
| 320 |
+
|
| 321 |
+
<div id="report-modal" class="modal">
|
| 322 |
+
<div class="modal-content">
|
| 323 |
+
<h2>Incident Report</h2>
|
| 324 |
+
<div class="report-text" id="report-content">Generating...</div>
|
| 325 |
+
<button class="btn btn-outline"
|
| 326 |
+
onclick="document.getElementById('report-modal').style.display='none'">Close</button>
|
| 327 |
+
</div>
|
| 328 |
+
</div>
|
| 329 |
+
|
| 330 |
+
<script>
|
| 331 |
+
function setMode(mode, element) {
|
| 332 |
+
// Update UI
|
| 333 |
+
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
|
| 334 |
+
element.classList.add('active');
|
| 335 |
+
|
| 336 |
+
const titles = { 'movement': 'Movement Analysis Feed', 'facemask': 'Facemask Detection Feed', 'weapon': 'Weapon Detection Feed' };
|
| 337 |
+
document.getElementById('mode-title').textContent = titles[mode];
|
| 338 |
+
|
| 339 |
+
// Call Backend
|
| 340 |
+
fetch('/set_mode', {
|
| 341 |
+
method: 'POST',
|
| 342 |
+
headers: { 'Content-Type': 'application/json' },
|
| 343 |
+
body: JSON.stringify({ mode: mode })
|
| 344 |
+
});
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
function setSource(source) {
|
| 348 |
+
fetch('/set_source', {
|
| 349 |
+
method: 'POST',
|
| 350 |
+
headers: { 'Content-Type': 'application/json' },
|
| 351 |
+
body: JSON.stringify({ source: source })
|
| 352 |
+
});
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
function handleFileUpload(input) {
|
| 356 |
+
if (input.files[0]) {
|
| 357 |
+
const formData = new FormData();
|
| 358 |
+
formData.append('file', input.files[0]);
|
| 359 |
+
fetch('/upload_video', { method: 'POST', body: formData });
|
| 360 |
+
}
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
function generateReport() {
|
| 364 |
+
document.getElementById('report-modal').style.display = 'flex';
|
| 365 |
+
document.getElementById('report-content').textContent = "Analyzing data...";
|
| 366 |
+
|
| 367 |
+
fetch('/generate_report', { method: 'POST' })
|
| 368 |
+
.then(r => r.json())
|
| 369 |
+
.then(data => {
|
| 370 |
+
document.getElementById('report-content').textContent = data.report;
|
| 371 |
+
});
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
function updateStats() {
|
| 375 |
+
fetch('/stats')
|
| 376 |
+
.then(r => r.json())
|
| 377 |
+
.then(data => {
|
| 378 |
+
// Update Score
|
| 379 |
+
const score = data.threat_score;
|
| 380 |
+
const scoreEl = document.getElementById('threat-score');
|
| 381 |
+
scoreEl.textContent = score;
|
| 382 |
+
if (score > 75) { scoreEl.style.color = 'var(--danger-red)'; scoreEl.nextElementSibling.textContent = 'CRITICAL'; }
|
| 383 |
+
else if (score > 40) { scoreEl.style.color = '#FFA500'; scoreEl.nextElementSibling.textContent = 'ELEVATED'; }
|
| 384 |
+
else { scoreEl.style.color = 'var(--success-green)'; scoreEl.nextElementSibling.textContent = 'LOW RISK'; }
|
| 385 |
+
|
| 386 |
+
// Update Stats Grid
|
| 387 |
+
const container = document.getElementById('stats-container');
|
| 388 |
+
container.innerHTML = '';
|
| 389 |
+
|
| 390 |
+
for (const [key, value] of Object.entries(data.details)) {
|
| 391 |
+
const div = document.createElement('div');
|
| 392 |
+
div.className = 'stat-item';
|
| 393 |
+
div.innerHTML = `<div class="stat-num">${value}</div><div class="stat-desc">${key.replace('_', ' ').toUpperCase()}</div>`;
|
| 394 |
+
container.appendChild(div);
|
| 395 |
+
}
|
| 396 |
+
});
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
setInterval(updateStats, 1000);
|
| 400 |
+
</script>
|
| 401 |
+
</body>
|
| 402 |
+
|
| 403 |
+
</html>
|
templates/sentinel_dashboard_v10.html
ADDED
|
@@ -0,0 +1,1385 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>SENTINEL V10 — Intelligence Grid</title>
|
| 8 |
+
<link
|
| 9 |
+
href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700;800;900&family=Rajdhani:wght@300;400;500;600;700&family=Share+Tech+Mono&display=swap"
|
| 10 |
+
rel="stylesheet">
|
| 11 |
+
<script src="https://unpkg.com/feather-icons"></script>
|
| 12 |
+
<style>
|
| 13 |
+
/* ═══════════════════════════════════════════════
|
| 14 |
+
CSS VARIABLES — SCI-FI PALETTE
|
| 15 |
+
═══════════════════════════════════════════════ */
|
| 16 |
+
:root {
|
| 17 |
+
--bg-deep: #05080f;
|
| 18 |
+
--bg-panel: rgba(8, 16, 32, 0.85);
|
| 19 |
+
--bg-card: rgba(10, 20, 40, 0.7);
|
| 20 |
+
--border-glow: rgba(0, 200, 255, 0.15);
|
| 21 |
+
--border-glow-hover: rgba(0, 200, 255, 0.4);
|
| 22 |
+
--cyan: #00d4ff;
|
| 23 |
+
--cyan-dim: #0090aa;
|
| 24 |
+
--neon-green: #00ff88;
|
| 25 |
+
--neon-red: #ff2040;
|
| 26 |
+
--neon-amber: #ffaa00;
|
| 27 |
+
--text-primary: #e0f0ff;
|
| 28 |
+
--text-secondary: #5a8aaa;
|
| 29 |
+
--text-dim: #2a4a5a;
|
| 30 |
+
--scanline-opacity: 0.03;
|
| 31 |
+
--font-display: 'Orbitron', sans-serif;
|
| 32 |
+
--font-body: 'Rajdhani', sans-serif;
|
| 33 |
+
--font-mono: 'Share Tech Mono', monospace;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/* ═══════════════════════════════════════════════
|
| 37 |
+
BASE RESET & BODY
|
| 38 |
+
═══════════════════════════════════════════════ */
|
| 39 |
+
*,
|
| 40 |
+
*::before,
|
| 41 |
+
*::after {
|
| 42 |
+
margin: 0;
|
| 43 |
+
padding: 0;
|
| 44 |
+
box-sizing: border-box;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
body {
|
| 48 |
+
font-family: var(--font-body);
|
| 49 |
+
background: var(--bg-deep);
|
| 50 |
+
color: var(--text-primary);
|
| 51 |
+
height: 100vh;
|
| 52 |
+
overflow: hidden;
|
| 53 |
+
display: flex;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/* Scanline overlay on body */
|
| 57 |
+
body::after {
|
| 58 |
+
content: '';
|
| 59 |
+
position: fixed;
|
| 60 |
+
top: 0;
|
| 61 |
+
left: 0;
|
| 62 |
+
right: 0;
|
| 63 |
+
bottom: 0;
|
| 64 |
+
background: repeating-linear-gradient(0deg,
|
| 65 |
+
transparent,
|
| 66 |
+
transparent 2px,
|
| 67 |
+
rgba(0, 200, 255, var(--scanline-opacity)) 2px,
|
| 68 |
+
rgba(0, 200, 255, var(--scanline-opacity)) 4px);
|
| 69 |
+
pointer-events: none;
|
| 70 |
+
z-index: 9999;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
/* ═══════════════════════════════════════════════
|
| 74 |
+
RED ALERT OVERLAY
|
| 75 |
+
═══════════════════════════════════════════════ */
|
| 76 |
+
.red-alert-overlay {
|
| 77 |
+
position: fixed;
|
| 78 |
+
top: 0;
|
| 79 |
+
left: 0;
|
| 80 |
+
right: 0;
|
| 81 |
+
bottom: 0;
|
| 82 |
+
pointer-events: none;
|
| 83 |
+
z-index: 9998;
|
| 84 |
+
opacity: 0;
|
| 85 |
+
transition: opacity 0.3s ease;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.red-alert-overlay.active {
|
| 89 |
+
opacity: 1;
|
| 90 |
+
animation: red-pulse-border 1s ease-in-out infinite;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.red-alert-overlay.active::before {
|
| 94 |
+
content: '';
|
| 95 |
+
position: absolute;
|
| 96 |
+
top: 0;
|
| 97 |
+
left: 0;
|
| 98 |
+
right: 0;
|
| 99 |
+
bottom: 0;
|
| 100 |
+
border: 4px solid var(--neon-red);
|
| 101 |
+
box-shadow: inset 0 0 80px rgba(255, 32, 64, 0.15),
|
| 102 |
+
0 0 40px rgba(255, 32, 64, 0.3);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.red-alert-banner {
|
| 106 |
+
position: fixed;
|
| 107 |
+
top: 0;
|
| 108 |
+
left: 50%;
|
| 109 |
+
transform: translateX(-50%);
|
| 110 |
+
background: rgba(255, 20, 40, 0.9);
|
| 111 |
+
color: #fff;
|
| 112 |
+
font-family: var(--font-display);
|
| 113 |
+
font-size: 0.85rem;
|
| 114 |
+
font-weight: 700;
|
| 115 |
+
letter-spacing: 4px;
|
| 116 |
+
padding: 8px 40px;
|
| 117 |
+
z-index: 10000;
|
| 118 |
+
border-bottom-left-radius: 8px;
|
| 119 |
+
border-bottom-right-radius: 8px;
|
| 120 |
+
box-shadow: 0 4px 30px rgba(255, 20, 40, 0.5);
|
| 121 |
+
display: none;
|
| 122 |
+
animation: alert-flash 0.8s ease-in-out infinite;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.red-alert-banner.active {
|
| 126 |
+
display: block;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
@keyframes red-pulse-border {
|
| 130 |
+
|
| 131 |
+
0%,
|
| 132 |
+
100% {
|
| 133 |
+
opacity: 0.6;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
50% {
|
| 137 |
+
opacity: 1;
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
@keyframes alert-flash {
|
| 142 |
+
|
| 143 |
+
0%,
|
| 144 |
+
100% {
|
| 145 |
+
opacity: 1;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
50% {
|
| 149 |
+
opacity: 0.6;
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
/* ═══════════════════════════════════════════════
|
| 154 |
+
SIDEBAR
|
| 155 |
+
═══════════════════════════════════════════════ */
|
| 156 |
+
.sidebar {
|
| 157 |
+
width: 260px;
|
| 158 |
+
min-width: 260px;
|
| 159 |
+
background: var(--bg-panel);
|
| 160 |
+
backdrop-filter: blur(20px);
|
| 161 |
+
-webkit-backdrop-filter: blur(20px);
|
| 162 |
+
border-right: 1px solid var(--border-glow);
|
| 163 |
+
display: flex;
|
| 164 |
+
flex-direction: column;
|
| 165 |
+
padding: 20px;
|
| 166 |
+
z-index: 100;
|
| 167 |
+
overflow-y: auto;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.logo {
|
| 171 |
+
font-family: var(--font-display);
|
| 172 |
+
font-size: 1rem;
|
| 173 |
+
font-weight: 700;
|
| 174 |
+
margin-bottom: 8px;
|
| 175 |
+
display: flex;
|
| 176 |
+
align-items: center;
|
| 177 |
+
gap: 10px;
|
| 178 |
+
color: var(--cyan);
|
| 179 |
+
letter-spacing: 2px;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.logo svg {
|
| 183 |
+
filter: drop-shadow(0 0 6px var(--cyan));
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
.version-tag {
|
| 187 |
+
font-family: var(--font-mono);
|
| 188 |
+
font-size: 0.65rem;
|
| 189 |
+
color: var(--text-dim);
|
| 190 |
+
margin-bottom: 24px;
|
| 191 |
+
letter-spacing: 1px;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.nav-group {
|
| 195 |
+
margin-bottom: 20px;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.nav-label {
|
| 199 |
+
font-family: var(--font-display);
|
| 200 |
+
font-size: 0.6rem;
|
| 201 |
+
font-weight: 600;
|
| 202 |
+
color: var(--text-dim);
|
| 203 |
+
margin-bottom: 8px;
|
| 204 |
+
text-transform: uppercase;
|
| 205 |
+
letter-spacing: 2px;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
.nav-item {
|
| 209 |
+
padding: 10px 12px;
|
| 210 |
+
margin-bottom: 3px;
|
| 211 |
+
border-radius: 6px;
|
| 212 |
+
cursor: pointer;
|
| 213 |
+
transition: all 0.25s ease;
|
| 214 |
+
display: flex;
|
| 215 |
+
align-items: center;
|
| 216 |
+
gap: 10px;
|
| 217 |
+
color: var(--text-secondary);
|
| 218 |
+
font-size: 0.9rem;
|
| 219 |
+
font-weight: 500;
|
| 220 |
+
border: 1px solid transparent;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.nav-item:hover {
|
| 224 |
+
background: rgba(0, 200, 255, 0.05);
|
| 225 |
+
color: var(--text-primary);
|
| 226 |
+
border-color: var(--border-glow);
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.nav-item.active {
|
| 230 |
+
background: rgba(0, 200, 255, 0.1);
|
| 231 |
+
color: var(--cyan);
|
| 232 |
+
border-color: rgba(0, 200, 255, 0.3);
|
| 233 |
+
box-shadow: 0 0 15px rgba(0, 200, 255, 0.05);
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
.nav-item svg {
|
| 237 |
+
width: 16px;
|
| 238 |
+
height: 16px;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
/* Audit Log Panel in sidebar */
|
| 242 |
+
.audit-section {
|
| 243 |
+
margin-top: auto;
|
| 244 |
+
flex-shrink: 0;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
.audit-log-container {
|
| 248 |
+
max-height: 200px;
|
| 249 |
+
overflow-y: auto;
|
| 250 |
+
scrollbar-width: thin;
|
| 251 |
+
scrollbar-color: var(--cyan-dim) transparent;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
.audit-log-container::-webkit-scrollbar {
|
| 255 |
+
width: 4px;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.audit-log-container::-webkit-scrollbar-track {
|
| 259 |
+
background: transparent;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.audit-log-container::-webkit-scrollbar-thumb {
|
| 263 |
+
background: var(--cyan-dim);
|
| 264 |
+
border-radius: 2px;
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
.audit-entry {
|
| 268 |
+
font-family: var(--font-mono);
|
| 269 |
+
font-size: 0.65rem;
|
| 270 |
+
padding: 6px 8px;
|
| 271 |
+
margin-bottom: 2px;
|
| 272 |
+
border-radius: 4px;
|
| 273 |
+
background: rgba(0, 0, 0, 0.3);
|
| 274 |
+
border-left: 2px solid var(--cyan-dim);
|
| 275 |
+
line-height: 1.4;
|
| 276 |
+
color: var(--text-secondary);
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
.audit-entry.severity-WARNING {
|
| 280 |
+
border-left-color: var(--neon-amber);
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
.audit-entry.severity-CRITICAL {
|
| 284 |
+
border-left-color: var(--neon-red);
|
| 285 |
+
color: var(--neon-red);
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
.audit-time {
|
| 289 |
+
color: var(--text-dim);
|
| 290 |
+
font-size: 0.6rem;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
/* ═══════════════════════════════════════════════
|
| 294 |
+
MAIN CONTENT AREA
|
| 295 |
+
═══════════════════════════════════════════════ */
|
| 296 |
+
.main-content {
|
| 297 |
+
flex: 1;
|
| 298 |
+
padding: 16px;
|
| 299 |
+
display: grid;
|
| 300 |
+
grid-template-columns: 1fr 320px;
|
| 301 |
+
gap: 16px;
|
| 302 |
+
overflow: hidden;
|
| 303 |
+
background: radial-gradient(ellipse at 20% 0%, rgba(0, 60, 100, 0.15) 0%, transparent 60%),
|
| 304 |
+
radial-gradient(ellipse at 80% 100%, rgba(0, 40, 80, 0.1) 0%, transparent 60%),
|
| 305 |
+
var(--bg-deep);
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
/* ══════════════════════════════════════════��════
|
| 309 |
+
MULTI-CAMERA GRID
|
| 310 |
+
═══════════════════════════════════════════════ */
|
| 311 |
+
.camera-grid {
|
| 312 |
+
display: grid;
|
| 313 |
+
grid-template-columns: 1fr 1fr;
|
| 314 |
+
grid-template-rows: 1fr 1fr;
|
| 315 |
+
gap: 8px;
|
| 316 |
+
height: 100%;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
.camera-grid.single-view {
|
| 320 |
+
grid-template-columns: 1fr;
|
| 321 |
+
grid-template-rows: 1fr;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.camera-grid.single-view .feed-cell:not(.expanded) {
|
| 325 |
+
display: none;
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
.feed-cell {
|
| 329 |
+
background: var(--bg-card);
|
| 330 |
+
border-radius: 10px;
|
| 331 |
+
border: 1px solid var(--border-glow);
|
| 332 |
+
overflow: hidden;
|
| 333 |
+
position: relative;
|
| 334 |
+
cursor: pointer;
|
| 335 |
+
transition: all 0.3s ease;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.feed-cell:hover {
|
| 339 |
+
border-color: var(--border-glow-hover);
|
| 340 |
+
box-shadow: 0 0 20px rgba(0, 200, 255, 0.1);
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
.feed-cell.red-alert-active {
|
| 344 |
+
border-color: var(--neon-red) !important;
|
| 345 |
+
box-shadow: 0 0 30px rgba(255, 32, 64, 0.3) !important;
|
| 346 |
+
animation: cell-red-pulse 1.5s ease-in-out infinite;
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
@keyframes cell-red-pulse {
|
| 350 |
+
|
| 351 |
+
0%,
|
| 352 |
+
100% {
|
| 353 |
+
box-shadow: 0 0 20px rgba(255, 32, 64, 0.2);
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
50% {
|
| 357 |
+
box-shadow: 0 0 40px rgba(255, 32, 64, 0.5);
|
| 358 |
+
}
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
.feed-header {
|
| 362 |
+
position: absolute;
|
| 363 |
+
top: 8px;
|
| 364 |
+
left: 10px;
|
| 365 |
+
right: 10px;
|
| 366 |
+
display: flex;
|
| 367 |
+
justify-content: space-between;
|
| 368 |
+
align-items: center;
|
| 369 |
+
z-index: 5;
|
| 370 |
+
pointer-events: none;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
.feed-badge {
|
| 374 |
+
background: rgba(0, 0, 0, 0.7);
|
| 375 |
+
backdrop-filter: blur(8px);
|
| 376 |
+
padding: 4px 10px;
|
| 377 |
+
border-radius: 4px;
|
| 378 |
+
font-family: var(--font-mono);
|
| 379 |
+
font-size: 0.65rem;
|
| 380 |
+
font-weight: 400;
|
| 381 |
+
border: 1px solid var(--border-glow);
|
| 382 |
+
display: flex;
|
| 383 |
+
align-items: center;
|
| 384 |
+
gap: 6px;
|
| 385 |
+
color: var(--cyan);
|
| 386 |
+
letter-spacing: 1px;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
.live-dot {
|
| 390 |
+
width: 6px;
|
| 391 |
+
height: 6px;
|
| 392 |
+
background: var(--neon-red);
|
| 393 |
+
border-radius: 50%;
|
| 394 |
+
box-shadow: 0 0 8px var(--neon-red);
|
| 395 |
+
animation: pulse-dot 2s infinite;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
@keyframes pulse-dot {
|
| 399 |
+
|
| 400 |
+
0%,
|
| 401 |
+
100% {
|
| 402 |
+
opacity: 1;
|
| 403 |
+
transform: scale(1);
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
50% {
|
| 407 |
+
opacity: 0.4;
|
| 408 |
+
transform: scale(1.3);
|
| 409 |
+
}
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
.feed-status {
|
| 413 |
+
font-family: var(--font-mono);
|
| 414 |
+
font-size: 0.6rem;
|
| 415 |
+
color: var(--neon-green);
|
| 416 |
+
background: rgba(0, 0, 0, 0.6);
|
| 417 |
+
padding: 3px 8px;
|
| 418 |
+
border-radius: 4px;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
.feed-stream {
|
| 422 |
+
width: 100%;
|
| 423 |
+
height: 100%;
|
| 424 |
+
object-fit: contain;
|
| 425 |
+
background: #000;
|
| 426 |
+
display: block;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.feed-offline {
|
| 430 |
+
display: flex;
|
| 431 |
+
flex-direction: column;
|
| 432 |
+
align-items: center;
|
| 433 |
+
justify-content: center;
|
| 434 |
+
height: 100%;
|
| 435 |
+
color: var(--text-dim);
|
| 436 |
+
font-family: var(--font-mono);
|
| 437 |
+
font-size: 0.75rem;
|
| 438 |
+
letter-spacing: 1px;
|
| 439 |
+
gap: 8px;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
.feed-offline svg {
|
| 443 |
+
width: 24px;
|
| 444 |
+
height: 24px;
|
| 445 |
+
color: var(--text-dim);
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
/* Fullscreen expand button */
|
| 449 |
+
.expand-btn {
|
| 450 |
+
position: absolute;
|
| 451 |
+
bottom: 8px;
|
| 452 |
+
right: 8px;
|
| 453 |
+
background: rgba(0, 0, 0, 0.6);
|
| 454 |
+
border: 1px solid var(--border-glow);
|
| 455 |
+
color: var(--cyan);
|
| 456 |
+
width: 28px;
|
| 457 |
+
height: 28px;
|
| 458 |
+
border-radius: 4px;
|
| 459 |
+
cursor: pointer;
|
| 460 |
+
display: flex;
|
| 461 |
+
align-items: center;
|
| 462 |
+
justify-content: center;
|
| 463 |
+
transition: all 0.2s;
|
| 464 |
+
z-index: 5;
|
| 465 |
+
pointer-events: all;
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
.expand-btn:hover {
|
| 469 |
+
background: rgba(0, 200, 255, 0.15);
|
| 470 |
+
border-color: var(--cyan);
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
.expand-btn svg {
|
| 474 |
+
width: 14px;
|
| 475 |
+
height: 14px;
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
/* ═══════════════════════════════════════════════
|
| 479 |
+
INTEL PANEL (Right Column)
|
| 480 |
+
═══════════════════════════════════════════════ */
|
| 481 |
+
.intel-panel {
|
| 482 |
+
display: flex;
|
| 483 |
+
flex-direction: column;
|
| 484 |
+
gap: 12px;
|
| 485 |
+
overflow-y: auto;
|
| 486 |
+
scrollbar-width: thin;
|
| 487 |
+
scrollbar-color: var(--cyan-dim) transparent;
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
.intel-panel::-webkit-scrollbar {
|
| 491 |
+
width: 4px;
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
.intel-panel::-webkit-scrollbar-thumb {
|
| 495 |
+
background: var(--cyan-dim);
|
| 496 |
+
border-radius: 2px;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
.card {
|
| 500 |
+
background: var(--bg-card);
|
| 501 |
+
backdrop-filter: blur(15px);
|
| 502 |
+
border-radius: 10px;
|
| 503 |
+
padding: 18px;
|
| 504 |
+
border: 1px solid var(--border-glow);
|
| 505 |
+
transition: border-color 0.3s;
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
.card:hover {
|
| 509 |
+
border-color: var(--border-glow-hover);
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
.card-header {
|
| 513 |
+
display: flex;
|
| 514 |
+
justify-content: space-between;
|
| 515 |
+
align-items: center;
|
| 516 |
+
margin-bottom: 14px;
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
.card-title {
|
| 520 |
+
font-family: var(--font-display);
|
| 521 |
+
font-size: 0.65rem;
|
| 522 |
+
font-weight: 600;
|
| 523 |
+
color: var(--cyan);
|
| 524 |
+
text-transform: uppercase;
|
| 525 |
+
letter-spacing: 2px;
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
.card-icon {
|
| 529 |
+
color: var(--text-dim);
|
| 530 |
+
width: 16px;
|
| 531 |
+
height: 16px;
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
/* Threat gauge */
|
| 535 |
+
.threat-gauge {
|
| 536 |
+
display: flex;
|
| 537 |
+
flex-direction: column;
|
| 538 |
+
align-items: center;
|
| 539 |
+
padding: 10px 0;
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
.score-ring {
|
| 543 |
+
position: relative;
|
| 544 |
+
width: 130px;
|
| 545 |
+
height: 130px;
|
| 546 |
+
margin-bottom: 12px;
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
.score-ring svg {
|
| 550 |
+
width: 130px;
|
| 551 |
+
height: 130px;
|
| 552 |
+
transform: rotate(-90deg);
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
.score-ring-bg {
|
| 556 |
+
fill: none;
|
| 557 |
+
stroke: rgba(255, 255, 255, 0.05);
|
| 558 |
+
stroke-width: 6;
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
.score-ring-fill {
|
| 562 |
+
fill: none;
|
| 563 |
+
stroke: var(--neon-green);
|
| 564 |
+
stroke-width: 6;
|
| 565 |
+
stroke-linecap: round;
|
| 566 |
+
stroke-dasharray: 377;
|
| 567 |
+
stroke-dashoffset: 377;
|
| 568 |
+
transition: stroke-dashoffset 0.8s cubic-bezier(0.4, 0, 0.2, 1),
|
| 569 |
+
stroke 0.5s ease;
|
| 570 |
+
filter: drop-shadow(0 0 8px var(--neon-green));
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
.score-text {
|
| 574 |
+
position: absolute;
|
| 575 |
+
top: 50%;
|
| 576 |
+
left: 50%;
|
| 577 |
+
transform: translate(-50%, -50%);
|
| 578 |
+
text-align: center;
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
.score-value {
|
| 582 |
+
font-family: var(--font-display);
|
| 583 |
+
font-size: 2.2rem;
|
| 584 |
+
font-weight: 800;
|
| 585 |
+
line-height: 1;
|
| 586 |
+
color: var(--text-primary);
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
.score-label {
|
| 590 |
+
font-family: var(--font-mono);
|
| 591 |
+
font-size: 0.55rem;
|
| 592 |
+
color: var(--text-dim);
|
| 593 |
+
letter-spacing: 2px;
|
| 594 |
+
margin-top: 4px;
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
.status-text {
|
| 598 |
+
font-family: var(--font-display);
|
| 599 |
+
font-size: 0.85rem;
|
| 600 |
+
font-weight: 700;
|
| 601 |
+
color: var(--neon-green);
|
| 602 |
+
letter-spacing: 3px;
|
| 603 |
+
text-shadow: 0 0 20px rgba(0, 255, 136, 0.3);
|
| 604 |
+
transition: all 0.5s ease;
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
/* Stats */
|
| 608 |
+
.stats-list {
|
| 609 |
+
display: flex;
|
| 610 |
+
flex-direction: column;
|
| 611 |
+
gap: 6px;
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
.stat-row {
|
| 615 |
+
display: flex;
|
| 616 |
+
justify-content: space-between;
|
| 617 |
+
align-items: center;
|
| 618 |
+
padding: 10px 12px;
|
| 619 |
+
background: rgba(0, 200, 255, 0.03);
|
| 620 |
+
border-radius: 6px;
|
| 621 |
+
border: 1px solid rgba(0, 200, 255, 0.05);
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
.stat-name {
|
| 625 |
+
font-size: 0.8rem;
|
| 626 |
+
color: var(--text-secondary);
|
| 627 |
+
font-weight: 500;
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
.stat-val {
|
| 631 |
+
font-family: var(--font-mono);
|
| 632 |
+
font-size: 0.85rem;
|
| 633 |
+
font-weight: 600;
|
| 634 |
+
color: var(--text-primary);
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
/* Mode indicator */
|
| 638 |
+
.mode-indicator {
|
| 639 |
+
display: flex;
|
| 640 |
+
align-items: center;
|
| 641 |
+
gap: 8px;
|
| 642 |
+
padding: 10px 14px;
|
| 643 |
+
background: rgba(0, 200, 255, 0.05);
|
| 644 |
+
border-radius: 6px;
|
| 645 |
+
border: 1px solid var(--border-glow);
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
.mode-dot {
|
| 649 |
+
width: 8px;
|
| 650 |
+
height: 8px;
|
| 651 |
+
background: var(--cyan);
|
| 652 |
+
border-radius: 50%;
|
| 653 |
+
box-shadow: 0 0 10px var(--cyan);
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
.mode-name {
|
| 657 |
+
font-family: var(--font-display);
|
| 658 |
+
font-size: 0.7rem;
|
| 659 |
+
font-weight: 600;
|
| 660 |
+
letter-spacing: 2px;
|
| 661 |
+
color: var(--cyan);
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
/* Buttons */
|
| 665 |
+
.btn {
|
| 666 |
+
width: 100%;
|
| 667 |
+
padding: 12px;
|
| 668 |
+
border: 1px solid var(--border-glow);
|
| 669 |
+
border-radius: 6px;
|
| 670 |
+
font-size: 0.8rem;
|
| 671 |
+
font-weight: 600;
|
| 672 |
+
cursor: pointer;
|
| 673 |
+
transition: all 0.25s;
|
| 674 |
+
font-family: var(--font-display);
|
| 675 |
+
letter-spacing: 1px;
|
| 676 |
+
text-transform: uppercase;
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
.btn-primary {
|
| 680 |
+
background: linear-gradient(135deg, rgba(0, 200, 255, 0.2), rgba(0, 100, 200, 0.1));
|
| 681 |
+
color: var(--cyan);
|
| 682 |
+
border-color: rgba(0, 200, 255, 0.3);
|
| 683 |
+
}
|
| 684 |
+
|
| 685 |
+
.btn-primary:hover {
|
| 686 |
+
background: linear-gradient(135deg, rgba(0, 200, 255, 0.3), rgba(0, 100, 200, 0.2));
|
| 687 |
+
box-shadow: 0 0 20px rgba(0, 200, 255, 0.15);
|
| 688 |
+
transform: translateY(-1px);
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
+
.btn-secondary {
|
| 692 |
+
background: rgba(255, 255, 255, 0.03);
|
| 693 |
+
color: var(--text-secondary);
|
| 694 |
+
}
|
| 695 |
+
|
| 696 |
+
.btn-secondary:hover {
|
| 697 |
+
background: rgba(255, 255, 255, 0.08);
|
| 698 |
+
color: var(--text-primary);
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
/* ═══════════════════════════════════════════════
|
| 702 |
+
MODAL
|
| 703 |
+
═══════════════════════════════════════════════ */
|
| 704 |
+
.modal-overlay {
|
| 705 |
+
position: fixed;
|
| 706 |
+
top: 0;
|
| 707 |
+
left: 0;
|
| 708 |
+
right: 0;
|
| 709 |
+
bottom: 0;
|
| 710 |
+
background: rgba(0, 0, 0, 0.8);
|
| 711 |
+
backdrop-filter: blur(10px);
|
| 712 |
+
display: none;
|
| 713 |
+
justify-content: center;
|
| 714 |
+
align-items: center;
|
| 715 |
+
z-index: 11000;
|
| 716 |
+
opacity: 0;
|
| 717 |
+
transition: opacity 0.3s ease;
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
.modal-overlay.show {
|
| 721 |
+
display: flex;
|
| 722 |
+
opacity: 1;
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
.modal-card {
|
| 726 |
+
background: var(--bg-panel);
|
| 727 |
+
width: 520px;
|
| 728 |
+
max-width: 90vw;
|
| 729 |
+
border-radius: 12px;
|
| 730 |
+
padding: 28px;
|
| 731 |
+
border: 1px solid var(--border-glow);
|
| 732 |
+
box-shadow: 0 0 60px rgba(0, 200, 255, 0.1);
|
| 733 |
+
transform: scale(0.95);
|
| 734 |
+
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
+
.modal-overlay.show .modal-card {
|
| 738 |
+
transform: scale(1);
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
.modal-title {
|
| 742 |
+
font-family: var(--font-display);
|
| 743 |
+
font-size: 1rem;
|
| 744 |
+
font-weight: 700;
|
| 745 |
+
color: var(--cyan);
|
| 746 |
+
letter-spacing: 2px;
|
| 747 |
+
margin-bottom: 6px;
|
| 748 |
+
}
|
| 749 |
+
|
| 750 |
+
.modal-subtitle {
|
| 751 |
+
font-family: var(--font-mono);
|
| 752 |
+
font-size: 0.7rem;
|
| 753 |
+
color: var(--text-dim);
|
| 754 |
+
margin-bottom: 16px;
|
| 755 |
+
}
|
| 756 |
+
|
| 757 |
+
.report-content {
|
| 758 |
+
background: rgba(0, 0, 0, 0.5);
|
| 759 |
+
padding: 16px;
|
| 760 |
+
border-radius: 8px;
|
| 761 |
+
font-family: var(--font-mono);
|
| 762 |
+
font-size: 0.75rem;
|
| 763 |
+
color: var(--neon-green);
|
| 764 |
+
margin-bottom: 16px;
|
| 765 |
+
line-height: 1.6;
|
| 766 |
+
border: 1px solid var(--border-glow);
|
| 767 |
+
max-height: 300px;
|
| 768 |
+
overflow-y: auto;
|
| 769 |
+
white-space: pre-wrap;
|
| 770 |
+
}
|
| 771 |
+
|
| 772 |
+
/* ═══════════════════════════════════════════════
|
| 773 |
+
MOBILE RESPONSIVE
|
| 774 |
+
═══════════════════════════════════════════════ */
|
| 775 |
+
@media (max-width: 1024px) {
|
| 776 |
+
.main-content {
|
| 777 |
+
grid-template-columns: 1fr 280px;
|
| 778 |
+
}
|
| 779 |
+
}
|
| 780 |
+
|
| 781 |
+
@media (max-width: 768px) {
|
| 782 |
+
body {
|
| 783 |
+
flex-direction: column;
|
| 784 |
+
overflow-y: auto;
|
| 785 |
+
}
|
| 786 |
+
|
| 787 |
+
.sidebar {
|
| 788 |
+
width: 100%;
|
| 789 |
+
min-width: 100%;
|
| 790 |
+
flex-direction: row;
|
| 791 |
+
flex-wrap: wrap;
|
| 792 |
+
padding: 10px 16px;
|
| 793 |
+
gap: 6px;
|
| 794 |
+
order: 2;
|
| 795 |
+
border-right: none;
|
| 796 |
+
border-top: 1px solid var(--border-glow);
|
| 797 |
+
overflow-y: visible;
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
.logo {
|
| 801 |
+
width: 100%;
|
| 802 |
+
margin-bottom: 6px;
|
| 803 |
+
font-size: 0.85rem;
|
| 804 |
+
}
|
| 805 |
+
|
| 806 |
+
.version-tag {
|
| 807 |
+
display: none;
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
+
.nav-group {
|
| 811 |
+
display: flex;
|
| 812 |
+
flex-wrap: wrap;
|
| 813 |
+
gap: 4px;
|
| 814 |
+
margin-bottom: 0;
|
| 815 |
+
}
|
| 816 |
+
|
| 817 |
+
.nav-label {
|
| 818 |
+
width: 100%;
|
| 819 |
+
margin-bottom: 4px;
|
| 820 |
+
}
|
| 821 |
+
|
| 822 |
+
.nav-item {
|
| 823 |
+
flex: none;
|
| 824 |
+
padding: 8px 10px;
|
| 825 |
+
font-size: 0.75rem;
|
| 826 |
+
}
|
| 827 |
+
|
| 828 |
+
.audit-section {
|
| 829 |
+
display: none;
|
| 830 |
+
}
|
| 831 |
+
|
| 832 |
+
.main-content {
|
| 833 |
+
grid-template-columns: 1fr;
|
| 834 |
+
grid-template-rows: auto auto;
|
| 835 |
+
order: 1;
|
| 836 |
+
overflow-y: auto;
|
| 837 |
+
padding: 10px;
|
| 838 |
+
}
|
| 839 |
+
|
| 840 |
+
.camera-grid {
|
| 841 |
+
grid-template-columns: 1fr;
|
| 842 |
+
grid-template-rows: auto;
|
| 843 |
+
min-height: 250px;
|
| 844 |
+
}
|
| 845 |
+
|
| 846 |
+
.feed-cell:not(:first-child) {
|
| 847 |
+
display: none;
|
| 848 |
+
}
|
| 849 |
+
|
| 850 |
+
.intel-panel {
|
| 851 |
+
overflow-y: visible;
|
| 852 |
+
}
|
| 853 |
+
}
|
| 854 |
+
|
| 855 |
+
@media (max-width: 480px) {
|
| 856 |
+
.sidebar .nav-item {
|
| 857 |
+
font-size: 0.7rem;
|
| 858 |
+
padding: 6px 8px;
|
| 859 |
+
gap: 6px;
|
| 860 |
+
}
|
| 861 |
+
|
| 862 |
+
.sidebar .nav-item svg {
|
| 863 |
+
width: 14px;
|
| 864 |
+
height: 14px;
|
| 865 |
+
}
|
| 866 |
+
|
| 867 |
+
.score-ring {
|
| 868 |
+
width: 100px;
|
| 869 |
+
height: 100px;
|
| 870 |
+
}
|
| 871 |
+
|
| 872 |
+
.score-ring svg {
|
| 873 |
+
width: 100px;
|
| 874 |
+
height: 100px;
|
| 875 |
+
}
|
| 876 |
+
|
| 877 |
+
.score-value {
|
| 878 |
+
font-size: 1.6rem;
|
| 879 |
+
}
|
| 880 |
+
}
|
| 881 |
+
</style>
|
| 882 |
+
</head>
|
| 883 |
+
|
| 884 |
+
<body>
|
| 885 |
+
<!-- Red Alert Overlay -->
|
| 886 |
+
<div class="red-alert-overlay" id="red-alert-overlay"></div>
|
| 887 |
+
<div class="red-alert-banner" id="red-alert-banner">⚠ RED ALERT — CRITICAL THREAT DETECTED ⚠</div>
|
| 888 |
+
|
| 889 |
+
<!-- SIDEBAR -->
|
| 890 |
+
<div class="sidebar">
|
| 891 |
+
<div class="logo">
|
| 892 |
+
<i data-feather="shield"></i>
|
| 893 |
+
SENTINEL
|
| 894 |
+
</div>
|
| 895 |
+
<div class="version-tag">V10.0 // STANDBY FIX</div>
|
| 896 |
+
|
| 897 |
+
<div class="nav-group">
|
| 898 |
+
<div class="nav-label">Detection Modules</div>
|
| 899 |
+
<div class="nav-item" onclick="setMode('standby', this)" id="nav-standby">
|
| 900 |
+
<i data-feather="pause-circle"></i> Standby
|
| 901 |
+
</div>
|
| 902 |
+
<div class="nav-item active" onclick="setMode('movement', this)" id="nav-movement">
|
| 903 |
+
<i data-feather="activity"></i> Movement
|
| 904 |
+
</div>
|
| 905 |
+
<div class="nav-item" onclick="setMode('facemask', this)" id="nav-facemask">
|
| 906 |
+
<i data-feather="eye"></i> Facemask
|
| 907 |
+
</div>
|
| 908 |
+
<div class="nav-item" onclick="setMode('weapon', this)" id="nav-weapon">
|
| 909 |
+
<i data-feather="crosshair"></i> Weapon
|
| 910 |
+
</div>
|
| 911 |
+
<div class="nav-item" onclick="setMode('public_safety', this)" id="nav-public_safety">
|
| 912 |
+
<i data-feather="users"></i> Public Safety
|
| 913 |
+
</div>
|
| 914 |
+
</div>
|
| 915 |
+
|
| 916 |
+
<div class="nav-group">
|
| 917 |
+
<div class="nav-label">Input Source</div>
|
| 918 |
+
<div class="nav-item" onclick="setSource('camera')">
|
| 919 |
+
<i data-feather="video"></i> Live Camera
|
| 920 |
+
</div>
|
| 921 |
+
<div class="nav-item" onclick="document.getElementById('upload-input').click()">
|
| 922 |
+
<i data-feather="upload-cloud"></i> Upload Video
|
| 923 |
+
</div>
|
| 924 |
+
<input type="file" id="upload-input" style="display: none" accept="video/*"
|
| 925 |
+
onchange="handleFileUpload(this)">
|
| 926 |
+
</div>
|
| 927 |
+
|
| 928 |
+
<div class="nav-group">
|
| 929 |
+
<div class="nav-label">Grid Layout</div>
|
| 930 |
+
<div class="nav-item" onclick="setGridLayout('quad')">
|
| 931 |
+
<i data-feather="grid"></i> 2×2 Grid
|
| 932 |
+
</div>
|
| 933 |
+
<div class="nav-item" onclick="setGridLayout('single')">
|
| 934 |
+
<i data-feather="maximize-2"></i> Single View
|
| 935 |
+
</div>
|
| 936 |
+
</div>
|
| 937 |
+
|
| 938 |
+
<!-- Audit Log -->
|
| 939 |
+
<div class="audit-section">
|
| 940 |
+
<div class="nav-label" style="margin-bottom: 8px;">Operator Log</div>
|
| 941 |
+
<div class="audit-log-container" id="audit-log-container">
|
| 942 |
+
<div class="audit-entry">
|
| 943 |
+
<div class="audit-time">--:--:--</div>
|
| 944 |
+
SYSTEM STANDBY
|
| 945 |
+
</div>
|
| 946 |
+
</div>
|
| 947 |
+
</div>
|
| 948 |
+
</div>
|
| 949 |
+
|
| 950 |
+
<!-- MAIN CONTENT -->
|
| 951 |
+
<div class="main-content">
|
| 952 |
+
<!-- Multi-Camera Grid -->
|
| 953 |
+
<div class="camera-grid" id="camera-grid">
|
| 954 |
+
<!-- Feed 0 — Primary -->
|
| 955 |
+
<div class="feed-cell expanded" id="feed-0" onclick="expandFeed(0)">
|
| 956 |
+
<div class="feed-header">
|
| 957 |
+
<div class="feed-badge">
|
| 958 |
+
<div class="live-dot"></div>
|
| 959 |
+
<span>FEED 01 // PRIMARY</span>
|
| 960 |
+
</div>
|
| 961 |
+
<div class="feed-status" id="feed-0-status">ACTIVE</div>
|
| 962 |
+
</div>
|
| 963 |
+
<img class="feed-stream" id="stream-0" src="/video_feed/0" alt="Feed 0">
|
| 964 |
+
<button class="expand-btn" onclick="event.stopPropagation(); expandFeed(0)" title="Expand">
|
| 965 |
+
<i data-feather="maximize-2"></i>
|
| 966 |
+
</button>
|
| 967 |
+
</div>
|
| 968 |
+
|
| 969 |
+
<!-- Feed 1 -->
|
| 970 |
+
<div class="feed-cell" id="feed-1">
|
| 971 |
+
<div class="feed-header">
|
| 972 |
+
<div class="feed-badge">
|
| 973 |
+
<div class="live-dot" style="background: var(--text-dim); box-shadow: none;"></div>
|
| 974 |
+
<span>FEED 02</span>
|
| 975 |
+
</div>
|
| 976 |
+
<div class="feed-status" id="feed-1-status" style="color: var(--text-dim);">STANDBY</div>
|
| 977 |
+
</div>
|
| 978 |
+
<div class="feed-offline" id="offline-1">
|
| 979 |
+
<i data-feather="video-off"></i>
|
| 980 |
+
NO SIGNAL
|
| 981 |
+
</div>
|
| 982 |
+
</div>
|
| 983 |
+
|
| 984 |
+
<!-- Feed 2 -->
|
| 985 |
+
<div class="feed-cell" id="feed-2">
|
| 986 |
+
<div class="feed-header">
|
| 987 |
+
<div class="feed-badge">
|
| 988 |
+
<div class="live-dot" style="background: var(--text-dim); box-shadow: none;"></div>
|
| 989 |
+
<span>FEED 03</span>
|
| 990 |
+
</div>
|
| 991 |
+
<div class="feed-status" id="feed-2-status" style="color: var(--text-dim);">STANDBY</div>
|
| 992 |
+
</div>
|
| 993 |
+
<div class="feed-offline" id="offline-2">
|
| 994 |
+
<i data-feather="video-off"></i>
|
| 995 |
+
NO SIGNAL
|
| 996 |
+
</div>
|
| 997 |
+
</div>
|
| 998 |
+
|
| 999 |
+
<!-- Feed 3 -->
|
| 1000 |
+
<div class="feed-cell" id="feed-3">
|
| 1001 |
+
<div class="feed-header">
|
| 1002 |
+
<div class="feed-badge">
|
| 1003 |
+
<div class="live-dot" style="background: var(--text-dim); box-shadow: none;"></div>
|
| 1004 |
+
<span>FEED 04</span>
|
| 1005 |
+
</div>
|
| 1006 |
+
<div class="feed-status" id="feed-3-status" style="color: var(--text-dim);">STANDBY</div>
|
| 1007 |
+
</div>
|
| 1008 |
+
<div class="feed-offline" id="offline-3">
|
| 1009 |
+
<i data-feather="video-off"></i>
|
| 1010 |
+
NO SIGNAL
|
| 1011 |
+
</div>
|
| 1012 |
+
</div>
|
| 1013 |
+
</div>
|
| 1014 |
+
|
| 1015 |
+
<!-- Intel Panel -->
|
| 1016 |
+
<div class="intel-panel">
|
| 1017 |
+
<!-- Active Mode -->
|
| 1018 |
+
<div class="card">
|
| 1019 |
+
<div class="card-header">
|
| 1020 |
+
<div class="card-title">Active Mode</div>
|
| 1021 |
+
</div>
|
| 1022 |
+
<div class="mode-indicator">
|
| 1023 |
+
<div class="mode-dot"></div>
|
| 1024 |
+
<div class="mode-name" id="mode-title">MOVEMENT ANALYSIS</div>
|
| 1025 |
+
</div>
|
| 1026 |
+
</div>
|
| 1027 |
+
|
| 1028 |
+
<!-- Threat Assessment -->
|
| 1029 |
+
<div class="card" id="threat-card">
|
| 1030 |
+
<div class="card-header">
|
| 1031 |
+
<div class="card-title">Threat Assessment</div>
|
| 1032 |
+
<i data-feather="alert-triangle" class="card-icon"></i>
|
| 1033 |
+
</div>
|
| 1034 |
+
<div class="threat-gauge">
|
| 1035 |
+
<div class="score-ring">
|
| 1036 |
+
<svg viewBox="0 0 130 130">
|
| 1037 |
+
<circle class="score-ring-bg" cx="65" cy="65" r="60"></circle>
|
| 1038 |
+
<circle class="score-ring-fill" id="score-ring-fill" cx="65" cy="65" r="60"></circle>
|
| 1039 |
+
</svg>
|
| 1040 |
+
<div class="score-text">
|
| 1041 |
+
<div class="score-value" id="threat-score">0</div>
|
| 1042 |
+
<div class="score-label">THREAT LEVEL</div>
|
| 1043 |
+
</div>
|
| 1044 |
+
</div>
|
| 1045 |
+
<div class="status-text" id="status-text">SECURE</div>
|
| 1046 |
+
</div>
|
| 1047 |
+
</div>
|
| 1048 |
+
|
| 1049 |
+
<!-- Live Metrics -->
|
| 1050 |
+
<div class="card">
|
| 1051 |
+
<div class="card-header">
|
| 1052 |
+
<div class="card-title">Live Metrics</div>
|
| 1053 |
+
<i data-feather="bar-chart-2" class="card-icon"></i>
|
| 1054 |
+
</div>
|
| 1055 |
+
<div class="stats-list" id="stats-container">
|
| 1056 |
+
<div class="stat-row">
|
| 1057 |
+
<span class="stat-name">System Status</span>
|
| 1058 |
+
<span class="stat-val">Initializing</span>
|
| 1059 |
+
</div>
|
| 1060 |
+
</div>
|
| 1061 |
+
</div>
|
| 1062 |
+
|
| 1063 |
+
<!-- Actions -->
|
| 1064 |
+
<div class="card">
|
| 1065 |
+
<div class="card-header">
|
| 1066 |
+
<div class="card-title">Actions</div>
|
| 1067 |
+
</div>
|
| 1068 |
+
<button class="btn btn-primary" onclick="generateReport()">Generate Incident Report</button>
|
| 1069 |
+
<button class="btn btn-secondary" onclick="refreshAuditLog()" style="margin-top: 8px;">Refresh Audit
|
| 1070 |
+
Log</button>
|
| 1071 |
+
</div>
|
| 1072 |
+
</div>
|
| 1073 |
+
</div>
|
| 1074 |
+
|
| 1075 |
+
<!-- Report Modal -->
|
| 1076 |
+
<div id="report-modal" class="modal-overlay">
|
| 1077 |
+
<div class="modal-card">
|
| 1078 |
+
<div class="modal-title">Incident Report</div>
|
| 1079 |
+
<div class="modal-subtitle">Generated by Sentinel AI Agent // Gemini 1.5</div>
|
| 1080 |
+
<div class="report-content" id="report-content">Analyzing data stream...</div>
|
| 1081 |
+
<button class="btn btn-secondary" onclick="closeModal()">Dismiss</button>
|
| 1082 |
+
</div>
|
| 1083 |
+
</div>
|
| 1084 |
+
|
| 1085 |
+
<!-- ═══════════════════════════════════════════════
|
| 1086 |
+
JAVASCRIPT
|
| 1087 |
+
═══════════════════════════════════════════════ -->
|
| 1088 |
+
<script>
|
| 1089 |
+
feather.replace();
|
| 1090 |
+
|
| 1091 |
+
// ─── State ───
|
| 1092 |
+
let currentLayout = 'quad'; // 'quad' or 'single'
|
| 1093 |
+
let expandedFeed = 0;
|
| 1094 |
+
let isRedAlert = false;
|
| 1095 |
+
|
| 1096 |
+
// ─── Mode Switching ───
|
| 1097 |
+
const modeTitles = {
|
| 1098 |
+
'standby': 'SYSTEM STANDBY',
|
| 1099 |
+
'movement': 'MOVEMENT ANALYSIS',
|
| 1100 |
+
'facemask': 'FACEMASK DETECTION',
|
| 1101 |
+
'weapon': 'WEAPON DETECTION',
|
| 1102 |
+
'public_safety': 'PUBLIC SAFETY'
|
| 1103 |
+
};
|
| 1104 |
+
|
| 1105 |
+
function setMode(mode, element) {
|
| 1106 |
+
// Radio-style selection: always activate the clicked mode
|
| 1107 |
+
// Remove active from all module buttons
|
| 1108 |
+
document.querySelectorAll('.nav-group:first-of-type .nav-item').forEach(el => el.classList.remove('active'));
|
| 1109 |
+
|
| 1110 |
+
// Add active to the clicked button
|
| 1111 |
+
element.classList.add('active');
|
| 1112 |
+
|
| 1113 |
+
// Update UI display
|
| 1114 |
+
document.getElementById('mode-title').textContent = modeTitles[mode] || mode.toUpperCase();
|
| 1115 |
+
|
| 1116 |
+
// Send to backend
|
| 1117 |
+
fetch('/set_mode', {
|
| 1118 |
+
method: 'POST',
|
| 1119 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1120 |
+
body: JSON.stringify({ mode: mode })
|
| 1121 |
+
});
|
| 1122 |
+
}
|
| 1123 |
+
|
| 1124 |
+
function setSource(source) {
|
| 1125 |
+
fetch('/set_source', {
|
| 1126 |
+
method: 'POST',
|
| 1127 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1128 |
+
body: JSON.stringify({ source: source })
|
| 1129 |
+
})
|
| 1130 |
+
.then(r => r.json())
|
| 1131 |
+
.then(data => {
|
| 1132 |
+
if (data.success) {
|
| 1133 |
+
// Force the browser to reconnect to the restarted MJPEG stream
|
| 1134 |
+
refreshFeedStream(0);
|
| 1135 |
+
}
|
| 1136 |
+
});
|
| 1137 |
+
}
|
| 1138 |
+
|
| 1139 |
+
function handleFileUpload(input) {
|
| 1140 |
+
if (input.files[0]) {
|
| 1141 |
+
const formData = new FormData();
|
| 1142 |
+
formData.append('file', input.files[0]);
|
| 1143 |
+
|
| 1144 |
+
// Show uploading state
|
| 1145 |
+
const statusEl = document.getElementById('feed-0-status');
|
| 1146 |
+
if (statusEl) statusEl.textContent = 'UPLOADING...';
|
| 1147 |
+
|
| 1148 |
+
fetch('/upload_video', { method: 'POST', body: formData })
|
| 1149 |
+
.then(r => r.json())
|
| 1150 |
+
.then(data => {
|
| 1151 |
+
if (data.success) {
|
| 1152 |
+
// Force the browser to reconnect to the restarted MJPEG stream
|
| 1153 |
+
refreshFeedStream(0);
|
| 1154 |
+
if (statusEl) statusEl.textContent = 'FILE FEED';
|
| 1155 |
+
}
|
| 1156 |
+
})
|
| 1157 |
+
.catch(() => {
|
| 1158 |
+
if (statusEl) statusEl.textContent = 'UPLOAD FAILED';
|
| 1159 |
+
});
|
| 1160 |
+
|
| 1161 |
+
// Reset file input so the same file can be re-uploaded
|
| 1162 |
+
input.value = '';
|
| 1163 |
+
}
|
| 1164 |
+
}
|
| 1165 |
+
|
| 1166 |
+
/**
|
| 1167 |
+
* Force-refresh an MJPEG stream by setting a new src with a cache-buster.
|
| 1168 |
+
* This drops the old HTTP connection and establishes a fresh one.
|
| 1169 |
+
*/
|
| 1170 |
+
function refreshFeedStream(feedId) {
|
| 1171 |
+
const img = document.getElementById('stream-' + feedId);
|
| 1172 |
+
if (img) {
|
| 1173 |
+
// Brief blank to visually signal the switch
|
| 1174 |
+
img.src = '';
|
| 1175 |
+
// Small delay lets the backend fully initialize the new feed
|
| 1176 |
+
setTimeout(() => {
|
| 1177 |
+
img.src = '/video_feed/' + feedId + '?t=' + Date.now();
|
| 1178 |
+
}, 300);
|
| 1179 |
+
}
|
| 1180 |
+
}
|
| 1181 |
+
|
| 1182 |
+
// ─── Grid Layout ───
|
| 1183 |
+
function setGridLayout(layout) {
|
| 1184 |
+
const grid = document.getElementById('camera-grid');
|
| 1185 |
+
currentLayout = layout;
|
| 1186 |
+
|
| 1187 |
+
if (layout === 'single') {
|
| 1188 |
+
grid.classList.add('single-view');
|
| 1189 |
+
// Show only the expanded feed
|
| 1190 |
+
document.querySelectorAll('.feed-cell').forEach((cell, i) => {
|
| 1191 |
+
cell.classList.toggle('expanded', i === expandedFeed);
|
| 1192 |
+
});
|
| 1193 |
+
} else {
|
| 1194 |
+
grid.classList.remove('single-view');
|
| 1195 |
+
document.querySelectorAll('.feed-cell').forEach(cell => {
|
| 1196 |
+
cell.classList.remove('expanded');
|
| 1197 |
+
});
|
| 1198 |
+
}
|
| 1199 |
+
}
|
| 1200 |
+
|
| 1201 |
+
function expandFeed(feedId) {
|
| 1202 |
+
expandedFeed = feedId;
|
| 1203 |
+
if (currentLayout === 'single') {
|
| 1204 |
+
document.querySelectorAll('.feed-cell').forEach((cell, i) => {
|
| 1205 |
+
cell.classList.toggle('expanded', i === feedId);
|
| 1206 |
+
});
|
| 1207 |
+
}
|
| 1208 |
+
}
|
| 1209 |
+
|
| 1210 |
+
// ─── Stats & Red Alert Updates ───
|
| 1211 |
+
function updateStats() {
|
| 1212 |
+
fetch('/stats')
|
| 1213 |
+
.then(r => r.json())
|
| 1214 |
+
.then(data => {
|
| 1215 |
+
const score = data.threat_score;
|
| 1216 |
+
const scoreEl = document.getElementById('threat-score');
|
| 1217 |
+
const statusEl = document.getElementById('status-text');
|
| 1218 |
+
const ringFill = document.getElementById('score-ring-fill');
|
| 1219 |
+
const threatCard = document.getElementById('threat-card');
|
| 1220 |
+
|
| 1221 |
+
scoreEl.textContent = score;
|
| 1222 |
+
|
| 1223 |
+
// Compute ring dash offset (circumference = 2 * π * 60 ≈ 377)
|
| 1224 |
+
const circumference = 377;
|
| 1225 |
+
const offset = circumference - (circumference * score / 100);
|
| 1226 |
+
ringFill.style.strokeDashoffset = offset;
|
| 1227 |
+
|
| 1228 |
+
// Color based on score
|
| 1229 |
+
let color, status, glow;
|
| 1230 |
+
if (score >= 80) {
|
| 1231 |
+
color = '#ff2040';
|
| 1232 |
+
status = 'CRITICAL';
|
| 1233 |
+
glow = 'rgba(255, 32, 64, 0.4)';
|
| 1234 |
+
} else if (score >= 50) {
|
| 1235 |
+
color = '#ffaa00';
|
| 1236 |
+
status = 'ELEVATED';
|
| 1237 |
+
glow = 'rgba(255, 170, 0, 0.3)';
|
| 1238 |
+
} else if (score >= 25) {
|
| 1239 |
+
color = '#00d4ff';
|
| 1240 |
+
status = 'GUARDED';
|
| 1241 |
+
glow = 'rgba(0, 200, 255, 0.3)';
|
| 1242 |
+
} else {
|
| 1243 |
+
color = '#00ff88';
|
| 1244 |
+
status = 'SECURE';
|
| 1245 |
+
glow = 'rgba(0, 255, 136, 0.3)';
|
| 1246 |
+
}
|
| 1247 |
+
|
| 1248 |
+
statusEl.textContent = status;
|
| 1249 |
+
statusEl.style.color = color;
|
| 1250 |
+
statusEl.style.textShadow = `0 0 20px ${glow}`;
|
| 1251 |
+
ringFill.style.stroke = color;
|
| 1252 |
+
ringFill.style.filter = `drop-shadow(0 0 8px ${glow})`;
|
| 1253 |
+
|
| 1254 |
+
// Red Alert state
|
| 1255 |
+
const alertOverlay = document.getElementById('red-alert-overlay');
|
| 1256 |
+
const alertBanner = document.getElementById('red-alert-banner');
|
| 1257 |
+
const feedCells = document.querySelectorAll('.feed-cell');
|
| 1258 |
+
|
| 1259 |
+
if (data.red_alert) {
|
| 1260 |
+
alertOverlay.classList.add('active');
|
| 1261 |
+
alertBanner.classList.add('active');
|
| 1262 |
+
feedCells.forEach(cell => cell.classList.add('red-alert-active'));
|
| 1263 |
+
threatCard.style.borderColor = 'rgba(255, 32, 64, 0.5)';
|
| 1264 |
+
|
| 1265 |
+
if (!isRedAlert) {
|
| 1266 |
+
playAlertTone();
|
| 1267 |
+
isRedAlert = true;
|
| 1268 |
+
}
|
| 1269 |
+
} else {
|
| 1270 |
+
alertOverlay.classList.remove('active');
|
| 1271 |
+
alertBanner.classList.remove('active');
|
| 1272 |
+
feedCells.forEach(cell => cell.classList.remove('red-alert-active'));
|
| 1273 |
+
threatCard.style.borderColor = '';
|
| 1274 |
+
isRedAlert = false;
|
| 1275 |
+
}
|
| 1276 |
+
|
| 1277 |
+
// Update mode display
|
| 1278 |
+
if (data.mode) {
|
| 1279 |
+
document.getElementById('mode-title').textContent = modeTitles[data.mode] || data.mode.toUpperCase();
|
| 1280 |
+
}
|
| 1281 |
+
|
| 1282 |
+
// Update live metrics
|
| 1283 |
+
const container = document.getElementById('stats-container');
|
| 1284 |
+
container.innerHTML = '';
|
| 1285 |
+
|
| 1286 |
+
if (!data.details || Object.keys(data.details).length === 0) {
|
| 1287 |
+
container.innerHTML = '<div class="stat-row"><span class="stat-name">System Status</span><span class="stat-val">Standby</span></div>';
|
| 1288 |
+
} else {
|
| 1289 |
+
for (const [key, value] of Object.entries(data.details)) {
|
| 1290 |
+
const div = document.createElement('div');
|
| 1291 |
+
div.className = 'stat-row';
|
| 1292 |
+
const formattedKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
| 1293 |
+
let displayVal = value;
|
| 1294 |
+
if (typeof value === 'boolean') {
|
| 1295 |
+
displayVal = value ? '⚠ YES' : 'No';
|
| 1296 |
+
}
|
| 1297 |
+
div.innerHTML = `<span class="stat-name">${formattedKey}</span><span class="stat-val">${displayVal}</span>`;
|
| 1298 |
+
container.appendChild(div);
|
| 1299 |
+
}
|
| 1300 |
+
}
|
| 1301 |
+
})
|
| 1302 |
+
.catch(() => { });
|
| 1303 |
+
}
|
| 1304 |
+
|
| 1305 |
+
// ─── Alert Tone (Web Audio API) ───
|
| 1306 |
+
function playAlertTone() {
|
| 1307 |
+
try {
|
| 1308 |
+
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
| 1309 |
+
const oscillator = audioCtx.createOscillator();
|
| 1310 |
+
const gainNode = audioCtx.createGain();
|
| 1311 |
+
|
| 1312 |
+
oscillator.connect(gainNode);
|
| 1313 |
+
gainNode.connect(audioCtx.destination);
|
| 1314 |
+
|
| 1315 |
+
oscillator.type = 'square';
|
| 1316 |
+
oscillator.frequency.setValueAtTime(880, audioCtx.currentTime);
|
| 1317 |
+
oscillator.frequency.setValueAtTime(660, audioCtx.currentTime + 0.15);
|
| 1318 |
+
oscillator.frequency.setValueAtTime(880, audioCtx.currentTime + 0.3);
|
| 1319 |
+
|
| 1320 |
+
gainNode.gain.setValueAtTime(0.08, audioCtx.currentTime);
|
| 1321 |
+
gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.5);
|
| 1322 |
+
|
| 1323 |
+
oscillator.start(audioCtx.currentTime);
|
| 1324 |
+
oscillator.stop(audioCtx.currentTime + 0.5);
|
| 1325 |
+
} catch (e) {
|
| 1326 |
+
// Audio not available — silent fallback
|
| 1327 |
+
}
|
| 1328 |
+
}
|
| 1329 |
+
|
| 1330 |
+
// ─── AI Report ───
|
| 1331 |
+
function generateReport() {
|
| 1332 |
+
const modal = document.getElementById('report-modal');
|
| 1333 |
+
modal.classList.add('show');
|
| 1334 |
+
document.getElementById('report-content').textContent = 'ANALYZING DATA STREAM...';
|
| 1335 |
+
|
| 1336 |
+
fetch('/generate_report', { method: 'POST' })
|
| 1337 |
+
.then(r => r.json())
|
| 1338 |
+
.then(data => {
|
| 1339 |
+
document.getElementById('report-content').textContent = data.report;
|
| 1340 |
+
})
|
| 1341 |
+
.catch(() => {
|
| 1342 |
+
document.getElementById('report-content').textContent = 'ERROR: Connection to AI Agent lost.';
|
| 1343 |
+
});
|
| 1344 |
+
}
|
| 1345 |
+
|
| 1346 |
+
function closeModal() {
|
| 1347 |
+
document.getElementById('report-modal').classList.remove('show');
|
| 1348 |
+
}
|
| 1349 |
+
|
| 1350 |
+
// ─── Audit Log Refresh ───
|
| 1351 |
+
function refreshAuditLog() {
|
| 1352 |
+
fetch('/audit_log')
|
| 1353 |
+
.then(r => r.json())
|
| 1354 |
+
.then(data => {
|
| 1355 |
+
const container = document.getElementById('audit-log-container');
|
| 1356 |
+
container.innerHTML = '';
|
| 1357 |
+
|
| 1358 |
+
if (data.log.length === 0) {
|
| 1359 |
+
container.innerHTML = '<div class="audit-entry"><div class="audit-time">--:--:--</div>NO ENTRIES</div>';
|
| 1360 |
+
return;
|
| 1361 |
+
}
|
| 1362 |
+
|
| 1363 |
+
data.log.slice(0, 30).forEach(entry => {
|
| 1364 |
+
const div = document.createElement('div');
|
| 1365 |
+
div.className = `audit-entry severity-${entry.severity}`;
|
| 1366 |
+
div.innerHTML = `
|
| 1367 |
+
<div class="audit-time">${entry.timestamp.split(' ')[1] || entry.timestamp}</div>
|
| 1368 |
+
${entry.action}: ${entry.details}
|
| 1369 |
+
`;
|
| 1370 |
+
container.appendChild(div);
|
| 1371 |
+
});
|
| 1372 |
+
})
|
| 1373 |
+
.catch(() => { });
|
| 1374 |
+
}
|
| 1375 |
+
|
| 1376 |
+
// ─── Intervals ───
|
| 1377 |
+
setInterval(updateStats, 1000);
|
| 1378 |
+
setInterval(refreshAuditLog, 5000);
|
| 1379 |
+
|
| 1380 |
+
// Initial load
|
| 1381 |
+
setTimeout(refreshAuditLog, 1500);
|
| 1382 |
+
</script>
|
| 1383 |
+
</body>
|
| 1384 |
+
|
| 1385 |
+
</html>
|
templates/sentinel_dashboard_v11.html
ADDED
|
@@ -0,0 +1,1413 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>SENTINEL V11 — Intelligence Grid</title>
|
| 8 |
+
<link
|
| 9 |
+
href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700;800;900&family=Rajdhani:wght@300;400;500;600;700&family=Share+Tech+Mono&display=swap"
|
| 10 |
+
rel="stylesheet">
|
| 11 |
+
<script src="https://unpkg.com/feather-icons"></script>
|
| 12 |
+
<style>
|
| 13 |
+
/* ═══════════════════════════════════════════════
|
| 14 |
+
CSS VARIABLES — SCI-FI PALETTE
|
| 15 |
+
═══════════════════════════════════════════════ */
|
| 16 |
+
:root {
|
| 17 |
+
--bg-deep: #05080f;
|
| 18 |
+
--bg-panel: rgba(8, 16, 32, 0.85);
|
| 19 |
+
--bg-card: rgba(10, 20, 40, 0.7);
|
| 20 |
+
--border-glow: rgba(0, 200, 255, 0.15);
|
| 21 |
+
--border-glow-hover: rgba(0, 200, 255, 0.4);
|
| 22 |
+
--cyan: #00d4ff;
|
| 23 |
+
--cyan-dim: #0090aa;
|
| 24 |
+
--neon-green: #00ff88;
|
| 25 |
+
--neon-red: #ff2040;
|
| 26 |
+
--neon-amber: #ffaa00;
|
| 27 |
+
--text-primary: #e0f0ff;
|
| 28 |
+
--text-secondary: #5a8aaa;
|
| 29 |
+
--text-dim: #2a4a5a;
|
| 30 |
+
--scanline-opacity: 0.03;
|
| 31 |
+
--font-display: 'Orbitron', sans-serif;
|
| 32 |
+
--font-body: 'Rajdhani', sans-serif;
|
| 33 |
+
--font-mono: 'Share Tech Mono', monospace;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/* ═══════════════════════════════════════════════
|
| 37 |
+
BASE RESET & BODY
|
| 38 |
+
═══════════════════════════════════════════════ */
|
| 39 |
+
*,
|
| 40 |
+
*::before,
|
| 41 |
+
*::after {
|
| 42 |
+
margin: 0;
|
| 43 |
+
padding: 0;
|
| 44 |
+
box-sizing: border-box;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
body {
|
| 48 |
+
font-family: var(--font-body);
|
| 49 |
+
background: var(--bg-deep);
|
| 50 |
+
color: var(--text-primary);
|
| 51 |
+
height: 100vh;
|
| 52 |
+
overflow: hidden;
|
| 53 |
+
display: flex;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/* Scanline overlay on body */
|
| 57 |
+
body::after {
|
| 58 |
+
content: '';
|
| 59 |
+
position: fixed;
|
| 60 |
+
top: 0;
|
| 61 |
+
left: 0;
|
| 62 |
+
right: 0;
|
| 63 |
+
bottom: 0;
|
| 64 |
+
background: repeating-linear-gradient(0deg,
|
| 65 |
+
transparent,
|
| 66 |
+
transparent 2px,
|
| 67 |
+
rgba(0, 200, 255, var(--scanline-opacity)) 2px,
|
| 68 |
+
rgba(0, 200, 255, var(--scanline-opacity)) 4px);
|
| 69 |
+
pointer-events: none;
|
| 70 |
+
z-index: 9999;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
/* ═══════════════════════════════════════════════
|
| 74 |
+
RED ALERT OVERLAY
|
| 75 |
+
═══════════════════════════════════════════════ */
|
| 76 |
+
.red-alert-overlay {
|
| 77 |
+
position: fixed;
|
| 78 |
+
top: 0;
|
| 79 |
+
left: 0;
|
| 80 |
+
right: 0;
|
| 81 |
+
bottom: 0;
|
| 82 |
+
pointer-events: none;
|
| 83 |
+
z-index: 9998;
|
| 84 |
+
opacity: 0;
|
| 85 |
+
transition: opacity 0.3s ease;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.red-alert-overlay.active {
|
| 89 |
+
opacity: 1;
|
| 90 |
+
animation: red-pulse-border 1s ease-in-out infinite;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.red-alert-overlay.active::before {
|
| 94 |
+
content: '';
|
| 95 |
+
position: absolute;
|
| 96 |
+
top: 0;
|
| 97 |
+
left: 0;
|
| 98 |
+
right: 0;
|
| 99 |
+
bottom: 0;
|
| 100 |
+
border: 4px solid var(--neon-red);
|
| 101 |
+
box-shadow: inset 0 0 80px rgba(255, 32, 64, 0.15),
|
| 102 |
+
0 0 40px rgba(255, 32, 64, 0.3);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.red-alert-banner {
|
| 106 |
+
position: fixed;
|
| 107 |
+
top: 0;
|
| 108 |
+
left: 50%;
|
| 109 |
+
transform: translateX(-50%);
|
| 110 |
+
background: rgba(255, 20, 40, 0.9);
|
| 111 |
+
color: #fff;
|
| 112 |
+
font-family: var(--font-display);
|
| 113 |
+
font-size: 0.85rem;
|
| 114 |
+
font-weight: 700;
|
| 115 |
+
letter-spacing: 4px;
|
| 116 |
+
padding: 8px 40px;
|
| 117 |
+
z-index: 10000;
|
| 118 |
+
border-bottom-left-radius: 8px;
|
| 119 |
+
border-bottom-right-radius: 8px;
|
| 120 |
+
box-shadow: 0 4px 30px rgba(255, 20, 40, 0.5);
|
| 121 |
+
display: none;
|
| 122 |
+
animation: alert-flash 0.8s ease-in-out infinite;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.red-alert-banner.active {
|
| 126 |
+
display: block;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
@keyframes red-pulse-border {
|
| 130 |
+
|
| 131 |
+
0%,
|
| 132 |
+
100% {
|
| 133 |
+
opacity: 0.6;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
50% {
|
| 137 |
+
opacity: 1;
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
@keyframes alert-flash {
|
| 142 |
+
|
| 143 |
+
0%,
|
| 144 |
+
100% {
|
| 145 |
+
opacity: 1;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
50% {
|
| 149 |
+
opacity: 0.6;
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
/* ═══════════════════════════════════════════════
|
| 154 |
+
SIDEBAR
|
| 155 |
+
═══════════════════════════════════════════════ */
|
| 156 |
+
.sidebar {
|
| 157 |
+
width: 260px;
|
| 158 |
+
min-width: 260px;
|
| 159 |
+
background: var(--bg-panel);
|
| 160 |
+
backdrop-filter: blur(20px);
|
| 161 |
+
-webkit-backdrop-filter: blur(20px);
|
| 162 |
+
border-right: 1px solid var(--border-glow);
|
| 163 |
+
display: flex;
|
| 164 |
+
flex-direction: column;
|
| 165 |
+
padding: 20px;
|
| 166 |
+
z-index: 100;
|
| 167 |
+
overflow-y: auto;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.logo {
|
| 171 |
+
font-family: var(--font-display);
|
| 172 |
+
font-size: 1rem;
|
| 173 |
+
font-weight: 700;
|
| 174 |
+
margin-bottom: 8px;
|
| 175 |
+
display: flex;
|
| 176 |
+
align-items: center;
|
| 177 |
+
gap: 10px;
|
| 178 |
+
color: var(--cyan);
|
| 179 |
+
letter-spacing: 2px;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.logo svg {
|
| 183 |
+
filter: drop-shadow(0 0 6px var(--cyan));
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
.version-tag {
|
| 187 |
+
font-family: var(--font-mono);
|
| 188 |
+
font-size: 0.65rem;
|
| 189 |
+
color: var(--text-dim);
|
| 190 |
+
margin-bottom: 24px;
|
| 191 |
+
letter-spacing: 1px;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.nav-group {
|
| 195 |
+
margin-bottom: 20px;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.nav-label {
|
| 199 |
+
font-family: var(--font-display);
|
| 200 |
+
font-size: 0.6rem;
|
| 201 |
+
font-weight: 600;
|
| 202 |
+
color: var(--text-dim);
|
| 203 |
+
margin-bottom: 8px;
|
| 204 |
+
text-transform: uppercase;
|
| 205 |
+
letter-spacing: 2px;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
.nav-item {
|
| 209 |
+
padding: 10px 12px;
|
| 210 |
+
margin-bottom: 3px;
|
| 211 |
+
border-radius: 6px;
|
| 212 |
+
cursor: pointer;
|
| 213 |
+
transition: all 0.25s ease;
|
| 214 |
+
display: flex;
|
| 215 |
+
align-items: center;
|
| 216 |
+
gap: 10px;
|
| 217 |
+
color: var(--text-secondary);
|
| 218 |
+
font-size: 0.9rem;
|
| 219 |
+
font-weight: 500;
|
| 220 |
+
border: 1px solid transparent;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.nav-item:hover {
|
| 224 |
+
background: rgba(0, 200, 255, 0.05);
|
| 225 |
+
color: var(--text-primary);
|
| 226 |
+
border-color: var(--border-glow);
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.nav-item.active {
|
| 230 |
+
background: rgba(0, 200, 255, 0.1);
|
| 231 |
+
color: var(--cyan);
|
| 232 |
+
border-color: rgba(0, 200, 255, 0.3);
|
| 233 |
+
box-shadow: 0 0 15px rgba(0, 200, 255, 0.05);
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
.nav-item svg {
|
| 237 |
+
width: 16px;
|
| 238 |
+
height: 16px;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
/* Audit Log Panel in sidebar */
|
| 242 |
+
.audit-section {
|
| 243 |
+
margin-top: auto;
|
| 244 |
+
flex-shrink: 0;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
.audit-log-container {
|
| 248 |
+
max-height: 200px;
|
| 249 |
+
overflow-y: auto;
|
| 250 |
+
scrollbar-width: thin;
|
| 251 |
+
scrollbar-color: var(--cyan-dim) transparent;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
.audit-log-container::-webkit-scrollbar {
|
| 255 |
+
width: 4px;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.audit-log-container::-webkit-scrollbar-track {
|
| 259 |
+
background: transparent;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.audit-log-container::-webkit-scrollbar-thumb {
|
| 263 |
+
background: var(--cyan-dim);
|
| 264 |
+
border-radius: 2px;
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
.audit-entry {
|
| 268 |
+
font-family: var(--font-mono);
|
| 269 |
+
font-size: 0.65rem;
|
| 270 |
+
padding: 6px 8px;
|
| 271 |
+
margin-bottom: 2px;
|
| 272 |
+
border-radius: 4px;
|
| 273 |
+
background: rgba(0, 0, 0, 0.3);
|
| 274 |
+
border-left: 2px solid var(--cyan-dim);
|
| 275 |
+
line-height: 1.4;
|
| 276 |
+
color: var(--text-secondary);
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
.audit-entry.severity-WARNING {
|
| 280 |
+
border-left-color: var(--neon-amber);
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
.audit-entry.severity-CRITICAL {
|
| 284 |
+
border-left-color: var(--neon-red);
|
| 285 |
+
color: var(--neon-red);
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
.audit-time {
|
| 289 |
+
color: var(--text-dim);
|
| 290 |
+
font-size: 0.6rem;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
/* ═══════════════════════════════════════════════
|
| 294 |
+
MAIN CONTENT AREA
|
| 295 |
+
═══════════════════════════════════════════════ */
|
| 296 |
+
.main-content {
|
| 297 |
+
flex: 1;
|
| 298 |
+
padding: 16px;
|
| 299 |
+
display: grid;
|
| 300 |
+
grid-template-columns: 1fr 320px;
|
| 301 |
+
gap: 16px;
|
| 302 |
+
overflow: hidden;
|
| 303 |
+
background: radial-gradient(ellipse at 20% 0%, rgba(0, 60, 100, 0.15) 0%, transparent 60%),
|
| 304 |
+
radial-gradient(ellipse at 80% 100%, rgba(0, 40, 80, 0.1) 0%, transparent 60%),
|
| 305 |
+
var(--bg-deep);
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
/* ══════════════════════════════════════════��════
|
| 309 |
+
MULTI-CAMERA GRID
|
| 310 |
+
═══════════════════════════════════════════════ */
|
| 311 |
+
.camera-grid {
|
| 312 |
+
display: grid;
|
| 313 |
+
grid-template-columns: 1fr 1fr;
|
| 314 |
+
grid-template-rows: 1fr 1fr;
|
| 315 |
+
gap: 8px;
|
| 316 |
+
height: 100%;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
.camera-grid.single-view {
|
| 320 |
+
grid-template-columns: 1fr;
|
| 321 |
+
grid-template-rows: 1fr;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.camera-grid.single-view .feed-cell:not(.expanded) {
|
| 325 |
+
display: none;
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
.feed-cell {
|
| 329 |
+
background: var(--bg-card);
|
| 330 |
+
border-radius: 10px;
|
| 331 |
+
border: 1px solid var(--border-glow);
|
| 332 |
+
overflow: hidden;
|
| 333 |
+
position: relative;
|
| 334 |
+
cursor: pointer;
|
| 335 |
+
transition: all 0.3s ease;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.feed-cell:hover {
|
| 339 |
+
border-color: var(--border-glow-hover);
|
| 340 |
+
box-shadow: 0 0 20px rgba(0, 200, 255, 0.1);
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
.feed-cell.red-alert-active {
|
| 344 |
+
border-color: var(--neon-red) !important;
|
| 345 |
+
box-shadow: 0 0 30px rgba(255, 32, 64, 0.3) !important;
|
| 346 |
+
animation: cell-red-pulse 1.5s ease-in-out infinite;
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
@keyframes cell-red-pulse {
|
| 350 |
+
|
| 351 |
+
0%,
|
| 352 |
+
100% {
|
| 353 |
+
box-shadow: 0 0 20px rgba(255, 32, 64, 0.2);
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
50% {
|
| 357 |
+
box-shadow: 0 0 40px rgba(255, 32, 64, 0.5);
|
| 358 |
+
}
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
.feed-header {
|
| 362 |
+
position: absolute;
|
| 363 |
+
top: 8px;
|
| 364 |
+
left: 10px;
|
| 365 |
+
right: 10px;
|
| 366 |
+
display: flex;
|
| 367 |
+
justify-content: space-between;
|
| 368 |
+
align-items: center;
|
| 369 |
+
z-index: 5;
|
| 370 |
+
pointer-events: none;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
.feed-badge {
|
| 374 |
+
background: rgba(0, 0, 0, 0.7);
|
| 375 |
+
backdrop-filter: blur(8px);
|
| 376 |
+
padding: 4px 10px;
|
| 377 |
+
border-radius: 4px;
|
| 378 |
+
font-family: var(--font-mono);
|
| 379 |
+
font-size: 0.65rem;
|
| 380 |
+
font-weight: 400;
|
| 381 |
+
border: 1px solid var(--border-glow);
|
| 382 |
+
display: flex;
|
| 383 |
+
align-items: center;
|
| 384 |
+
gap: 6px;
|
| 385 |
+
color: var(--cyan);
|
| 386 |
+
letter-spacing: 1px;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
.live-dot {
|
| 390 |
+
width: 6px;
|
| 391 |
+
height: 6px;
|
| 392 |
+
background: var(--neon-red);
|
| 393 |
+
border-radius: 50%;
|
| 394 |
+
box-shadow: 0 0 8px var(--neon-red);
|
| 395 |
+
animation: pulse-dot 2s infinite;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
@keyframes pulse-dot {
|
| 399 |
+
|
| 400 |
+
0%,
|
| 401 |
+
100% {
|
| 402 |
+
opacity: 1;
|
| 403 |
+
transform: scale(1);
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
50% {
|
| 407 |
+
opacity: 0.4;
|
| 408 |
+
transform: scale(1.3);
|
| 409 |
+
}
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
.feed-status {
|
| 413 |
+
font-family: var(--font-mono);
|
| 414 |
+
font-size: 0.6rem;
|
| 415 |
+
color: var(--neon-green);
|
| 416 |
+
background: rgba(0, 0, 0, 0.6);
|
| 417 |
+
padding: 3px 8px;
|
| 418 |
+
border-radius: 4px;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
.feed-stream {
|
| 422 |
+
width: 100%;
|
| 423 |
+
height: 100%;
|
| 424 |
+
object-fit: contain;
|
| 425 |
+
background: #000;
|
| 426 |
+
display: block;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.feed-offline {
|
| 430 |
+
display: flex;
|
| 431 |
+
flex-direction: column;
|
| 432 |
+
align-items: center;
|
| 433 |
+
justify-content: center;
|
| 434 |
+
height: 100%;
|
| 435 |
+
color: var(--text-dim);
|
| 436 |
+
font-family: var(--font-mono);
|
| 437 |
+
font-size: 0.75rem;
|
| 438 |
+
letter-spacing: 1px;
|
| 439 |
+
gap: 8px;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
.feed-offline svg {
|
| 443 |
+
width: 24px;
|
| 444 |
+
height: 24px;
|
| 445 |
+
color: var(--text-dim);
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
/* Fullscreen expand button */
|
| 449 |
+
.expand-btn {
|
| 450 |
+
position: absolute;
|
| 451 |
+
bottom: 8px;
|
| 452 |
+
right: 8px;
|
| 453 |
+
background: rgba(0, 0, 0, 0.6);
|
| 454 |
+
border: 1px solid var(--border-glow);
|
| 455 |
+
color: var(--cyan);
|
| 456 |
+
width: 28px;
|
| 457 |
+
height: 28px;
|
| 458 |
+
border-radius: 4px;
|
| 459 |
+
cursor: pointer;
|
| 460 |
+
display: flex;
|
| 461 |
+
align-items: center;
|
| 462 |
+
justify-content: center;
|
| 463 |
+
transition: all 0.2s;
|
| 464 |
+
z-index: 5;
|
| 465 |
+
pointer-events: all;
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
.expand-btn:hover {
|
| 469 |
+
background: rgba(0, 200, 255, 0.15);
|
| 470 |
+
border-color: var(--cyan);
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
.expand-btn svg {
|
| 474 |
+
width: 14px;
|
| 475 |
+
height: 14px;
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
/* ═══════════════════════════════════════════════
|
| 479 |
+
INTEL PANEL (Right Column)
|
| 480 |
+
═══════════════════════════════════════════════ */
|
| 481 |
+
.intel-panel {
|
| 482 |
+
display: flex;
|
| 483 |
+
flex-direction: column;
|
| 484 |
+
gap: 12px;
|
| 485 |
+
overflow-y: auto;
|
| 486 |
+
scrollbar-width: thin;
|
| 487 |
+
scrollbar-color: var(--cyan-dim) transparent;
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
.intel-panel::-webkit-scrollbar {
|
| 491 |
+
width: 4px;
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
.intel-panel::-webkit-scrollbar-thumb {
|
| 495 |
+
background: var(--cyan-dim);
|
| 496 |
+
border-radius: 2px;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
.card {
|
| 500 |
+
background: var(--bg-card);
|
| 501 |
+
backdrop-filter: blur(15px);
|
| 502 |
+
border-radius: 10px;
|
| 503 |
+
padding: 18px;
|
| 504 |
+
border: 1px solid var(--border-glow);
|
| 505 |
+
transition: border-color 0.3s;
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
.card:hover {
|
| 509 |
+
border-color: var(--border-glow-hover);
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
.card-header {
|
| 513 |
+
display: flex;
|
| 514 |
+
justify-content: space-between;
|
| 515 |
+
align-items: center;
|
| 516 |
+
margin-bottom: 14px;
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
.card-title {
|
| 520 |
+
font-family: var(--font-display);
|
| 521 |
+
font-size: 0.65rem;
|
| 522 |
+
font-weight: 600;
|
| 523 |
+
color: var(--cyan);
|
| 524 |
+
text-transform: uppercase;
|
| 525 |
+
letter-spacing: 2px;
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
.card-icon {
|
| 529 |
+
color: var(--text-dim);
|
| 530 |
+
width: 16px;
|
| 531 |
+
height: 16px;
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
/* Threat gauge */
|
| 535 |
+
.threat-gauge {
|
| 536 |
+
display: flex;
|
| 537 |
+
flex-direction: column;
|
| 538 |
+
align-items: center;
|
| 539 |
+
padding: 10px 0;
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
.score-ring {
|
| 543 |
+
position: relative;
|
| 544 |
+
width: 130px;
|
| 545 |
+
height: 130px;
|
| 546 |
+
margin-bottom: 12px;
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
.score-ring svg {
|
| 550 |
+
width: 130px;
|
| 551 |
+
height: 130px;
|
| 552 |
+
transform: rotate(-90deg);
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
.score-ring-bg {
|
| 556 |
+
fill: none;
|
| 557 |
+
stroke: rgba(255, 255, 255, 0.05);
|
| 558 |
+
stroke-width: 6;
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
.score-ring-fill {
|
| 562 |
+
fill: none;
|
| 563 |
+
stroke: var(--neon-green);
|
| 564 |
+
stroke-width: 6;
|
| 565 |
+
stroke-linecap: round;
|
| 566 |
+
stroke-dasharray: 377;
|
| 567 |
+
stroke-dashoffset: 377;
|
| 568 |
+
transition: stroke-dashoffset 0.8s cubic-bezier(0.4, 0, 0.2, 1),
|
| 569 |
+
stroke 0.5s ease;
|
| 570 |
+
filter: drop-shadow(0 0 8px var(--neon-green));
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
.score-text {
|
| 574 |
+
position: absolute;
|
| 575 |
+
top: 50%;
|
| 576 |
+
left: 50%;
|
| 577 |
+
transform: translate(-50%, -50%);
|
| 578 |
+
text-align: center;
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
.score-value {
|
| 582 |
+
font-family: var(--font-display);
|
| 583 |
+
font-size: 2.2rem;
|
| 584 |
+
font-weight: 800;
|
| 585 |
+
line-height: 1;
|
| 586 |
+
color: var(--text-primary);
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
.score-label {
|
| 590 |
+
font-family: var(--font-mono);
|
| 591 |
+
font-size: 0.55rem;
|
| 592 |
+
color: var(--text-dim);
|
| 593 |
+
letter-spacing: 2px;
|
| 594 |
+
margin-top: 4px;
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
.status-text {
|
| 598 |
+
font-family: var(--font-display);
|
| 599 |
+
font-size: 0.85rem;
|
| 600 |
+
font-weight: 700;
|
| 601 |
+
color: var(--neon-green);
|
| 602 |
+
letter-spacing: 3px;
|
| 603 |
+
text-shadow: 0 0 20px rgba(0, 255, 136, 0.3);
|
| 604 |
+
transition: all 0.5s ease;
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
/* Stats */
|
| 608 |
+
.stats-list {
|
| 609 |
+
display: flex;
|
| 610 |
+
flex-direction: column;
|
| 611 |
+
gap: 6px;
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
.stat-row {
|
| 615 |
+
display: flex;
|
| 616 |
+
justify-content: space-between;
|
| 617 |
+
align-items: center;
|
| 618 |
+
padding: 10px 12px;
|
| 619 |
+
background: rgba(0, 200, 255, 0.03);
|
| 620 |
+
border-radius: 6px;
|
| 621 |
+
border: 1px solid rgba(0, 200, 255, 0.05);
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
.stat-name {
|
| 625 |
+
font-size: 0.8rem;
|
| 626 |
+
color: var(--text-secondary);
|
| 627 |
+
font-weight: 500;
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
.stat-val {
|
| 631 |
+
font-family: var(--font-mono);
|
| 632 |
+
font-size: 0.85rem;
|
| 633 |
+
font-weight: 600;
|
| 634 |
+
color: var(--text-primary);
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
/* Mode indicator */
|
| 638 |
+
.mode-indicator {
|
| 639 |
+
display: flex;
|
| 640 |
+
align-items: center;
|
| 641 |
+
gap: 8px;
|
| 642 |
+
padding: 10px 14px;
|
| 643 |
+
background: rgba(0, 200, 255, 0.05);
|
| 644 |
+
border-radius: 6px;
|
| 645 |
+
border: 1px solid var(--border-glow);
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
.mode-dot {
|
| 649 |
+
width: 8px;
|
| 650 |
+
height: 8px;
|
| 651 |
+
background: var(--cyan);
|
| 652 |
+
border-radius: 50%;
|
| 653 |
+
box-shadow: 0 0 10px var(--cyan);
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
.mode-name {
|
| 657 |
+
font-family: var(--font-display);
|
| 658 |
+
font-size: 0.7rem;
|
| 659 |
+
font-weight: 600;
|
| 660 |
+
letter-spacing: 2px;
|
| 661 |
+
color: var(--cyan);
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
/* Buttons */
|
| 665 |
+
.btn {
|
| 666 |
+
width: 100%;
|
| 667 |
+
padding: 12px;
|
| 668 |
+
border: 1px solid var(--border-glow);
|
| 669 |
+
border-radius: 6px;
|
| 670 |
+
font-size: 0.8rem;
|
| 671 |
+
font-weight: 600;
|
| 672 |
+
cursor: pointer;
|
| 673 |
+
transition: all 0.25s;
|
| 674 |
+
font-family: var(--font-display);
|
| 675 |
+
letter-spacing: 1px;
|
| 676 |
+
text-transform: uppercase;
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
.btn-primary {
|
| 680 |
+
background: linear-gradient(135deg, rgba(0, 200, 255, 0.2), rgba(0, 100, 200, 0.1));
|
| 681 |
+
color: var(--cyan);
|
| 682 |
+
border-color: rgba(0, 200, 255, 0.3);
|
| 683 |
+
}
|
| 684 |
+
|
| 685 |
+
.btn-primary:hover {
|
| 686 |
+
background: linear-gradient(135deg, rgba(0, 200, 255, 0.3), rgba(0, 100, 200, 0.2));
|
| 687 |
+
box-shadow: 0 0 20px rgba(0, 200, 255, 0.15);
|
| 688 |
+
transform: translateY(-1px);
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
+
.btn-secondary {
|
| 692 |
+
background: rgba(255, 255, 255, 0.03);
|
| 693 |
+
color: var(--text-secondary);
|
| 694 |
+
}
|
| 695 |
+
|
| 696 |
+
.btn-secondary:hover {
|
| 697 |
+
background: rgba(255, 255, 255, 0.08);
|
| 698 |
+
color: var(--text-primary);
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
/* ═══════════════════════════════════════════════
|
| 702 |
+
MODAL
|
| 703 |
+
═══════════════════════════════════════════════ */
|
| 704 |
+
.modal-overlay {
|
| 705 |
+
position: fixed;
|
| 706 |
+
top: 0;
|
| 707 |
+
left: 0;
|
| 708 |
+
right: 0;
|
| 709 |
+
bottom: 0;
|
| 710 |
+
background: rgba(0, 0, 0, 0.8);
|
| 711 |
+
backdrop-filter: blur(10px);
|
| 712 |
+
display: none;
|
| 713 |
+
justify-content: center;
|
| 714 |
+
align-items: center;
|
| 715 |
+
z-index: 11000;
|
| 716 |
+
opacity: 0;
|
| 717 |
+
transition: opacity 0.3s ease;
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
.modal-overlay.show {
|
| 721 |
+
display: flex;
|
| 722 |
+
opacity: 1;
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
.modal-card {
|
| 726 |
+
background: var(--bg-panel);
|
| 727 |
+
width: 520px;
|
| 728 |
+
max-width: 90vw;
|
| 729 |
+
border-radius: 12px;
|
| 730 |
+
padding: 28px;
|
| 731 |
+
border: 1px solid var(--border-glow);
|
| 732 |
+
box-shadow: 0 0 60px rgba(0, 200, 255, 0.1);
|
| 733 |
+
transform: scale(0.95);
|
| 734 |
+
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
+
.modal-overlay.show .modal-card {
|
| 738 |
+
transform: scale(1);
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
.modal-title {
|
| 742 |
+
font-family: var(--font-display);
|
| 743 |
+
font-size: 1rem;
|
| 744 |
+
font-weight: 700;
|
| 745 |
+
color: var(--cyan);
|
| 746 |
+
letter-spacing: 2px;
|
| 747 |
+
margin-bottom: 6px;
|
| 748 |
+
}
|
| 749 |
+
|
| 750 |
+
.modal-subtitle {
|
| 751 |
+
font-family: var(--font-mono);
|
| 752 |
+
font-size: 0.7rem;
|
| 753 |
+
color: var(--text-dim);
|
| 754 |
+
margin-bottom: 16px;
|
| 755 |
+
}
|
| 756 |
+
|
| 757 |
+
.report-content {
|
| 758 |
+
background: rgba(0, 0, 0, 0.5);
|
| 759 |
+
padding: 16px;
|
| 760 |
+
border-radius: 8px;
|
| 761 |
+
font-family: var(--font-mono);
|
| 762 |
+
font-size: 0.75rem;
|
| 763 |
+
color: var(--neon-green);
|
| 764 |
+
margin-bottom: 16px;
|
| 765 |
+
line-height: 1.6;
|
| 766 |
+
border: 1px solid var(--border-glow);
|
| 767 |
+
max-height: 300px;
|
| 768 |
+
overflow-y: auto;
|
| 769 |
+
white-space: pre-wrap;
|
| 770 |
+
}
|
| 771 |
+
|
| 772 |
+
/* ═══════════════════════════════════════════════
|
| 773 |
+
MOBILE RESPONSIVE
|
| 774 |
+
═══════════════════════════════════════════════ */
|
| 775 |
+
@media (max-width: 1024px) {
|
| 776 |
+
.main-content {
|
| 777 |
+
grid-template-columns: 1fr 280px;
|
| 778 |
+
}
|
| 779 |
+
}
|
| 780 |
+
|
| 781 |
+
@media (max-width: 768px) {
|
| 782 |
+
body {
|
| 783 |
+
flex-direction: column;
|
| 784 |
+
overflow-y: auto;
|
| 785 |
+
}
|
| 786 |
+
|
| 787 |
+
.sidebar {
|
| 788 |
+
width: 100%;
|
| 789 |
+
min-width: 100%;
|
| 790 |
+
flex-direction: row;
|
| 791 |
+
flex-wrap: wrap;
|
| 792 |
+
padding: 10px 16px;
|
| 793 |
+
gap: 6px;
|
| 794 |
+
order: 2;
|
| 795 |
+
border-right: none;
|
| 796 |
+
border-top: 1px solid var(--border-glow);
|
| 797 |
+
overflow-y: visible;
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
.logo {
|
| 801 |
+
width: 100%;
|
| 802 |
+
margin-bottom: 6px;
|
| 803 |
+
font-size: 0.85rem;
|
| 804 |
+
}
|
| 805 |
+
|
| 806 |
+
.version-tag {
|
| 807 |
+
display: none;
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
+
.nav-group {
|
| 811 |
+
display: flex;
|
| 812 |
+
flex-wrap: wrap;
|
| 813 |
+
gap: 4px;
|
| 814 |
+
margin-bottom: 0;
|
| 815 |
+
}
|
| 816 |
+
|
| 817 |
+
.nav-label {
|
| 818 |
+
width: 100%;
|
| 819 |
+
margin-bottom: 4px;
|
| 820 |
+
}
|
| 821 |
+
|
| 822 |
+
.nav-item {
|
| 823 |
+
flex: none;
|
| 824 |
+
padding: 8px 10px;
|
| 825 |
+
font-size: 0.75rem;
|
| 826 |
+
}
|
| 827 |
+
|
| 828 |
+
.audit-section {
|
| 829 |
+
display: none;
|
| 830 |
+
}
|
| 831 |
+
|
| 832 |
+
.main-content {
|
| 833 |
+
grid-template-columns: 1fr;
|
| 834 |
+
grid-template-rows: auto auto;
|
| 835 |
+
order: 1;
|
| 836 |
+
overflow-y: auto;
|
| 837 |
+
padding: 10px;
|
| 838 |
+
}
|
| 839 |
+
|
| 840 |
+
.camera-grid {
|
| 841 |
+
grid-template-columns: 1fr;
|
| 842 |
+
grid-template-rows: auto;
|
| 843 |
+
min-height: 250px;
|
| 844 |
+
}
|
| 845 |
+
|
| 846 |
+
.feed-cell:not(:first-child) {
|
| 847 |
+
display: none;
|
| 848 |
+
}
|
| 849 |
+
|
| 850 |
+
.intel-panel {
|
| 851 |
+
overflow-y: visible;
|
| 852 |
+
}
|
| 853 |
+
}
|
| 854 |
+
|
| 855 |
+
@media (max-width: 480px) {
|
| 856 |
+
.sidebar .nav-item {
|
| 857 |
+
font-size: 0.7rem;
|
| 858 |
+
padding: 6px 8px;
|
| 859 |
+
gap: 6px;
|
| 860 |
+
}
|
| 861 |
+
|
| 862 |
+
.sidebar .nav-item svg {
|
| 863 |
+
width: 14px;
|
| 864 |
+
height: 14px;
|
| 865 |
+
}
|
| 866 |
+
|
| 867 |
+
.score-ring {
|
| 868 |
+
width: 100px;
|
| 869 |
+
height: 100px;
|
| 870 |
+
}
|
| 871 |
+
|
| 872 |
+
.score-ring svg {
|
| 873 |
+
width: 100px;
|
| 874 |
+
height: 100px;
|
| 875 |
+
}
|
| 876 |
+
|
| 877 |
+
.score-value {
|
| 878 |
+
font-size: 1.6rem;
|
| 879 |
+
}
|
| 880 |
+
}
|
| 881 |
+
</style>
|
| 882 |
+
</head>
|
| 883 |
+
|
| 884 |
+
<body>
|
| 885 |
+
<!-- Red Alert Overlay -->
|
| 886 |
+
<div class="red-alert-overlay" id="red-alert-overlay"></div>
|
| 887 |
+
<div class="red-alert-banner" id="red-alert-banner">
|
| 888 |
+
⚠ RED ALERT — CRITICAL THREAT DETECTED ⚠
|
| 889 |
+
<button class="btn btn-secondary" onclick="resetSystem()"
|
| 890 |
+
style="width: auto; margin-top: 10px; background: rgba(0,0,0,0.5); border: 1px solid #ff2040; color: #ff2040;">RESET
|
| 891 |
+
SYSTEM</button>
|
| 892 |
+
</div>
|
| 893 |
+
|
| 894 |
+
<!-- SIDEBAR -->
|
| 895 |
+
<div class="sidebar">
|
| 896 |
+
<div class="logo">
|
| 897 |
+
<i data-feather="shield"></i>
|
| 898 |
+
SENTINEL
|
| 899 |
+
</div>
|
| 900 |
+
<div class="version-tag">V11.0 // MULTI-MODULE</div>
|
| 901 |
+
|
| 902 |
+
<div class="nav-group">
|
| 903 |
+
<div class="nav-label">Detection Modules</div>
|
| 904 |
+
<div class="nav-item active" onclick="toggleModule('movement', this)" id="nav-movement">
|
| 905 |
+
<i data-feather="activity"></i> Movement
|
| 906 |
+
</div>
|
| 907 |
+
<div class="nav-item" onclick="toggleModule('facemask', this)" id="nav-facemask">
|
| 908 |
+
<i data-feather="eye"></i> Facemask
|
| 909 |
+
</div>
|
| 910 |
+
<div class="nav-item" onclick="toggleModule('weapon', this)" id="nav-weapon">
|
| 911 |
+
<i data-feather="crosshair"></i> Weapon
|
| 912 |
+
</div>
|
| 913 |
+
<div class="nav-item" onclick="toggleModule('public_safety', this)" id="nav-public_safety">
|
| 914 |
+
<i data-feather="users"></i> Public Safety
|
| 915 |
+
</div>
|
| 916 |
+
</div>
|
| 917 |
+
|
| 918 |
+
<div class="nav-group">
|
| 919 |
+
<div class="nav-label">Input Source</div>
|
| 920 |
+
<div class="nav-item" onclick="setSource('camera')">
|
| 921 |
+
<i data-feather="video"></i> Live Camera
|
| 922 |
+
</div>
|
| 923 |
+
<div class="nav-item" onclick="document.getElementById('upload-input').click()">
|
| 924 |
+
<i data-feather="upload-cloud"></i> Upload Video
|
| 925 |
+
</div>
|
| 926 |
+
<input type="file" id="upload-input" style="display: none" accept="video/*"
|
| 927 |
+
onchange="handleFileUpload(this)">
|
| 928 |
+
</div>
|
| 929 |
+
|
| 930 |
+
<div class="nav-group">
|
| 931 |
+
<div class="nav-label">Grid Layout</div>
|
| 932 |
+
<div class="nav-item" onclick="setGridLayout('quad')">
|
| 933 |
+
<i data-feather="grid"></i> 2×2 Grid
|
| 934 |
+
</div>
|
| 935 |
+
<div class="nav-item" onclick="setGridLayout('single')">
|
| 936 |
+
<i data-feather="maximize-2"></i> Single View
|
| 937 |
+
</div>
|
| 938 |
+
</div>
|
| 939 |
+
|
| 940 |
+
<!-- Audit Log -->
|
| 941 |
+
<div class="audit-section">
|
| 942 |
+
<div class="nav-label" style="margin-bottom: 8px;">Operator Log</div>
|
| 943 |
+
<div class="audit-log-container" id="audit-log-container">
|
| 944 |
+
<div class="audit-entry">
|
| 945 |
+
<div class="audit-time">--:--:--</div>
|
| 946 |
+
SYSTEM STANDBY
|
| 947 |
+
</div>
|
| 948 |
+
</div>
|
| 949 |
+
</div>
|
| 950 |
+
</div>
|
| 951 |
+
|
| 952 |
+
<!-- MAIN CONTENT -->
|
| 953 |
+
<div class="main-content">
|
| 954 |
+
<!-- Multi-Camera Grid -->
|
| 955 |
+
<div class="camera-grid" id="camera-grid">
|
| 956 |
+
<!-- Feed 0 — Primary -->
|
| 957 |
+
<div class="feed-cell expanded" id="feed-0" onclick="expandFeed(0)">
|
| 958 |
+
<div class="feed-header">
|
| 959 |
+
<div class="feed-badge">
|
| 960 |
+
<div class="live-dot"></div>
|
| 961 |
+
<span>FEED 01 // PRIMARY</span>
|
| 962 |
+
</div>
|
| 963 |
+
<div class="feed-status" id="feed-0-status">ACTIVE</div>
|
| 964 |
+
</div>
|
| 965 |
+
<img class="feed-stream" id="stream-0" src="/video_feed/0" alt="Feed 0">
|
| 966 |
+
<button class="expand-btn" onclick="event.stopPropagation(); expandFeed(0)" title="Expand">
|
| 967 |
+
<i data-feather="maximize-2"></i>
|
| 968 |
+
</button>
|
| 969 |
+
</div>
|
| 970 |
+
|
| 971 |
+
<!-- Feed 1 -->
|
| 972 |
+
<div class="feed-cell" id="feed-1">
|
| 973 |
+
<div class="feed-header">
|
| 974 |
+
<div class="feed-badge">
|
| 975 |
+
<div class="live-dot" style="background: var(--text-dim); box-shadow: none;"></div>
|
| 976 |
+
<span>FEED 02</span>
|
| 977 |
+
</div>
|
| 978 |
+
<div class="feed-status" id="feed-1-status" style="color: var(--text-dim);">STANDBY</div>
|
| 979 |
+
</div>
|
| 980 |
+
<div class="feed-offline" id="offline-1">
|
| 981 |
+
<i data-feather="video-off"></i>
|
| 982 |
+
NO SIGNAL
|
| 983 |
+
</div>
|
| 984 |
+
</div>
|
| 985 |
+
|
| 986 |
+
<!-- Feed 2 -->
|
| 987 |
+
<div class="feed-cell" id="feed-2">
|
| 988 |
+
<div class="feed-header">
|
| 989 |
+
<div class="feed-badge">
|
| 990 |
+
<div class="live-dot" style="background: var(--text-dim); box-shadow: none;"></div>
|
| 991 |
+
<span>FEED 03</span>
|
| 992 |
+
</div>
|
| 993 |
+
<div class="feed-status" id="feed-2-status" style="color: var(--text-dim);">STANDBY</div>
|
| 994 |
+
</div>
|
| 995 |
+
<div class="feed-offline" id="offline-2">
|
| 996 |
+
<i data-feather="video-off"></i>
|
| 997 |
+
NO SIGNAL
|
| 998 |
+
</div>
|
| 999 |
+
</div>
|
| 1000 |
+
|
| 1001 |
+
<!-- Feed 3 -->
|
| 1002 |
+
<div class="feed-cell" id="feed-3">
|
| 1003 |
+
<div class="feed-header">
|
| 1004 |
+
<div class="feed-badge">
|
| 1005 |
+
<div class="live-dot" style="background: var(--text-dim); box-shadow: none;"></div>
|
| 1006 |
+
<span>FEED 04</span>
|
| 1007 |
+
</div>
|
| 1008 |
+
<div class="feed-status" id="feed-3-status" style="color: var(--text-dim);">STANDBY</div>
|
| 1009 |
+
</div>
|
| 1010 |
+
<div class="feed-offline" id="offline-3">
|
| 1011 |
+
<i data-feather="video-off"></i>
|
| 1012 |
+
NO SIGNAL
|
| 1013 |
+
</div>
|
| 1014 |
+
</div>
|
| 1015 |
+
</div>
|
| 1016 |
+
|
| 1017 |
+
<!-- Intel Panel -->
|
| 1018 |
+
<div class="intel-panel">
|
| 1019 |
+
<!-- Active Mode -->
|
| 1020 |
+
<div class="card">
|
| 1021 |
+
<div class="card-header">
|
| 1022 |
+
<div class="card-title">Active Mode</div>
|
| 1023 |
+
</div>
|
| 1024 |
+
<div class="mode-indicator">
|
| 1025 |
+
<div class="mode-dot"></div>
|
| 1026 |
+
<div class="mode-name" id="mode-title">1 MODULE ACTIVE</div>
|
| 1027 |
+
</div>
|
| 1028 |
+
</div>
|
| 1029 |
+
|
| 1030 |
+
<!-- Threat Assessment -->
|
| 1031 |
+
<div class="card" id="threat-card">
|
| 1032 |
+
<div class="card-header">
|
| 1033 |
+
<div class="card-title">Threat Assessment</div>
|
| 1034 |
+
<i data-feather="alert-triangle" class="card-icon"></i>
|
| 1035 |
+
</div>
|
| 1036 |
+
<div class="threat-gauge">
|
| 1037 |
+
<div class="score-ring">
|
| 1038 |
+
<svg viewBox="0 0 130 130">
|
| 1039 |
+
<circle class="score-ring-bg" cx="65" cy="65" r="60"></circle>
|
| 1040 |
+
<circle class="score-ring-fill" id="score-ring-fill" cx="65" cy="65" r="60"></circle>
|
| 1041 |
+
</svg>
|
| 1042 |
+
<div class="score-text">
|
| 1043 |
+
<div class="score-value" id="threat-score">0</div>
|
| 1044 |
+
<div class="score-label">THREAT LEVEL</div>
|
| 1045 |
+
</div>
|
| 1046 |
+
</div>
|
| 1047 |
+
<div class="status-text" id="status-text">SECURE</div>
|
| 1048 |
+
</div>
|
| 1049 |
+
</div>
|
| 1050 |
+
|
| 1051 |
+
<!-- Live Metrics -->
|
| 1052 |
+
<div class="card">
|
| 1053 |
+
<div class="card-header">
|
| 1054 |
+
<div class="card-title">Live Metrics</div>
|
| 1055 |
+
<i data-feather="bar-chart-2" class="card-icon"></i>
|
| 1056 |
+
</div>
|
| 1057 |
+
<div class="stats-list" id="stats-container">
|
| 1058 |
+
<div class="stat-row">
|
| 1059 |
+
<span class="stat-name">System Status</span>
|
| 1060 |
+
<span class="stat-val">Initializing</span>
|
| 1061 |
+
</div>
|
| 1062 |
+
</div>
|
| 1063 |
+
</div>
|
| 1064 |
+
|
| 1065 |
+
<!-- Actions -->
|
| 1066 |
+
<div class="card">
|
| 1067 |
+
<div class="card-header">
|
| 1068 |
+
<div class="card-title">Actions</div>
|
| 1069 |
+
</div>
|
| 1070 |
+
<button class="btn btn-primary" onclick="generateReport()">Generate Incident Report</button>
|
| 1071 |
+
<button class="btn btn-secondary" onclick="refreshAuditLog()" style="margin-top: 8px;">Refresh Audit
|
| 1072 |
+
Log</button>
|
| 1073 |
+
<button class="btn btn-secondary" onclick="resetSystem()"
|
| 1074 |
+
style="margin-top: 8px; border-color: #ff2040; color: #ff2040;">⚠ SYSTEM RESET</button>
|
| 1075 |
+
</div>
|
| 1076 |
+
</div>
|
| 1077 |
+
</div>
|
| 1078 |
+
|
| 1079 |
+
<!-- Report Modal -->
|
| 1080 |
+
<div id="report-modal" class="modal-overlay">
|
| 1081 |
+
<div class="modal-card">
|
| 1082 |
+
<div class="modal-title">Incident Report</div>
|
| 1083 |
+
<div class="modal-subtitle">Generated by Sentinel AI Agent // Gemini 1.5</div>
|
| 1084 |
+
<div class="report-content" id="report-content">Analyzing data stream...</div>
|
| 1085 |
+
<button class="btn btn-secondary" onclick="closeModal()">Dismiss</button>
|
| 1086 |
+
</div>
|
| 1087 |
+
</div>
|
| 1088 |
+
|
| 1089 |
+
<!-- ════════════════════���══════════════════════════
|
| 1090 |
+
JAVASCRIPT
|
| 1091 |
+
═══════════════════════════════════════════════ -->
|
| 1092 |
+
<script>
|
| 1093 |
+
feather.replace();
|
| 1094 |
+
|
| 1095 |
+
// ─── State ───
|
| 1096 |
+
let currentLayout = 'quad'; // 'quad' or 'single'
|
| 1097 |
+
let expandedFeed = 0;
|
| 1098 |
+
let isRedAlert = false;
|
| 1099 |
+
|
| 1100 |
+
// ─── Module Toggling ───
|
| 1101 |
+
const modeTitles = {
|
| 1102 |
+
'movement': 'MOVEMENT ANALYSIS',
|
| 1103 |
+
'facemask': 'FACEMASK DETECTION',
|
| 1104 |
+
'weapon': 'WEAPON DETECTION',
|
| 1105 |
+
'public_safety': 'PUBLIC SAFETY'
|
| 1106 |
+
};
|
| 1107 |
+
|
| 1108 |
+
function toggleModule(module, buttonElement) {
|
| 1109 |
+
buttonElement.classList.toggle('active');
|
| 1110 |
+
|
| 1111 |
+
fetch('/toggle_module', {
|
| 1112 |
+
method: 'POST',
|
| 1113 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1114 |
+
body: JSON.stringify({ module: module })
|
| 1115 |
+
})
|
| 1116 |
+
.then(r => r.json())
|
| 1117 |
+
.then(data => {
|
| 1118 |
+
updateActiveModulesDisplay(data.active_modules);
|
| 1119 |
+
});
|
| 1120 |
+
}
|
| 1121 |
+
|
| 1122 |
+
function updateActiveModulesDisplay(activeModules) {
|
| 1123 |
+
const modeTitle = document.getElementById('mode-title');
|
| 1124 |
+
const count = activeModules.length;
|
| 1125 |
+
|
| 1126 |
+
if (count === 0) {
|
| 1127 |
+
modeTitle.textContent = 'NO MODULES ACTIVE';
|
| 1128 |
+
} else if (count === 1) {
|
| 1129 |
+
modeTitle.textContent = modeTitles[activeModules[0]] || activeModules[0].toUpperCase();
|
| 1130 |
+
} else {
|
| 1131 |
+
modeTitle.textContent = `${count} MODULES ACTIVE`;
|
| 1132 |
+
}
|
| 1133 |
+
}
|
| 1134 |
+
|
| 1135 |
+
|
| 1136 |
+
function setSource(source) {
|
| 1137 |
+
fetch('/set_source', {
|
| 1138 |
+
method: 'POST',
|
| 1139 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1140 |
+
body: JSON.stringify({ source: source })
|
| 1141 |
+
})
|
| 1142 |
+
.then(r => r.json())
|
| 1143 |
+
.then(data => {
|
| 1144 |
+
if (data.success) {
|
| 1145 |
+
// Force the browser to reconnect to the restarted MJPEG stream
|
| 1146 |
+
refreshFeedStream(0);
|
| 1147 |
+
}
|
| 1148 |
+
});
|
| 1149 |
+
}
|
| 1150 |
+
|
| 1151 |
+
function handleFileUpload(input) {
|
| 1152 |
+
if (input.files[0]) {
|
| 1153 |
+
const formData = new FormData();
|
| 1154 |
+
formData.append('file', input.files[0]);
|
| 1155 |
+
|
| 1156 |
+
// Show uploading state
|
| 1157 |
+
const statusEl = document.getElementById('feed-0-status');
|
| 1158 |
+
if (statusEl) statusEl.textContent = 'UPLOADING...';
|
| 1159 |
+
|
| 1160 |
+
fetch('/upload_video', { method: 'POST', body: formData })
|
| 1161 |
+
.then(r => r.json())
|
| 1162 |
+
.then(data => {
|
| 1163 |
+
if (data.success) {
|
| 1164 |
+
// Force the browser to reconnect to the restarted MJPEG stream
|
| 1165 |
+
refreshFeedStream(0);
|
| 1166 |
+
if (statusEl) statusEl.textContent = 'FILE FEED';
|
| 1167 |
+
}
|
| 1168 |
+
})
|
| 1169 |
+
.catch(() => {
|
| 1170 |
+
if (statusEl) statusEl.textContent = 'UPLOAD FAILED';
|
| 1171 |
+
});
|
| 1172 |
+
|
| 1173 |
+
// Reset file input so the same file can be re-uploaded
|
| 1174 |
+
input.value = '';
|
| 1175 |
+
}
|
| 1176 |
+
}
|
| 1177 |
+
|
| 1178 |
+
/**
|
| 1179 |
+
* Force-refresh an MJPEG stream by setting a new src with a cache-buster.
|
| 1180 |
+
* This drops the old HTTP connection and establishes a fresh one.
|
| 1181 |
+
*/
|
| 1182 |
+
function refreshFeedStream(feedId) {
|
| 1183 |
+
const img = document.getElementById('stream-' + feedId);
|
| 1184 |
+
if (img) {
|
| 1185 |
+
// Brief blank to visually signal the switch
|
| 1186 |
+
img.src = '';
|
| 1187 |
+
// Small delay lets the backend fully initialize the new feed
|
| 1188 |
+
setTimeout(() => {
|
| 1189 |
+
img.src = '/video_feed/' + feedId + '?t=' + Date.now();
|
| 1190 |
+
}, 300);
|
| 1191 |
+
}
|
| 1192 |
+
}
|
| 1193 |
+
|
| 1194 |
+
// ─── Grid Layout ───
|
| 1195 |
+
function setGridLayout(layout) {
|
| 1196 |
+
const grid = document.getElementById('camera-grid');
|
| 1197 |
+
currentLayout = layout;
|
| 1198 |
+
|
| 1199 |
+
if (layout === 'single') {
|
| 1200 |
+
grid.classList.add('single-view');
|
| 1201 |
+
// Show only the expanded feed
|
| 1202 |
+
document.querySelectorAll('.feed-cell').forEach((cell, i) => {
|
| 1203 |
+
cell.classList.toggle('expanded', i === expandedFeed);
|
| 1204 |
+
});
|
| 1205 |
+
} else {
|
| 1206 |
+
grid.classList.remove('single-view');
|
| 1207 |
+
document.querySelectorAll('.feed-cell').forEach(cell => {
|
| 1208 |
+
cell.classList.remove('expanded');
|
| 1209 |
+
});
|
| 1210 |
+
}
|
| 1211 |
+
}
|
| 1212 |
+
|
| 1213 |
+
function expandFeed(feedId) {
|
| 1214 |
+
expandedFeed = feedId;
|
| 1215 |
+
if (currentLayout === 'single') {
|
| 1216 |
+
document.querySelectorAll('.feed-cell').forEach((cell, i) => {
|
| 1217 |
+
cell.classList.toggle('expanded', i === feedId);
|
| 1218 |
+
});
|
| 1219 |
+
}
|
| 1220 |
+
}
|
| 1221 |
+
|
| 1222 |
+
// ─── Stats & Red Alert Updates ───
|
| 1223 |
+
function updateStats() {
|
| 1224 |
+
fetch('/stats')
|
| 1225 |
+
.then(r => r.json())
|
| 1226 |
+
.then(data => {
|
| 1227 |
+
const score = data.threat_score;
|
| 1228 |
+
const scoreEl = document.getElementById('threat-score');
|
| 1229 |
+
const statusEl = document.getElementById('status-text');
|
| 1230 |
+
const ringFill = document.getElementById('score-ring-fill');
|
| 1231 |
+
const threatCard = document.getElementById('threat-card');
|
| 1232 |
+
|
| 1233 |
+
scoreEl.textContent = score;
|
| 1234 |
+
|
| 1235 |
+
// Compute ring dash offset (circumference = 2 * π * 60 ≈ 377)
|
| 1236 |
+
const circumference = 377;
|
| 1237 |
+
const offset = circumference - (circumference * score / 100);
|
| 1238 |
+
ringFill.style.strokeDashoffset = offset;
|
| 1239 |
+
|
| 1240 |
+
// Color based on score
|
| 1241 |
+
let color, status, glow;
|
| 1242 |
+
if (score >= 80) {
|
| 1243 |
+
color = '#ff2040';
|
| 1244 |
+
status = 'CRITICAL';
|
| 1245 |
+
glow = 'rgba(255, 32, 64, 0.4)';
|
| 1246 |
+
} else if (score >= 50) {
|
| 1247 |
+
color = '#ffaa00';
|
| 1248 |
+
status = 'ELEVATED';
|
| 1249 |
+
glow = 'rgba(255, 170, 0, 0.3)';
|
| 1250 |
+
} else if (score >= 25) {
|
| 1251 |
+
color = '#00d4ff';
|
| 1252 |
+
status = 'GUARDED';
|
| 1253 |
+
glow = 'rgba(0, 200, 255, 0.3)';
|
| 1254 |
+
} else {
|
| 1255 |
+
color = '#00ff88';
|
| 1256 |
+
status = 'SECURE';
|
| 1257 |
+
glow = 'rgba(0, 255, 136, 0.3)';
|
| 1258 |
+
}
|
| 1259 |
+
|
| 1260 |
+
statusEl.textContent = status;
|
| 1261 |
+
statusEl.style.color = color;
|
| 1262 |
+
statusEl.style.textShadow = `0 0 20px ${glow}`;
|
| 1263 |
+
ringFill.style.stroke = color;
|
| 1264 |
+
ringFill.style.filter = `drop-shadow(0 0 8px ${glow})`;
|
| 1265 |
+
|
| 1266 |
+
// Red Alert state
|
| 1267 |
+
const alertOverlay = document.getElementById('red-alert-overlay');
|
| 1268 |
+
const alertBanner = document.getElementById('red-alert-banner');
|
| 1269 |
+
const feedCells = document.querySelectorAll('.feed-cell');
|
| 1270 |
+
|
| 1271 |
+
if (data.red_alert) {
|
| 1272 |
+
alertOverlay.classList.add('active');
|
| 1273 |
+
alertBanner.classList.add('active');
|
| 1274 |
+
feedCells.forEach(cell => cell.classList.add('red-alert-active'));
|
| 1275 |
+
threatCard.style.borderColor = 'rgba(255, 32, 64, 0.5)';
|
| 1276 |
+
|
| 1277 |
+
if (!isRedAlert) {
|
| 1278 |
+
playAlertTone();
|
| 1279 |
+
isRedAlert = true;
|
| 1280 |
+
}
|
| 1281 |
+
} else {
|
| 1282 |
+
alertOverlay.classList.remove('active');
|
| 1283 |
+
alertBanner.classList.remove('active');
|
| 1284 |
+
feedCells.forEach(cell => cell.classList.remove('red-alert-active'));
|
| 1285 |
+
threatCard.style.borderColor = '';
|
| 1286 |
+
isRedAlert = false;
|
| 1287 |
+
}
|
| 1288 |
+
|
| 1289 |
+
// Update mode display
|
| 1290 |
+
if (data.active_modules) {
|
| 1291 |
+
updateActiveModulesDisplay(data.active_modules);
|
| 1292 |
+
}
|
| 1293 |
+
|
| 1294 |
+
// Update live metrics
|
| 1295 |
+
const container = document.getElementById('stats-container');
|
| 1296 |
+
container.innerHTML = '';
|
| 1297 |
+
|
| 1298 |
+
if (!data.details || Object.keys(data.details).length === 0) {
|
| 1299 |
+
container.innerHTML = '<div class="stat-row"><span class="stat-name">System Status</span><span class="stat-val">Standby</span></div>';
|
| 1300 |
+
} else {
|
| 1301 |
+
for (const [key, value] of Object.entries(data.details)) {
|
| 1302 |
+
const div = document.createElement('div');
|
| 1303 |
+
div.className = 'stat-row';
|
| 1304 |
+
const formattedKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
| 1305 |
+
let displayVal = value;
|
| 1306 |
+
if (typeof value === 'boolean') {
|
| 1307 |
+
displayVal = value ? '⚠ YES' : 'No';
|
| 1308 |
+
}
|
| 1309 |
+
div.innerHTML = `<span class="stat-name">${formattedKey}</span><span class="stat-val">${displayVal}</span>`;
|
| 1310 |
+
container.appendChild(div);
|
| 1311 |
+
}
|
| 1312 |
+
}
|
| 1313 |
+
})
|
| 1314 |
+
.catch(() => { });
|
| 1315 |
+
}
|
| 1316 |
+
|
| 1317 |
+
// ─── Alert Tone (Web Audio API) ───
|
| 1318 |
+
function playAlertTone() {
|
| 1319 |
+
try {
|
| 1320 |
+
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
| 1321 |
+
const oscillator = audioCtx.createOscillator();
|
| 1322 |
+
const gainNode = audioCtx.createGain();
|
| 1323 |
+
|
| 1324 |
+
oscillator.connect(gainNode);
|
| 1325 |
+
gainNode.connect(audioCtx.destination);
|
| 1326 |
+
|
| 1327 |
+
oscillator.type = 'square';
|
| 1328 |
+
oscillator.frequency.setValueAtTime(880, audioCtx.currentTime);
|
| 1329 |
+
oscillator.frequency.setValueAtTime(660, audioCtx.currentTime + 0.15);
|
| 1330 |
+
oscillator.frequency.setValueAtTime(880, audioCtx.currentTime + 0.3);
|
| 1331 |
+
|
| 1332 |
+
gainNode.gain.setValueAtTime(0.08, audioCtx.currentTime);
|
| 1333 |
+
gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.5);
|
| 1334 |
+
|
| 1335 |
+
oscillator.start(audioCtx.currentTime);
|
| 1336 |
+
oscillator.stop(audioCtx.currentTime + 0.5);
|
| 1337 |
+
} catch (e) {
|
| 1338 |
+
// Audio not available — silent fallback
|
| 1339 |
+
}
|
| 1340 |
+
}
|
| 1341 |
+
|
| 1342 |
+
// ─── AI Report ───
|
| 1343 |
+
function generateReport() {
|
| 1344 |
+
const modal = document.getElementById('report-modal');
|
| 1345 |
+
modal.classList.add('show');
|
| 1346 |
+
document.getElementById('report-content').textContent = 'ANALYZING DATA STREAM...';
|
| 1347 |
+
|
| 1348 |
+
fetch('/generate_report', { method: 'POST' })
|
| 1349 |
+
.then(r => r.json())
|
| 1350 |
+
.then(data => {
|
| 1351 |
+
document.getElementById('report-content').textContent = data.report;
|
| 1352 |
+
})
|
| 1353 |
+
.catch(() => {
|
| 1354 |
+
document.getElementById('report-content').textContent = 'ERROR: Connection to AI Agent lost.';
|
| 1355 |
+
});
|
| 1356 |
+
}
|
| 1357 |
+
|
| 1358 |
+
function closeModal() {
|
| 1359 |
+
document.getElementById('report-modal').classList.remove('show');
|
| 1360 |
+
}
|
| 1361 |
+
|
| 1362 |
+
// ─── Audit Log Refresh ───
|
| 1363 |
+
function refreshAuditLog() {
|
| 1364 |
+
fetch('/audit_log')
|
| 1365 |
+
.then(r => r.json())
|
| 1366 |
+
.then(data => {
|
| 1367 |
+
const container = document.getElementById('audit-log-container');
|
| 1368 |
+
container.innerHTML = '';
|
| 1369 |
+
|
| 1370 |
+
if (data.log.length === 0) {
|
| 1371 |
+
container.innerHTML = '<div class="audit-entry"><div class="audit-time">--:--:--</div>NO ENTRIES</div>';
|
| 1372 |
+
return;
|
| 1373 |
+
}
|
| 1374 |
+
|
| 1375 |
+
data.log.slice(0, 30).forEach(entry => {
|
| 1376 |
+
const div = document.createElement('div');
|
| 1377 |
+
div.className = `audit-entry severity-${entry.severity}`;
|
| 1378 |
+
div.innerHTML = `
|
| 1379 |
+
<div class="audit-time">${entry.timestamp.split(' ')[1] || entry.timestamp}</div>
|
| 1380 |
+
${entry.action}: ${entry.details}
|
| 1381 |
+
`;
|
| 1382 |
+
container.appendChild(div);
|
| 1383 |
+
});
|
| 1384 |
+
})
|
| 1385 |
+
.catch(() => { });
|
| 1386 |
+
}
|
| 1387 |
+
|
| 1388 |
+
// ─── System Reset ───
|
| 1389 |
+
function resetSystem() {
|
| 1390 |
+
if (!confirm("⚠ CONFIRM SYSTEM RESET ⚠\n\nThis will clear all tracking history, threat scores, and active alerts.\nThe system will revert to default state.")) {
|
| 1391 |
+
return;
|
| 1392 |
+
}
|
| 1393 |
+
|
| 1394 |
+
fetch('/reset_system', { method: 'POST' })
|
| 1395 |
+
.then(r => r.json())
|
| 1396 |
+
.then(data => {
|
| 1397 |
+
if (data.success) {
|
| 1398 |
+
// Reload page to refresh all UI states cleanly
|
| 1399 |
+
window.location.reload();
|
| 1400 |
+
}
|
| 1401 |
+
});
|
| 1402 |
+
}
|
| 1403 |
+
|
| 1404 |
+
// ─── Intervals ───
|
| 1405 |
+
setInterval(updateStats, 1000);
|
| 1406 |
+
setInterval(refreshAuditLog, 5000);
|
| 1407 |
+
|
| 1408 |
+
// Initial load
|
| 1409 |
+
setTimeout(refreshAuditLog, 1500);
|
| 1410 |
+
</script>
|
| 1411 |
+
</body>
|
| 1412 |
+
|
| 1413 |
+
</html>
|
templates/sentinel_dashboard_v11_partial.html
ADDED
|
@@ -0,0 +1,1382 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>SENTINEL V11 — Intelligence Grid</title>
|
| 8 |
+
<link
|
| 9 |
+
href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700;800;900&family=Rajdhani:wght@300;400;500;600;700&family=Share+Tech+Mono&display=swap"
|
| 10 |
+
rel="stylesheet">
|
| 11 |
+
<script src="https://unpkg.com/feather-icons"></script>
|
| 12 |
+
<style>
|
| 13 |
+
/* ═══════════════════════════════════════════════
|
| 14 |
+
CSS VARIABLES — SCI-FI PALETTE
|
| 15 |
+
═══════════════════════════════════════════════ */
|
| 16 |
+
:root {
|
| 17 |
+
--bg-deep: #05080f;
|
| 18 |
+
--bg-panel: rgba(8, 16, 32, 0.85);
|
| 19 |
+
--bg-card: rgba(10, 20, 40, 0.7);
|
| 20 |
+
--border-glow: rgba(0, 200, 255, 0.15);
|
| 21 |
+
--border-glow-hover: rgba(0, 200, 255, 0.4);
|
| 22 |
+
--cyan: #00d4ff;
|
| 23 |
+
--cyan-dim: #0090aa;
|
| 24 |
+
--neon-green: #00ff88;
|
| 25 |
+
--neon-red: #ff2040;
|
| 26 |
+
--neon-amber: #ffaa00;
|
| 27 |
+
--text-primary: #e0f0ff;
|
| 28 |
+
--text-secondary: #5a8aaa;
|
| 29 |
+
--text-dim: #2a4a5a;
|
| 30 |
+
--scanline-opacity: 0.03;
|
| 31 |
+
--font-display: 'Orbitron', sans-serif;
|
| 32 |
+
--font-body: 'Rajdhani', sans-serif;
|
| 33 |
+
--font-mono: 'Share Tech Mono', monospace;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/* ═══════════════════════════════════════════════
|
| 37 |
+
BASE RESET & BODY
|
| 38 |
+
═══════════════════════════════════════════════ */
|
| 39 |
+
*,
|
| 40 |
+
*::before,
|
| 41 |
+
*::after {
|
| 42 |
+
margin: 0;
|
| 43 |
+
padding: 0;
|
| 44 |
+
box-sizing: border-box;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
body {
|
| 48 |
+
font-family: var(--font-body);
|
| 49 |
+
background: var(--bg-deep);
|
| 50 |
+
color: var(--text-primary);
|
| 51 |
+
height: 100vh;
|
| 52 |
+
overflow: hidden;
|
| 53 |
+
display: flex;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/* Scanline overlay on body */
|
| 57 |
+
body::after {
|
| 58 |
+
content: '';
|
| 59 |
+
position: fixed;
|
| 60 |
+
top: 0;
|
| 61 |
+
left: 0;
|
| 62 |
+
right: 0;
|
| 63 |
+
bottom: 0;
|
| 64 |
+
background: repeating-linear-gradient(0deg,
|
| 65 |
+
transparent,
|
| 66 |
+
transparent 2px,
|
| 67 |
+
rgba(0, 200, 255, var(--scanline-opacity)) 2px,
|
| 68 |
+
rgba(0, 200, 255, var(--scanline-opacity)) 4px);
|
| 69 |
+
pointer-events: none;
|
| 70 |
+
z-index: 9999;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
/* ═══════════════════════════════════════════════
|
| 74 |
+
RED ALERT OVERLAY
|
| 75 |
+
═══════════════════════════════════════════════ */
|
| 76 |
+
.red-alert-overlay {
|
| 77 |
+
position: fixed;
|
| 78 |
+
top: 0;
|
| 79 |
+
left: 0;
|
| 80 |
+
right: 0;
|
| 81 |
+
bottom: 0;
|
| 82 |
+
pointer-events: none;
|
| 83 |
+
z-index: 9998;
|
| 84 |
+
opacity: 0;
|
| 85 |
+
transition: opacity 0.3s ease;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.red-alert-overlay.active {
|
| 89 |
+
opacity: 1;
|
| 90 |
+
animation: red-pulse-border 1s ease-in-out infinite;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.red-alert-overlay.active::before {
|
| 94 |
+
content: '';
|
| 95 |
+
position: absolute;
|
| 96 |
+
top: 0;
|
| 97 |
+
left: 0;
|
| 98 |
+
right: 0;
|
| 99 |
+
bottom: 0;
|
| 100 |
+
border: 4px solid var(--neon-red);
|
| 101 |
+
box-shadow: inset 0 0 80px rgba(255, 32, 64, 0.15),
|
| 102 |
+
0 0 40px rgba(255, 32, 64, 0.3);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.red-alert-banner {
|
| 106 |
+
position: fixed;
|
| 107 |
+
top: 0;
|
| 108 |
+
left: 50%;
|
| 109 |
+
transform: translateX(-50%);
|
| 110 |
+
background: rgba(255, 20, 40, 0.9);
|
| 111 |
+
color: #fff;
|
| 112 |
+
font-family: var(--font-display);
|
| 113 |
+
font-size: 0.85rem;
|
| 114 |
+
font-weight: 700;
|
| 115 |
+
letter-spacing: 4px;
|
| 116 |
+
padding: 8px 40px;
|
| 117 |
+
z-index: 10000;
|
| 118 |
+
border-bottom-left-radius: 8px;
|
| 119 |
+
border-bottom-right-radius: 8px;
|
| 120 |
+
box-shadow: 0 4px 30px rgba(255, 20, 40, 0.5);
|
| 121 |
+
display: none;
|
| 122 |
+
animation: alert-flash 0.8s ease-in-out infinite;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.red-alert-banner.active {
|
| 126 |
+
display: block;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
@keyframes red-pulse-border {
|
| 130 |
+
|
| 131 |
+
0%,
|
| 132 |
+
100% {
|
| 133 |
+
opacity: 0.6;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
50% {
|
| 137 |
+
opacity: 1;
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
@keyframes alert-flash {
|
| 142 |
+
|
| 143 |
+
0%,
|
| 144 |
+
100% {
|
| 145 |
+
opacity: 1;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
50% {
|
| 149 |
+
opacity: 0.6;
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
/* ═══════════════════════════════════════════════
|
| 154 |
+
SIDEBAR
|
| 155 |
+
═══════════════════════════════════════════════ */
|
| 156 |
+
.sidebar {
|
| 157 |
+
width: 260px;
|
| 158 |
+
min-width: 260px;
|
| 159 |
+
background: var(--bg-panel);
|
| 160 |
+
backdrop-filter: blur(20px);
|
| 161 |
+
-webkit-backdrop-filter: blur(20px);
|
| 162 |
+
border-right: 1px solid var(--border-glow);
|
| 163 |
+
display: flex;
|
| 164 |
+
flex-direction: column;
|
| 165 |
+
padding: 20px;
|
| 166 |
+
z-index: 100;
|
| 167 |
+
overflow-y: auto;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.logo {
|
| 171 |
+
font-family: var(--font-display);
|
| 172 |
+
font-size: 1rem;
|
| 173 |
+
font-weight: 700;
|
| 174 |
+
margin-bottom: 8px;
|
| 175 |
+
display: flex;
|
| 176 |
+
align-items: center;
|
| 177 |
+
gap: 10px;
|
| 178 |
+
color: var(--cyan);
|
| 179 |
+
letter-spacing: 2px;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.logo svg {
|
| 183 |
+
filter: drop-shadow(0 0 6px var(--cyan));
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
.version-tag {
|
| 187 |
+
font-family: var(--font-mono);
|
| 188 |
+
font-size: 0.65rem;
|
| 189 |
+
color: var(--text-dim);
|
| 190 |
+
margin-bottom: 24px;
|
| 191 |
+
letter-spacing: 1px;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.nav-group {
|
| 195 |
+
margin-bottom: 20px;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.nav-label {
|
| 199 |
+
font-family: var(--font-display);
|
| 200 |
+
font-size: 0.6rem;
|
| 201 |
+
font-weight: 600;
|
| 202 |
+
color: var(--text-dim);
|
| 203 |
+
margin-bottom: 8px;
|
| 204 |
+
text-transform: uppercase;
|
| 205 |
+
letter-spacing: 2px;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
.nav-item {
|
| 209 |
+
padding: 10px 12px;
|
| 210 |
+
margin-bottom: 3px;
|
| 211 |
+
border-radius: 6px;
|
| 212 |
+
cursor: pointer;
|
| 213 |
+
transition: all 0.25s ease;
|
| 214 |
+
display: flex;
|
| 215 |
+
align-items: center;
|
| 216 |
+
gap: 10px;
|
| 217 |
+
color: var(--text-secondary);
|
| 218 |
+
font-size: 0.9rem;
|
| 219 |
+
font-weight: 500;
|
| 220 |
+
border: 1px solid transparent;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.nav-item:hover {
|
| 224 |
+
background: rgba(0, 200, 255, 0.05);
|
| 225 |
+
color: var(--text-primary);
|
| 226 |
+
border-color: var(--border-glow);
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.nav-item.active {
|
| 230 |
+
background: rgba(0, 200, 255, 0.1);
|
| 231 |
+
color: var(--cyan);
|
| 232 |
+
border-color: rgba(0, 200, 255, 0.3);
|
| 233 |
+
box-shadow: 0 0 15px rgba(0, 200, 255, 0.05);
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
.nav-item svg {
|
| 237 |
+
width: 16px;
|
| 238 |
+
height: 16px;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
/* Audit Log Panel in sidebar */
|
| 242 |
+
.audit-section {
|
| 243 |
+
margin-top: auto;
|
| 244 |
+
flex-shrink: 0;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
.audit-log-container {
|
| 248 |
+
max-height: 200px;
|
| 249 |
+
overflow-y: auto;
|
| 250 |
+
scrollbar-width: thin;
|
| 251 |
+
scrollbar-color: var(--cyan-dim) transparent;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
.audit-log-container::-webkit-scrollbar {
|
| 255 |
+
width: 4px;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.audit-log-container::-webkit-scrollbar-track {
|
| 259 |
+
background: transparent;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.audit-log-container::-webkit-scrollbar-thumb {
|
| 263 |
+
background: var(--cyan-dim);
|
| 264 |
+
border-radius: 2px;
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
.audit-entry {
|
| 268 |
+
font-family: var(--font-mono);
|
| 269 |
+
font-size: 0.65rem;
|
| 270 |
+
padding: 6px 8px;
|
| 271 |
+
margin-bottom: 2px;
|
| 272 |
+
border-radius: 4px;
|
| 273 |
+
background: rgba(0, 0, 0, 0.3);
|
| 274 |
+
border-left: 2px solid var(--cyan-dim);
|
| 275 |
+
line-height: 1.4;
|
| 276 |
+
color: var(--text-secondary);
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
.audit-entry.severity-WARNING {
|
| 280 |
+
border-left-color: var(--neon-amber);
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
.audit-entry.severity-CRITICAL {
|
| 284 |
+
border-left-color: var(--neon-red);
|
| 285 |
+
color: var(--neon-red);
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
.audit-time {
|
| 289 |
+
color: var(--text-dim);
|
| 290 |
+
font-size: 0.6rem;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
/* ═══════════════════════════════════════════════
|
| 294 |
+
MAIN CONTENT AREA
|
| 295 |
+
═══════════════════════════════════════════════ */
|
| 296 |
+
.main-content {
|
| 297 |
+
flex: 1;
|
| 298 |
+
padding: 16px;
|
| 299 |
+
display: grid;
|
| 300 |
+
grid-template-columns: 1fr 320px;
|
| 301 |
+
gap: 16px;
|
| 302 |
+
overflow: hidden;
|
| 303 |
+
background: radial-gradient(ellipse at 20% 0%, rgba(0, 60, 100, 0.15) 0%, transparent 60%),
|
| 304 |
+
radial-gradient(ellipse at 80% 100%, rgba(0, 40, 80, 0.1) 0%, transparent 60%),
|
| 305 |
+
var(--bg-deep);
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
/* ══════════════════════════════════════════��════
|
| 309 |
+
MULTI-CAMERA GRID
|
| 310 |
+
═══════════════════════════════════════════════ */
|
| 311 |
+
.camera-grid {
|
| 312 |
+
display: grid;
|
| 313 |
+
grid-template-columns: 1fr 1fr;
|
| 314 |
+
grid-template-rows: 1fr 1fr;
|
| 315 |
+
gap: 8px;
|
| 316 |
+
height: 100%;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
.camera-grid.single-view {
|
| 320 |
+
grid-template-columns: 1fr;
|
| 321 |
+
grid-template-rows: 1fr;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.camera-grid.single-view .feed-cell:not(.expanded) {
|
| 325 |
+
display: none;
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
.feed-cell {
|
| 329 |
+
background: var(--bg-card);
|
| 330 |
+
border-radius: 10px;
|
| 331 |
+
border: 1px solid var(--border-glow);
|
| 332 |
+
overflow: hidden;
|
| 333 |
+
position: relative;
|
| 334 |
+
cursor: pointer;
|
| 335 |
+
transition: all 0.3s ease;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.feed-cell:hover {
|
| 339 |
+
border-color: var(--border-glow-hover);
|
| 340 |
+
box-shadow: 0 0 20px rgba(0, 200, 255, 0.1);
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
.feed-cell.red-alert-active {
|
| 344 |
+
border-color: var(--neon-red) !important;
|
| 345 |
+
box-shadow: 0 0 30px rgba(255, 32, 64, 0.3) !important;
|
| 346 |
+
animation: cell-red-pulse 1.5s ease-in-out infinite;
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
@keyframes cell-red-pulse {
|
| 350 |
+
|
| 351 |
+
0%,
|
| 352 |
+
100% {
|
| 353 |
+
box-shadow: 0 0 20px rgba(255, 32, 64, 0.2);
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
50% {
|
| 357 |
+
box-shadow: 0 0 40px rgba(255, 32, 64, 0.5);
|
| 358 |
+
}
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
.feed-header {
|
| 362 |
+
position: absolute;
|
| 363 |
+
top: 8px;
|
| 364 |
+
left: 10px;
|
| 365 |
+
right: 10px;
|
| 366 |
+
display: flex;
|
| 367 |
+
justify-content: space-between;
|
| 368 |
+
align-items: center;
|
| 369 |
+
z-index: 5;
|
| 370 |
+
pointer-events: none;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
.feed-badge {
|
| 374 |
+
background: rgba(0, 0, 0, 0.7);
|
| 375 |
+
backdrop-filter: blur(8px);
|
| 376 |
+
padding: 4px 10px;
|
| 377 |
+
border-radius: 4px;
|
| 378 |
+
font-family: var(--font-mono);
|
| 379 |
+
font-size: 0.65rem;
|
| 380 |
+
font-weight: 400;
|
| 381 |
+
border: 1px solid var(--border-glow);
|
| 382 |
+
display: flex;
|
| 383 |
+
align-items: center;
|
| 384 |
+
gap: 6px;
|
| 385 |
+
color: var(--cyan);
|
| 386 |
+
letter-spacing: 1px;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
.live-dot {
|
| 390 |
+
width: 6px;
|
| 391 |
+
height: 6px;
|
| 392 |
+
background: var(--neon-red);
|
| 393 |
+
border-radius: 50%;
|
| 394 |
+
box-shadow: 0 0 8px var(--neon-red);
|
| 395 |
+
animation: pulse-dot 2s infinite;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
@keyframes pulse-dot {
|
| 399 |
+
|
| 400 |
+
0%,
|
| 401 |
+
100% {
|
| 402 |
+
opacity: 1;
|
| 403 |
+
transform: scale(1);
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
50% {
|
| 407 |
+
opacity: 0.4;
|
| 408 |
+
transform: scale(1.3);
|
| 409 |
+
}
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
.feed-status {
|
| 413 |
+
font-family: var(--font-mono);
|
| 414 |
+
font-size: 0.6rem;
|
| 415 |
+
color: var(--neon-green);
|
| 416 |
+
background: rgba(0, 0, 0, 0.6);
|
| 417 |
+
padding: 3px 8px;
|
| 418 |
+
border-radius: 4px;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
.feed-stream {
|
| 422 |
+
width: 100%;
|
| 423 |
+
height: 100%;
|
| 424 |
+
object-fit: contain;
|
| 425 |
+
background: #000;
|
| 426 |
+
display: block;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.feed-offline {
|
| 430 |
+
display: flex;
|
| 431 |
+
flex-direction: column;
|
| 432 |
+
align-items: center;
|
| 433 |
+
justify-content: center;
|
| 434 |
+
height: 100%;
|
| 435 |
+
color: var(--text-dim);
|
| 436 |
+
font-family: var(--font-mono);
|
| 437 |
+
font-size: 0.75rem;
|
| 438 |
+
letter-spacing: 1px;
|
| 439 |
+
gap: 8px;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
.feed-offline svg {
|
| 443 |
+
width: 24px;
|
| 444 |
+
height: 24px;
|
| 445 |
+
color: var(--text-dim);
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
/* Fullscreen expand button */
|
| 449 |
+
.expand-btn {
|
| 450 |
+
position: absolute;
|
| 451 |
+
bottom: 8px;
|
| 452 |
+
right: 8px;
|
| 453 |
+
background: rgba(0, 0, 0, 0.6);
|
| 454 |
+
border: 1px solid var(--border-glow);
|
| 455 |
+
color: var(--cyan);
|
| 456 |
+
width: 28px;
|
| 457 |
+
height: 28px;
|
| 458 |
+
border-radius: 4px;
|
| 459 |
+
cursor: pointer;
|
| 460 |
+
display: flex;
|
| 461 |
+
align-items: center;
|
| 462 |
+
justify-content: center;
|
| 463 |
+
transition: all 0.2s;
|
| 464 |
+
z-index: 5;
|
| 465 |
+
pointer-events: all;
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
.expand-btn:hover {
|
| 469 |
+
background: rgba(0, 200, 255, 0.15);
|
| 470 |
+
border-color: var(--cyan);
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
.expand-btn svg {
|
| 474 |
+
width: 14px;
|
| 475 |
+
height: 14px;
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
/* ═══════════════════════════════════════════════
|
| 479 |
+
INTEL PANEL (Right Column)
|
| 480 |
+
═══════════════════════════════════════════════ */
|
| 481 |
+
.intel-panel {
|
| 482 |
+
display: flex;
|
| 483 |
+
flex-direction: column;
|
| 484 |
+
gap: 12px;
|
| 485 |
+
overflow-y: auto;
|
| 486 |
+
scrollbar-width: thin;
|
| 487 |
+
scrollbar-color: var(--cyan-dim) transparent;
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
.intel-panel::-webkit-scrollbar {
|
| 491 |
+
width: 4px;
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
.intel-panel::-webkit-scrollbar-thumb {
|
| 495 |
+
background: var(--cyan-dim);
|
| 496 |
+
border-radius: 2px;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
.card {
|
| 500 |
+
background: var(--bg-card);
|
| 501 |
+
backdrop-filter: blur(15px);
|
| 502 |
+
border-radius: 10px;
|
| 503 |
+
padding: 18px;
|
| 504 |
+
border: 1px solid var(--border-glow);
|
| 505 |
+
transition: border-color 0.3s;
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
.card:hover {
|
| 509 |
+
border-color: var(--border-glow-hover);
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
.card-header {
|
| 513 |
+
display: flex;
|
| 514 |
+
justify-content: space-between;
|
| 515 |
+
align-items: center;
|
| 516 |
+
margin-bottom: 14px;
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
.card-title {
|
| 520 |
+
font-family: var(--font-display);
|
| 521 |
+
font-size: 0.65rem;
|
| 522 |
+
font-weight: 600;
|
| 523 |
+
color: var(--cyan);
|
| 524 |
+
text-transform: uppercase;
|
| 525 |
+
letter-spacing: 2px;
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
.card-icon {
|
| 529 |
+
color: var(--text-dim);
|
| 530 |
+
width: 16px;
|
| 531 |
+
height: 16px;
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
/* Threat gauge */
|
| 535 |
+
.threat-gauge {
|
| 536 |
+
display: flex;
|
| 537 |
+
flex-direction: column;
|
| 538 |
+
align-items: center;
|
| 539 |
+
padding: 10px 0;
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
.score-ring {
|
| 543 |
+
position: relative;
|
| 544 |
+
width: 130px;
|
| 545 |
+
height: 130px;
|
| 546 |
+
margin-bottom: 12px;
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
.score-ring svg {
|
| 550 |
+
width: 130px;
|
| 551 |
+
height: 130px;
|
| 552 |
+
transform: rotate(-90deg);
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
.score-ring-bg {
|
| 556 |
+
fill: none;
|
| 557 |
+
stroke: rgba(255, 255, 255, 0.05);
|
| 558 |
+
stroke-width: 6;
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
.score-ring-fill {
|
| 562 |
+
fill: none;
|
| 563 |
+
stroke: var(--neon-green);
|
| 564 |
+
stroke-width: 6;
|
| 565 |
+
stroke-linecap: round;
|
| 566 |
+
stroke-dasharray: 377;
|
| 567 |
+
stroke-dashoffset: 377;
|
| 568 |
+
transition: stroke-dashoffset 0.8s cubic-bezier(0.4, 0, 0.2, 1),
|
| 569 |
+
stroke 0.5s ease;
|
| 570 |
+
filter: drop-shadow(0 0 8px var(--neon-green));
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
.score-text {
|
| 574 |
+
position: absolute;
|
| 575 |
+
top: 50%;
|
| 576 |
+
left: 50%;
|
| 577 |
+
transform: translate(-50%, -50%);
|
| 578 |
+
text-align: center;
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
.score-value {
|
| 582 |
+
font-family: var(--font-display);
|
| 583 |
+
font-size: 2.2rem;
|
| 584 |
+
font-weight: 800;
|
| 585 |
+
line-height: 1;
|
| 586 |
+
color: var(--text-primary);
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
.score-label {
|
| 590 |
+
font-family: var(--font-mono);
|
| 591 |
+
font-size: 0.55rem;
|
| 592 |
+
color: var(--text-dim);
|
| 593 |
+
letter-spacing: 2px;
|
| 594 |
+
margin-top: 4px;
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
.status-text {
|
| 598 |
+
font-family: var(--font-display);
|
| 599 |
+
font-size: 0.85rem;
|
| 600 |
+
font-weight: 700;
|
| 601 |
+
color: var(--neon-green);
|
| 602 |
+
letter-spacing: 3px;
|
| 603 |
+
text-shadow: 0 0 20px rgba(0, 255, 136, 0.3);
|
| 604 |
+
transition: all 0.5s ease;
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
/* Stats */
|
| 608 |
+
.stats-list {
|
| 609 |
+
display: flex;
|
| 610 |
+
flex-direction: column;
|
| 611 |
+
gap: 6px;
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
.stat-row {
|
| 615 |
+
display: flex;
|
| 616 |
+
justify-content: space-between;
|
| 617 |
+
align-items: center;
|
| 618 |
+
padding: 10px 12px;
|
| 619 |
+
background: rgba(0, 200, 255, 0.03);
|
| 620 |
+
border-radius: 6px;
|
| 621 |
+
border: 1px solid rgba(0, 200, 255, 0.05);
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
.stat-name {
|
| 625 |
+
font-size: 0.8rem;
|
| 626 |
+
color: var(--text-secondary);
|
| 627 |
+
font-weight: 500;
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
.stat-val {
|
| 631 |
+
font-family: var(--font-mono);
|
| 632 |
+
font-size: 0.85rem;
|
| 633 |
+
font-weight: 600;
|
| 634 |
+
color: var(--text-primary);
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
/* Mode indicator */
|
| 638 |
+
.mode-indicator {
|
| 639 |
+
display: flex;
|
| 640 |
+
align-items: center;
|
| 641 |
+
gap: 8px;
|
| 642 |
+
padding: 10px 14px;
|
| 643 |
+
background: rgba(0, 200, 255, 0.05);
|
| 644 |
+
border-radius: 6px;
|
| 645 |
+
border: 1px solid var(--border-glow);
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
.mode-dot {
|
| 649 |
+
width: 8px;
|
| 650 |
+
height: 8px;
|
| 651 |
+
background: var(--cyan);
|
| 652 |
+
border-radius: 50%;
|
| 653 |
+
box-shadow: 0 0 10px var(--cyan);
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
.mode-name {
|
| 657 |
+
font-family: var(--font-display);
|
| 658 |
+
font-size: 0.7rem;
|
| 659 |
+
font-weight: 600;
|
| 660 |
+
letter-spacing: 2px;
|
| 661 |
+
color: var(--cyan);
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
/* Buttons */
|
| 665 |
+
.btn {
|
| 666 |
+
width: 100%;
|
| 667 |
+
padding: 12px;
|
| 668 |
+
border: 1px solid var(--border-glow);
|
| 669 |
+
border-radius: 6px;
|
| 670 |
+
font-size: 0.8rem;
|
| 671 |
+
font-weight: 600;
|
| 672 |
+
cursor: pointer;
|
| 673 |
+
transition: all 0.25s;
|
| 674 |
+
font-family: var(--font-display);
|
| 675 |
+
letter-spacing: 1px;
|
| 676 |
+
text-transform: uppercase;
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
.btn-primary {
|
| 680 |
+
background: linear-gradient(135deg, rgba(0, 200, 255, 0.2), rgba(0, 100, 200, 0.1));
|
| 681 |
+
color: var(--cyan);
|
| 682 |
+
border-color: rgba(0, 200, 255, 0.3);
|
| 683 |
+
}
|
| 684 |
+
|
| 685 |
+
.btn-primary:hover {
|
| 686 |
+
background: linear-gradient(135deg, rgba(0, 200, 255, 0.3), rgba(0, 100, 200, 0.2));
|
| 687 |
+
box-shadow: 0 0 20px rgba(0, 200, 255, 0.15);
|
| 688 |
+
transform: translateY(-1px);
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
+
.btn-secondary {
|
| 692 |
+
background: rgba(255, 255, 255, 0.03);
|
| 693 |
+
color: var(--text-secondary);
|
| 694 |
+
}
|
| 695 |
+
|
| 696 |
+
.btn-secondary:hover {
|
| 697 |
+
background: rgba(255, 255, 255, 0.08);
|
| 698 |
+
color: var(--text-primary);
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
/* ═══════════════════════════════════════════════
|
| 702 |
+
MODAL
|
| 703 |
+
═══════════════════════════════════════════════ */
|
| 704 |
+
.modal-overlay {
|
| 705 |
+
position: fixed;
|
| 706 |
+
top: 0;
|
| 707 |
+
left: 0;
|
| 708 |
+
right: 0;
|
| 709 |
+
bottom: 0;
|
| 710 |
+
background: rgba(0, 0, 0, 0.8);
|
| 711 |
+
backdrop-filter: blur(10px);
|
| 712 |
+
display: none;
|
| 713 |
+
justify-content: center;
|
| 714 |
+
align-items: center;
|
| 715 |
+
z-index: 11000;
|
| 716 |
+
opacity: 0;
|
| 717 |
+
transition: opacity 0.3s ease;
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
.modal-overlay.show {
|
| 721 |
+
display: flex;
|
| 722 |
+
opacity: 1;
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
.modal-card {
|
| 726 |
+
background: var(--bg-panel);
|
| 727 |
+
width: 520px;
|
| 728 |
+
max-width: 90vw;
|
| 729 |
+
border-radius: 12px;
|
| 730 |
+
padding: 28px;
|
| 731 |
+
border: 1px solid var(--border-glow);
|
| 732 |
+
box-shadow: 0 0 60px rgba(0, 200, 255, 0.1);
|
| 733 |
+
transform: scale(0.95);
|
| 734 |
+
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
+
.modal-overlay.show .modal-card {
|
| 738 |
+
transform: scale(1);
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
.modal-title {
|
| 742 |
+
font-family: var(--font-display);
|
| 743 |
+
font-size: 1rem;
|
| 744 |
+
font-weight: 700;
|
| 745 |
+
color: var(--cyan);
|
| 746 |
+
letter-spacing: 2px;
|
| 747 |
+
margin-bottom: 6px;
|
| 748 |
+
}
|
| 749 |
+
|
| 750 |
+
.modal-subtitle {
|
| 751 |
+
font-family: var(--font-mono);
|
| 752 |
+
font-size: 0.7rem;
|
| 753 |
+
color: var(--text-dim);
|
| 754 |
+
margin-bottom: 16px;
|
| 755 |
+
}
|
| 756 |
+
|
| 757 |
+
.report-content {
|
| 758 |
+
background: rgba(0, 0, 0, 0.5);
|
| 759 |
+
padding: 16px;
|
| 760 |
+
border-radius: 8px;
|
| 761 |
+
font-family: var(--font-mono);
|
| 762 |
+
font-size: 0.75rem;
|
| 763 |
+
color: var(--neon-green);
|
| 764 |
+
margin-bottom: 16px;
|
| 765 |
+
line-height: 1.6;
|
| 766 |
+
border: 1px solid var(--border-glow);
|
| 767 |
+
max-height: 300px;
|
| 768 |
+
overflow-y: auto;
|
| 769 |
+
white-space: pre-wrap;
|
| 770 |
+
}
|
| 771 |
+
|
| 772 |
+
/* ═══════════════════════════════════════════════
|
| 773 |
+
MOBILE RESPONSIVE
|
| 774 |
+
═══════════════════════════════════════════════ */
|
| 775 |
+
@media (max-width: 1024px) {
|
| 776 |
+
.main-content {
|
| 777 |
+
grid-template-columns: 1fr 280px;
|
| 778 |
+
}
|
| 779 |
+
}
|
| 780 |
+
|
| 781 |
+
@media (max-width: 768px) {
|
| 782 |
+
body {
|
| 783 |
+
flex-direction: column;
|
| 784 |
+
overflow-y: auto;
|
| 785 |
+
}
|
| 786 |
+
|
| 787 |
+
.sidebar {
|
| 788 |
+
width: 100%;
|
| 789 |
+
min-width: 100%;
|
| 790 |
+
flex-direction: row;
|
| 791 |
+
flex-wrap: wrap;
|
| 792 |
+
padding: 10px 16px;
|
| 793 |
+
gap: 6px;
|
| 794 |
+
order: 2;
|
| 795 |
+
border-right: none;
|
| 796 |
+
border-top: 1px solid var(--border-glow);
|
| 797 |
+
overflow-y: visible;
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
.logo {
|
| 801 |
+
width: 100%;
|
| 802 |
+
margin-bottom: 6px;
|
| 803 |
+
font-size: 0.85rem;
|
| 804 |
+
}
|
| 805 |
+
|
| 806 |
+
.version-tag {
|
| 807 |
+
display: none;
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
+
.nav-group {
|
| 811 |
+
display: flex;
|
| 812 |
+
flex-wrap: wrap;
|
| 813 |
+
gap: 4px;
|
| 814 |
+
margin-bottom: 0;
|
| 815 |
+
}
|
| 816 |
+
|
| 817 |
+
.nav-label {
|
| 818 |
+
width: 100%;
|
| 819 |
+
margin-bottom: 4px;
|
| 820 |
+
}
|
| 821 |
+
|
| 822 |
+
.nav-item {
|
| 823 |
+
flex: none;
|
| 824 |
+
padding: 8px 10px;
|
| 825 |
+
font-size: 0.75rem;
|
| 826 |
+
}
|
| 827 |
+
|
| 828 |
+
.audit-section {
|
| 829 |
+
display: none;
|
| 830 |
+
}
|
| 831 |
+
|
| 832 |
+
.main-content {
|
| 833 |
+
grid-template-columns: 1fr;
|
| 834 |
+
grid-template-rows: auto auto;
|
| 835 |
+
order: 1;
|
| 836 |
+
overflow-y: auto;
|
| 837 |
+
padding: 10px;
|
| 838 |
+
}
|
| 839 |
+
|
| 840 |
+
.camera-grid {
|
| 841 |
+
grid-template-columns: 1fr;
|
| 842 |
+
grid-template-rows: auto;
|
| 843 |
+
min-height: 250px;
|
| 844 |
+
}
|
| 845 |
+
|
| 846 |
+
.feed-cell:not(:first-child) {
|
| 847 |
+
display: none;
|
| 848 |
+
}
|
| 849 |
+
|
| 850 |
+
.intel-panel {
|
| 851 |
+
overflow-y: visible;
|
| 852 |
+
}
|
| 853 |
+
}
|
| 854 |
+
|
| 855 |
+
@media (max-width: 480px) {
|
| 856 |
+
.sidebar .nav-item {
|
| 857 |
+
font-size: 0.7rem;
|
| 858 |
+
padding: 6px 8px;
|
| 859 |
+
gap: 6px;
|
| 860 |
+
}
|
| 861 |
+
|
| 862 |
+
.sidebar .nav-item svg {
|
| 863 |
+
width: 14px;
|
| 864 |
+
height: 14px;
|
| 865 |
+
}
|
| 866 |
+
|
| 867 |
+
.score-ring {
|
| 868 |
+
width: 100px;
|
| 869 |
+
height: 100px;
|
| 870 |
+
}
|
| 871 |
+
|
| 872 |
+
.score-ring svg {
|
| 873 |
+
width: 100px;
|
| 874 |
+
height: 100px;
|
| 875 |
+
}
|
| 876 |
+
|
| 877 |
+
.score-value {
|
| 878 |
+
font-size: 1.6rem;
|
| 879 |
+
}
|
| 880 |
+
}
|
| 881 |
+
</style>
|
| 882 |
+
</head>
|
| 883 |
+
|
| 884 |
+
<body>
|
| 885 |
+
<!-- Red Alert Overlay -->
|
| 886 |
+
<div class="red-alert-overlay" id="red-alert-overlay"></div>
|
| 887 |
+
<div class="red-alert-banner" id="red-alert-banner">⚠ RED ALERT — CRITICAL THREAT DETECTED ⚠</div>
|
| 888 |
+
|
| 889 |
+
<!-- SIDEBAR -->
|
| 890 |
+
<div class="sidebar">
|
| 891 |
+
<div class="logo">
|
| 892 |
+
<i data-feather="shield"></i>
|
| 893 |
+
SENTINEL
|
| 894 |
+
</div>
|
| 895 |
+
<div class="version-tag">V11.0 // MULTI-MODULE</div>
|
| 896 |
+
|
| 897 |
+
<div class="nav-group">
|
| 898 |
+
<div class="nav-label">Detection Modules</div>
|
| 899 |
+
<div class="nav-item active" onclick="toggleModule('movement', this)" id="nav-movement">
|
| 900 |
+
<i data-feather="activity"></i> Movement
|
| 901 |
+
</div>
|
| 902 |
+
<div class="nav-item" onclick="toggleModule('facemask', this)" id="nav-facemask">
|
| 903 |
+
<i data-feather="eye"></i> Facemask
|
| 904 |
+
</div>
|
| 905 |
+
<div class="nav-item" onclick="toggleModule('weapon', this)" id="nav-weapon">
|
| 906 |
+
<i data-feather="crosshair"></i> Weapon
|
| 907 |
+
</div>
|
| 908 |
+
<div class="nav-item" onclick="toggleModule('public_safety', this)" id="nav-public_safety">
|
| 909 |
+
<i data-feather="users"></i> Public Safety
|
| 910 |
+
</div>
|
| 911 |
+
</div>
|
| 912 |
+
|
| 913 |
+
<div class="nav-group">
|
| 914 |
+
<div class="nav-label">Input Source</div>
|
| 915 |
+
<div class="nav-item" onclick="setSource('camera')">
|
| 916 |
+
<i data-feather="video"></i> Live Camera
|
| 917 |
+
</div>
|
| 918 |
+
<div class="nav-item" onclick="document.getElementById('upload-input').click()">
|
| 919 |
+
<i data-feather="upload-cloud"></i> Upload Video
|
| 920 |
+
</div>
|
| 921 |
+
<input type="file" id="upload-input" style="display: none" accept="video/*"
|
| 922 |
+
onchange="handleFileUpload(this)">
|
| 923 |
+
</div>
|
| 924 |
+
|
| 925 |
+
<div class="nav-group">
|
| 926 |
+
<div class="nav-label">Grid Layout</div>
|
| 927 |
+
<div class="nav-item" onclick="setGridLayout('quad')">
|
| 928 |
+
<i data-feather="grid"></i> 2×2 Grid
|
| 929 |
+
</div>
|
| 930 |
+
<div class="nav-item" onclick="setGridLayout('single')">
|
| 931 |
+
<i data-feather="maximize-2"></i> Single View
|
| 932 |
+
</div>
|
| 933 |
+
</div>
|
| 934 |
+
|
| 935 |
+
<!-- Audit Log -->
|
| 936 |
+
<div class="audit-section">
|
| 937 |
+
<div class="nav-label" style="margin-bottom: 8px;">Operator Log</div>
|
| 938 |
+
<div class="audit-log-container" id="audit-log-container">
|
| 939 |
+
<div class="audit-entry">
|
| 940 |
+
<div class="audit-time">--:--:--</div>
|
| 941 |
+
SYSTEM STANDBY
|
| 942 |
+
</div>
|
| 943 |
+
</div>
|
| 944 |
+
</div>
|
| 945 |
+
</div>
|
| 946 |
+
|
| 947 |
+
<!-- MAIN CONTENT -->
|
| 948 |
+
<div class="main-content">
|
| 949 |
+
<!-- Multi-Camera Grid -->
|
| 950 |
+
<div class="camera-grid" id="camera-grid">
|
| 951 |
+
<!-- Feed 0 — Primary -->
|
| 952 |
+
<div class="feed-cell expanded" id="feed-0" onclick="expandFeed(0)">
|
| 953 |
+
<div class="feed-header">
|
| 954 |
+
<div class="feed-badge">
|
| 955 |
+
<div class="live-dot"></div>
|
| 956 |
+
<span>FEED 01 // PRIMARY</span>
|
| 957 |
+
</div>
|
| 958 |
+
<div class="feed-status" id="feed-0-status">ACTIVE</div>
|
| 959 |
+
</div>
|
| 960 |
+
<img class="feed-stream" id="stream-0" src="/video_feed/0" alt="Feed 0">
|
| 961 |
+
<button class="expand-btn" onclick="event.stopPropagation(); expandFeed(0)" title="Expand">
|
| 962 |
+
<i data-feather="maximize-2"></i>
|
| 963 |
+
</button>
|
| 964 |
+
</div>
|
| 965 |
+
|
| 966 |
+
<!-- Feed 1 -->
|
| 967 |
+
<div class="feed-cell" id="feed-1">
|
| 968 |
+
<div class="feed-header">
|
| 969 |
+
<div class="feed-badge">
|
| 970 |
+
<div class="live-dot" style="background: var(--text-dim); box-shadow: none;"></div>
|
| 971 |
+
<span>FEED 02</span>
|
| 972 |
+
</div>
|
| 973 |
+
<div class="feed-status" id="feed-1-status" style="color: var(--text-dim);">STANDBY</div>
|
| 974 |
+
</div>
|
| 975 |
+
<div class="feed-offline" id="offline-1">
|
| 976 |
+
<i data-feather="video-off"></i>
|
| 977 |
+
NO SIGNAL
|
| 978 |
+
</div>
|
| 979 |
+
</div>
|
| 980 |
+
|
| 981 |
+
<!-- Feed 2 -->
|
| 982 |
+
<div class="feed-cell" id="feed-2">
|
| 983 |
+
<div class="feed-header">
|
| 984 |
+
<div class="feed-badge">
|
| 985 |
+
<div class="live-dot" style="background: var(--text-dim); box-shadow: none;"></div>
|
| 986 |
+
<span>FEED 03</span>
|
| 987 |
+
</div>
|
| 988 |
+
<div class="feed-status" id="feed-2-status" style="color: var(--text-dim);">STANDBY</div>
|
| 989 |
+
</div>
|
| 990 |
+
<div class="feed-offline" id="offline-2">
|
| 991 |
+
<i data-feather="video-off"></i>
|
| 992 |
+
NO SIGNAL
|
| 993 |
+
</div>
|
| 994 |
+
</div>
|
| 995 |
+
|
| 996 |
+
<!-- Feed 3 -->
|
| 997 |
+
<div class="feed-cell" id="feed-3">
|
| 998 |
+
<div class="feed-header">
|
| 999 |
+
<div class="feed-badge">
|
| 1000 |
+
<div class="live-dot" style="background: var(--text-dim); box-shadow: none;"></div>
|
| 1001 |
+
<span>FEED 04</span>
|
| 1002 |
+
</div>
|
| 1003 |
+
<div class="feed-status" id="feed-3-status" style="color: var(--text-dim);">STANDBY</div>
|
| 1004 |
+
</div>
|
| 1005 |
+
<div class="feed-offline" id="offline-3">
|
| 1006 |
+
<i data-feather="video-off"></i>
|
| 1007 |
+
NO SIGNAL
|
| 1008 |
+
</div>
|
| 1009 |
+
</div>
|
| 1010 |
+
</div>
|
| 1011 |
+
|
| 1012 |
+
<!-- Intel Panel -->
|
| 1013 |
+
<div class="intel-panel">
|
| 1014 |
+
<!-- Active Mode -->
|
| 1015 |
+
<div class="card">
|
| 1016 |
+
<div class="card-header">
|
| 1017 |
+
<div class="card-title">Active Mode</div>
|
| 1018 |
+
</div>
|
| 1019 |
+
<div class="mode-indicator">
|
| 1020 |
+
<div class="mode-dot"></div>
|
| 1021 |
+
<div class="mode-name" id="mode-title">1 MODULE ACTIVE</div>
|
| 1022 |
+
</div>
|
| 1023 |
+
</div>
|
| 1024 |
+
|
| 1025 |
+
<!-- Threat Assessment -->
|
| 1026 |
+
<div class="card" id="threat-card">
|
| 1027 |
+
<div class="card-header">
|
| 1028 |
+
<div class="card-title">Threat Assessment</div>
|
| 1029 |
+
<i data-feather="alert-triangle" class="card-icon"></i>
|
| 1030 |
+
</div>
|
| 1031 |
+
<div class="threat-gauge">
|
| 1032 |
+
<div class="score-ring">
|
| 1033 |
+
<svg viewBox="0 0 130 130">
|
| 1034 |
+
<circle class="score-ring-bg" cx="65" cy="65" r="60"></circle>
|
| 1035 |
+
<circle class="score-ring-fill" id="score-ring-fill" cx="65" cy="65" r="60"></circle>
|
| 1036 |
+
</svg>
|
| 1037 |
+
<div class="score-text">
|
| 1038 |
+
<div class="score-value" id="threat-score">0</div>
|
| 1039 |
+
<div class="score-label">THREAT LEVEL</div>
|
| 1040 |
+
</div>
|
| 1041 |
+
</div>
|
| 1042 |
+
<div class="status-text" id="status-text">SECURE</div>
|
| 1043 |
+
</div>
|
| 1044 |
+
</div>
|
| 1045 |
+
|
| 1046 |
+
<!-- Live Metrics -->
|
| 1047 |
+
<div class="card">
|
| 1048 |
+
<div class="card-header">
|
| 1049 |
+
<div class="card-title">Live Metrics</div>
|
| 1050 |
+
<i data-feather="bar-chart-2" class="card-icon"></i>
|
| 1051 |
+
</div>
|
| 1052 |
+
<div class="stats-list" id="stats-container">
|
| 1053 |
+
<div class="stat-row">
|
| 1054 |
+
<span class="stat-name">System Status</span>
|
| 1055 |
+
<span class="stat-val">Initializing</span>
|
| 1056 |
+
</div>
|
| 1057 |
+
</div>
|
| 1058 |
+
</div>
|
| 1059 |
+
|
| 1060 |
+
<!-- Actions -->
|
| 1061 |
+
<div class="card">
|
| 1062 |
+
<div class="card-header">
|
| 1063 |
+
<div class="card-title">Actions</div>
|
| 1064 |
+
</div>
|
| 1065 |
+
<button class="btn btn-primary" onclick="generateReport()">Generate Incident Report</button>
|
| 1066 |
+
<button class="btn btn-secondary" onclick="refreshAuditLog()" style="margin-top: 8px;">Refresh Audit
|
| 1067 |
+
Log</button>
|
| 1068 |
+
</div>
|
| 1069 |
+
</div>
|
| 1070 |
+
</div>
|
| 1071 |
+
|
| 1072 |
+
<!-- Report Modal -->
|
| 1073 |
+
<div id="report-modal" class="modal-overlay">
|
| 1074 |
+
<div class="modal-card">
|
| 1075 |
+
<div class="modal-title">Incident Report</div>
|
| 1076 |
+
<div class="modal-subtitle">Generated by Sentinel AI Agent // Gemini 1.5</div>
|
| 1077 |
+
<div class="report-content" id="report-content">Analyzing data stream...</div>
|
| 1078 |
+
<button class="btn btn-secondary" onclick="closeModal()">Dismiss</button>
|
| 1079 |
+
</div>
|
| 1080 |
+
</div>
|
| 1081 |
+
|
| 1082 |
+
<!-- ═══════════════════════════════════════════════
|
| 1083 |
+
JAVASCRIPT
|
| 1084 |
+
═══════════════════════════════════════════════ -->
|
| 1085 |
+
<script>
|
| 1086 |
+
feather.replace();
|
| 1087 |
+
|
| 1088 |
+
// ─── State ───
|
| 1089 |
+
let currentLayout = 'quad'; // 'quad' or 'single'
|
| 1090 |
+
let expandedFeed = 0;
|
| 1091 |
+
let isRedAlert = false;
|
| 1092 |
+
|
| 1093 |
+
// ─── Mode Switching ───
|
| 1094 |
+
const modeTitles = {
|
| 1095 |
+
'standby': 'SYSTEM STANDBY',
|
| 1096 |
+
'movement': 'MOVEMENT ANALYSIS',
|
| 1097 |
+
'facemask': 'FACEMASK DETECTION',
|
| 1098 |
+
'weapon': 'WEAPON DETECTION',
|
| 1099 |
+
'public_safety': 'PUBLIC SAFETY'
|
| 1100 |
+
};
|
| 1101 |
+
|
| 1102 |
+
function setMode(mode, element) {
|
| 1103 |
+
// Radio-style selection: always activate the clicked mode
|
| 1104 |
+
// Remove active from all module buttons
|
| 1105 |
+
document.querySelectorAll('.nav-group:first-of-type .nav-item').forEach(el => el.classList.remove('active'));
|
| 1106 |
+
|
| 1107 |
+
// Add active to the clicked button
|
| 1108 |
+
element.classList.add('active');
|
| 1109 |
+
|
| 1110 |
+
// Update UI display
|
| 1111 |
+
document.getElementById('mode-title').textContent = modeTitles[mode] || mode.toUpperCase();
|
| 1112 |
+
|
| 1113 |
+
// Send to backend
|
| 1114 |
+
fetch('/set_mode', {
|
| 1115 |
+
method: 'POST',
|
| 1116 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1117 |
+
body: JSON.stringify({ mode: mode })
|
| 1118 |
+
});
|
| 1119 |
+
}
|
| 1120 |
+
|
| 1121 |
+
function setSource(source) {
|
| 1122 |
+
fetch('/set_source', {
|
| 1123 |
+
method: 'POST',
|
| 1124 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1125 |
+
body: JSON.stringify({ source: source })
|
| 1126 |
+
})
|
| 1127 |
+
.then(r => r.json())
|
| 1128 |
+
.then(data => {
|
| 1129 |
+
if (data.success) {
|
| 1130 |
+
// Force the browser to reconnect to the restarted MJPEG stream
|
| 1131 |
+
refreshFeedStream(0);
|
| 1132 |
+
}
|
| 1133 |
+
});
|
| 1134 |
+
}
|
| 1135 |
+
|
| 1136 |
+
function handleFileUpload(input) {
|
| 1137 |
+
if (input.files[0]) {
|
| 1138 |
+
const formData = new FormData();
|
| 1139 |
+
formData.append('file', input.files[0]);
|
| 1140 |
+
|
| 1141 |
+
// Show uploading state
|
| 1142 |
+
const statusEl = document.getElementById('feed-0-status');
|
| 1143 |
+
if (statusEl) statusEl.textContent = 'UPLOADING...';
|
| 1144 |
+
|
| 1145 |
+
fetch('/upload_video', { method: 'POST', body: formData })
|
| 1146 |
+
.then(r => r.json())
|
| 1147 |
+
.then(data => {
|
| 1148 |
+
if (data.success) {
|
| 1149 |
+
// Force the browser to reconnect to the restarted MJPEG stream
|
| 1150 |
+
refreshFeedStream(0);
|
| 1151 |
+
if (statusEl) statusEl.textContent = 'FILE FEED';
|
| 1152 |
+
}
|
| 1153 |
+
})
|
| 1154 |
+
.catch(() => {
|
| 1155 |
+
if (statusEl) statusEl.textContent = 'UPLOAD FAILED';
|
| 1156 |
+
});
|
| 1157 |
+
|
| 1158 |
+
// Reset file input so the same file can be re-uploaded
|
| 1159 |
+
input.value = '';
|
| 1160 |
+
}
|
| 1161 |
+
}
|
| 1162 |
+
|
| 1163 |
+
/**
|
| 1164 |
+
* Force-refresh an MJPEG stream by setting a new src with a cache-buster.
|
| 1165 |
+
* This drops the old HTTP connection and establishes a fresh one.
|
| 1166 |
+
*/
|
| 1167 |
+
function refreshFeedStream(feedId) {
|
| 1168 |
+
const img = document.getElementById('stream-' + feedId);
|
| 1169 |
+
if (img) {
|
| 1170 |
+
// Brief blank to visually signal the switch
|
| 1171 |
+
img.src = '';
|
| 1172 |
+
// Small delay lets the backend fully initialize the new feed
|
| 1173 |
+
setTimeout(() => {
|
| 1174 |
+
img.src = '/video_feed/' + feedId + '?t=' + Date.now();
|
| 1175 |
+
}, 300);
|
| 1176 |
+
}
|
| 1177 |
+
}
|
| 1178 |
+
|
| 1179 |
+
// ─── Grid Layout ───
|
| 1180 |
+
function setGridLayout(layout) {
|
| 1181 |
+
const grid = document.getElementById('camera-grid');
|
| 1182 |
+
currentLayout = layout;
|
| 1183 |
+
|
| 1184 |
+
if (layout === 'single') {
|
| 1185 |
+
grid.classList.add('single-view');
|
| 1186 |
+
// Show only the expanded feed
|
| 1187 |
+
document.querySelectorAll('.feed-cell').forEach((cell, i) => {
|
| 1188 |
+
cell.classList.toggle('expanded', i === expandedFeed);
|
| 1189 |
+
});
|
| 1190 |
+
} else {
|
| 1191 |
+
grid.classList.remove('single-view');
|
| 1192 |
+
document.querySelectorAll('.feed-cell').forEach(cell => {
|
| 1193 |
+
cell.classList.remove('expanded');
|
| 1194 |
+
});
|
| 1195 |
+
}
|
| 1196 |
+
}
|
| 1197 |
+
|
| 1198 |
+
function expandFeed(feedId) {
|
| 1199 |
+
expandedFeed = feedId;
|
| 1200 |
+
if (currentLayout === 'single') {
|
| 1201 |
+
document.querySelectorAll('.feed-cell').forEach((cell, i) => {
|
| 1202 |
+
cell.classList.toggle('expanded', i === feedId);
|
| 1203 |
+
});
|
| 1204 |
+
}
|
| 1205 |
+
}
|
| 1206 |
+
|
| 1207 |
+
// ─── Stats & Red Alert Updates ───
|
| 1208 |
+
function updateStats() {
|
| 1209 |
+
fetch('/stats')
|
| 1210 |
+
.then(r => r.json())
|
| 1211 |
+
.then(data => {
|
| 1212 |
+
const score = data.threat_score;
|
| 1213 |
+
const scoreEl = document.getElementById('threat-score');
|
| 1214 |
+
const statusEl = document.getElementById('status-text');
|
| 1215 |
+
const ringFill = document.getElementById('score-ring-fill');
|
| 1216 |
+
const threatCard = document.getElementById('threat-card');
|
| 1217 |
+
|
| 1218 |
+
scoreEl.textContent = score;
|
| 1219 |
+
|
| 1220 |
+
// Compute ring dash offset (circumference = 2 * π * 60 ≈ 377)
|
| 1221 |
+
const circumference = 377;
|
| 1222 |
+
const offset = circumference - (circumference * score / 100);
|
| 1223 |
+
ringFill.style.strokeDashoffset = offset;
|
| 1224 |
+
|
| 1225 |
+
// Color based on score
|
| 1226 |
+
let color, status, glow;
|
| 1227 |
+
if (score >= 80) {
|
| 1228 |
+
color = '#ff2040';
|
| 1229 |
+
status = 'CRITICAL';
|
| 1230 |
+
glow = 'rgba(255, 32, 64, 0.4)';
|
| 1231 |
+
} else if (score >= 50) {
|
| 1232 |
+
color = '#ffaa00';
|
| 1233 |
+
status = 'ELEVATED';
|
| 1234 |
+
glow = 'rgba(255, 170, 0, 0.3)';
|
| 1235 |
+
} else if (score >= 25) {
|
| 1236 |
+
color = '#00d4ff';
|
| 1237 |
+
status = 'GUARDED';
|
| 1238 |
+
glow = 'rgba(0, 200, 255, 0.3)';
|
| 1239 |
+
} else {
|
| 1240 |
+
color = '#00ff88';
|
| 1241 |
+
status = 'SECURE';
|
| 1242 |
+
glow = 'rgba(0, 255, 136, 0.3)';
|
| 1243 |
+
}
|
| 1244 |
+
|
| 1245 |
+
statusEl.textContent = status;
|
| 1246 |
+
statusEl.style.color = color;
|
| 1247 |
+
statusEl.style.textShadow = `0 0 20px ${glow}`;
|
| 1248 |
+
ringFill.style.stroke = color;
|
| 1249 |
+
ringFill.style.filter = `drop-shadow(0 0 8px ${glow})`;
|
| 1250 |
+
|
| 1251 |
+
// Red Alert state
|
| 1252 |
+
const alertOverlay = document.getElementById('red-alert-overlay');
|
| 1253 |
+
const alertBanner = document.getElementById('red-alert-banner');
|
| 1254 |
+
const feedCells = document.querySelectorAll('.feed-cell');
|
| 1255 |
+
|
| 1256 |
+
if (data.red_alert) {
|
| 1257 |
+
alertOverlay.classList.add('active');
|
| 1258 |
+
alertBanner.classList.add('active');
|
| 1259 |
+
feedCells.forEach(cell => cell.classList.add('red-alert-active'));
|
| 1260 |
+
threatCard.style.borderColor = 'rgba(255, 32, 64, 0.5)';
|
| 1261 |
+
|
| 1262 |
+
if (!isRedAlert) {
|
| 1263 |
+
playAlertTone();
|
| 1264 |
+
isRedAlert = true;
|
| 1265 |
+
}
|
| 1266 |
+
} else {
|
| 1267 |
+
alertOverlay.classList.remove('active');
|
| 1268 |
+
alertBanner.classList.remove('active');
|
| 1269 |
+
feedCells.forEach(cell => cell.classList.remove('red-alert-active'));
|
| 1270 |
+
threatCard.style.borderColor = '';
|
| 1271 |
+
isRedAlert = false;
|
| 1272 |
+
}
|
| 1273 |
+
|
| 1274 |
+
// Update mode display
|
| 1275 |
+
if (data.active_modules) {
|
| 1276 |
+
updateActiveModulesDisplay(data.active_modules);
|
| 1277 |
+
}
|
| 1278 |
+
|
| 1279 |
+
// Update live metrics
|
| 1280 |
+
const container = document.getElementById('stats-container');
|
| 1281 |
+
container.innerHTML = '';
|
| 1282 |
+
|
| 1283 |
+
if (!data.details || Object.keys(data.details).length === 0) {
|
| 1284 |
+
container.innerHTML = '<div class="stat-row"><span class="stat-name">System Status</span><span class="stat-val">Standby</span></div>';
|
| 1285 |
+
} else {
|
| 1286 |
+
for (const [key, value] of Object.entries(data.details)) {
|
| 1287 |
+
const div = document.createElement('div');
|
| 1288 |
+
div.className = 'stat-row';
|
| 1289 |
+
const formattedKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
| 1290 |
+
let displayVal = value;
|
| 1291 |
+
if (typeof value === 'boolean') {
|
| 1292 |
+
displayVal = value ? '⚠ YES' : 'No';
|
| 1293 |
+
}
|
| 1294 |
+
div.innerHTML = `<span class="stat-name">${formattedKey}</span><span class="stat-val">${displayVal}</span>`;
|
| 1295 |
+
container.appendChild(div);
|
| 1296 |
+
}
|
| 1297 |
+
}
|
| 1298 |
+
})
|
| 1299 |
+
.catch(() => { });
|
| 1300 |
+
}
|
| 1301 |
+
|
| 1302 |
+
// ─── Alert Tone (Web Audio API) ───
|
| 1303 |
+
function playAlertTone() {
|
| 1304 |
+
try {
|
| 1305 |
+
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
| 1306 |
+
const oscillator = audioCtx.createOscillator();
|
| 1307 |
+
const gainNode = audioCtx.createGain();
|
| 1308 |
+
|
| 1309 |
+
oscillator.connect(gainNode);
|
| 1310 |
+
gainNode.connect(audioCtx.destination);
|
| 1311 |
+
|
| 1312 |
+
oscillator.type = 'square';
|
| 1313 |
+
oscillator.frequency.setValueAtTime(880, audioCtx.currentTime);
|
| 1314 |
+
oscillator.frequency.setValueAtTime(660, audioCtx.currentTime + 0.15);
|
| 1315 |
+
oscillator.frequency.setValueAtTime(880, audioCtx.currentTime + 0.3);
|
| 1316 |
+
|
| 1317 |
+
gainNode.gain.setValueAtTime(0.08, audioCtx.currentTime);
|
| 1318 |
+
gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.5);
|
| 1319 |
+
|
| 1320 |
+
oscillator.start(audioCtx.currentTime);
|
| 1321 |
+
oscillator.stop(audioCtx.currentTime + 0.5);
|
| 1322 |
+
} catch (e) {
|
| 1323 |
+
// Audio not available — silent fallback
|
| 1324 |
+
}
|
| 1325 |
+
}
|
| 1326 |
+
|
| 1327 |
+
// ─── AI Report ───
|
| 1328 |
+
function generateReport() {
|
| 1329 |
+
const modal = document.getElementById('report-modal');
|
| 1330 |
+
modal.classList.add('show');
|
| 1331 |
+
document.getElementById('report-content').textContent = 'ANALYZING DATA STREAM...';
|
| 1332 |
+
|
| 1333 |
+
fetch('/generate_report', { method: 'POST' })
|
| 1334 |
+
.then(r => r.json())
|
| 1335 |
+
.then(data => {
|
| 1336 |
+
document.getElementById('report-content').textContent = data.report;
|
| 1337 |
+
})
|
| 1338 |
+
.catch(() => {
|
| 1339 |
+
document.getElementById('report-content').textContent = 'ERROR: Connection to AI Agent lost.';
|
| 1340 |
+
});
|
| 1341 |
+
}
|
| 1342 |
+
|
| 1343 |
+
function closeModal() {
|
| 1344 |
+
document.getElementById('report-modal').classList.remove('show');
|
| 1345 |
+
}
|
| 1346 |
+
|
| 1347 |
+
// ─── Audit Log Refresh ───
|
| 1348 |
+
function refreshAuditLog() {
|
| 1349 |
+
fetch('/audit_log')
|
| 1350 |
+
.then(r => r.json())
|
| 1351 |
+
.then(data => {
|
| 1352 |
+
const container = document.getElementById('audit-log-container');
|
| 1353 |
+
container.innerHTML = '';
|
| 1354 |
+
|
| 1355 |
+
if (data.log.length === 0) {
|
| 1356 |
+
container.innerHTML = '<div class="audit-entry"><div class="audit-time">--:--:--</div>NO ENTRIES</div>';
|
| 1357 |
+
return;
|
| 1358 |
+
}
|
| 1359 |
+
|
| 1360 |
+
data.log.slice(0, 30).forEach(entry => {
|
| 1361 |
+
const div = document.createElement('div');
|
| 1362 |
+
div.className = `audit-entry severity-${entry.severity}`;
|
| 1363 |
+
div.innerHTML = `
|
| 1364 |
+
<div class="audit-time">${entry.timestamp.split(' ')[1] || entry.timestamp}</div>
|
| 1365 |
+
${entry.action}: ${entry.details}
|
| 1366 |
+
`;
|
| 1367 |
+
container.appendChild(div);
|
| 1368 |
+
});
|
| 1369 |
+
})
|
| 1370 |
+
.catch(() => { });
|
| 1371 |
+
}
|
| 1372 |
+
|
| 1373 |
+
// ─── Intervals ───
|
| 1374 |
+
setInterval(updateStats, 1000);
|
| 1375 |
+
setInterval(refreshAuditLog, 5000);
|
| 1376 |
+
|
| 1377 |
+
// Initial load
|
| 1378 |
+
setTimeout(refreshAuditLog, 1500);
|
| 1379 |
+
</script>
|
| 1380 |
+
</body>
|
| 1381 |
+
|
| 1382 |
+
</html>
|
templates/sentinel_dashboard_v12.html
ADDED
|
@@ -0,0 +1,1454 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>SENTINEL V11 — Intelligence Grid</title>
|
| 8 |
+
<link
|
| 9 |
+
href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700;800;900&family=Rajdhani:wght@300;400;500;600;700&family=Share+Tech+Mono&display=swap"
|
| 10 |
+
rel="stylesheet">
|
| 11 |
+
<script src="https://unpkg.com/feather-icons"></script>
|
| 12 |
+
<style>
|
| 13 |
+
/* ═══════════════════════════════════════════════
|
| 14 |
+
CSS VARIABLES — SCI-FI PALETTE
|
| 15 |
+
═══════════════════════════════════════════════ */
|
| 16 |
+
:root {
|
| 17 |
+
--bg-deep: #05080f;
|
| 18 |
+
--bg-panel: rgba(8, 16, 32, 0.85);
|
| 19 |
+
--bg-card: rgba(10, 20, 40, 0.7);
|
| 20 |
+
--border-glow: rgba(0, 200, 255, 0.15);
|
| 21 |
+
--border-glow-hover: rgba(0, 200, 255, 0.4);
|
| 22 |
+
--cyan: #00d4ff;
|
| 23 |
+
--cyan-dim: #0090aa;
|
| 24 |
+
--neon-green: #00ff88;
|
| 25 |
+
--neon-red: #ff2040;
|
| 26 |
+
--neon-amber: #ffaa00;
|
| 27 |
+
--text-primary: #e0f0ff;
|
| 28 |
+
--text-secondary: #5a8aaa;
|
| 29 |
+
--text-dim: #2a4a5a;
|
| 30 |
+
--scanline-opacity: 0.03;
|
| 31 |
+
--font-display: 'Orbitron', sans-serif;
|
| 32 |
+
--font-body: 'Rajdhani', sans-serif;
|
| 33 |
+
--font-mono: 'Share Tech Mono', monospace;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/* ═══════════════════════════════════════════════
|
| 37 |
+
BASE RESET & BODY
|
| 38 |
+
═══════════════════════════════════════════════ */
|
| 39 |
+
*,
|
| 40 |
+
*::before,
|
| 41 |
+
*::after {
|
| 42 |
+
margin: 0;
|
| 43 |
+
padding: 0;
|
| 44 |
+
box-sizing: border-box;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
body {
|
| 48 |
+
font-family: var(--font-body);
|
| 49 |
+
background: var(--bg-deep);
|
| 50 |
+
color: var(--text-primary);
|
| 51 |
+
height: 100vh;
|
| 52 |
+
overflow: hidden;
|
| 53 |
+
display: flex;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/* Scanline overlay on body */
|
| 57 |
+
body::after {
|
| 58 |
+
content: '';
|
| 59 |
+
position: fixed;
|
| 60 |
+
top: 0;
|
| 61 |
+
left: 0;
|
| 62 |
+
right: 0;
|
| 63 |
+
bottom: 0;
|
| 64 |
+
background: repeating-linear-gradient(0deg,
|
| 65 |
+
transparent,
|
| 66 |
+
transparent 2px,
|
| 67 |
+
rgba(0, 200, 255, var(--scanline-opacity)) 2px,
|
| 68 |
+
rgba(0, 200, 255, var(--scanline-opacity)) 4px);
|
| 69 |
+
pointer-events: none;
|
| 70 |
+
z-index: 9999;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
/* ═══════════════════════════════════════════════
|
| 74 |
+
RED ALERT OVERLAY
|
| 75 |
+
═══════════════════════════════════════════════ */
|
| 76 |
+
.red-alert-overlay {
|
| 77 |
+
position: fixed;
|
| 78 |
+
top: 0;
|
| 79 |
+
left: 0;
|
| 80 |
+
right: 0;
|
| 81 |
+
bottom: 0;
|
| 82 |
+
pointer-events: none;
|
| 83 |
+
z-index: 9998;
|
| 84 |
+
opacity: 0;
|
| 85 |
+
transition: opacity 0.3s ease;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.red-alert-overlay.active {
|
| 89 |
+
opacity: 1;
|
| 90 |
+
animation: red-pulse-border 1s ease-in-out infinite;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.red-alert-overlay.active::before {
|
| 94 |
+
content: '';
|
| 95 |
+
position: absolute;
|
| 96 |
+
top: 0;
|
| 97 |
+
left: 0;
|
| 98 |
+
right: 0;
|
| 99 |
+
bottom: 0;
|
| 100 |
+
border: 4px solid var(--neon-red);
|
| 101 |
+
box-shadow: inset 0 0 80px rgba(255, 32, 64, 0.15),
|
| 102 |
+
0 0 40px rgba(255, 32, 64, 0.3);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.red-alert-banner {
|
| 106 |
+
position: fixed;
|
| 107 |
+
top: 0;
|
| 108 |
+
left: 50%;
|
| 109 |
+
transform: translateX(-50%);
|
| 110 |
+
background: rgba(255, 20, 40, 0.9);
|
| 111 |
+
color: #fff;
|
| 112 |
+
font-family: var(--font-display);
|
| 113 |
+
font-size: 0.85rem;
|
| 114 |
+
font-weight: 700;
|
| 115 |
+
letter-spacing: 4px;
|
| 116 |
+
padding: 8px 40px;
|
| 117 |
+
z-index: 10000;
|
| 118 |
+
border-bottom-left-radius: 8px;
|
| 119 |
+
border-bottom-right-radius: 8px;
|
| 120 |
+
box-shadow: 0 4px 30px rgba(255, 20, 40, 0.5);
|
| 121 |
+
display: none;
|
| 122 |
+
animation: alert-flash 0.8s ease-in-out infinite;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.red-alert-banner.active {
|
| 126 |
+
display: block;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
@keyframes red-pulse-border {
|
| 130 |
+
|
| 131 |
+
0%,
|
| 132 |
+
100% {
|
| 133 |
+
opacity: 0.6;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
50% {
|
| 137 |
+
opacity: 1;
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
@keyframes alert-flash {
|
| 142 |
+
|
| 143 |
+
0%,
|
| 144 |
+
100% {
|
| 145 |
+
opacity: 1;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
50% {
|
| 149 |
+
opacity: 0.6;
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
/* ═══════════════════════════════════════════════
|
| 154 |
+
SIDEBAR
|
| 155 |
+
═══════════════════════════════════════════════ */
|
| 156 |
+
.sidebar {
|
| 157 |
+
width: 260px;
|
| 158 |
+
min-width: 260px;
|
| 159 |
+
background: var(--bg-panel);
|
| 160 |
+
backdrop-filter: blur(20px);
|
| 161 |
+
-webkit-backdrop-filter: blur(20px);
|
| 162 |
+
border-right: 1px solid var(--border-glow);
|
| 163 |
+
display: flex;
|
| 164 |
+
flex-direction: column;
|
| 165 |
+
padding: 20px;
|
| 166 |
+
z-index: 100;
|
| 167 |
+
overflow-y: auto;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.logo {
|
| 171 |
+
font-family: var(--font-display);
|
| 172 |
+
font-size: 1rem;
|
| 173 |
+
font-weight: 700;
|
| 174 |
+
margin-bottom: 8px;
|
| 175 |
+
display: flex;
|
| 176 |
+
align-items: center;
|
| 177 |
+
gap: 10px;
|
| 178 |
+
color: var(--cyan);
|
| 179 |
+
letter-spacing: 2px;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.logo svg {
|
| 183 |
+
filter: drop-shadow(0 0 6px var(--cyan));
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
.version-tag {
|
| 187 |
+
font-family: var(--font-mono);
|
| 188 |
+
font-size: 0.65rem;
|
| 189 |
+
color: var(--text-dim);
|
| 190 |
+
margin-bottom: 24px;
|
| 191 |
+
letter-spacing: 1px;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.nav-group {
|
| 195 |
+
margin-bottom: 20px;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.nav-label {
|
| 199 |
+
font-family: var(--font-display);
|
| 200 |
+
font-size: 0.6rem;
|
| 201 |
+
font-weight: 600;
|
| 202 |
+
color: var(--text-dim);
|
| 203 |
+
margin-bottom: 8px;
|
| 204 |
+
text-transform: uppercase;
|
| 205 |
+
letter-spacing: 2px;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
.nav-item {
|
| 209 |
+
padding: 10px 12px;
|
| 210 |
+
margin-bottom: 3px;
|
| 211 |
+
border-radius: 6px;
|
| 212 |
+
cursor: pointer;
|
| 213 |
+
transition: all 0.25s ease;
|
| 214 |
+
display: flex;
|
| 215 |
+
align-items: center;
|
| 216 |
+
gap: 10px;
|
| 217 |
+
color: var(--text-secondary);
|
| 218 |
+
font-size: 0.9rem;
|
| 219 |
+
font-weight: 500;
|
| 220 |
+
border: 1px solid transparent;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.nav-item:hover {
|
| 224 |
+
background: rgba(0, 200, 255, 0.05);
|
| 225 |
+
color: var(--text-primary);
|
| 226 |
+
border-color: var(--border-glow);
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.nav-item.active {
|
| 230 |
+
background: rgba(0, 200, 255, 0.1);
|
| 231 |
+
color: var(--cyan);
|
| 232 |
+
border-color: rgba(0, 200, 255, 0.3);
|
| 233 |
+
box-shadow: 0 0 15px rgba(0, 200, 255, 0.05);
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
.nav-item svg {
|
| 237 |
+
width: 16px;
|
| 238 |
+
height: 16px;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
/* Audit Log Panel in sidebar */
|
| 242 |
+
.audit-section {
|
| 243 |
+
margin-top: auto;
|
| 244 |
+
flex-shrink: 0;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
.audit-log-container {
|
| 248 |
+
max-height: 200px;
|
| 249 |
+
overflow-y: auto;
|
| 250 |
+
scrollbar-width: thin;
|
| 251 |
+
scrollbar-color: var(--cyan-dim) transparent;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
.audit-log-container::-webkit-scrollbar {
|
| 255 |
+
width: 4px;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.audit-log-container::-webkit-scrollbar-track {
|
| 259 |
+
background: transparent;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.audit-log-container::-webkit-scrollbar-thumb {
|
| 263 |
+
background: var(--cyan-dim);
|
| 264 |
+
border-radius: 2px;
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
.audit-entry {
|
| 268 |
+
font-family: var(--font-mono);
|
| 269 |
+
font-size: 0.65rem;
|
| 270 |
+
padding: 6px 8px;
|
| 271 |
+
margin-bottom: 2px;
|
| 272 |
+
border-radius: 4px;
|
| 273 |
+
background: rgba(0, 0, 0, 0.3);
|
| 274 |
+
border-left: 2px solid var(--cyan-dim);
|
| 275 |
+
line-height: 1.4;
|
| 276 |
+
color: var(--text-secondary);
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
.audit-entry.severity-WARNING {
|
| 280 |
+
border-left-color: var(--neon-amber);
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
.audit-entry.severity-CRITICAL {
|
| 284 |
+
border-left-color: var(--neon-red);
|
| 285 |
+
color: var(--neon-red);
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
.audit-time {
|
| 289 |
+
color: var(--text-dim);
|
| 290 |
+
font-size: 0.6rem;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
/* ═══════════════════════════════════════════════
|
| 294 |
+
MAIN CONTENT AREA
|
| 295 |
+
═══════════════════════════════════════════════ */
|
| 296 |
+
.main-content {
|
| 297 |
+
flex: 1;
|
| 298 |
+
padding: 16px;
|
| 299 |
+
display: grid;
|
| 300 |
+
grid-template-columns: 1fr 320px;
|
| 301 |
+
gap: 16px;
|
| 302 |
+
overflow: hidden;
|
| 303 |
+
background: radial-gradient(ellipse at 20% 0%, rgba(0, 60, 100, 0.15) 0%, transparent 60%),
|
| 304 |
+
radial-gradient(ellipse at 80% 100%, rgba(0, 40, 80, 0.1) 0%, transparent 60%),
|
| 305 |
+
var(--bg-deep);
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
/* ══════════════════════════════════════════��════
|
| 309 |
+
MULTI-CAMERA GRID
|
| 310 |
+
═══════════════════════════════════════════════ */
|
| 311 |
+
.camera-grid {
|
| 312 |
+
display: grid;
|
| 313 |
+
grid-template-columns: 1fr 1fr;
|
| 314 |
+
grid-template-rows: 1fr 1fr;
|
| 315 |
+
gap: 8px;
|
| 316 |
+
height: 100%;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
.camera-grid.single-view {
|
| 320 |
+
grid-template-columns: 1fr;
|
| 321 |
+
grid-template-rows: 1fr;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.camera-grid.single-view .feed-cell:not(.expanded) {
|
| 325 |
+
display: none;
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
.feed-cell {
|
| 329 |
+
background: var(--bg-card);
|
| 330 |
+
border-radius: 10px;
|
| 331 |
+
border: 1px solid var(--border-glow);
|
| 332 |
+
overflow: hidden;
|
| 333 |
+
position: relative;
|
| 334 |
+
cursor: pointer;
|
| 335 |
+
transition: all 0.3s ease;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.feed-cell:hover {
|
| 339 |
+
border-color: var(--border-glow-hover);
|
| 340 |
+
box-shadow: 0 0 20px rgba(0, 200, 255, 0.1);
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
.feed-cell.red-alert-active {
|
| 344 |
+
border-color: var(--neon-red) !important;
|
| 345 |
+
box-shadow: 0 0 30px rgba(255, 32, 64, 0.3) !important;
|
| 346 |
+
animation: cell-red-pulse 1.5s ease-in-out infinite;
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
@keyframes cell-red-pulse {
|
| 350 |
+
|
| 351 |
+
0%,
|
| 352 |
+
100% {
|
| 353 |
+
box-shadow: 0 0 20px rgba(255, 32, 64, 0.2);
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
50% {
|
| 357 |
+
box-shadow: 0 0 40px rgba(255, 32, 64, 0.5);
|
| 358 |
+
}
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
.feed-header {
|
| 362 |
+
position: absolute;
|
| 363 |
+
top: 8px;
|
| 364 |
+
left: 10px;
|
| 365 |
+
right: 10px;
|
| 366 |
+
display: flex;
|
| 367 |
+
justify-content: space-between;
|
| 368 |
+
align-items: center;
|
| 369 |
+
z-index: 5;
|
| 370 |
+
pointer-events: none;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
.feed-badge {
|
| 374 |
+
background: rgba(0, 0, 0, 0.7);
|
| 375 |
+
backdrop-filter: blur(8px);
|
| 376 |
+
padding: 4px 10px;
|
| 377 |
+
border-radius: 4px;
|
| 378 |
+
font-family: var(--font-mono);
|
| 379 |
+
font-size: 0.65rem;
|
| 380 |
+
font-weight: 400;
|
| 381 |
+
border: 1px solid var(--border-glow);
|
| 382 |
+
display: flex;
|
| 383 |
+
align-items: center;
|
| 384 |
+
gap: 6px;
|
| 385 |
+
color: var(--cyan);
|
| 386 |
+
letter-spacing: 1px;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
.live-dot {
|
| 390 |
+
width: 6px;
|
| 391 |
+
height: 6px;
|
| 392 |
+
background: var(--neon-red);
|
| 393 |
+
border-radius: 50%;
|
| 394 |
+
box-shadow: 0 0 8px var(--neon-red);
|
| 395 |
+
animation: pulse-dot 2s infinite;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
@keyframes pulse-dot {
|
| 399 |
+
|
| 400 |
+
0%,
|
| 401 |
+
100% {
|
| 402 |
+
opacity: 1;
|
| 403 |
+
transform: scale(1);
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
50% {
|
| 407 |
+
opacity: 0.4;
|
| 408 |
+
transform: scale(1.3);
|
| 409 |
+
}
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
.feed-status {
|
| 413 |
+
font-family: var(--font-mono);
|
| 414 |
+
font-size: 0.6rem;
|
| 415 |
+
color: var(--neon-green);
|
| 416 |
+
background: rgba(0, 0, 0, 0.6);
|
| 417 |
+
padding: 3px 8px;
|
| 418 |
+
border-radius: 4px;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
.feed-stream {
|
| 422 |
+
width: 100%;
|
| 423 |
+
height: 100%;
|
| 424 |
+
object-fit: contain;
|
| 425 |
+
background: #000;
|
| 426 |
+
display: block;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.feed-offline {
|
| 430 |
+
display: flex;
|
| 431 |
+
flex-direction: column;
|
| 432 |
+
align-items: center;
|
| 433 |
+
justify-content: center;
|
| 434 |
+
height: 100%;
|
| 435 |
+
color: var(--text-dim);
|
| 436 |
+
font-family: var(--font-mono);
|
| 437 |
+
font-size: 0.75rem;
|
| 438 |
+
letter-spacing: 1px;
|
| 439 |
+
gap: 8px;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
.feed-offline svg {
|
| 443 |
+
width: 24px;
|
| 444 |
+
height: 24px;
|
| 445 |
+
color: var(--text-dim);
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
/* Fullscreen expand button */
|
| 449 |
+
.expand-btn {
|
| 450 |
+
position: absolute;
|
| 451 |
+
bottom: 8px;
|
| 452 |
+
right: 8px;
|
| 453 |
+
background: rgba(0, 0, 0, 0.6);
|
| 454 |
+
border: 1px solid var(--border-glow);
|
| 455 |
+
color: var(--cyan);
|
| 456 |
+
width: 28px;
|
| 457 |
+
height: 28px;
|
| 458 |
+
border-radius: 4px;
|
| 459 |
+
cursor: pointer;
|
| 460 |
+
display: flex;
|
| 461 |
+
align-items: center;
|
| 462 |
+
justify-content: center;
|
| 463 |
+
transition: all 0.2s;
|
| 464 |
+
z-index: 5;
|
| 465 |
+
pointer-events: all;
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
.expand-btn:hover {
|
| 469 |
+
background: rgba(0, 200, 255, 0.15);
|
| 470 |
+
border-color: var(--cyan);
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
.expand-btn svg {
|
| 474 |
+
width: 14px;
|
| 475 |
+
height: 14px;
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
/* ═══════════════════════════════════════════════
|
| 479 |
+
INTEL PANEL (Right Column)
|
| 480 |
+
═══════════════════════════════════════════════ */
|
| 481 |
+
.intel-panel {
|
| 482 |
+
display: flex;
|
| 483 |
+
flex-direction: column;
|
| 484 |
+
gap: 12px;
|
| 485 |
+
overflow-y: auto;
|
| 486 |
+
scrollbar-width: thin;
|
| 487 |
+
scrollbar-color: var(--cyan-dim) transparent;
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
.intel-panel::-webkit-scrollbar {
|
| 491 |
+
width: 4px;
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
.intel-panel::-webkit-scrollbar-thumb {
|
| 495 |
+
background: var(--cyan-dim);
|
| 496 |
+
border-radius: 2px;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
.card {
|
| 500 |
+
background: var(--bg-card);
|
| 501 |
+
backdrop-filter: blur(15px);
|
| 502 |
+
border-radius: 10px;
|
| 503 |
+
padding: 18px;
|
| 504 |
+
border: 1px solid var(--border-glow);
|
| 505 |
+
transition: border-color 0.3s;
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
.card:hover {
|
| 509 |
+
border-color: var(--border-glow-hover);
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
.card-header {
|
| 513 |
+
display: flex;
|
| 514 |
+
justify-content: space-between;
|
| 515 |
+
align-items: center;
|
| 516 |
+
margin-bottom: 14px;
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
.card-title {
|
| 520 |
+
font-family: var(--font-display);
|
| 521 |
+
font-size: 0.65rem;
|
| 522 |
+
font-weight: 600;
|
| 523 |
+
color: var(--cyan);
|
| 524 |
+
text-transform: uppercase;
|
| 525 |
+
letter-spacing: 2px;
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
.card-icon {
|
| 529 |
+
color: var(--text-dim);
|
| 530 |
+
width: 16px;
|
| 531 |
+
height: 16px;
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
/* Threat gauge */
|
| 535 |
+
.threat-gauge {
|
| 536 |
+
display: flex;
|
| 537 |
+
flex-direction: column;
|
| 538 |
+
align-items: center;
|
| 539 |
+
padding: 10px 0;
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
.score-ring {
|
| 543 |
+
position: relative;
|
| 544 |
+
width: 130px;
|
| 545 |
+
height: 130px;
|
| 546 |
+
margin-bottom: 12px;
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
.score-ring svg {
|
| 550 |
+
width: 130px;
|
| 551 |
+
height: 130px;
|
| 552 |
+
transform: rotate(-90deg);
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
.score-ring-bg {
|
| 556 |
+
fill: none;
|
| 557 |
+
stroke: rgba(255, 255, 255, 0.05);
|
| 558 |
+
stroke-width: 6;
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
.score-ring-fill {
|
| 562 |
+
fill: none;
|
| 563 |
+
stroke: var(--neon-green);
|
| 564 |
+
stroke-width: 6;
|
| 565 |
+
stroke-linecap: round;
|
| 566 |
+
stroke-dasharray: 377;
|
| 567 |
+
stroke-dashoffset: 377;
|
| 568 |
+
transition: stroke-dashoffset 0.8s cubic-bezier(0.4, 0, 0.2, 1),
|
| 569 |
+
stroke 0.5s ease;
|
| 570 |
+
filter: drop-shadow(0 0 8px var(--neon-green));
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
.score-text {
|
| 574 |
+
position: absolute;
|
| 575 |
+
top: 50%;
|
| 576 |
+
left: 50%;
|
| 577 |
+
transform: translate(-50%, -50%);
|
| 578 |
+
text-align: center;
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
.score-value {
|
| 582 |
+
font-family: var(--font-display);
|
| 583 |
+
font-size: 2.2rem;
|
| 584 |
+
font-weight: 800;
|
| 585 |
+
line-height: 1;
|
| 586 |
+
color: var(--text-primary);
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
.score-label {
|
| 590 |
+
font-family: var(--font-mono);
|
| 591 |
+
font-size: 0.55rem;
|
| 592 |
+
color: var(--text-dim);
|
| 593 |
+
letter-spacing: 2px;
|
| 594 |
+
margin-top: 4px;
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
.status-text {
|
| 598 |
+
font-family: var(--font-display);
|
| 599 |
+
font-size: 0.85rem;
|
| 600 |
+
font-weight: 700;
|
| 601 |
+
color: var(--neon-green);
|
| 602 |
+
letter-spacing: 3px;
|
| 603 |
+
text-shadow: 0 0 20px rgba(0, 255, 136, 0.3);
|
| 604 |
+
transition: all 0.5s ease;
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
/* Stats */
|
| 608 |
+
.stats-list {
|
| 609 |
+
display: flex;
|
| 610 |
+
flex-direction: column;
|
| 611 |
+
gap: 6px;
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
.stat-row {
|
| 615 |
+
display: flex;
|
| 616 |
+
justify-content: space-between;
|
| 617 |
+
align-items: center;
|
| 618 |
+
padding: 10px 12px;
|
| 619 |
+
background: rgba(0, 200, 255, 0.03);
|
| 620 |
+
border-radius: 6px;
|
| 621 |
+
border: 1px solid rgba(0, 200, 255, 0.05);
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
.stat-name {
|
| 625 |
+
font-size: 0.8rem;
|
| 626 |
+
color: var(--text-secondary);
|
| 627 |
+
font-weight: 500;
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
.stat-val {
|
| 631 |
+
font-family: var(--font-mono);
|
| 632 |
+
font-size: 0.85rem;
|
| 633 |
+
font-weight: 600;
|
| 634 |
+
color: var(--text-primary);
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
/* Mode indicator */
|
| 638 |
+
.mode-indicator {
|
| 639 |
+
display: flex;
|
| 640 |
+
align-items: center;
|
| 641 |
+
gap: 8px;
|
| 642 |
+
padding: 10px 14px;
|
| 643 |
+
background: rgba(0, 200, 255, 0.05);
|
| 644 |
+
border-radius: 6px;
|
| 645 |
+
border: 1px solid var(--border-glow);
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
.mode-dot {
|
| 649 |
+
width: 8px;
|
| 650 |
+
height: 8px;
|
| 651 |
+
background: var(--cyan);
|
| 652 |
+
border-radius: 50%;
|
| 653 |
+
box-shadow: 0 0 10px var(--cyan);
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
.mode-name {
|
| 657 |
+
font-family: var(--font-display);
|
| 658 |
+
font-size: 0.7rem;
|
| 659 |
+
font-weight: 600;
|
| 660 |
+
letter-spacing: 2px;
|
| 661 |
+
color: var(--cyan);
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
/* Buttons */
|
| 665 |
+
.btn {
|
| 666 |
+
width: 100%;
|
| 667 |
+
padding: 12px;
|
| 668 |
+
border: 1px solid var(--border-glow);
|
| 669 |
+
border-radius: 6px;
|
| 670 |
+
font-size: 0.8rem;
|
| 671 |
+
font-weight: 600;
|
| 672 |
+
cursor: pointer;
|
| 673 |
+
transition: all 0.25s;
|
| 674 |
+
font-family: var(--font-display);
|
| 675 |
+
letter-spacing: 1px;
|
| 676 |
+
text-transform: uppercase;
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
.btn-primary {
|
| 680 |
+
background: linear-gradient(135deg, rgba(0, 200, 255, 0.2), rgba(0, 100, 200, 0.1));
|
| 681 |
+
color: var(--cyan);
|
| 682 |
+
border-color: rgba(0, 200, 255, 0.3);
|
| 683 |
+
}
|
| 684 |
+
|
| 685 |
+
.btn-primary:hover {
|
| 686 |
+
background: linear-gradient(135deg, rgba(0, 200, 255, 0.3), rgba(0, 100, 200, 0.2));
|
| 687 |
+
box-shadow: 0 0 20px rgba(0, 200, 255, 0.15);
|
| 688 |
+
transform: translateY(-1px);
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
+
.btn-secondary {
|
| 692 |
+
background: rgba(255, 255, 255, 0.03);
|
| 693 |
+
color: var(--text-secondary);
|
| 694 |
+
}
|
| 695 |
+
|
| 696 |
+
.btn-secondary:hover {
|
| 697 |
+
background: rgba(255, 255, 255, 0.08);
|
| 698 |
+
color: var(--text-primary);
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
/* ═══════════════════════════════════════════════
|
| 702 |
+
MODAL
|
| 703 |
+
═══════════════════════════════════════════════ */
|
| 704 |
+
.modal-overlay {
|
| 705 |
+
position: fixed;
|
| 706 |
+
top: 0;
|
| 707 |
+
left: 0;
|
| 708 |
+
right: 0;
|
| 709 |
+
bottom: 0;
|
| 710 |
+
background: rgba(0, 0, 0, 0.8);
|
| 711 |
+
backdrop-filter: blur(10px);
|
| 712 |
+
display: none;
|
| 713 |
+
justify-content: center;
|
| 714 |
+
align-items: center;
|
| 715 |
+
z-index: 11000;
|
| 716 |
+
opacity: 0;
|
| 717 |
+
transition: opacity 0.3s ease;
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
.modal-overlay.show {
|
| 721 |
+
display: flex;
|
| 722 |
+
opacity: 1;
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
.modal-card {
|
| 726 |
+
background: var(--bg-panel);
|
| 727 |
+
width: 520px;
|
| 728 |
+
max-width: 90vw;
|
| 729 |
+
border-radius: 12px;
|
| 730 |
+
padding: 28px;
|
| 731 |
+
border: 1px solid var(--border-glow);
|
| 732 |
+
box-shadow: 0 0 60px rgba(0, 200, 255, 0.1);
|
| 733 |
+
transform: scale(0.95);
|
| 734 |
+
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
+
.modal-overlay.show .modal-card {
|
| 738 |
+
transform: scale(1);
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
.modal-title {
|
| 742 |
+
font-family: var(--font-display);
|
| 743 |
+
font-size: 1rem;
|
| 744 |
+
font-weight: 700;
|
| 745 |
+
color: var(--cyan);
|
| 746 |
+
letter-spacing: 2px;
|
| 747 |
+
margin-bottom: 6px;
|
| 748 |
+
}
|
| 749 |
+
|
| 750 |
+
.modal-subtitle {
|
| 751 |
+
font-family: var(--font-mono);
|
| 752 |
+
font-size: 0.7rem;
|
| 753 |
+
color: var(--text-dim);
|
| 754 |
+
margin-bottom: 16px;
|
| 755 |
+
}
|
| 756 |
+
|
| 757 |
+
.report-content {
|
| 758 |
+
background: rgba(0, 0, 0, 0.5);
|
| 759 |
+
padding: 16px;
|
| 760 |
+
border-radius: 8px;
|
| 761 |
+
font-family: var(--font-mono);
|
| 762 |
+
font-size: 0.75rem;
|
| 763 |
+
color: var(--neon-green);
|
| 764 |
+
margin-bottom: 16px;
|
| 765 |
+
line-height: 1.6;
|
| 766 |
+
border: 1px solid var(--border-glow);
|
| 767 |
+
max-height: 300px;
|
| 768 |
+
overflow-y: auto;
|
| 769 |
+
white-space: pre-wrap;
|
| 770 |
+
}
|
| 771 |
+
|
| 772 |
+
/* ═══════════════════════════════════════════════
|
| 773 |
+
MOBILE RESPONSIVE
|
| 774 |
+
═══════════════════════════════════════════════ */
|
| 775 |
+
@media (max-width: 1024px) {
|
| 776 |
+
.main-content {
|
| 777 |
+
grid-template-columns: 1fr 280px;
|
| 778 |
+
}
|
| 779 |
+
}
|
| 780 |
+
|
| 781 |
+
@media (max-width: 768px) {
|
| 782 |
+
body {
|
| 783 |
+
flex-direction: column;
|
| 784 |
+
overflow-y: auto;
|
| 785 |
+
}
|
| 786 |
+
|
| 787 |
+
.sidebar {
|
| 788 |
+
width: 100%;
|
| 789 |
+
min-width: 100%;
|
| 790 |
+
flex-direction: row;
|
| 791 |
+
flex-wrap: wrap;
|
| 792 |
+
padding: 10px 16px;
|
| 793 |
+
gap: 6px;
|
| 794 |
+
order: 2;
|
| 795 |
+
border-right: none;
|
| 796 |
+
border-top: 1px solid var(--border-glow);
|
| 797 |
+
overflow-y: visible;
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
.logo {
|
| 801 |
+
width: 100%;
|
| 802 |
+
margin-bottom: 6px;
|
| 803 |
+
font-size: 0.85rem;
|
| 804 |
+
}
|
| 805 |
+
|
| 806 |
+
.version-tag {
|
| 807 |
+
display: none;
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
+
.nav-group {
|
| 811 |
+
display: flex;
|
| 812 |
+
flex-wrap: wrap;
|
| 813 |
+
gap: 4px;
|
| 814 |
+
margin-bottom: 0;
|
| 815 |
+
}
|
| 816 |
+
|
| 817 |
+
.nav-label {
|
| 818 |
+
width: 100%;
|
| 819 |
+
margin-bottom: 4px;
|
| 820 |
+
}
|
| 821 |
+
|
| 822 |
+
.nav-item {
|
| 823 |
+
flex: none;
|
| 824 |
+
padding: 8px 10px;
|
| 825 |
+
font-size: 0.75rem;
|
| 826 |
+
}
|
| 827 |
+
|
| 828 |
+
.audit-section {
|
| 829 |
+
display: none;
|
| 830 |
+
}
|
| 831 |
+
|
| 832 |
+
.main-content {
|
| 833 |
+
grid-template-columns: 1fr;
|
| 834 |
+
grid-template-rows: auto auto;
|
| 835 |
+
order: 1;
|
| 836 |
+
overflow-y: auto;
|
| 837 |
+
padding: 10px;
|
| 838 |
+
}
|
| 839 |
+
|
| 840 |
+
.camera-grid {
|
| 841 |
+
grid-template-columns: 1fr;
|
| 842 |
+
grid-template-rows: auto;
|
| 843 |
+
min-height: 250px;
|
| 844 |
+
}
|
| 845 |
+
|
| 846 |
+
.feed-cell:not(:first-child) {
|
| 847 |
+
display: none;
|
| 848 |
+
}
|
| 849 |
+
|
| 850 |
+
.intel-panel {
|
| 851 |
+
overflow-y: visible;
|
| 852 |
+
}
|
| 853 |
+
}
|
| 854 |
+
|
| 855 |
+
@media (max-width: 480px) {
|
| 856 |
+
.sidebar .nav-item {
|
| 857 |
+
font-size: 0.7rem;
|
| 858 |
+
padding: 6px 8px;
|
| 859 |
+
gap: 6px;
|
| 860 |
+
}
|
| 861 |
+
|
| 862 |
+
.sidebar .nav-item svg {
|
| 863 |
+
width: 14px;
|
| 864 |
+
height: 14px;
|
| 865 |
+
}
|
| 866 |
+
|
| 867 |
+
.score-ring {
|
| 868 |
+
width: 100px;
|
| 869 |
+
height: 100px;
|
| 870 |
+
}
|
| 871 |
+
|
| 872 |
+
.score-ring svg {
|
| 873 |
+
width: 100px;
|
| 874 |
+
height: 100px;
|
| 875 |
+
}
|
| 876 |
+
|
| 877 |
+
.score-value {
|
| 878 |
+
font-size: 1.6rem;
|
| 879 |
+
}
|
| 880 |
+
}
|
| 881 |
+
</style>
|
| 882 |
+
</head>
|
| 883 |
+
|
| 884 |
+
<body>
|
| 885 |
+
<!-- Red Alert Overlay -->
|
| 886 |
+
<div class="red-alert-overlay" id="red-alert-overlay"></div>
|
| 887 |
+
<div class="red-alert-banner" id="red-alert-banner">
|
| 888 |
+
⚠ RED ALERT — CRITICAL THREAT DETECTED ⚠
|
| 889 |
+
<button class="btn btn-secondary" onclick="resetSystem()"
|
| 890 |
+
style="width: auto; margin-top: 10px; background: rgba(0,0,0,0.5); border: 1px solid #ff2040; color: #ff2040;">RESET
|
| 891 |
+
SYSTEM</button>
|
| 892 |
+
</div>
|
| 893 |
+
|
| 894 |
+
<!-- SIDEBAR -->
|
| 895 |
+
<div class="sidebar">
|
| 896 |
+
<div class="logo">
|
| 897 |
+
<i data-feather="shield"></i>
|
| 898 |
+
SENTINEL
|
| 899 |
+
</div>
|
| 900 |
+
<div class="version-tag">V12.0 // RE-ID INTELLIGENCE</div>
|
| 901 |
+
|
| 902 |
+
<div class="nav-group">
|
| 903 |
+
<div class="nav-label">Detection Modules</div>
|
| 904 |
+
<div class="nav-item active" onclick="toggleModule('movement', this)" id="nav-movement">
|
| 905 |
+
<i data-feather="activity"></i> Movement
|
| 906 |
+
</div>
|
| 907 |
+
<div class="nav-item" onclick="toggleModule('facemask', this)" id="nav-facemask">
|
| 908 |
+
<i data-feather="eye"></i> Facemask
|
| 909 |
+
</div>
|
| 910 |
+
<div class="nav-item" onclick="toggleModule('weapon', this)" id="nav-weapon">
|
| 911 |
+
<i data-feather="crosshair"></i> Weapon
|
| 912 |
+
</div>
|
| 913 |
+
<div class="nav-item" onclick="toggleModule('public_safety', this)" id="nav-public_safety">
|
| 914 |
+
<i data-feather="users"></i> Public Safety
|
| 915 |
+
</div>
|
| 916 |
+
</div>
|
| 917 |
+
|
| 918 |
+
<div class="nav-group">
|
| 919 |
+
<div class="nav-label">Input Source</div>
|
| 920 |
+
<div class="nav-item" onclick="setSource('camera')">
|
| 921 |
+
<i data-feather="video"></i> Live Camera
|
| 922 |
+
</div>
|
| 923 |
+
<div class="nav-item" onclick="document.getElementById('upload-input').click()">
|
| 924 |
+
<i data-feather="upload-cloud"></i> Upload Video
|
| 925 |
+
</div>
|
| 926 |
+
<input type="file" id="upload-input" style="display: none" accept="video/*"
|
| 927 |
+
onchange="handleFileUpload(this)">
|
| 928 |
+
</div>
|
| 929 |
+
|
| 930 |
+
<div class="nav-group">
|
| 931 |
+
<div class="nav-label">Grid Layout</div>
|
| 932 |
+
<div class="nav-item" onclick="setGridLayout('quad')">
|
| 933 |
+
<i data-feather="grid"></i> 2×2 Grid
|
| 934 |
+
</div>
|
| 935 |
+
<div class="nav-item" onclick="setGridLayout('single')">
|
| 936 |
+
<i data-feather="maximize-2"></i> Single View
|
| 937 |
+
</div>
|
| 938 |
+
</div>
|
| 939 |
+
|
| 940 |
+
<!-- Audit Log -->
|
| 941 |
+
<div class="audit-section">
|
| 942 |
+
<div class="nav-label" style="margin-bottom: 8px;">Operator Log</div>
|
| 943 |
+
<div class="audit-log-container" id="audit-log-container">
|
| 944 |
+
<div class="audit-entry">
|
| 945 |
+
<div class="audit-time">--:--:--</div>
|
| 946 |
+
SYSTEM STANDBY
|
| 947 |
+
</div>
|
| 948 |
+
</div>
|
| 949 |
+
</div>
|
| 950 |
+
</div>
|
| 951 |
+
|
| 952 |
+
<!-- MAIN CONTENT -->
|
| 953 |
+
<div class="main-content">
|
| 954 |
+
<!-- Multi-Camera Grid -->
|
| 955 |
+
<div class="camera-grid" id="camera-grid">
|
| 956 |
+
<!-- Feed 0 — Primary -->
|
| 957 |
+
<div class="feed-cell expanded" id="feed-0" onclick="expandFeed(0)">
|
| 958 |
+
<div class="feed-header">
|
| 959 |
+
<div class="feed-badge">
|
| 960 |
+
<div class="live-dot"></div>
|
| 961 |
+
<span>FEED 01 // PRIMARY</span>
|
| 962 |
+
</div>
|
| 963 |
+
<div class="feed-status" id="feed-0-status">ACTIVE</div>
|
| 964 |
+
</div>
|
| 965 |
+
<img class="feed-stream" id="stream-0" src="/video_feed/0" alt="Feed 0">
|
| 966 |
+
<button class="expand-btn" onclick="event.stopPropagation(); expandFeed(0)" title="Expand">
|
| 967 |
+
<i data-feather="maximize-2"></i>
|
| 968 |
+
</button>
|
| 969 |
+
</div>
|
| 970 |
+
|
| 971 |
+
<!-- Feed 1 -->
|
| 972 |
+
<div class="feed-cell" id="feed-1">
|
| 973 |
+
<div class="feed-header">
|
| 974 |
+
<div class="feed-badge">
|
| 975 |
+
<div class="live-dot" style="background: var(--text-dim); box-shadow: none;"></div>
|
| 976 |
+
<span>FEED 02</span>
|
| 977 |
+
</div>
|
| 978 |
+
<div class="feed-status" id="feed-1-status" style="color: var(--text-dim);">STANDBY</div>
|
| 979 |
+
</div>
|
| 980 |
+
<div class="feed-offline" id="offline-1">
|
| 981 |
+
<i data-feather="video-off"></i>
|
| 982 |
+
NO SIGNAL
|
| 983 |
+
</div>
|
| 984 |
+
</div>
|
| 985 |
+
|
| 986 |
+
<!-- Feed 2 -->
|
| 987 |
+
<div class="feed-cell" id="feed-2">
|
| 988 |
+
<div class="feed-header">
|
| 989 |
+
<div class="feed-badge">
|
| 990 |
+
<div class="live-dot" style="background: var(--text-dim); box-shadow: none;"></div>
|
| 991 |
+
<span>FEED 03</span>
|
| 992 |
+
</div>
|
| 993 |
+
<div class="feed-status" id="feed-2-status" style="color: var(--text-dim);">STANDBY</div>
|
| 994 |
+
</div>
|
| 995 |
+
<div class="feed-offline" id="offline-2">
|
| 996 |
+
<i data-feather="video-off"></i>
|
| 997 |
+
NO SIGNAL
|
| 998 |
+
</div>
|
| 999 |
+
</div>
|
| 1000 |
+
|
| 1001 |
+
<!-- Feed 3 -->
|
| 1002 |
+
<div class="feed-cell" id="feed-3">
|
| 1003 |
+
<div class="feed-header">
|
| 1004 |
+
<div class="feed-badge">
|
| 1005 |
+
<div class="live-dot" style="background: var(--text-dim); box-shadow: none;"></div>
|
| 1006 |
+
<span>FEED 04</span>
|
| 1007 |
+
</div>
|
| 1008 |
+
<div class="feed-status" id="feed-3-status" style="color: var(--text-dim);">STANDBY</div>
|
| 1009 |
+
</div>
|
| 1010 |
+
<div class="feed-offline" id="offline-3">
|
| 1011 |
+
<i data-feather="video-off"></i>
|
| 1012 |
+
NO SIGNAL
|
| 1013 |
+
</div>
|
| 1014 |
+
</div>
|
| 1015 |
+
</div>
|
| 1016 |
+
|
| 1017 |
+
<!-- Intel Panel -->
|
| 1018 |
+
<div class="intel-panel">
|
| 1019 |
+
+ <!-- Suspect Journey -->
|
| 1020 |
+
+ <div class="card" id="journey-card">
|
| 1021 |
+
+ <div class="card-header">
|
| 1022 |
+
+ <div class="card-title">Suspect Journey</div>
|
| 1023 |
+
+ <i data-feather="map" class="card-icon"></i>
|
| 1024 |
+
+ </div>
|
| 1025 |
+
+ <div class="audit-log-container" id="journey-container" style="max-height: 250px;">
|
| 1026 |
+
+ <div class="audit-entry">
|
| 1027 |
+
+ <div class="audit-time">--:--:--</div>
|
| 1028 |
+
+ SCANNING FOR SUBJECTS...
|
| 1029 |
+
+ </div>
|
| 1030 |
+
+ </div>
|
| 1031 |
+
+ </div>
|
| 1032 |
+
+
|
| 1033 |
+
<!-- Active Mode -->
|
| 1034 |
+
<div class="card">
|
| 1035 |
+
<div class="card-header">
|
| 1036 |
+
<div class="card-title">Active Mode</div>
|
| 1037 |
+
</div>
|
| 1038 |
+
<div class="mode-indicator">
|
| 1039 |
+
<div class="mode-dot"></div>
|
| 1040 |
+
<div class="mode-name" id="mode-title">1 MODULE ACTIVE</div>
|
| 1041 |
+
</div>
|
| 1042 |
+
</div>
|
| 1043 |
+
|
| 1044 |
+
<!-- Threat Assessment -->
|
| 1045 |
+
<div class="card" id="threat-card">
|
| 1046 |
+
<div class="card-header">
|
| 1047 |
+
<div class="card-title">Threat Assessment</div>
|
| 1048 |
+
<i data-feather="alert-triangle" class="card-icon"></i>
|
| 1049 |
+
</div>
|
| 1050 |
+
<div class="threat-gauge">
|
| 1051 |
+
<div class="score-ring">
|
| 1052 |
+
<svg viewBox="0 0 130 130">
|
| 1053 |
+
<circle class="score-ring-bg" cx="65" cy="65" r="60"></circle>
|
| 1054 |
+
<circle class="score-ring-fill" id="score-ring-fill" cx="65" cy="65" r="60"></circle>
|
| 1055 |
+
</svg>
|
| 1056 |
+
<div class="score-text">
|
| 1057 |
+
<div class="score-value" id="threat-score">0</div>
|
| 1058 |
+
<div class="score-label">THREAT LEVEL</div>
|
| 1059 |
+
</div>
|
| 1060 |
+
</div>
|
| 1061 |
+
<div class="status-text" id="status-text">SECURE</div>
|
| 1062 |
+
</div>
|
| 1063 |
+
</div>
|
| 1064 |
+
|
| 1065 |
+
<!-- Live Metrics -->
|
| 1066 |
+
<div class="card">
|
| 1067 |
+
<div class="card-header">
|
| 1068 |
+
<div class="card-title">Live Metrics</div>
|
| 1069 |
+
<i data-feather="bar-chart-2" class="card-icon"></i>
|
| 1070 |
+
</div>
|
| 1071 |
+
<div class="stats-list" id="stats-container">
|
| 1072 |
+
<div class="stat-row">
|
| 1073 |
+
<span class="stat-name">System Status</span>
|
| 1074 |
+
<span class="stat-val">Initializing</span>
|
| 1075 |
+
</div>
|
| 1076 |
+
</div>
|
| 1077 |
+
</div>
|
| 1078 |
+
|
| 1079 |
+
<!-- Actions -->
|
| 1080 |
+
<div class="card">
|
| 1081 |
+
<div class="card-header">
|
| 1082 |
+
<div class="card-title">Actions</div>
|
| 1083 |
+
</div>
|
| 1084 |
+
<button class="btn btn-primary" onclick="generateReport()">Generate Incident Report</button>
|
| 1085 |
+
<button class="btn btn-secondary" onclick="refreshAuditLog()" style="margin-top: 8px;">Refresh Audit
|
| 1086 |
+
Log</button>
|
| 1087 |
+
<button class="btn btn-secondary" onclick="resetSystem()"
|
| 1088 |
+
style="margin-top: 8px; border-color: #ff2040; color: #ff2040;">⚠ SYSTEM RESET</button>
|
| 1089 |
+
</div>
|
| 1090 |
+
</div>
|
| 1091 |
+
</div>
|
| 1092 |
+
|
| 1093 |
+
<!-- Report Modal -->
|
| 1094 |
+
<div id="report-modal" class="modal-overlay">
|
| 1095 |
+
<div class="modal-card">
|
| 1096 |
+
<div class="modal-title">Incident Report</div>
|
| 1097 |
+
<div class="modal-subtitle">Generated by Sentinel AI Agent // Gemini 1.5</div>
|
| 1098 |
+
<div class="report-content" id="report-content">Analyzing data stream...</div>
|
| 1099 |
+
<button class="btn btn-secondary" onclick="closeModal()">Dismiss</button>
|
| 1100 |
+
</div>
|
| 1101 |
+
</div>
|
| 1102 |
+
|
| 1103 |
+
<!-- ═══════════════════════════════════════════════
|
| 1104 |
+
JAVASCRIPT
|
| 1105 |
+
═══════════════════════════════════════════════ -->
|
| 1106 |
+
<script>
|
| 1107 |
+
feather.replace();
|
| 1108 |
+
|
| 1109 |
+
// ─── State ───
|
| 1110 |
+
let currentLayout = 'quad'; // 'quad' or 'single'
|
| 1111 |
+
let expandedFeed = 0;
|
| 1112 |
+
let isRedAlert = false;
|
| 1113 |
+
|
| 1114 |
+
// ─── Module Toggling ───
|
| 1115 |
+
const modeTitles = {
|
| 1116 |
+
'movement': 'MOVEMENT ANALYSIS',
|
| 1117 |
+
'facemask': 'FACEMASK DETECTION',
|
| 1118 |
+
'weapon': 'WEAPON DETECTION',
|
| 1119 |
+
'public_safety': 'PUBLIC SAFETY'
|
| 1120 |
+
};
|
| 1121 |
+
|
| 1122 |
+
function toggleModule(module, buttonElement) {
|
| 1123 |
+
buttonElement.classList.toggle('active');
|
| 1124 |
+
|
| 1125 |
+
fetch('/toggle_module', {
|
| 1126 |
+
method: 'POST',
|
| 1127 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1128 |
+
body: JSON.stringify({ module: module })
|
| 1129 |
+
})
|
| 1130 |
+
.then(r => r.json())
|
| 1131 |
+
.then(data => {
|
| 1132 |
+
updateActiveModulesDisplay(data.active_modules);
|
| 1133 |
+
});
|
| 1134 |
+
}
|
| 1135 |
+
|
| 1136 |
+
function updateActiveModulesDisplay(activeModules) {
|
| 1137 |
+
const modeTitle = document.getElementById('mode-title');
|
| 1138 |
+
const count = activeModules.length;
|
| 1139 |
+
|
| 1140 |
+
if (count === 0) {
|
| 1141 |
+
modeTitle.textContent = 'NO MODULES ACTIVE';
|
| 1142 |
+
} else if (count === 1) {
|
| 1143 |
+
modeTitle.textContent = modeTitles[activeModules[0]] || activeModules[0].toUpperCase();
|
| 1144 |
+
} else {
|
| 1145 |
+
modeTitle.textContent = `${count} MODULES ACTIVE`;
|
| 1146 |
+
}
|
| 1147 |
+
}
|
| 1148 |
+
|
| 1149 |
+
|
| 1150 |
+
function setSource(source) {
|
| 1151 |
+
fetch('/set_source', {
|
| 1152 |
+
method: 'POST',
|
| 1153 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1154 |
+
body: JSON.stringify({ source: source })
|
| 1155 |
+
})
|
| 1156 |
+
.then(r => r.json())
|
| 1157 |
+
.then(data => {
|
| 1158 |
+
if (data.success) {
|
| 1159 |
+
// Force the browser to reconnect to the restarted MJPEG stream
|
| 1160 |
+
refreshFeedStream(0);
|
| 1161 |
+
}
|
| 1162 |
+
});
|
| 1163 |
+
}
|
| 1164 |
+
|
| 1165 |
+
function handleFileUpload(input) {
|
| 1166 |
+
if (input.files[0]) {
|
| 1167 |
+
const formData = new FormData();
|
| 1168 |
+
formData.append('file', input.files[0]);
|
| 1169 |
+
|
| 1170 |
+
// Show uploading state
|
| 1171 |
+
const statusEl = document.getElementById('feed-0-status');
|
| 1172 |
+
if (statusEl) statusEl.textContent = 'UPLOADING...';
|
| 1173 |
+
|
| 1174 |
+
fetch('/upload_video', { method: 'POST', body: formData })
|
| 1175 |
+
.then(r => r.json())
|
| 1176 |
+
.then(data => {
|
| 1177 |
+
if (data.success) {
|
| 1178 |
+
// Force the browser to reconnect to the restarted MJPEG stream
|
| 1179 |
+
refreshFeedStream(0);
|
| 1180 |
+
if (statusEl) statusEl.textContent = 'FILE FEED';
|
| 1181 |
+
}
|
| 1182 |
+
})
|
| 1183 |
+
.catch(() => {
|
| 1184 |
+
if (statusEl) statusEl.textContent = 'UPLOAD FAILED';
|
| 1185 |
+
});
|
| 1186 |
+
|
| 1187 |
+
// Reset file input so the same file can be re-uploaded
|
| 1188 |
+
input.value = '';
|
| 1189 |
+
}
|
| 1190 |
+
}
|
| 1191 |
+
|
| 1192 |
+
/**
|
| 1193 |
+
* Force-refresh an MJPEG stream by setting a new src with a cache-buster.
|
| 1194 |
+
* This drops the old HTTP connection and establishes a fresh one.
|
| 1195 |
+
*/
|
| 1196 |
+
function refreshFeedStream(feedId) {
|
| 1197 |
+
const img = document.getElementById('stream-' + feedId);
|
| 1198 |
+
if (img) {
|
| 1199 |
+
// Brief blank to visually signal the switch
|
| 1200 |
+
img.src = '';
|
| 1201 |
+
// Small delay lets the backend fully initialize the new feed
|
| 1202 |
+
setTimeout(() => {
|
| 1203 |
+
img.src = '/video_feed/' + feedId + '?t=' + Date.now();
|
| 1204 |
+
}, 300);
|
| 1205 |
+
}
|
| 1206 |
+
}
|
| 1207 |
+
|
| 1208 |
+
// ─── Grid Layout ───
|
| 1209 |
+
function setGridLayout(layout) {
|
| 1210 |
+
const grid = document.getElementById('camera-grid');
|
| 1211 |
+
currentLayout = layout;
|
| 1212 |
+
|
| 1213 |
+
if (layout === 'single') {
|
| 1214 |
+
grid.classList.add('single-view');
|
| 1215 |
+
// Show only the expanded feed
|
| 1216 |
+
document.querySelectorAll('.feed-cell').forEach((cell, i) => {
|
| 1217 |
+
cell.classList.toggle('expanded', i === expandedFeed);
|
| 1218 |
+
});
|
| 1219 |
+
} else {
|
| 1220 |
+
grid.classList.remove('single-view');
|
| 1221 |
+
document.querySelectorAll('.feed-cell').forEach(cell => {
|
| 1222 |
+
cell.classList.remove('expanded');
|
| 1223 |
+
});
|
| 1224 |
+
}
|
| 1225 |
+
}
|
| 1226 |
+
|
| 1227 |
+
function expandFeed(feedId) {
|
| 1228 |
+
expandedFeed = feedId;
|
| 1229 |
+
if (currentLayout === 'single') {
|
| 1230 |
+
document.querySelectorAll('.feed-cell').forEach((cell, i) => {
|
| 1231 |
+
cell.classList.toggle('expanded', i === feedId);
|
| 1232 |
+
});
|
| 1233 |
+
}
|
| 1234 |
+
}
|
| 1235 |
+
|
| 1236 |
+
// ─── Stats & Red Alert Updates ───
|
| 1237 |
+
function updateStats() {
|
| 1238 |
+
fetch('/stats')
|
| 1239 |
+
.then(r => r.json())
|
| 1240 |
+
.then(data => {
|
| 1241 |
+
const score = data.threat_score;
|
| 1242 |
+
const scoreEl = document.getElementById('threat-score');
|
| 1243 |
+
const statusEl = document.getElementById('status-text');
|
| 1244 |
+
const ringFill = document.getElementById('score-ring-fill');
|
| 1245 |
+
const threatCard = document.getElementById('threat-card');
|
| 1246 |
+
|
| 1247 |
+
scoreEl.textContent = score;
|
| 1248 |
+
|
| 1249 |
+
// Compute ring dash offset (circumference = 2 * π * 60 ≈ 377)
|
| 1250 |
+
const circumference = 377;
|
| 1251 |
+
const offset = circumference - (circumference * score / 100);
|
| 1252 |
+
ringFill.style.strokeDashoffset = offset;
|
| 1253 |
+
|
| 1254 |
+
// Color based on score
|
| 1255 |
+
let color, status, glow;
|
| 1256 |
+
if (score >= 80) {
|
| 1257 |
+
color = '#ff2040';
|
| 1258 |
+
status = 'CRITICAL';
|
| 1259 |
+
glow = 'rgba(255, 32, 64, 0.4)';
|
| 1260 |
+
} else if (score >= 50) {
|
| 1261 |
+
color = '#ffaa00';
|
| 1262 |
+
status = 'ELEVATED';
|
| 1263 |
+
glow = 'rgba(255, 170, 0, 0.3)';
|
| 1264 |
+
} else if (score >= 25) {
|
| 1265 |
+
color = '#00d4ff';
|
| 1266 |
+
status = 'GUARDED';
|
| 1267 |
+
glow = 'rgba(0, 200, 255, 0.3)';
|
| 1268 |
+
} else {
|
| 1269 |
+
color = '#00ff88';
|
| 1270 |
+
status = 'SECURE';
|
| 1271 |
+
glow = 'rgba(0, 255, 136, 0.3)';
|
| 1272 |
+
}
|
| 1273 |
+
|
| 1274 |
+
statusEl.textContent = status;
|
| 1275 |
+
statusEl.style.color = color;
|
| 1276 |
+
statusEl.style.textShadow = `0 0 20px ${glow}`;
|
| 1277 |
+
ringFill.style.stroke = color;
|
| 1278 |
+
ringFill.style.filter = `drop-shadow(0 0 8px ${glow})`;
|
| 1279 |
+
|
| 1280 |
+
updateJourneyData();
|
| 1281 |
+
|
| 1282 |
+
// Red Alert state
|
| 1283 |
+
const alertOverlay = document.getElementById('red-alert-overlay');
|
| 1284 |
+
const alertBanner = document.getElementById('red-alert-banner');
|
| 1285 |
+
const feedCells = document.querySelectorAll('.feed-cell');
|
| 1286 |
+
|
| 1287 |
+
if (data.red_alert) {
|
| 1288 |
+
alertOverlay.classList.add('active');
|
| 1289 |
+
alertBanner.classList.add('active');
|
| 1290 |
+
feedCells.forEach(cell => cell.classList.add('red-alert-active'));
|
| 1291 |
+
threatCard.style.borderColor = 'rgba(255, 32, 64, 0.5)';
|
| 1292 |
+
|
| 1293 |
+
if (!isRedAlert) {
|
| 1294 |
+
playAlertTone();
|
| 1295 |
+
isRedAlert = true;
|
| 1296 |
+
}
|
| 1297 |
+
} else {
|
| 1298 |
+
alertOverlay.classList.remove('active');
|
| 1299 |
+
alertBanner.classList.remove('active');
|
| 1300 |
+
feedCells.forEach(cell => cell.classList.remove('red-alert-active'));
|
| 1301 |
+
threatCard.style.borderColor = '';
|
| 1302 |
+
isRedAlert = false;
|
| 1303 |
+
}
|
| 1304 |
+
|
| 1305 |
+
// Update mode display
|
| 1306 |
+
if (data.active_modules) {
|
| 1307 |
+
updateActiveModulesDisplay(data.active_modules);
|
| 1308 |
+
}
|
| 1309 |
+
|
| 1310 |
+
// Update live metrics
|
| 1311 |
+
const container = document.getElementById('stats-container');
|
| 1312 |
+
container.innerHTML = '';
|
| 1313 |
+
|
| 1314 |
+
if (!data.details || Object.keys(data.details).length === 0) {
|
| 1315 |
+
container.innerHTML = '<div class="stat-row"><span class="stat-name">System Status</span><span class="stat-val">Standby</span></div>';
|
| 1316 |
+
} else {
|
| 1317 |
+
for (const [key, value] of Object.entries(data.details)) {
|
| 1318 |
+
const div = document.createElement('div');
|
| 1319 |
+
div.className = 'stat-row';
|
| 1320 |
+
const formattedKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
| 1321 |
+
let displayVal = value;
|
| 1322 |
+
if (typeof value === 'boolean') {
|
| 1323 |
+
displayVal = value ? '⚠ YES' : 'No';
|
| 1324 |
+
}
|
| 1325 |
+
div.innerHTML = `<span class="stat-name">${formattedKey}</span><span class="stat-val">${displayVal}</span>`;
|
| 1326 |
+
container.appendChild(div);
|
| 1327 |
+
}
|
| 1328 |
+
}
|
| 1329 |
+
})
|
| 1330 |
+
.catch(() => { });
|
| 1331 |
+
}
|
| 1332 |
+
|
| 1333 |
+
// ─── Alert Tone (Web Audio API) ───
|
| 1334 |
+
function playAlertTone() {
|
| 1335 |
+
try {
|
| 1336 |
+
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
| 1337 |
+
const oscillator = audioCtx.createOscillator();
|
| 1338 |
+
const gainNode = audioCtx.createGain();
|
| 1339 |
+
|
| 1340 |
+
oscillator.connect(gainNode);
|
| 1341 |
+
gainNode.connect(audioCtx.destination);
|
| 1342 |
+
|
| 1343 |
+
oscillator.type = 'square';
|
| 1344 |
+
oscillator.frequency.setValueAtTime(880, audioCtx.currentTime);
|
| 1345 |
+
oscillator.frequency.setValueAtTime(660, audioCtx.currentTime + 0.15);
|
| 1346 |
+
oscillator.frequency.setValueAtTime(880, audioCtx.currentTime + 0.3);
|
| 1347 |
+
|
| 1348 |
+
gainNode.gain.setValueAtTime(0.08, audioCtx.currentTime);
|
| 1349 |
+
gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.5);
|
| 1350 |
+
|
| 1351 |
+
oscillator.start(audioCtx.currentTime);
|
| 1352 |
+
oscillator.stop(audioCtx.currentTime + 0.5);
|
| 1353 |
+
} catch (e) {
|
| 1354 |
+
// Audio not available — silent fallback
|
| 1355 |
+
}
|
| 1356 |
+
}
|
| 1357 |
+
|
| 1358 |
+
// ─── AI Report ───
|
| 1359 |
+
function generateReport() {
|
| 1360 |
+
const modal = document.getElementById('report-modal');
|
| 1361 |
+
modal.classList.add('show');
|
| 1362 |
+
document.getElementById('report-content').textContent = 'ANALYZING DATA STREAM...';
|
| 1363 |
+
|
| 1364 |
+
fetch('/generate_report', { method: 'POST' })
|
| 1365 |
+
.then(r => r.json())
|
| 1366 |
+
.then(data => {
|
| 1367 |
+
document.getElementById('report-content').textContent = data.report;
|
| 1368 |
+
})
|
| 1369 |
+
.catch(() => {
|
| 1370 |
+
document.getElementById('report-content').textContent = 'ERROR: Connection to AI Agent lost.';
|
| 1371 |
+
});
|
| 1372 |
+
}
|
| 1373 |
+
|
| 1374 |
+
function closeModal() {
|
| 1375 |
+
document.getElementById('report-modal').classList.remove('show');
|
| 1376 |
+
}
|
| 1377 |
+
|
| 1378 |
+
// ─── Audit Log Refresh ───
|
| 1379 |
+
function refreshAuditLog() {
|
| 1380 |
+
fetch('/audit_log')
|
| 1381 |
+
.then(r => r.json())
|
| 1382 |
+
.then(data => {
|
| 1383 |
+
const container = document.getElementById('audit-log-container');
|
| 1384 |
+
container.innerHTML = '';
|
| 1385 |
+
|
| 1386 |
+
if (data.log.length === 0) {
|
| 1387 |
+
container.innerHTML = '<div class="audit-entry"><div class="audit-time">--:--:--</div>NO ENTRIES</div>';
|
| 1388 |
+
return;
|
| 1389 |
+
}
|
| 1390 |
+
|
| 1391 |
+
data.log.slice(0, 30).forEach(entry => {
|
| 1392 |
+
const div = document.createElement('div');
|
| 1393 |
+
div.className = `audit-entry severity-${entry.severity}`;
|
| 1394 |
+
div.innerHTML = `
|
| 1395 |
+
<div class="audit-time">${entry.timestamp.split(' ')[1] || entry.timestamp}</div>
|
| 1396 |
+
${entry.action}: ${entry.details}
|
| 1397 |
+
`;
|
| 1398 |
+
container.appendChild(div);
|
| 1399 |
+
});
|
| 1400 |
+
})
|
| 1401 |
+
.catch(() => { });
|
| 1402 |
+
}
|
| 1403 |
+
|
| 1404 |
+
// ─── System Reset ───
|
| 1405 |
+
function resetSystem() {
|
| 1406 |
+
if (!confirm("⚠ CONFIRM SYSTEM RESET ⚠\n\nThis will clear all tracking history, threat scores, and active alerts.\nThe system will revert to default state.")) {
|
| 1407 |
+
return;
|
| 1408 |
+
}
|
| 1409 |
+
|
| 1410 |
+
fetch('/reset_system', { method: 'POST' })
|
| 1411 |
+
.then(r => r.json())
|
| 1412 |
+
.then(data => {
|
| 1413 |
+
if (data.success) {
|
| 1414 |
+
// Reload page to refresh all UI states cleanly
|
| 1415 |
+
window.location.reload();
|
| 1416 |
+
}
|
| 1417 |
+
});
|
| 1418 |
+
}
|
| 1419 |
+
|
| 1420 |
+
function updateJourneyData() {
|
| 1421 |
+
fetch('/journey_data')
|
| 1422 |
+
.then(r => r.json())
|
| 1423 |
+
.then(data => {
|
| 1424 |
+
const container = document.getElementById('journey-container');
|
| 1425 |
+
if (data.length === 0) {
|
| 1426 |
+
container.innerHTML = '<div class="audit-entry"><div class="audit-time">--:--:--</div>SCANNING...</div>';
|
| 1427 |
+
return;
|
| 1428 |
+
}
|
| 1429 |
+
|
| 1430 |
+
let html = '';
|
| 1431 |
+
data.forEach(subject => {
|
| 1432 |
+
const historyHtml = subject.history.map(h => `F${h.feed}`).join(' → ');
|
| 1433 |
+
html += `
|
| 1434 |
+
<div class="audit-entry">
|
| 1435 |
+
<div class="audit-time">${subject.last_seen}</div>
|
| 1436 |
+
<span style="color:var(--cyan)">SUBJ #${subject.id}</span> [FEED ${subject.last_feed}]
|
| 1437 |
+
<div style="font-size: 0.6rem; color: var(--text-dim); margin-top:2px;">${historyHtml}</div>
|
| 1438 |
+
</div>
|
| 1439 |
+
`;
|
| 1440 |
+
});
|
| 1441 |
+
container.innerHTML = html;
|
| 1442 |
+
});
|
| 1443 |
+
}
|
| 1444 |
+
|
| 1445 |
+
// ─── Intervals ───
|
| 1446 |
+
setInterval(updateStats, 1000);
|
| 1447 |
+
setInterval(refreshAuditLog, 5000);
|
| 1448 |
+
|
| 1449 |
+
// Initial load
|
| 1450 |
+
setTimeout(refreshAuditLog, 1500);
|
| 1451 |
+
</script>
|
| 1452 |
+
</body>
|
| 1453 |
+
|
| 1454 |
+
</html>
|
templates/sentinel_dashboard_v13.html
ADDED
|
@@ -0,0 +1,2154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>SENTINEL V14 — Dispatch Integration</title>
|
| 8 |
+
<link
|
| 9 |
+
href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700;800;900&family=Rajdhani:wght@300;400;500;600;700&family=Share+Tech+Mono&display=swap"
|
| 10 |
+
rel="stylesheet">
|
| 11 |
+
<script src="https://unpkg.com/feather-icons"></script>
|
| 12 |
+
<style>
|
| 13 |
+
/* ═══════════════════════════════════════════════
|
| 14 |
+
CSS VARIABLES — SCI-FI PALETTE
|
| 15 |
+
═══════════════════════════════════════════════ */
|
| 16 |
+
:root {
|
| 17 |
+
--bg-deep: #05080f;
|
| 18 |
+
--bg-panel: rgba(8, 16, 32, 0.85);
|
| 19 |
+
--bg-card: rgba(10, 20, 40, 0.7);
|
| 20 |
+
--border-glow: rgba(0, 200, 255, 0.15);
|
| 21 |
+
--border-glow-hover: rgba(0, 200, 255, 0.4);
|
| 22 |
+
--cyan: #00d4ff;
|
| 23 |
+
--cyan-dim: #0090aa;
|
| 24 |
+
--neon-green: #00ff88;
|
| 25 |
+
--neon-red: #ff2040;
|
| 26 |
+
--neon-amber: #ffaa00;
|
| 27 |
+
--text-primary: #e0f0ff;
|
| 28 |
+
--text-secondary: #5a8aaa;
|
| 29 |
+
--text-dim: #2a4a5a;
|
| 30 |
+
--scanline-opacity: 0.03;
|
| 31 |
+
--font-display: 'Orbitron', sans-serif;
|
| 32 |
+
--font-body: 'Rajdhani', sans-serif;
|
| 33 |
+
--font-mono: 'Share Tech Mono', monospace;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/* ═══════════════════════════════════════════════
|
| 37 |
+
BASE RESET & BODY
|
| 38 |
+
═══════════════════════════════════════════════ */
|
| 39 |
+
*,
|
| 40 |
+
*::before,
|
| 41 |
+
*::after {
|
| 42 |
+
margin: 0;
|
| 43 |
+
padding: 0;
|
| 44 |
+
box-sizing: border-box;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
body {
|
| 48 |
+
font-family: var(--font-body);
|
| 49 |
+
background: var(--bg-deep);
|
| 50 |
+
color: var(--text-primary);
|
| 51 |
+
height: 100vh;
|
| 52 |
+
overflow: hidden;
|
| 53 |
+
display: flex;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/* Scanline overlay on body */
|
| 57 |
+
body::after {
|
| 58 |
+
content: '';
|
| 59 |
+
position: fixed;
|
| 60 |
+
top: 0;
|
| 61 |
+
left: 0;
|
| 62 |
+
right: 0;
|
| 63 |
+
bottom: 0;
|
| 64 |
+
background: repeating-linear-gradient(0deg,
|
| 65 |
+
transparent,
|
| 66 |
+
transparent 2px,
|
| 67 |
+
rgba(0, 200, 255, var(--scanline-opacity)) 2px,
|
| 68 |
+
rgba(0, 200, 255, var(--scanline-opacity)) 4px);
|
| 69 |
+
pointer-events: none;
|
| 70 |
+
z-index: 9999;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
/* ═══════════════════════════════════════════════
|
| 74 |
+
RED ALERT OVERLAY
|
| 75 |
+
═══════════════════════════════════════════════ */
|
| 76 |
+
.red-alert-overlay {
|
| 77 |
+
position: fixed;
|
| 78 |
+
top: 0;
|
| 79 |
+
left: 0;
|
| 80 |
+
right: 0;
|
| 81 |
+
bottom: 0;
|
| 82 |
+
pointer-events: none;
|
| 83 |
+
z-index: 9998;
|
| 84 |
+
opacity: 0;
|
| 85 |
+
transition: opacity 0.3s ease;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.red-alert-overlay.active {
|
| 89 |
+
opacity: 1;
|
| 90 |
+
animation: red-pulse-border 1s ease-in-out infinite;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.red-alert-overlay.active::before {
|
| 94 |
+
content: '';
|
| 95 |
+
position: absolute;
|
| 96 |
+
top: 0;
|
| 97 |
+
left: 0;
|
| 98 |
+
right: 0;
|
| 99 |
+
bottom: 0;
|
| 100 |
+
border: 4px solid var(--neon-red);
|
| 101 |
+
box-shadow: inset 0 0 80px rgba(255, 32, 64, 0.15),
|
| 102 |
+
0 0 40px rgba(255, 32, 64, 0.3);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.red-alert-banner {
|
| 106 |
+
position: fixed;
|
| 107 |
+
top: 0;
|
| 108 |
+
left: 50%;
|
| 109 |
+
transform: translateX(-50%);
|
| 110 |
+
background: rgba(255, 20, 40, 0.9);
|
| 111 |
+
color: #fff;
|
| 112 |
+
font-family: var(--font-display);
|
| 113 |
+
font-size: 0.85rem;
|
| 114 |
+
font-weight: 700;
|
| 115 |
+
letter-spacing: 4px;
|
| 116 |
+
padding: 8px 40px;
|
| 117 |
+
z-index: 10000;
|
| 118 |
+
border-bottom-left-radius: 8px;
|
| 119 |
+
border-bottom-right-radius: 8px;
|
| 120 |
+
box-shadow: 0 4px 30px rgba(255, 20, 40, 0.5);
|
| 121 |
+
display: none;
|
| 122 |
+
animation: alert-flash 0.8s ease-in-out infinite;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.red-alert-banner.active {
|
| 126 |
+
display: block;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
@keyframes red-pulse-border {
|
| 130 |
+
|
| 131 |
+
0%,
|
| 132 |
+
100% {
|
| 133 |
+
opacity: 0.6;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
50% {
|
| 137 |
+
opacity: 1;
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
@keyframes alert-flash {
|
| 142 |
+
|
| 143 |
+
0%,
|
| 144 |
+
100% {
|
| 145 |
+
opacity: 1;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
50% {
|
| 149 |
+
opacity: 0.6;
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
/* ═══════════════════════════════════════════════
|
| 154 |
+
SIDEBAR
|
| 155 |
+
═══════════════════════════════════════════════ */
|
| 156 |
+
.sidebar {
|
| 157 |
+
width: 260px;
|
| 158 |
+
min-width: 260px;
|
| 159 |
+
background: var(--bg-panel);
|
| 160 |
+
backdrop-filter: blur(20px);
|
| 161 |
+
-webkit-backdrop-filter: blur(20px);
|
| 162 |
+
border-right: 1px solid var(--border-glow);
|
| 163 |
+
display: flex;
|
| 164 |
+
flex-direction: column;
|
| 165 |
+
padding: 20px;
|
| 166 |
+
z-index: 100;
|
| 167 |
+
overflow-y: auto;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.logo {
|
| 171 |
+
font-family: var(--font-display);
|
| 172 |
+
font-size: 1rem;
|
| 173 |
+
font-weight: 700;
|
| 174 |
+
margin-bottom: 8px;
|
| 175 |
+
display: flex;
|
| 176 |
+
align-items: center;
|
| 177 |
+
gap: 10px;
|
| 178 |
+
color: var(--cyan);
|
| 179 |
+
letter-spacing: 2px;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.logo svg {
|
| 183 |
+
filter: drop-shadow(0 0 6px var(--cyan));
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
.version-tag {
|
| 187 |
+
font-family: var(--font-mono);
|
| 188 |
+
font-size: 0.65rem;
|
| 189 |
+
color: var(--text-dim);
|
| 190 |
+
margin-bottom: 24px;
|
| 191 |
+
letter-spacing: 1px;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.nav-group {
|
| 195 |
+
margin-bottom: 20px;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.nav-label {
|
| 199 |
+
font-family: var(--font-display);
|
| 200 |
+
font-size: 0.6rem;
|
| 201 |
+
font-weight: 600;
|
| 202 |
+
color: var(--text-dim);
|
| 203 |
+
margin-bottom: 8px;
|
| 204 |
+
text-transform: uppercase;
|
| 205 |
+
letter-spacing: 2px;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
.nav-item {
|
| 209 |
+
padding: 10px 12px;
|
| 210 |
+
margin-bottom: 3px;
|
| 211 |
+
border-radius: 6px;
|
| 212 |
+
cursor: pointer;
|
| 213 |
+
transition: all 0.25s ease;
|
| 214 |
+
display: flex;
|
| 215 |
+
align-items: center;
|
| 216 |
+
gap: 10px;
|
| 217 |
+
color: var(--text-secondary);
|
| 218 |
+
font-size: 0.9rem;
|
| 219 |
+
font-weight: 500;
|
| 220 |
+
border: 1px solid transparent;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.nav-item:hover {
|
| 224 |
+
background: rgba(0, 200, 255, 0.05);
|
| 225 |
+
color: var(--text-primary);
|
| 226 |
+
border-color: var(--border-glow);
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.nav-item.active {
|
| 230 |
+
background: rgba(0, 200, 255, 0.1);
|
| 231 |
+
color: var(--cyan);
|
| 232 |
+
border-color: rgba(0, 200, 255, 0.3);
|
| 233 |
+
box-shadow: 0 0 15px rgba(0, 200, 255, 0.05);
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
.nav-item svg {
|
| 237 |
+
width: 16px;
|
| 238 |
+
height: 16px;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
/* Audit Log Panel in sidebar */
|
| 242 |
+
.audit-section {
|
| 243 |
+
margin-top: auto;
|
| 244 |
+
flex-shrink: 0;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
.audit-log-container {
|
| 248 |
+
max-height: 200px;
|
| 249 |
+
overflow-y: auto;
|
| 250 |
+
scrollbar-width: thin;
|
| 251 |
+
scrollbar-color: var(--cyan-dim) transparent;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
.audit-log-container::-webkit-scrollbar {
|
| 255 |
+
width: 4px;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.audit-log-container::-webkit-scrollbar-track {
|
| 259 |
+
background: transparent;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.audit-log-container::-webkit-scrollbar-thumb {
|
| 263 |
+
background: var(--cyan-dim);
|
| 264 |
+
border-radius: 2px;
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
.audit-entry {
|
| 268 |
+
font-family: var(--font-mono);
|
| 269 |
+
font-size: 0.65rem;
|
| 270 |
+
padding: 6px 8px;
|
| 271 |
+
margin-bottom: 2px;
|
| 272 |
+
border-radius: 4px;
|
| 273 |
+
background: rgba(0, 0, 0, 0.3);
|
| 274 |
+
border-left: 2px solid var(--cyan-dim);
|
| 275 |
+
line-height: 1.4;
|
| 276 |
+
color: var(--text-secondary);
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
.audit-entry.severity-WARNING {
|
| 280 |
+
border-left-color: var(--neon-amber);
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
.audit-entry.severity-CRITICAL {
|
| 284 |
+
border-left-color: var(--neon-red);
|
| 285 |
+
color: var(--neon-red);
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
.audit-time {
|
| 289 |
+
color: var(--text-dim);
|
| 290 |
+
font-size: 0.6rem;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
/* ═══════════════════════════════════════════════
|
| 294 |
+
MAIN CONTENT AREA
|
| 295 |
+
═══════════════════════════════════════════════ */
|
| 296 |
+
.main-content {
|
| 297 |
+
flex: 1;
|
| 298 |
+
padding: 16px;
|
| 299 |
+
display: grid;
|
| 300 |
+
grid-template-columns: 1fr 320px;
|
| 301 |
+
gap: 16px;
|
| 302 |
+
overflow: hidden;
|
| 303 |
+
background: radial-gradient(ellipse at 20% 0%, rgba(0, 60, 100, 0.15) 0%, transparent 60%),
|
| 304 |
+
radial-gradient(ellipse at 80% 100%, rgba(0, 40, 80, 0.1) 0%, transparent 60%),
|
| 305 |
+
var(--bg-deep);
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
/* ═════════════════════════════════════════��═════
|
| 309 |
+
MULTI-CAMERA GRID
|
| 310 |
+
═══════════════════════════════════════════════ */
|
| 311 |
+
.camera-grid {
|
| 312 |
+
display: grid;
|
| 313 |
+
grid-template-columns: 1fr 1fr;
|
| 314 |
+
grid-template-rows: 1fr 1fr;
|
| 315 |
+
gap: 8px;
|
| 316 |
+
height: 100%;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
.camera-grid.single-view {
|
| 320 |
+
grid-template-columns: 1fr;
|
| 321 |
+
grid-template-rows: 1fr;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.camera-grid.single-view .feed-cell:not(.expanded) {
|
| 325 |
+
display: none;
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
.feed-cell {
|
| 329 |
+
background: var(--bg-card);
|
| 330 |
+
border-radius: 10px;
|
| 331 |
+
border: 1px solid var(--border-glow);
|
| 332 |
+
overflow: hidden;
|
| 333 |
+
position: relative;
|
| 334 |
+
cursor: pointer;
|
| 335 |
+
transition: all 0.3s ease;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.feed-cell:hover {
|
| 339 |
+
border-color: var(--border-glow-hover);
|
| 340 |
+
box-shadow: 0 0 20px rgba(0, 200, 255, 0.1);
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
.feed-cell.red-alert-active {
|
| 344 |
+
border-color: var(--neon-red) !important;
|
| 345 |
+
box-shadow: 0 0 30px rgba(255, 32, 64, 0.3) !important;
|
| 346 |
+
animation: cell-red-pulse 1.5s ease-in-out infinite;
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
@keyframes cell-red-pulse {
|
| 350 |
+
|
| 351 |
+
0%,
|
| 352 |
+
100% {
|
| 353 |
+
box-shadow: 0 0 20px rgba(255, 32, 64, 0.2);
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
50% {
|
| 357 |
+
box-shadow: 0 0 40px rgba(255, 32, 64, 0.5);
|
| 358 |
+
}
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
.feed-header {
|
| 362 |
+
position: absolute;
|
| 363 |
+
top: 8px;
|
| 364 |
+
left: 10px;
|
| 365 |
+
right: 10px;
|
| 366 |
+
display: flex;
|
| 367 |
+
justify-content: space-between;
|
| 368 |
+
align-items: center;
|
| 369 |
+
z-index: 5;
|
| 370 |
+
pointer-events: none;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
.feed-badge {
|
| 374 |
+
background: rgba(0, 0, 0, 0.7);
|
| 375 |
+
backdrop-filter: blur(8px);
|
| 376 |
+
padding: 4px 10px;
|
| 377 |
+
border-radius: 4px;
|
| 378 |
+
font-family: var(--font-mono);
|
| 379 |
+
font-size: 0.65rem;
|
| 380 |
+
font-weight: 400;
|
| 381 |
+
border: 1px solid var(--border-glow);
|
| 382 |
+
display: flex;
|
| 383 |
+
align-items: center;
|
| 384 |
+
gap: 6px;
|
| 385 |
+
color: var(--cyan);
|
| 386 |
+
letter-spacing: 1px;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
.live-dot {
|
| 390 |
+
width: 6px;
|
| 391 |
+
height: 6px;
|
| 392 |
+
background: var(--neon-red);
|
| 393 |
+
border-radius: 50%;
|
| 394 |
+
box-shadow: 0 0 8px var(--neon-red);
|
| 395 |
+
animation: pulse-dot 2s infinite;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
@keyframes pulse-dot {
|
| 399 |
+
|
| 400 |
+
0%,
|
| 401 |
+
100% {
|
| 402 |
+
opacity: 1;
|
| 403 |
+
transform: scale(1);
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
50% {
|
| 407 |
+
opacity: 0.4;
|
| 408 |
+
transform: scale(1.3);
|
| 409 |
+
}
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
.feed-status {
|
| 413 |
+
font-family: var(--font-mono);
|
| 414 |
+
font-size: 0.6rem;
|
| 415 |
+
color: var(--neon-green);
|
| 416 |
+
background: rgba(0, 0, 0, 0.6);
|
| 417 |
+
padding: 3px 8px;
|
| 418 |
+
border-radius: 4px;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
.feed-stream {
|
| 422 |
+
width: 100%;
|
| 423 |
+
height: 100%;
|
| 424 |
+
object-fit: contain;
|
| 425 |
+
background: #000;
|
| 426 |
+
display: block;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.feed-offline {
|
| 430 |
+
display: flex;
|
| 431 |
+
flex-direction: column;
|
| 432 |
+
align-items: center;
|
| 433 |
+
justify-content: center;
|
| 434 |
+
height: 100%;
|
| 435 |
+
color: var(--text-dim);
|
| 436 |
+
font-family: var(--font-mono);
|
| 437 |
+
font-size: 0.75rem;
|
| 438 |
+
letter-spacing: 1px;
|
| 439 |
+
gap: 8px;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
.feed-offline svg {
|
| 443 |
+
width: 24px;
|
| 444 |
+
height: 24px;
|
| 445 |
+
color: var(--text-dim);
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
/* Fullscreen expand button */
|
| 449 |
+
.expand-btn {
|
| 450 |
+
position: absolute;
|
| 451 |
+
bottom: 8px;
|
| 452 |
+
right: 8px;
|
| 453 |
+
background: rgba(0, 0, 0, 0.6);
|
| 454 |
+
border: 1px solid var(--border-glow);
|
| 455 |
+
color: var(--cyan);
|
| 456 |
+
width: 28px;
|
| 457 |
+
height: 28px;
|
| 458 |
+
border-radius: 4px;
|
| 459 |
+
cursor: pointer;
|
| 460 |
+
display: flex;
|
| 461 |
+
align-items: center;
|
| 462 |
+
justify-content: center;
|
| 463 |
+
transition: all 0.2s;
|
| 464 |
+
z-index: 5;
|
| 465 |
+
pointer-events: all;
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
.expand-btn:hover {
|
| 469 |
+
background: rgba(0, 200, 255, 0.15);
|
| 470 |
+
border-color: var(--cyan);
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
.expand-btn svg {
|
| 474 |
+
width: 14px;
|
| 475 |
+
height: 14px;
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
/* ═══════════════════════════════════════════════
|
| 479 |
+
INTEL PANEL (Right Column)
|
| 480 |
+
═══════════════════════════════════════════════ */
|
| 481 |
+
.intel-panel {
|
| 482 |
+
display: flex;
|
| 483 |
+
flex-direction: column;
|
| 484 |
+
gap: 12px;
|
| 485 |
+
overflow-y: auto;
|
| 486 |
+
scrollbar-width: thin;
|
| 487 |
+
scrollbar-color: var(--cyan-dim) transparent;
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
.intel-panel::-webkit-scrollbar {
|
| 491 |
+
width: 4px;
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
.intel-panel::-webkit-scrollbar-thumb {
|
| 495 |
+
background: var(--cyan-dim);
|
| 496 |
+
border-radius: 2px;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
.card {
|
| 500 |
+
background: var(--bg-card);
|
| 501 |
+
backdrop-filter: blur(15px);
|
| 502 |
+
border-radius: 10px;
|
| 503 |
+
padding: 18px;
|
| 504 |
+
border: 1px solid var(--border-glow);
|
| 505 |
+
transition: border-color 0.3s;
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
.card:hover {
|
| 509 |
+
border-color: var(--border-glow-hover);
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
.card-header {
|
| 513 |
+
display: flex;
|
| 514 |
+
justify-content: space-between;
|
| 515 |
+
align-items: center;
|
| 516 |
+
margin-bottom: 14px;
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
.card-title {
|
| 520 |
+
font-family: var(--font-display);
|
| 521 |
+
font-size: 0.65rem;
|
| 522 |
+
font-weight: 600;
|
| 523 |
+
color: var(--cyan);
|
| 524 |
+
text-transform: uppercase;
|
| 525 |
+
letter-spacing: 2px;
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
.card-icon {
|
| 529 |
+
color: var(--text-dim);
|
| 530 |
+
width: 16px;
|
| 531 |
+
height: 16px;
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
/* Threat gauge */
|
| 535 |
+
.threat-gauge {
|
| 536 |
+
display: flex;
|
| 537 |
+
flex-direction: column;
|
| 538 |
+
align-items: center;
|
| 539 |
+
padding: 10px 0;
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
.score-ring {
|
| 543 |
+
position: relative;
|
| 544 |
+
width: 130px;
|
| 545 |
+
height: 130px;
|
| 546 |
+
margin-bottom: 12px;
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
.score-ring svg {
|
| 550 |
+
width: 130px;
|
| 551 |
+
height: 130px;
|
| 552 |
+
transform: rotate(-90deg);
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
.score-ring-bg {
|
| 556 |
+
fill: none;
|
| 557 |
+
stroke: rgba(255, 255, 255, 0.05);
|
| 558 |
+
stroke-width: 6;
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
.score-ring-fill {
|
| 562 |
+
fill: none;
|
| 563 |
+
stroke: var(--neon-green);
|
| 564 |
+
stroke-width: 6;
|
| 565 |
+
stroke-linecap: round;
|
| 566 |
+
stroke-dasharray: 377;
|
| 567 |
+
stroke-dashoffset: 377;
|
| 568 |
+
transition: stroke-dashoffset 0.8s cubic-bezier(0.4, 0, 0.2, 1),
|
| 569 |
+
stroke 0.5s ease;
|
| 570 |
+
filter: drop-shadow(0 0 8px var(--neon-green));
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
.score-text {
|
| 574 |
+
position: absolute;
|
| 575 |
+
top: 50%;
|
| 576 |
+
left: 50%;
|
| 577 |
+
transform: translate(-50%, -50%);
|
| 578 |
+
text-align: center;
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
.score-value {
|
| 582 |
+
font-family: var(--font-display);
|
| 583 |
+
font-size: 2.2rem;
|
| 584 |
+
font-weight: 800;
|
| 585 |
+
line-height: 1;
|
| 586 |
+
color: var(--text-primary);
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
.score-label {
|
| 590 |
+
font-family: var(--font-mono);
|
| 591 |
+
font-size: 0.55rem;
|
| 592 |
+
color: var(--text-dim);
|
| 593 |
+
letter-spacing: 2px;
|
| 594 |
+
margin-top: 4px;
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
.status-text {
|
| 598 |
+
font-family: var(--font-display);
|
| 599 |
+
font-size: 0.85rem;
|
| 600 |
+
font-weight: 700;
|
| 601 |
+
color: var(--neon-green);
|
| 602 |
+
letter-spacing: 3px;
|
| 603 |
+
text-shadow: 0 0 20px rgba(0, 255, 136, 0.3);
|
| 604 |
+
transition: all 0.5s ease;
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
/* Stats */
|
| 608 |
+
.stats-list {
|
| 609 |
+
display: flex;
|
| 610 |
+
flex-direction: column;
|
| 611 |
+
gap: 6px;
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
.stat-row {
|
| 615 |
+
display: flex;
|
| 616 |
+
justify-content: space-between;
|
| 617 |
+
align-items: center;
|
| 618 |
+
padding: 10px 12px;
|
| 619 |
+
background: rgba(0, 200, 255, 0.03);
|
| 620 |
+
border-radius: 6px;
|
| 621 |
+
border: 1px solid rgba(0, 200, 255, 0.05);
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
.stat-name {
|
| 625 |
+
font-size: 0.8rem;
|
| 626 |
+
color: var(--text-secondary);
|
| 627 |
+
font-weight: 500;
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
.stat-val {
|
| 631 |
+
font-family: var(--font-mono);
|
| 632 |
+
font-size: 0.85rem;
|
| 633 |
+
font-weight: 600;
|
| 634 |
+
color: var(--text-primary);
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
/* Mode indicator */
|
| 638 |
+
.mode-indicator {
|
| 639 |
+
display: flex;
|
| 640 |
+
align-items: center;
|
| 641 |
+
gap: 8px;
|
| 642 |
+
padding: 10px 14px;
|
| 643 |
+
background: rgba(0, 200, 255, 0.05);
|
| 644 |
+
border-radius: 6px;
|
| 645 |
+
border: 1px solid var(--border-glow);
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
.mode-dot {
|
| 649 |
+
width: 8px;
|
| 650 |
+
height: 8px;
|
| 651 |
+
background: var(--cyan);
|
| 652 |
+
border-radius: 50%;
|
| 653 |
+
box-shadow: 0 0 10px var(--cyan);
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
.mode-name {
|
| 657 |
+
font-family: var(--font-display);
|
| 658 |
+
font-size: 0.7rem;
|
| 659 |
+
font-weight: 600;
|
| 660 |
+
letter-spacing: 2px;
|
| 661 |
+
color: var(--cyan);
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
/* Buttons */
|
| 665 |
+
.btn {
|
| 666 |
+
width: 100%;
|
| 667 |
+
padding: 12px;
|
| 668 |
+
border: 1px solid var(--border-glow);
|
| 669 |
+
border-radius: 6px;
|
| 670 |
+
font-size: 0.8rem;
|
| 671 |
+
font-weight: 600;
|
| 672 |
+
cursor: pointer;
|
| 673 |
+
transition: all 0.25s;
|
| 674 |
+
font-family: var(--font-display);
|
| 675 |
+
letter-spacing: 1px;
|
| 676 |
+
text-transform: uppercase;
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
.btn-primary {
|
| 680 |
+
background: linear-gradient(135deg, rgba(0, 200, 255, 0.2), rgba(0, 100, 200, 0.1));
|
| 681 |
+
color: var(--cyan);
|
| 682 |
+
border-color: rgba(0, 200, 255, 0.3);
|
| 683 |
+
}
|
| 684 |
+
|
| 685 |
+
.btn-primary:hover {
|
| 686 |
+
background: linear-gradient(135deg, rgba(0, 200, 255, 0.3), rgba(0, 100, 200, 0.2));
|
| 687 |
+
box-shadow: 0 0 20px rgba(0, 200, 255, 0.15);
|
| 688 |
+
transform: translateY(-1px);
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
+
.btn-secondary {
|
| 692 |
+
background: rgba(255, 255, 255, 0.03);
|
| 693 |
+
color: var(--text-secondary);
|
| 694 |
+
}
|
| 695 |
+
|
| 696 |
+
.btn-secondary:hover {
|
| 697 |
+
background: rgba(255, 255, 255, 0.08);
|
| 698 |
+
color: var(--text-primary);
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
/* ═══════════════════════════════════════════════
|
| 702 |
+
MODAL
|
| 703 |
+
═══════════════════════════════════════════════ */
|
| 704 |
+
.modal-overlay {
|
| 705 |
+
position: fixed;
|
| 706 |
+
top: 0;
|
| 707 |
+
left: 0;
|
| 708 |
+
right: 0;
|
| 709 |
+
bottom: 0;
|
| 710 |
+
background: rgba(0, 0, 0, 0.8);
|
| 711 |
+
backdrop-filter: blur(10px);
|
| 712 |
+
display: none;
|
| 713 |
+
justify-content: center;
|
| 714 |
+
align-items: center;
|
| 715 |
+
z-index: 11000;
|
| 716 |
+
opacity: 0;
|
| 717 |
+
transition: opacity 0.3s ease;
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
.modal-overlay.show {
|
| 721 |
+
display: flex;
|
| 722 |
+
opacity: 1;
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
.modal-card {
|
| 726 |
+
background: var(--bg-panel);
|
| 727 |
+
width: 520px;
|
| 728 |
+
max-width: 90vw;
|
| 729 |
+
border-radius: 12px;
|
| 730 |
+
padding: 28px;
|
| 731 |
+
border: 1px solid var(--border-glow);
|
| 732 |
+
box-shadow: 0 0 60px rgba(0, 200, 255, 0.1);
|
| 733 |
+
transform: scale(0.95);
|
| 734 |
+
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
+
.modal-overlay.show .modal-card {
|
| 738 |
+
transform: scale(1);
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
.modal-title {
|
| 742 |
+
font-family: var(--font-display);
|
| 743 |
+
font-size: 1rem;
|
| 744 |
+
font-weight: 700;
|
| 745 |
+
color: var(--cyan);
|
| 746 |
+
letter-spacing: 2px;
|
| 747 |
+
margin-bottom: 6px;
|
| 748 |
+
}
|
| 749 |
+
|
| 750 |
+
.modal-subtitle {
|
| 751 |
+
font-family: var(--font-mono);
|
| 752 |
+
font-size: 0.7rem;
|
| 753 |
+
color: var(--text-dim);
|
| 754 |
+
margin-bottom: 16px;
|
| 755 |
+
}
|
| 756 |
+
|
| 757 |
+
.report-content {
|
| 758 |
+
background: rgba(0, 0, 0, 0.5);
|
| 759 |
+
padding: 16px;
|
| 760 |
+
border-radius: 8px;
|
| 761 |
+
font-family: var(--font-mono);
|
| 762 |
+
font-size: 0.75rem;
|
| 763 |
+
color: var(--neon-green);
|
| 764 |
+
margin-bottom: 16px;
|
| 765 |
+
line-height: 1.6;
|
| 766 |
+
border: 1px solid var(--border-glow);
|
| 767 |
+
max-height: 300px;
|
| 768 |
+
overflow-y: auto;
|
| 769 |
+
white-space: pre-wrap;
|
| 770 |
+
}
|
| 771 |
+
|
| 772 |
+
/* ═══════════════════════════════════════════════
|
| 773 |
+
MOBILE RESPONSIVE
|
| 774 |
+
═══════════════════════════════════════════════ */
|
| 775 |
+
@media (max-width: 1024px) {
|
| 776 |
+
.main-content {
|
| 777 |
+
grid-template-columns: 1fr 280px;
|
| 778 |
+
}
|
| 779 |
+
}
|
| 780 |
+
|
| 781 |
+
@media (max-width: 768px) {
|
| 782 |
+
body {
|
| 783 |
+
flex-direction: column;
|
| 784 |
+
overflow-y: auto;
|
| 785 |
+
}
|
| 786 |
+
|
| 787 |
+
.sidebar {
|
| 788 |
+
width: 100%;
|
| 789 |
+
min-width: 100%;
|
| 790 |
+
flex-direction: row;
|
| 791 |
+
flex-wrap: wrap;
|
| 792 |
+
padding: 10px 16px;
|
| 793 |
+
gap: 6px;
|
| 794 |
+
order: 2;
|
| 795 |
+
border-right: none;
|
| 796 |
+
border-top: 1px solid var(--border-glow);
|
| 797 |
+
overflow-y: visible;
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
.logo {
|
| 801 |
+
width: 100%;
|
| 802 |
+
margin-bottom: 6px;
|
| 803 |
+
font-size: 0.85rem;
|
| 804 |
+
}
|
| 805 |
+
|
| 806 |
+
.version-tag {
|
| 807 |
+
display: none;
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
+
.nav-group {
|
| 811 |
+
display: flex;
|
| 812 |
+
flex-wrap: wrap;
|
| 813 |
+
gap: 4px;
|
| 814 |
+
margin-bottom: 0;
|
| 815 |
+
}
|
| 816 |
+
|
| 817 |
+
.nav-label {
|
| 818 |
+
width: 100%;
|
| 819 |
+
margin-bottom: 4px;
|
| 820 |
+
}
|
| 821 |
+
|
| 822 |
+
.nav-item {
|
| 823 |
+
flex: none;
|
| 824 |
+
padding: 8px 10px;
|
| 825 |
+
font-size: 0.75rem;
|
| 826 |
+
}
|
| 827 |
+
|
| 828 |
+
.audit-section {
|
| 829 |
+
display: none;
|
| 830 |
+
}
|
| 831 |
+
|
| 832 |
+
.main-content {
|
| 833 |
+
grid-template-columns: 1fr;
|
| 834 |
+
grid-template-rows: auto auto;
|
| 835 |
+
order: 1;
|
| 836 |
+
overflow-y: auto;
|
| 837 |
+
padding: 10px;
|
| 838 |
+
}
|
| 839 |
+
|
| 840 |
+
.camera-grid {
|
| 841 |
+
grid-template-columns: 1fr;
|
| 842 |
+
grid-template-rows: auto;
|
| 843 |
+
min-height: 250px;
|
| 844 |
+
}
|
| 845 |
+
|
| 846 |
+
.feed-cell:not(:first-child) {
|
| 847 |
+
display: none;
|
| 848 |
+
}
|
| 849 |
+
|
| 850 |
+
.intel-panel {
|
| 851 |
+
overflow-y: visible;
|
| 852 |
+
}
|
| 853 |
+
}
|
| 854 |
+
|
| 855 |
+
@media (max-width: 480px) {
|
| 856 |
+
.sidebar .nav-item {
|
| 857 |
+
font-size: 0.7rem;
|
| 858 |
+
padding: 6px 8px;
|
| 859 |
+
gap: 6px;
|
| 860 |
+
}
|
| 861 |
+
|
| 862 |
+
.sidebar .nav-item svg {
|
| 863 |
+
width: 14px;
|
| 864 |
+
height: 14px;
|
| 865 |
+
}
|
| 866 |
+
|
| 867 |
+
.score-ring {
|
| 868 |
+
width: 100px;
|
| 869 |
+
height: 100px;
|
| 870 |
+
}
|
| 871 |
+
|
| 872 |
+
.score-ring svg {
|
| 873 |
+
width: 100px;
|
| 874 |
+
height: 100px;
|
| 875 |
+
}
|
| 876 |
+
|
| 877 |
+
.score-value {
|
| 878 |
+
font-size: 1.6rem;
|
| 879 |
+
}
|
| 880 |
+
}
|
| 881 |
+
|
| 882 |
+
/* ═══════════════════════════════════════════════
|
| 883 |
+
DISPATCH CENTER STYLES
|
| 884 |
+
═══════════════════════════════════════════════ */
|
| 885 |
+
.dispatch-badge {
|
| 886 |
+
display: inline-block;
|
| 887 |
+
padding: 2px 8px;
|
| 888 |
+
border-radius: 3px;
|
| 889 |
+
font-family: var(--font-mono);
|
| 890 |
+
font-size: 0.55rem;
|
| 891 |
+
font-weight: 600;
|
| 892 |
+
letter-spacing: 1px;
|
| 893 |
+
text-transform: uppercase;
|
| 894 |
+
}
|
| 895 |
+
|
| 896 |
+
.dispatch-badge.sent {
|
| 897 |
+
background: rgba(0, 255, 136, 0.15);
|
| 898 |
+
color: var(--neon-green);
|
| 899 |
+
border: 1px solid rgba(0, 255, 136, 0.3);
|
| 900 |
+
}
|
| 901 |
+
|
| 902 |
+
.dispatch-badge.pending {
|
| 903 |
+
background: rgba(255, 170, 0, 0.15);
|
| 904 |
+
color: var(--neon-amber);
|
| 905 |
+
border: 1px solid rgba(255, 170, 0, 0.3);
|
| 906 |
+
animation: pulse-dot 2s infinite;
|
| 907 |
+
}
|
| 908 |
+
|
| 909 |
+
.dispatch-badge.rejected {
|
| 910 |
+
background: rgba(255, 32, 64, 0.15);
|
| 911 |
+
color: var(--neon-red);
|
| 912 |
+
border: 1px solid rgba(255, 32, 64, 0.3);
|
| 913 |
+
}
|
| 914 |
+
|
| 915 |
+
.dispatch-badge.failed {
|
| 916 |
+
background: rgba(255, 32, 64, 0.1);
|
| 917 |
+
color: #ff6080;
|
| 918 |
+
border: 1px solid rgba(255, 96, 128, 0.3);
|
| 919 |
+
}
|
| 920 |
+
|
| 921 |
+
.dispatch-entry {
|
| 922 |
+
font-family: var(--font-mono);
|
| 923 |
+
font-size: 0.65rem;
|
| 924 |
+
padding: 8px 10px;
|
| 925 |
+
margin-bottom: 4px;
|
| 926 |
+
border-radius: 4px;
|
| 927 |
+
background: rgba(0, 0, 0, 0.3);
|
| 928 |
+
border-left: 2px solid var(--cyan-dim);
|
| 929 |
+
line-height: 1.5;
|
| 930 |
+
color: var(--text-secondary);
|
| 931 |
+
display: flex;
|
| 932 |
+
justify-content: space-between;
|
| 933 |
+
align-items: flex-start;
|
| 934 |
+
gap: 8px;
|
| 935 |
+
}
|
| 936 |
+
|
| 937 |
+
.dispatch-entry.pending-entry {
|
| 938 |
+
border-left-color: var(--neon-amber);
|
| 939 |
+
background: rgba(255, 170, 0, 0.05);
|
| 940 |
+
}
|
| 941 |
+
|
| 942 |
+
.dispatch-entry .dispatch-info {
|
| 943 |
+
flex: 1;
|
| 944 |
+
}
|
| 945 |
+
|
| 946 |
+
.dispatch-entry .dispatch-score {
|
| 947 |
+
font-family: var(--font-display);
|
| 948 |
+
font-size: 0.8rem;
|
| 949 |
+
font-weight: 700;
|
| 950 |
+
min-width: 30px;
|
| 951 |
+
text-align: right;
|
| 952 |
+
}
|
| 953 |
+
|
| 954 |
+
.dispatch-actions {
|
| 955 |
+
display: flex;
|
| 956 |
+
gap: 4px;
|
| 957 |
+
margin-top: 6px;
|
| 958 |
+
}
|
| 959 |
+
|
| 960 |
+
.dispatch-actions button {
|
| 961 |
+
padding: 4px 12px;
|
| 962 |
+
border-radius: 4px;
|
| 963 |
+
font-family: var(--font-display);
|
| 964 |
+
font-size: 0.6rem;
|
| 965 |
+
font-weight: 600;
|
| 966 |
+
letter-spacing: 1px;
|
| 967 |
+
cursor: pointer;
|
| 968 |
+
transition: all 0.2s;
|
| 969 |
+
border: 1px solid;
|
| 970 |
+
}
|
| 971 |
+
|
| 972 |
+
.btn-approve {
|
| 973 |
+
background: rgba(0, 255, 136, 0.1);
|
| 974 |
+
border-color: rgba(0, 255, 136, 0.4) !important;
|
| 975 |
+
color: var(--neon-green);
|
| 976 |
+
}
|
| 977 |
+
|
| 978 |
+
.btn-approve:hover {
|
| 979 |
+
background: rgba(0, 255, 136, 0.25);
|
| 980 |
+
box-shadow: 0 0 12px rgba(0, 255, 136, 0.2);
|
| 981 |
+
}
|
| 982 |
+
|
| 983 |
+
.btn-reject {
|
| 984 |
+
background: rgba(255, 32, 64, 0.1);
|
| 985 |
+
border-color: rgba(255, 32, 64, 0.4) !important;
|
| 986 |
+
color: var(--neon-red);
|
| 987 |
+
}
|
| 988 |
+
|
| 989 |
+
.btn-reject:hover {
|
| 990 |
+
background: rgba(255, 32, 64, 0.25);
|
| 991 |
+
box-shadow: 0 0 12px rgba(255, 32, 64, 0.2);
|
| 992 |
+
}
|
| 993 |
+
|
| 994 |
+
.dispatch-config-row {
|
| 995 |
+
display: flex;
|
| 996 |
+
justify-content: space-between;
|
| 997 |
+
align-items: center;
|
| 998 |
+
padding: 10px 0;
|
| 999 |
+
border-bottom: 1px solid rgba(0, 200, 255, 0.05);
|
| 1000 |
+
}
|
| 1001 |
+
|
| 1002 |
+
.dispatch-config-row:last-child {
|
| 1003 |
+
border-bottom: none;
|
| 1004 |
+
}
|
| 1005 |
+
|
| 1006 |
+
.dispatch-config-label {
|
| 1007 |
+
font-size: 0.8rem;
|
| 1008 |
+
color: var(--text-secondary);
|
| 1009 |
+
font-weight: 500;
|
| 1010 |
+
}
|
| 1011 |
+
|
| 1012 |
+
.toggle-switch {
|
| 1013 |
+
position: relative;
|
| 1014 |
+
width: 42px;
|
| 1015 |
+
height: 22px;
|
| 1016 |
+
cursor: pointer;
|
| 1017 |
+
}
|
| 1018 |
+
|
| 1019 |
+
.toggle-switch input {
|
| 1020 |
+
opacity: 0;
|
| 1021 |
+
width: 0;
|
| 1022 |
+
height: 0;
|
| 1023 |
+
}
|
| 1024 |
+
|
| 1025 |
+
.toggle-slider {
|
| 1026 |
+
position: absolute;
|
| 1027 |
+
top: 0;
|
| 1028 |
+
left: 0;
|
| 1029 |
+
right: 0;
|
| 1030 |
+
bottom: 0;
|
| 1031 |
+
background: rgba(255, 255, 255, 0.1);
|
| 1032 |
+
border-radius: 22px;
|
| 1033 |
+
transition: 0.3s;
|
| 1034 |
+
border: 1px solid rgba(255, 255, 255, 0.15);
|
| 1035 |
+
}
|
| 1036 |
+
|
| 1037 |
+
.toggle-slider::before {
|
| 1038 |
+
content: '';
|
| 1039 |
+
position: absolute;
|
| 1040 |
+
width: 16px;
|
| 1041 |
+
height: 16px;
|
| 1042 |
+
left: 2px;
|
| 1043 |
+
bottom: 2px;
|
| 1044 |
+
background: var(--text-secondary);
|
| 1045 |
+
border-radius: 50%;
|
| 1046 |
+
transition: 0.3s;
|
| 1047 |
+
}
|
| 1048 |
+
|
| 1049 |
+
.toggle-switch input:checked+.toggle-slider {
|
| 1050 |
+
background: rgba(0, 200, 255, 0.3);
|
| 1051 |
+
border-color: var(--cyan);
|
| 1052 |
+
}
|
| 1053 |
+
|
| 1054 |
+
.toggle-switch input:checked+.toggle-slider::before {
|
| 1055 |
+
transform: translateX(20px);
|
| 1056 |
+
background: var(--cyan);
|
| 1057 |
+
}
|
| 1058 |
+
|
| 1059 |
+
.config-input {
|
| 1060 |
+
background: rgba(0, 0, 0, 0.4);
|
| 1061 |
+
border: 1px solid var(--border-glow);
|
| 1062 |
+
border-radius: 4px;
|
| 1063 |
+
color: var(--text-primary);
|
| 1064 |
+
font-family: var(--font-mono);
|
| 1065 |
+
font-size: 0.75rem;
|
| 1066 |
+
padding: 6px 10px;
|
| 1067 |
+
width: 100%;
|
| 1068 |
+
transition: border-color 0.3s;
|
| 1069 |
+
}
|
| 1070 |
+
|
| 1071 |
+
.config-input:focus {
|
| 1072 |
+
outline: none;
|
| 1073 |
+
border-color: var(--cyan);
|
| 1074 |
+
box-shadow: 0 0 8px rgba(0, 200, 255, 0.15);
|
| 1075 |
+
}
|
| 1076 |
+
|
| 1077 |
+
.config-input::placeholder {
|
| 1078 |
+
color: var(--text-dim);
|
| 1079 |
+
}
|
| 1080 |
+
|
| 1081 |
+
.cooldown-slider {
|
| 1082 |
+
-webkit-appearance: none;
|
| 1083 |
+
appearance: none;
|
| 1084 |
+
width: 100%;
|
| 1085 |
+
height: 4px;
|
| 1086 |
+
border-radius: 2px;
|
| 1087 |
+
background: rgba(255, 255, 255, 0.1);
|
| 1088 |
+
outline: none;
|
| 1089 |
+
margin: 8px 0;
|
| 1090 |
+
}
|
| 1091 |
+
|
| 1092 |
+
.cooldown-slider::-webkit-slider-thumb {
|
| 1093 |
+
-webkit-appearance: none;
|
| 1094 |
+
width: 14px;
|
| 1095 |
+
height: 14px;
|
| 1096 |
+
border-radius: 50%;
|
| 1097 |
+
background: var(--cyan);
|
| 1098 |
+
cursor: pointer;
|
| 1099 |
+
box-shadow: 0 0 8px rgba(0, 200, 255, 0.4);
|
| 1100 |
+
}
|
| 1101 |
+
|
| 1102 |
+
.pending-count-badge {
|
| 1103 |
+
background: var(--neon-amber);
|
| 1104 |
+
color: #000;
|
| 1105 |
+
font-family: var(--font-mono);
|
| 1106 |
+
font-size: 0.55rem;
|
| 1107 |
+
font-weight: 700;
|
| 1108 |
+
width: 16px;
|
| 1109 |
+
height: 16px;
|
| 1110 |
+
border-radius: 50%;
|
| 1111 |
+
display: none;
|
| 1112 |
+
align-items: center;
|
| 1113 |
+
justify-content: center;
|
| 1114 |
+
margin-left: auto;
|
| 1115 |
+
}
|
| 1116 |
+
|
| 1117 |
+
.pending-count-badge.visible {
|
| 1118 |
+
display: flex;
|
| 1119 |
+
}
|
| 1120 |
+
|
| 1121 |
+
.dispatch-status-bar {
|
| 1122 |
+
display: flex;
|
| 1123 |
+
align-items: center;
|
| 1124 |
+
gap: 6px;
|
| 1125 |
+
padding: 6px 10px;
|
| 1126 |
+
border-radius: 4px;
|
| 1127 |
+
font-family: var(--font-mono);
|
| 1128 |
+
font-size: 0.6rem;
|
| 1129 |
+
margin-bottom: 8px;
|
| 1130 |
+
}
|
| 1131 |
+
|
| 1132 |
+
.dispatch-status-bar.configured {
|
| 1133 |
+
background: rgba(0, 255, 136, 0.05);
|
| 1134 |
+
border: 1px solid rgba(0, 255, 136, 0.15);
|
| 1135 |
+
color: var(--neon-green);
|
| 1136 |
+
}
|
| 1137 |
+
|
| 1138 |
+
.dispatch-status-bar.not-configured {
|
| 1139 |
+
background: rgba(255, 170, 0, 0.05);
|
| 1140 |
+
border: 1px solid rgba(255, 170, 0, 0.15);
|
| 1141 |
+
color: var(--neon-amber);
|
| 1142 |
+
}
|
| 1143 |
+
|
| 1144 |
+
.dispatch-dot {
|
| 1145 |
+
width: 6px;
|
| 1146 |
+
height: 6px;
|
| 1147 |
+
border-radius: 50%;
|
| 1148 |
+
}
|
| 1149 |
+
|
| 1150 |
+
.dispatch-dot.online {
|
| 1151 |
+
background: var(--neon-green);
|
| 1152 |
+
box-shadow: 0 0 6px var(--neon-green);
|
| 1153 |
+
}
|
| 1154 |
+
|
| 1155 |
+
.dispatch-dot.offline {
|
| 1156 |
+
background: var(--neon-amber);
|
| 1157 |
+
box-shadow: 0 0 6px var(--neon-amber);
|
| 1158 |
+
}
|
| 1159 |
+
</style>
|
| 1160 |
+
</head>
|
| 1161 |
+
|
| 1162 |
+
<body>
|
| 1163 |
+
<!-- Red Alert Overlay -->
|
| 1164 |
+
<div class="red-alert-overlay" id="red-alert-overlay"></div>
|
| 1165 |
+
<div class="red-alert-banner" id="red-alert-banner">
|
| 1166 |
+
⚠ RED ALERT — CRITICAL THREAT DETECTED ⚠
|
| 1167 |
+
<button class="btn btn-secondary" onclick="resetSystem()"
|
| 1168 |
+
style="width: auto; margin-top: 10px; background: rgba(0,0,0,0.5); border: 1px solid #ff2040; color: #ff2040;">RESET
|
| 1169 |
+
SYSTEM</button>
|
| 1170 |
+
</div>
|
| 1171 |
+
|
| 1172 |
+
<!-- SIDEBAR -->
|
| 1173 |
+
<div class="sidebar">
|
| 1174 |
+
<div class="logo">
|
| 1175 |
+
<i data-feather="shield"></i>
|
| 1176 |
+
SENTINEL
|
| 1177 |
+
</div>
|
| 1178 |
+
<div class="version-tag">V14.0 // DISPATCH INTEGRATION</div>
|
| 1179 |
+
|
| 1180 |
+
<div class="nav-group">
|
| 1181 |
+
<div class="nav-label">Detection Modules</div>
|
| 1182 |
+
<div class="nav-item active" onclick="toggleModule('movement', this)" id="nav-movement">
|
| 1183 |
+
<i data-feather="activity"></i> Movement
|
| 1184 |
+
</div>
|
| 1185 |
+
<div class="nav-item" onclick="toggleModule('facemask', this)" id="nav-facemask">
|
| 1186 |
+
<i data-feather="eye"></i> Facemask
|
| 1187 |
+
</div>
|
| 1188 |
+
<div class="nav-item" onclick="toggleModule('weapon', this)" id="nav-weapon">
|
| 1189 |
+
<i data-feather="crosshair"></i> Weapon
|
| 1190 |
+
</div>
|
| 1191 |
+
<div class="nav-item" onclick="toggleModule('public_safety', this)" id="nav-public_safety">
|
| 1192 |
+
<i data-feather="users"></i> Public Safety
|
| 1193 |
+
</div>
|
| 1194 |
+
<div class="nav-item" onclick="toggleModule('suspect_journey', this)" id="nav-suspect_journey"
|
| 1195 |
+
style="border-color: rgba(255,255,0,0.3);">
|
| 1196 |
+
<i data-feather="map-pin"></i> Suspect Journey
|
| 1197 |
+
</div>
|
| 1198 |
+
</div>
|
| 1199 |
+
|
| 1200 |
+
<div class="nav-group">
|
| 1201 |
+
<div class="nav-label">Dispatch</div>
|
| 1202 |
+
<div class="nav-item" onclick="openDispatchSettings()" id="nav-dispatch">
|
| 1203 |
+
<i data-feather="bell"></i> Dispatch Settings
|
| 1204 |
+
<span class="pending-count-badge" id="pending-badge">0</span>
|
| 1205 |
+
</div>
|
| 1206 |
+
</div>
|
| 1207 |
+
|
| 1208 |
+
<div class="nav-group">
|
| 1209 |
+
<div class="nav-label">Grid Layout</div>
|
| 1210 |
+
<div class="nav-item" onclick="setGridLayout('quad')">
|
| 1211 |
+
<i data-feather="grid"></i> 2×2 Grid
|
| 1212 |
+
</div>
|
| 1213 |
+
<div class="nav-item" onclick="setGridLayout('single')">
|
| 1214 |
+
<i data-feather="maximize-2"></i> Single View
|
| 1215 |
+
</div>
|
| 1216 |
+
</div>
|
| 1217 |
+
|
| 1218 |
+
<!-- Audit Log -->
|
| 1219 |
+
<div class="audit-section">
|
| 1220 |
+
<div class="nav-label" style="margin-bottom: 8px;">Operator Log</div>
|
| 1221 |
+
<div class="audit-log-container" id="audit-log-container">
|
| 1222 |
+
<div class="audit-entry">
|
| 1223 |
+
<div class="audit-time">--:--:--</div>
|
| 1224 |
+
SYSTEM STANDBY
|
| 1225 |
+
</div>
|
| 1226 |
+
</div>
|
| 1227 |
+
</div>
|
| 1228 |
+
</div>
|
| 1229 |
+
|
| 1230 |
+
<!-- MAIN CONTENT -->
|
| 1231 |
+
<div class="main-content">
|
| 1232 |
+
<!-- Multi-Camera Grid -->
|
| 1233 |
+
<div class="camera-grid" id="camera-grid">
|
| 1234 |
+
<!-- Feed 0 — Primary -->
|
| 1235 |
+
<div class="feed-cell expanded" id="feed-0" onclick="expandFeed(0)">
|
| 1236 |
+
<div class="feed-header">
|
| 1237 |
+
<div class="feed-badge">
|
| 1238 |
+
<div class="live-dot"></div>
|
| 1239 |
+
<span>FEED 01 // PRIMARY</span>
|
| 1240 |
+
</div>
|
| 1241 |
+
<div class="feed-status" id="feed-0-status">ACTIVE</div>
|
| 1242 |
+
</div>
|
| 1243 |
+
<img class="feed-stream" id="stream-0" src="/video_feed/0" alt="Feed 0">
|
| 1244 |
+
<button class="expand-btn" onclick="event.stopPropagation(); expandFeed(0)" title="Expand">
|
| 1245 |
+
<i data-feather="maximize-2"></i>
|
| 1246 |
+
</button>
|
| 1247 |
+
<button class="expand-btn"
|
| 1248 |
+
style="right:40px; background: rgba(0,200,255,0.2); border-color: var(--cyan);"
|
| 1249 |
+
onclick="event.stopPropagation(); triggerFeedUpload(0)" title="Upload to this feed">
|
| 1250 |
+
<i data-feather="upload-cloud"></i>
|
| 1251 |
+
</button>
|
| 1252 |
+
</div>
|
| 1253 |
+
|
| 1254 |
+
<!-- Feed 1 -->
|
| 1255 |
+
<div class="feed-cell" id="feed-1">
|
| 1256 |
+
<div class="feed-header">
|
| 1257 |
+
<div class="feed-badge">
|
| 1258 |
+
<div class="live-dot" style="background: var(--text-dim); box-shadow: none;"></div>
|
| 1259 |
+
<span>FEED 02</span>
|
| 1260 |
+
</div>
|
| 1261 |
+
<div class="feed-status" id="feed-1-status" style="color: var(--text-dim);">STANDBY</div>
|
| 1262 |
+
</div>
|
| 1263 |
+
<div class="feed-offline" id="offline-1">
|
| 1264 |
+
<i data-feather="video-off"></i>
|
| 1265 |
+
<span>NO SIGNAL</span>
|
| 1266 |
+
<button class="btn btn-primary"
|
| 1267 |
+
style="width:auto; margin-top:10px; padding:8px 16px; font-size:0.7rem;"
|
| 1268 |
+
onclick="event.stopPropagation(); triggerFeedUpload(1)">
|
| 1269 |
+
<i data-feather="upload-cloud" style="width:14px;height:14px;"></i> UPLOAD CLIP
|
| 1270 |
+
</button>
|
| 1271 |
+
</div>
|
| 1272 |
+
</div>
|
| 1273 |
+
|
| 1274 |
+
<!-- Feed 2 -->
|
| 1275 |
+
<div class="feed-cell" id="feed-2">
|
| 1276 |
+
<div class="feed-header">
|
| 1277 |
+
<div class="feed-badge">
|
| 1278 |
+
<div class="live-dot" style="background: var(--text-dim); box-shadow: none;"></div>
|
| 1279 |
+
<span>FEED 03</span>
|
| 1280 |
+
</div>
|
| 1281 |
+
<div class="feed-status" id="feed-2-status" style="color: var(--text-dim);">STANDBY</div>
|
| 1282 |
+
</div>
|
| 1283 |
+
<div class="feed-offline" id="offline-2">
|
| 1284 |
+
<i data-feather="video-off"></i>
|
| 1285 |
+
<span>NO SIGNAL</span>
|
| 1286 |
+
<button class="btn btn-primary"
|
| 1287 |
+
style="width:auto; margin-top:10px; padding:8px 16px; font-size:0.7rem;"
|
| 1288 |
+
onclick="event.stopPropagation(); triggerFeedUpload(2)">
|
| 1289 |
+
<i data-feather="upload-cloud" style="width:14px;height:14px;"></i> UPLOAD CLIP
|
| 1290 |
+
</button>
|
| 1291 |
+
</div>
|
| 1292 |
+
</div>
|
| 1293 |
+
|
| 1294 |
+
<!-- Feed 3 -->
|
| 1295 |
+
<div class="feed-cell" id="feed-3">
|
| 1296 |
+
<div class="feed-header">
|
| 1297 |
+
<div class="feed-badge">
|
| 1298 |
+
<div class="live-dot" style="background: var(--text-dim); box-shadow: none;"></div>
|
| 1299 |
+
<span>FEED 04</span>
|
| 1300 |
+
</div>
|
| 1301 |
+
<div class="feed-status" id="feed-3-status" style="color: var(--text-dim);">STANDBY</div>
|
| 1302 |
+
</div>
|
| 1303 |
+
<div class="feed-offline" id="offline-3">
|
| 1304 |
+
<i data-feather="video-off"></i>
|
| 1305 |
+
<span>NO SIGNAL</span>
|
| 1306 |
+
<button class="btn btn-primary"
|
| 1307 |
+
style="width:auto; margin-top:10px; padding:8px 16px; font-size:0.7rem;"
|
| 1308 |
+
onclick="event.stopPropagation(); triggerFeedUpload(3)">
|
| 1309 |
+
<i data-feather="upload-cloud" style="width:14px;height:14px;"></i> UPLOAD CLIP
|
| 1310 |
+
</button>
|
| 1311 |
+
</div>
|
| 1312 |
+
</div>
|
| 1313 |
+
</div>
|
| 1314 |
+
|
| 1315 |
+
<!-- Hidden file input for per-feed upload -->
|
| 1316 |
+
<input type="file" id="feed-upload-input" style="display: none" accept="video/*,.gif">
|
| 1317 |
+
|
| 1318 |
+
|
| 1319 |
+
<!-- Intel Panel -->
|
| 1320 |
+
<div class="intel-panel">
|
| 1321 |
+
<!-- Suspect Journey -->
|
| 1322 |
+
<div class="card" id="journey-card">
|
| 1323 |
+
<div class="card-header">
|
| 1324 |
+
<div class="card-title">Suspect Journey</div>
|
| 1325 |
+
<i data-feather="map" class="card-icon"></i>
|
| 1326 |
+
</div>
|
| 1327 |
+
<div class="audit-log-container" id="journey-container" style="max-height: 250px;">
|
| 1328 |
+
<div class="audit-entry">
|
| 1329 |
+
<div class="audit-time">--:--:--</div>
|
| 1330 |
+
ACTIVATE 'SUSPECT JOURNEY' MODULE TO BEGIN
|
| 1331 |
+
</div>
|
| 1332 |
+
</div>
|
| 1333 |
+
</div>
|
| 1334 |
+
|
| 1335 |
+
<!-- Active Mode -->
|
| 1336 |
+
<div class="card">
|
| 1337 |
+
<div class="card-header">
|
| 1338 |
+
<div class="card-title">Active Mode</div>
|
| 1339 |
+
</div>
|
| 1340 |
+
<div class="mode-indicator">
|
| 1341 |
+
<div class="mode-dot"></div>
|
| 1342 |
+
<div class="mode-name" id="mode-title">1 MODULE ACTIVE</div>
|
| 1343 |
+
</div>
|
| 1344 |
+
</div>
|
| 1345 |
+
|
| 1346 |
+
<!-- Threat Assessment -->
|
| 1347 |
+
<div class="card" id="threat-card">
|
| 1348 |
+
<div class="card-header">
|
| 1349 |
+
<div class="card-title">Threat Assessment</div>
|
| 1350 |
+
<i data-feather="alert-triangle" class="card-icon"></i>
|
| 1351 |
+
</div>
|
| 1352 |
+
<div class="threat-gauge">
|
| 1353 |
+
<div class="score-ring">
|
| 1354 |
+
<svg viewBox="0 0 130 130">
|
| 1355 |
+
<circle class="score-ring-bg" cx="65" cy="65" r="60"></circle>
|
| 1356 |
+
<circle class="score-ring-fill" id="score-ring-fill" cx="65" cy="65" r="60"></circle>
|
| 1357 |
+
</svg>
|
| 1358 |
+
<div class="score-text">
|
| 1359 |
+
<div class="score-value" id="threat-score">0</div>
|
| 1360 |
+
<div class="score-label">THREAT LEVEL</div>
|
| 1361 |
+
</div>
|
| 1362 |
+
</div>
|
| 1363 |
+
<div class="status-text" id="status-text">SECURE</div>
|
| 1364 |
+
</div>
|
| 1365 |
+
</div>
|
| 1366 |
+
|
| 1367 |
+
<!-- Live Metrics -->
|
| 1368 |
+
<div class="card">
|
| 1369 |
+
<div class="card-header">
|
| 1370 |
+
<div class="card-title">Live Metrics</div>
|
| 1371 |
+
<i data-feather="bar-chart-2" class="card-icon"></i>
|
| 1372 |
+
</div>
|
| 1373 |
+
<div class="stats-list" id="stats-container">
|
| 1374 |
+
<div class="stat-row">
|
| 1375 |
+
<span class="stat-name">System Status</span>
|
| 1376 |
+
<span class="stat-val">Initializing</span>
|
| 1377 |
+
</div>
|
| 1378 |
+
</div>
|
| 1379 |
+
</div>
|
| 1380 |
+
|
| 1381 |
+
<!-- Dispatch Center -->
|
| 1382 |
+
<div class="card" id="dispatch-card">
|
| 1383 |
+
<div class="card-header">
|
| 1384 |
+
<div class="card-title">Dispatch Center</div>
|
| 1385 |
+
<i data-feather="send" class="card-icon"></i>
|
| 1386 |
+
</div>
|
| 1387 |
+
<div class="dispatch-status-bar not-configured" id="dispatch-status-bar">
|
| 1388 |
+
<div class="dispatch-dot offline" id="dispatch-dot"></div>
|
| 1389 |
+
<span id="dispatch-status-text">TELEGRAM NOT CONFIGURED</span>
|
| 1390 |
+
</div>
|
| 1391 |
+
|
| 1392 |
+
<!-- Pending Approvals -->
|
| 1393 |
+
<div id="pending-section" style="display:none; margin-bottom: 10px;">
|
| 1394 |
+
<div
|
| 1395 |
+
style="font-family: var(--font-display); font-size: 0.6rem; color: var(--neon-amber); letter-spacing: 1px; margin-bottom: 6px;">
|
| 1396 |
+
⏳ PENDING APPROVAL</div>
|
| 1397 |
+
<div id="pending-container"></div>
|
| 1398 |
+
</div>
|
| 1399 |
+
|
| 1400 |
+
<!-- Dispatch Log -->
|
| 1401 |
+
<div class="audit-log-container" id="dispatch-log-container" style="max-height: 180px;">
|
| 1402 |
+
<div class="dispatch-entry">
|
| 1403 |
+
<div class="dispatch-info">
|
| 1404 |
+
<div class="audit-time">--:--:--</div>
|
| 1405 |
+
NO DISPATCH HISTORY
|
| 1406 |
+
</div>
|
| 1407 |
+
</div>
|
| 1408 |
+
</div>
|
| 1409 |
+
|
| 1410 |
+
<!-- Quick Actions -->
|
| 1411 |
+
<div style="display: flex; gap: 6px; margin-top: 10px;">
|
| 1412 |
+
<button class="btn btn-primary" onclick="manualDispatch()"
|
| 1413 |
+
style="font-size: 0.65rem; padding: 8px;">📡 SEND ALERT</button>
|
| 1414 |
+
<button class="btn btn-secondary" onclick="testDispatch()"
|
| 1415 |
+
style="font-size: 0.65rem; padding: 8px;">🧪 TEST</button>
|
| 1416 |
+
<button class="btn btn-secondary" onclick="openDispatchSettings()"
|
| 1417 |
+
style="font-size: 0.65rem; padding: 8px;">⚙</button>
|
| 1418 |
+
</div>
|
| 1419 |
+
</div>
|
| 1420 |
+
|
| 1421 |
+
<!-- Actions -->
|
| 1422 |
+
<div class="card">
|
| 1423 |
+
<div class="card-header">
|
| 1424 |
+
<div class="card-title">Actions</div>
|
| 1425 |
+
</div>
|
| 1426 |
+
<button class="btn btn-primary" onclick="generateReport()">Generate Incident Report</button>
|
| 1427 |
+
<button class="btn btn-secondary" onclick="refreshAuditLog()" style="margin-top: 8px;">Refresh Audit
|
| 1428 |
+
Log</button>
|
| 1429 |
+
<button class="btn btn-secondary" onclick="resetSystem()"
|
| 1430 |
+
style="margin-top: 8px; border-color: #ff2040; color: #ff2040;">⚠ SYSTEM RESET</button>
|
| 1431 |
+
</div>
|
| 1432 |
+
</div>
|
| 1433 |
+
</div>
|
| 1434 |
+
|
| 1435 |
+
<!-- Report Modal -->
|
| 1436 |
+
<div id="report-modal" class="modal-overlay">
|
| 1437 |
+
<div class="modal-card">
|
| 1438 |
+
<div class="modal-title">Incident Report</div>
|
| 1439 |
+
<div class="modal-subtitle">Generated by Sentinel AI Agent // Gemini 1.5</div>
|
| 1440 |
+
<div class="report-content" id="report-content">Analyzing data stream...</div>
|
| 1441 |
+
<button class="btn btn-secondary" onclick="closeModal()">Dismiss</button>
|
| 1442 |
+
</div>
|
| 1443 |
+
</div>
|
| 1444 |
+
|
| 1445 |
+
<!-- Dispatch Settings Modal -->
|
| 1446 |
+
<div id="dispatch-settings-modal" class="modal-overlay">
|
| 1447 |
+
<div class="modal-card" style="width: 480px;">
|
| 1448 |
+
<div class="modal-title">⚙ Dispatch Settings</div>
|
| 1449 |
+
<div class="modal-subtitle">Telegram Bot Integration // Automated Alert System</div>
|
| 1450 |
+
|
| 1451 |
+
<div style="margin-bottom: 16px;">
|
| 1452 |
+
<div class="dispatch-config-row">
|
| 1453 |
+
<span class="dispatch-config-label">Master Switch</span>
|
| 1454 |
+
<label class="toggle-switch">
|
| 1455 |
+
<input type="checkbox" id="cfg-enabled" checked onchange="saveDispatchSettings()">
|
| 1456 |
+
<span class="toggle-slider"></span>
|
| 1457 |
+
</label>
|
| 1458 |
+
</div>
|
| 1459 |
+
|
| 1460 |
+
<div class="dispatch-config-row">
|
| 1461 |
+
<span class="dispatch-config-label">Auto-Dispatch (skip approval)</span>
|
| 1462 |
+
<label class="toggle-switch">
|
| 1463 |
+
<input type="checkbox" id="cfg-auto-dispatch" onchange="saveDispatchSettings()">
|
| 1464 |
+
<span class="toggle-slider"></span>
|
| 1465 |
+
</label>
|
| 1466 |
+
</div>
|
| 1467 |
+
|
| 1468 |
+
<div class="dispatch-config-row" style="flex-direction: column; align-items: flex-start; gap: 6px;">
|
| 1469 |
+
<span class="dispatch-config-label">Cooldown: <span id="cooldown-display"
|
| 1470 |
+
style="color: var(--cyan);">60s</span></span>
|
| 1471 |
+
<input type="range" class="cooldown-slider" id="cfg-cooldown" min="10" max="300" value="60"
|
| 1472 |
+
oninput="document.getElementById('cooldown-display').textContent = this.value + 's'"
|
| 1473 |
+
onchange="saveDispatchSettings()">
|
| 1474 |
+
</div>
|
| 1475 |
+
</div>
|
| 1476 |
+
|
| 1477 |
+
<div
|
| 1478 |
+
style="font-family: var(--font-display); font-size: 0.6rem; color: var(--cyan); letter-spacing: 1px; margin-bottom: 10px;">
|
| 1479 |
+
TELEGRAM CONFIGURATION</div>
|
| 1480 |
+
|
| 1481 |
+
<div style="margin-bottom: 10px;">
|
| 1482 |
+
<label style="font-size: 0.7rem; color: var(--text-secondary); display: block; margin-bottom: 4px;">Bot
|
| 1483 |
+
Token</label>
|
| 1484 |
+
<input type="text" class="config-input" id="cfg-bot-token" placeholder="e.g. 8659917680:AAFHai-..."
|
| 1485 |
+
style="margin-bottom: 8px;">
|
| 1486 |
+
|
| 1487 |
+
<label style="font-size: 0.7rem; color: var(--text-secondary); display: block; margin-bottom: 4px;">Chat
|
| 1488 |
+
ID <span style="color: var(--text-dim);">(send /start to your bot, then click
|
| 1489 |
+
Auto-Detect)</span></label>
|
| 1490 |
+
<div style="display: flex; gap: 6px;">
|
| 1491 |
+
<input type="text" class="config-input" id="cfg-chat-id" placeholder="e.g. 123456789"
|
| 1492 |
+
style="flex: 1;">
|
| 1493 |
+
<button class="btn btn-secondary" onclick="autoDetectChatId()"
|
| 1494 |
+
style="width: auto; padding: 6px 12px; font-size: 0.65rem;">AUTO-DETECT</button>
|
| 1495 |
+
</div>
|
| 1496 |
+
</div>
|
| 1497 |
+
|
| 1498 |
+
<div style="display: flex; gap: 8px; margin-top: 16px;">
|
| 1499 |
+
<button class="btn btn-primary" onclick="saveTelegramConfig()" style="flex: 1;">SAVE CONFIG</button>
|
| 1500 |
+
<button class="btn btn-primary" onclick="testDispatch()"
|
| 1501 |
+
style="flex: 1; background: rgba(0, 255, 136, 0.1); border-color: rgba(0, 255, 136, 0.3); color: var(--neon-green);">🧪
|
| 1502 |
+
SEND TEST</button>
|
| 1503 |
+
</div>
|
| 1504 |
+
<button class="btn btn-secondary" onclick="closeDispatchSettings()" style="margin-top: 8px;">Close</button>
|
| 1505 |
+
</div>
|
| 1506 |
+
</div>
|
| 1507 |
+
|
| 1508 |
+
<!-- ═══════════════════════════════════════════════
|
| 1509 |
+
JAVASCRIPT
|
| 1510 |
+
═══════════════════════════════════════════════ -->
|
| 1511 |
+
<script>
|
| 1512 |
+
feather.replace();
|
| 1513 |
+
|
| 1514 |
+
// ─── State ───
|
| 1515 |
+
let currentLayout = 'quad'; // 'quad' or 'single'
|
| 1516 |
+
let expandedFeed = 0;
|
| 1517 |
+
let isRedAlert = false;
|
| 1518 |
+
|
| 1519 |
+
// ─── Module Toggling ───
|
| 1520 |
+
const modeTitles = {
|
| 1521 |
+
'movement': 'MOVEMENT ANALYSIS',
|
| 1522 |
+
'facemask': 'FACEMASK DETECTION',
|
| 1523 |
+
'weapon': 'WEAPON DETECTION',
|
| 1524 |
+
'public_safety': 'PUBLIC SAFETY',
|
| 1525 |
+
'suspect_journey': 'SUSPECT JOURNEY'
|
| 1526 |
+
};
|
| 1527 |
+
|
| 1528 |
+
function toggleModule(module, buttonElement) {
|
| 1529 |
+
buttonElement.classList.toggle('active');
|
| 1530 |
+
|
| 1531 |
+
fetch('/toggle_module', {
|
| 1532 |
+
method: 'POST',
|
| 1533 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1534 |
+
body: JSON.stringify({ module: module })
|
| 1535 |
+
})
|
| 1536 |
+
.then(r => r.json())
|
| 1537 |
+
.then(data => {
|
| 1538 |
+
updateActiveModulesDisplay(data.active_modules);
|
| 1539 |
+
});
|
| 1540 |
+
}
|
| 1541 |
+
|
| 1542 |
+
function updateActiveModulesDisplay(activeModules) {
|
| 1543 |
+
const modeTitle = document.getElementById('mode-title');
|
| 1544 |
+
const count = activeModules.length;
|
| 1545 |
+
|
| 1546 |
+
if (count === 0) {
|
| 1547 |
+
modeTitle.textContent = 'NO MODULES ACTIVE';
|
| 1548 |
+
} else if (count === 1) {
|
| 1549 |
+
modeTitle.textContent = modeTitles[activeModules[0]] || activeModules[0].toUpperCase();
|
| 1550 |
+
} else {
|
| 1551 |
+
modeTitle.textContent = `${count} MODULES ACTIVE`;
|
| 1552 |
+
}
|
| 1553 |
+
}
|
| 1554 |
+
|
| 1555 |
+
|
| 1556 |
+
// ── Per-Feed Upload ──
|
| 1557 |
+
let uploadTargetFeed = 0;
|
| 1558 |
+
|
| 1559 |
+
function triggerFeedUpload(feedId) {
|
| 1560 |
+
uploadTargetFeed = feedId;
|
| 1561 |
+
document.getElementById('feed-upload-input').click();
|
| 1562 |
+
}
|
| 1563 |
+
|
| 1564 |
+
document.addEventListener('DOMContentLoaded', function () {
|
| 1565 |
+
const feedInput = document.getElementById('feed-upload-input');
|
| 1566 |
+
if (feedInput) {
|
| 1567 |
+
feedInput.addEventListener('change', function () {
|
| 1568 |
+
if (this.files[0]) {
|
| 1569 |
+
const feedId = uploadTargetFeed;
|
| 1570 |
+
const formData = new FormData();
|
| 1571 |
+
formData.append('file', this.files[0]);
|
| 1572 |
+
|
| 1573 |
+
const statusEl = document.getElementById('feed-' + feedId + '-status');
|
| 1574 |
+
if (statusEl) statusEl.textContent = 'UPLOADING...';
|
| 1575 |
+
|
| 1576 |
+
fetch('/upload_video/' + feedId, { method: 'POST', body: formData })
|
| 1577 |
+
.then(r => r.json())
|
| 1578 |
+
.then(data => {
|
| 1579 |
+
if (data.success) {
|
| 1580 |
+
activateFeedUI(feedId);
|
| 1581 |
+
if (statusEl) statusEl.textContent = 'FILE FEED';
|
| 1582 |
+
}
|
| 1583 |
+
})
|
| 1584 |
+
.catch(() => {
|
| 1585 |
+
if (statusEl) statusEl.textContent = 'UPLOAD FAILED';
|
| 1586 |
+
});
|
| 1587 |
+
|
| 1588 |
+
this.value = '';
|
| 1589 |
+
}
|
| 1590 |
+
});
|
| 1591 |
+
}
|
| 1592 |
+
});
|
| 1593 |
+
|
| 1594 |
+
function activateFeedUI(feedId) {
|
| 1595 |
+
// Hide the "NO SIGNAL" overlay and show the stream
|
| 1596 |
+
const offline = document.getElementById('offline-' + feedId);
|
| 1597 |
+
if (offline) offline.style.display = 'none';
|
| 1598 |
+
|
| 1599 |
+
const cell = document.getElementById('feed-' + feedId);
|
| 1600 |
+
// If no stream img exists yet, create one
|
| 1601 |
+
let img = document.getElementById('stream-' + feedId);
|
| 1602 |
+
if (!img) {
|
| 1603 |
+
img = document.createElement('img');
|
| 1604 |
+
img.className = 'feed-stream';
|
| 1605 |
+
img.id = 'stream-' + feedId;
|
| 1606 |
+
img.alt = 'Feed ' + feedId;
|
| 1607 |
+
cell.appendChild(img);
|
| 1608 |
+
}
|
| 1609 |
+
|
| 1610 |
+
// Force refresh
|
| 1611 |
+
img.src = '';
|
| 1612 |
+
setTimeout(() => {
|
| 1613 |
+
img.src = '/video_feed/' + feedId + '?t=' + Date.now();
|
| 1614 |
+
}, 400);
|
| 1615 |
+
|
| 1616 |
+
// Update the live dot
|
| 1617 |
+
const badge = cell.querySelector('.live-dot');
|
| 1618 |
+
if (badge) {
|
| 1619 |
+
badge.style.background = '';
|
| 1620 |
+
badge.style.boxShadow = '';
|
| 1621 |
+
}
|
| 1622 |
+
|
| 1623 |
+
feather.replace();
|
| 1624 |
+
}
|
| 1625 |
+
|
| 1626 |
+
/**
|
| 1627 |
+
* Force-refresh an MJPEG stream by setting a new src with a cache-buster.
|
| 1628 |
+
* This drops the old HTTP connection and establishes a fresh one.
|
| 1629 |
+
*/
|
| 1630 |
+
function refreshFeedStream(feedId) {
|
| 1631 |
+
const img = document.getElementById('stream-' + feedId);
|
| 1632 |
+
if (img) {
|
| 1633 |
+
// Brief blank to visually signal the switch
|
| 1634 |
+
img.src = '';
|
| 1635 |
+
// Small delay lets the backend fully initialize the new feed
|
| 1636 |
+
setTimeout(() => {
|
| 1637 |
+
img.src = '/video_feed/' + feedId + '?t=' + Date.now();
|
| 1638 |
+
}, 300);
|
| 1639 |
+
}
|
| 1640 |
+
}
|
| 1641 |
+
|
| 1642 |
+
// ─── Grid Layout ───
|
| 1643 |
+
function setGridLayout(layout) {
|
| 1644 |
+
const grid = document.getElementById('camera-grid');
|
| 1645 |
+
currentLayout = layout;
|
| 1646 |
+
|
| 1647 |
+
if (layout === 'single') {
|
| 1648 |
+
grid.classList.add('single-view');
|
| 1649 |
+
// Show only the expanded feed
|
| 1650 |
+
document.querySelectorAll('.feed-cell').forEach((cell, i) => {
|
| 1651 |
+
cell.classList.toggle('expanded', i === expandedFeed);
|
| 1652 |
+
});
|
| 1653 |
+
} else {
|
| 1654 |
+
grid.classList.remove('single-view');
|
| 1655 |
+
document.querySelectorAll('.feed-cell').forEach(cell => {
|
| 1656 |
+
cell.classList.remove('expanded');
|
| 1657 |
+
});
|
| 1658 |
+
}
|
| 1659 |
+
}
|
| 1660 |
+
|
| 1661 |
+
function expandFeed(feedId) {
|
| 1662 |
+
expandedFeed = feedId;
|
| 1663 |
+
if (currentLayout === 'single') {
|
| 1664 |
+
document.querySelectorAll('.feed-cell').forEach((cell, i) => {
|
| 1665 |
+
cell.classList.toggle('expanded', i === feedId);
|
| 1666 |
+
});
|
| 1667 |
+
}
|
| 1668 |
+
}
|
| 1669 |
+
|
| 1670 |
+
// ─── Stats & Red Alert Updates ───
|
| 1671 |
+
function updateStats() {
|
| 1672 |
+
fetch('/stats')
|
| 1673 |
+
.then(r => r.json())
|
| 1674 |
+
.then(data => {
|
| 1675 |
+
const score = data.threat_score;
|
| 1676 |
+
const scoreEl = document.getElementById('threat-score');
|
| 1677 |
+
const statusEl = document.getElementById('status-text');
|
| 1678 |
+
const ringFill = document.getElementById('score-ring-fill');
|
| 1679 |
+
const threatCard = document.getElementById('threat-card');
|
| 1680 |
+
|
| 1681 |
+
scoreEl.textContent = score;
|
| 1682 |
+
|
| 1683 |
+
// Compute ring dash offset (circumference = 2 * π * 60 ≈ 377)
|
| 1684 |
+
const circumference = 377;
|
| 1685 |
+
const offset = circumference - (circumference * score / 100);
|
| 1686 |
+
ringFill.style.strokeDashoffset = offset;
|
| 1687 |
+
|
| 1688 |
+
// Color based on score
|
| 1689 |
+
let color, status, glow;
|
| 1690 |
+
if (score >= 80) {
|
| 1691 |
+
color = '#ff2040';
|
| 1692 |
+
status = 'CRITICAL';
|
| 1693 |
+
glow = 'rgba(255, 32, 64, 0.4)';
|
| 1694 |
+
} else if (score >= 50) {
|
| 1695 |
+
color = '#ffaa00';
|
| 1696 |
+
status = 'ELEVATED';
|
| 1697 |
+
glow = 'rgba(255, 170, 0, 0.3)';
|
| 1698 |
+
} else if (score >= 25) {
|
| 1699 |
+
color = '#00d4ff';
|
| 1700 |
+
status = 'GUARDED';
|
| 1701 |
+
glow = 'rgba(0, 200, 255, 0.3)';
|
| 1702 |
+
} else {
|
| 1703 |
+
color = '#00ff88';
|
| 1704 |
+
status = 'SECURE';
|
| 1705 |
+
glow = 'rgba(0, 255, 136, 0.3)';
|
| 1706 |
+
}
|
| 1707 |
+
|
| 1708 |
+
statusEl.textContent = status;
|
| 1709 |
+
statusEl.style.color = color;
|
| 1710 |
+
statusEl.style.textShadow = `0 0 20px ${glow}`;
|
| 1711 |
+
ringFill.style.stroke = color;
|
| 1712 |
+
ringFill.style.filter = `drop-shadow(0 0 8px ${glow})`;
|
| 1713 |
+
|
| 1714 |
+
updateJourneyData();
|
| 1715 |
+
|
| 1716 |
+
// Red Alert state
|
| 1717 |
+
const alertOverlay = document.getElementById('red-alert-overlay');
|
| 1718 |
+
const alertBanner = document.getElementById('red-alert-banner');
|
| 1719 |
+
const feedCells = document.querySelectorAll('.feed-cell');
|
| 1720 |
+
|
| 1721 |
+
if (data.red_alert) {
|
| 1722 |
+
alertOverlay.classList.add('active');
|
| 1723 |
+
alertBanner.classList.add('active');
|
| 1724 |
+
feedCells.forEach(cell => cell.classList.add('red-alert-active'));
|
| 1725 |
+
threatCard.style.borderColor = 'rgba(255, 32, 64, 0.5)';
|
| 1726 |
+
|
| 1727 |
+
if (!isRedAlert) {
|
| 1728 |
+
playAlertTone();
|
| 1729 |
+
isRedAlert = true;
|
| 1730 |
+
}
|
| 1731 |
+
} else {
|
| 1732 |
+
alertOverlay.classList.remove('active');
|
| 1733 |
+
alertBanner.classList.remove('active');
|
| 1734 |
+
feedCells.forEach(cell => cell.classList.remove('red-alert-active'));
|
| 1735 |
+
threatCard.style.borderColor = '';
|
| 1736 |
+
isRedAlert = false;
|
| 1737 |
+
}
|
| 1738 |
+
|
| 1739 |
+
// Update mode display
|
| 1740 |
+
if (data.active_modules) {
|
| 1741 |
+
updateActiveModulesDisplay(data.active_modules);
|
| 1742 |
+
}
|
| 1743 |
+
|
| 1744 |
+
// Update live metrics
|
| 1745 |
+
const container = document.getElementById('stats-container');
|
| 1746 |
+
container.innerHTML = '';
|
| 1747 |
+
|
| 1748 |
+
if (!data.details || Object.keys(data.details).length === 0) {
|
| 1749 |
+
container.innerHTML = '<div class="stat-row"><span class="stat-name">System Status</span><span class="stat-val">Standby</span></div>';
|
| 1750 |
+
} else {
|
| 1751 |
+
for (const [key, value] of Object.entries(data.details)) {
|
| 1752 |
+
const div = document.createElement('div');
|
| 1753 |
+
div.className = 'stat-row';
|
| 1754 |
+
const formattedKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
| 1755 |
+
let displayVal = value;
|
| 1756 |
+
if (typeof value === 'boolean') {
|
| 1757 |
+
displayVal = value ? '⚠ YES' : 'No';
|
| 1758 |
+
}
|
| 1759 |
+
div.innerHTML = `<span class="stat-name">${formattedKey}</span><span class="stat-val">${displayVal}</span>`;
|
| 1760 |
+
container.appendChild(div);
|
| 1761 |
+
}
|
| 1762 |
+
}
|
| 1763 |
+
})
|
| 1764 |
+
.catch(() => { });
|
| 1765 |
+
}
|
| 1766 |
+
|
| 1767 |
+
// ─── Alert Tone (Web Audio API) ───
|
| 1768 |
+
function playAlertTone() {
|
| 1769 |
+
try {
|
| 1770 |
+
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
| 1771 |
+
const oscillator = audioCtx.createOscillator();
|
| 1772 |
+
const gainNode = audioCtx.createGain();
|
| 1773 |
+
|
| 1774 |
+
oscillator.connect(gainNode);
|
| 1775 |
+
gainNode.connect(audioCtx.destination);
|
| 1776 |
+
|
| 1777 |
+
oscillator.type = 'square';
|
| 1778 |
+
oscillator.frequency.setValueAtTime(880, audioCtx.currentTime);
|
| 1779 |
+
oscillator.frequency.setValueAtTime(660, audioCtx.currentTime + 0.15);
|
| 1780 |
+
oscillator.frequency.setValueAtTime(880, audioCtx.currentTime + 0.3);
|
| 1781 |
+
|
| 1782 |
+
gainNode.gain.setValueAtTime(0.08, audioCtx.currentTime);
|
| 1783 |
+
gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.5);
|
| 1784 |
+
|
| 1785 |
+
oscillator.start(audioCtx.currentTime);
|
| 1786 |
+
oscillator.stop(audioCtx.currentTime + 0.5);
|
| 1787 |
+
} catch (e) {
|
| 1788 |
+
// Audio not available — silent fallback
|
| 1789 |
+
}
|
| 1790 |
+
}
|
| 1791 |
+
|
| 1792 |
+
// ─── AI Report ───
|
| 1793 |
+
function generateReport() {
|
| 1794 |
+
const modal = document.getElementById('report-modal');
|
| 1795 |
+
modal.classList.add('show');
|
| 1796 |
+
document.getElementById('report-content').textContent = 'ANALYZING DATA STREAM...';
|
| 1797 |
+
|
| 1798 |
+
fetch('/generate_report', { method: 'POST' })
|
| 1799 |
+
.then(r => r.json())
|
| 1800 |
+
.then(data => {
|
| 1801 |
+
document.getElementById('report-content').textContent = data.report;
|
| 1802 |
+
})
|
| 1803 |
+
.catch(() => {
|
| 1804 |
+
document.getElementById('report-content').textContent = 'ERROR: Connection to AI Agent lost.';
|
| 1805 |
+
});
|
| 1806 |
+
}
|
| 1807 |
+
|
| 1808 |
+
function closeModal() {
|
| 1809 |
+
document.getElementById('report-modal').classList.remove('show');
|
| 1810 |
+
}
|
| 1811 |
+
|
| 1812 |
+
// ─── Audit Log Refresh ───
|
| 1813 |
+
function refreshAuditLog() {
|
| 1814 |
+
fetch('/audit_log')
|
| 1815 |
+
.then(r => r.json())
|
| 1816 |
+
.then(data => {
|
| 1817 |
+
const container = document.getElementById('audit-log-container');
|
| 1818 |
+
container.innerHTML = '';
|
| 1819 |
+
|
| 1820 |
+
if (data.log.length === 0) {
|
| 1821 |
+
container.innerHTML = '<div class="audit-entry"><div class="audit-time">--:--:--</div>NO ENTRIES</div>';
|
| 1822 |
+
return;
|
| 1823 |
+
}
|
| 1824 |
+
|
| 1825 |
+
data.log.slice(0, 30).forEach(entry => {
|
| 1826 |
+
const div = document.createElement('div');
|
| 1827 |
+
div.className = `audit-entry severity-${entry.severity}`;
|
| 1828 |
+
div.innerHTML = `
|
| 1829 |
+
<div class="audit-time">${entry.timestamp.split(' ')[1] || entry.timestamp}</div>
|
| 1830 |
+
${entry.action}: ${entry.details}
|
| 1831 |
+
`;
|
| 1832 |
+
container.appendChild(div);
|
| 1833 |
+
});
|
| 1834 |
+
})
|
| 1835 |
+
.catch(() => { });
|
| 1836 |
+
}
|
| 1837 |
+
|
| 1838 |
+
// ─── System Reset ───
|
| 1839 |
+
function resetSystem() {
|
| 1840 |
+
if (!confirm("⚠ CONFIRM SYSTEM RESET ⚠\n\nThis will clear all tracking history, threat scores, and active alerts.\nThe system will revert to default state.")) {
|
| 1841 |
+
return;
|
| 1842 |
+
}
|
| 1843 |
+
|
| 1844 |
+
fetch('/reset_system', { method: 'POST' })
|
| 1845 |
+
.then(r => r.json())
|
| 1846 |
+
.then(data => {
|
| 1847 |
+
if (data.success) {
|
| 1848 |
+
// Reload page to refresh all UI states cleanly
|
| 1849 |
+
window.location.reload();
|
| 1850 |
+
}
|
| 1851 |
+
});
|
| 1852 |
+
}
|
| 1853 |
+
|
| 1854 |
+
function updateJourneyData() {
|
| 1855 |
+
fetch('/journey_data')
|
| 1856 |
+
.then(r => r.json())
|
| 1857 |
+
.then(data => {
|
| 1858 |
+
const container = document.getElementById('journey-container');
|
| 1859 |
+
if (data.length === 0) {
|
| 1860 |
+
container.innerHTML = '<div class="audit-entry"><div class="audit-time">--:--:--</div>SCANNING...</div>';
|
| 1861 |
+
return;
|
| 1862 |
+
}
|
| 1863 |
+
|
| 1864 |
+
let html = '';
|
| 1865 |
+
data.forEach(subject => {
|
| 1866 |
+
const historyHtml = subject.history.map(h => `F${h.feed}`).join(' → ');
|
| 1867 |
+
html += `
|
| 1868 |
+
<div class="audit-entry">
|
| 1869 |
+
<div class="audit-time">${subject.last_seen}</div>
|
| 1870 |
+
<span style="color:var(--cyan)">SUBJ #${subject.id}</span> [FEED ${subject.last_feed}]
|
| 1871 |
+
<div style="font-size: 0.6rem; color: var(--text-dim); margin-top:2px;">${historyHtml}</div>
|
| 1872 |
+
</div>
|
| 1873 |
+
`;
|
| 1874 |
+
});
|
| 1875 |
+
container.innerHTML = html;
|
| 1876 |
+
});
|
| 1877 |
+
}
|
| 1878 |
+
|
| 1879 |
+
// ═══════════════════════════════════════════════════
|
| 1880 |
+
// DISPATCH CENTER
|
| 1881 |
+
// ═══════════════════════════════════════════════════
|
| 1882 |
+
|
| 1883 |
+
function updateDispatchCenter() {
|
| 1884 |
+
fetch('/dispatch_log')
|
| 1885 |
+
.then(r => r.json())
|
| 1886 |
+
.then(data => {
|
| 1887 |
+
// Update status bar
|
| 1888 |
+
const statusBar = document.getElementById('dispatch-status-bar');
|
| 1889 |
+
const statusDot = document.getElementById('dispatch-dot');
|
| 1890 |
+
const statusText = document.getElementById('dispatch-status-text');
|
| 1891 |
+
|
| 1892 |
+
if (data.settings.telegram_configured) {
|
| 1893 |
+
statusBar.className = 'dispatch-status-bar configured';
|
| 1894 |
+
statusDot.className = 'dispatch-dot online';
|
| 1895 |
+
const mode = data.settings.auto_dispatch ? 'AUTO' : 'MANUAL';
|
| 1896 |
+
statusText.textContent = `TELEGRAM ONLINE // ${mode} MODE`;
|
| 1897 |
+
} else {
|
| 1898 |
+
statusBar.className = 'dispatch-status-bar not-configured';
|
| 1899 |
+
statusDot.className = 'dispatch-dot offline';
|
| 1900 |
+
statusText.textContent = 'TELEGRAM NOT CONFIGURED';
|
| 1901 |
+
}
|
| 1902 |
+
|
| 1903 |
+
// Update pending approvals
|
| 1904 |
+
const pendingSection = document.getElementById('pending-section');
|
| 1905 |
+
const pendingContainer = document.getElementById('pending-container');
|
| 1906 |
+
const pendingBadge = document.getElementById('pending-badge');
|
| 1907 |
+
|
| 1908 |
+
if (data.pending && data.pending.length > 0) {
|
| 1909 |
+
pendingSection.style.display = 'block';
|
| 1910 |
+
pendingBadge.textContent = data.pending.length;
|
| 1911 |
+
pendingBadge.classList.add('visible');
|
| 1912 |
+
|
| 1913 |
+
let pendingHtml = '';
|
| 1914 |
+
data.pending.forEach(evt => {
|
| 1915 |
+
const scoreColor = evt.threat_score >= 80 ? 'var(--neon-red)' :
|
| 1916 |
+
evt.threat_score >= 50 ? 'var(--neon-amber)' : 'var(--cyan)';
|
| 1917 |
+
pendingHtml += `
|
| 1918 |
+
<div class="dispatch-entry pending-entry">
|
| 1919 |
+
<div class="dispatch-info">
|
| 1920 |
+
<div class="audit-time">${evt.timestamp}</div>
|
| 1921 |
+
<span class="dispatch-badge pending">PENDING</span>
|
| 1922 |
+
Threat Score: ${evt.threat_score} // ${evt.active_modules.join(', ').toUpperCase()}
|
| 1923 |
+
<div class="dispatch-actions">
|
| 1924 |
+
<button class="btn-approve" onclick="approveDispatch('${evt.id}')">✓ APPROVE</button>
|
| 1925 |
+
<button class="btn-reject" onclick="rejectDispatch('${evt.id}')">✗ REJECT</button>
|
| 1926 |
+
</div>
|
| 1927 |
+
</div>
|
| 1928 |
+
<div class="dispatch-score" style="color: ${scoreColor};">${evt.threat_score}</div>
|
| 1929 |
+
</div>
|
| 1930 |
+
`;
|
| 1931 |
+
});
|
| 1932 |
+
pendingContainer.innerHTML = pendingHtml;
|
| 1933 |
+
} else {
|
| 1934 |
+
pendingSection.style.display = 'none';
|
| 1935 |
+
pendingBadge.classList.remove('visible');
|
| 1936 |
+
}
|
| 1937 |
+
|
| 1938 |
+
// Update dispatch log
|
| 1939 |
+
const logContainer = document.getElementById('dispatch-log-container');
|
| 1940 |
+
if (data.log.length === 0 && (!data.pending || data.pending.length === 0)) {
|
| 1941 |
+
logContainer.innerHTML = '<div class="dispatch-entry"><div class="dispatch-info"><div class="audit-time">--:--:--</div>NO DISPATCH HISTORY</div></div>';
|
| 1942 |
+
} else {
|
| 1943 |
+
let logHtml = '';
|
| 1944 |
+
data.log.slice(0, 15).forEach(evt => {
|
| 1945 |
+
const badgeClass = evt.status;
|
| 1946 |
+
const statusLabel = evt.status.toUpperCase();
|
| 1947 |
+
const scoreColor = evt.threat_score >= 80 ? 'var(--neon-red)' :
|
| 1948 |
+
evt.threat_score >= 50 ? 'var(--neon-amber)' : 'var(--cyan)';
|
| 1949 |
+
logHtml += `
|
| 1950 |
+
<div class="dispatch-entry">
|
| 1951 |
+
<div class="dispatch-info">
|
| 1952 |
+
<div class="audit-time">${evt.timestamp}</div>
|
| 1953 |
+
<span class="dispatch-badge ${badgeClass}">${statusLabel}</span>
|
| 1954 |
+
Score: ${evt.threat_score} // ${(evt.active_modules || []).join(', ').toUpperCase() || 'N/A'}
|
| 1955 |
+
</div>
|
| 1956 |
+
<div class="dispatch-score" style="color: ${scoreColor};">${evt.threat_score}</div>
|
| 1957 |
+
</div>
|
| 1958 |
+
`;
|
| 1959 |
+
});
|
| 1960 |
+
logContainer.innerHTML = logHtml;
|
| 1961 |
+
}
|
| 1962 |
+
})
|
| 1963 |
+
.catch(() => { });
|
| 1964 |
+
}
|
| 1965 |
+
|
| 1966 |
+
function approveDispatch(eventId) {
|
| 1967 |
+
fetch('/approve_dispatch/' + eventId, { method: 'POST' })
|
| 1968 |
+
.then(r => r.json())
|
| 1969 |
+
.then(data => {
|
| 1970 |
+
updateDispatchCenter();
|
| 1971 |
+
if (data.status === 'sent') {
|
| 1972 |
+
showDispatchToast('✅ Alert dispatched via Telegram');
|
| 1973 |
+
} else {
|
| 1974 |
+
showDispatchToast('⚠ Dispatch failed — check Telegram config');
|
| 1975 |
+
}
|
| 1976 |
+
});
|
| 1977 |
+
}
|
| 1978 |
+
|
| 1979 |
+
function rejectDispatch(eventId) {
|
| 1980 |
+
fetch('/reject_dispatch/' + eventId, { method: 'POST' })
|
| 1981 |
+
.then(r => r.json())
|
| 1982 |
+
.then(() => {
|
| 1983 |
+
updateDispatchCenter();
|
| 1984 |
+
showDispatchToast('🔴 Alert rejected');
|
| 1985 |
+
});
|
| 1986 |
+
}
|
| 1987 |
+
|
| 1988 |
+
function manualDispatch() {
|
| 1989 |
+
if (!confirm('📡 DISPATCH ALERT NOW?\n\nThis will send the current threat status to Telegram immediately.')) return;
|
| 1990 |
+
fetch('/dispatch_alert', { method: 'POST' })
|
| 1991 |
+
.then(r => r.json())
|
| 1992 |
+
.then(data => {
|
| 1993 |
+
if (data.success) {
|
| 1994 |
+
showDispatchToast('✅ Alert dispatched via Telegram');
|
| 1995 |
+
} else {
|
| 1996 |
+
showDispatchToast('⚠ ' + (data.error || 'Dispatch failed'));
|
| 1997 |
+
}
|
| 1998 |
+
updateDispatchCenter();
|
| 1999 |
+
})
|
| 2000 |
+
.catch(() => showDispatchToast('⚠ Network error'));
|
| 2001 |
+
}
|
| 2002 |
+
|
| 2003 |
+
function testDispatch() {
|
| 2004 |
+
showDispatchToast('🧪 Sending test message...');
|
| 2005 |
+
fetch('/test_dispatch', { method: 'POST' })
|
| 2006 |
+
.then(r => r.json())
|
| 2007 |
+
.then(data => {
|
| 2008 |
+
if (data.ok) {
|
| 2009 |
+
showDispatchToast('✅ Test message sent to Telegram!');
|
| 2010 |
+
updateDispatchCenter();
|
| 2011 |
+
} else {
|
| 2012 |
+
showDispatchToast('⚠ ' + (data.error || 'Test failed'));
|
| 2013 |
+
}
|
| 2014 |
+
})
|
| 2015 |
+
.catch(() => showDispatchToast('⚠ Connection error'));
|
| 2016 |
+
}
|
| 2017 |
+
|
| 2018 |
+
// ─── Dispatch Settings Modal ───
|
| 2019 |
+
function openDispatchSettings() {
|
| 2020 |
+
const modal = document.getElementById('dispatch-settings-modal');
|
| 2021 |
+
modal.classList.add('show');
|
| 2022 |
+
|
| 2023 |
+
// Load current settings
|
| 2024 |
+
fetch('/dispatch_settings')
|
| 2025 |
+
.then(r => r.json())
|
| 2026 |
+
.then(data => {
|
| 2027 |
+
document.getElementById('cfg-enabled').checked = data.enabled;
|
| 2028 |
+
document.getElementById('cfg-auto-dispatch').checked = data.auto_dispatch;
|
| 2029 |
+
document.getElementById('cfg-cooldown').value = data.cooldown_seconds;
|
| 2030 |
+
document.getElementById('cooldown-display').textContent = data.cooldown_seconds + 's';
|
| 2031 |
+
if (data.chat_id) document.getElementById('cfg-chat-id').value = data.chat_id;
|
| 2032 |
+
// Don't populate bot token for security (show masked version in placeholder)
|
| 2033 |
+
const tokenInput = document.getElementById('cfg-bot-token');
|
| 2034 |
+
if (data.bot_token) tokenInput.placeholder = data.bot_token;
|
| 2035 |
+
});
|
| 2036 |
+
}
|
| 2037 |
+
|
| 2038 |
+
function closeDispatchSettings() {
|
| 2039 |
+
document.getElementById('dispatch-settings-modal').classList.remove('show');
|
| 2040 |
+
}
|
| 2041 |
+
|
| 2042 |
+
function saveDispatchSettings() {
|
| 2043 |
+
fetch('/dispatch_settings', {
|
| 2044 |
+
method: 'POST',
|
| 2045 |
+
headers: { 'Content-Type': 'application/json' },
|
| 2046 |
+
body: JSON.stringify({
|
| 2047 |
+
enabled: document.getElementById('cfg-enabled').checked,
|
| 2048 |
+
auto_dispatch: document.getElementById('cfg-auto-dispatch').checked,
|
| 2049 |
+
cooldown_seconds: parseInt(document.getElementById('cfg-cooldown').value)
|
| 2050 |
+
})
|
| 2051 |
+
}).then(() => updateDispatchCenter());
|
| 2052 |
+
}
|
| 2053 |
+
|
| 2054 |
+
function saveTelegramConfig() {
|
| 2055 |
+
const token = document.getElementById('cfg-bot-token').value.trim();
|
| 2056 |
+
const chatId = document.getElementById('cfg-chat-id').value.trim();
|
| 2057 |
+
|
| 2058 |
+
if (!token && !chatId) {
|
| 2059 |
+
showDispatchToast('⚠ Please enter bot token and/or chat ID');
|
| 2060 |
+
return;
|
| 2061 |
+
}
|
| 2062 |
+
|
| 2063 |
+
const payload = {};
|
| 2064 |
+
if (token) payload.bot_token = token;
|
| 2065 |
+
if (chatId) payload.chat_id = chatId;
|
| 2066 |
+
|
| 2067 |
+
fetch('/dispatch_settings', {
|
| 2068 |
+
method: 'POST',
|
| 2069 |
+
headers: { 'Content-Type': 'application/json' },
|
| 2070 |
+
body: JSON.stringify(payload)
|
| 2071 |
+
})
|
| 2072 |
+
.then(r => r.json())
|
| 2073 |
+
.then(() => {
|
| 2074 |
+
showDispatchToast('✅ Telegram config saved!');
|
| 2075 |
+
updateDispatchCenter();
|
| 2076 |
+
});
|
| 2077 |
+
}
|
| 2078 |
+
|
| 2079 |
+
function autoDetectChatId() {
|
| 2080 |
+
showDispatchToast('🔍 Scanning for chat ID...');
|
| 2081 |
+
// First save any token that was entered
|
| 2082 |
+
const token = document.getElementById('cfg-bot-token').value.trim();
|
| 2083 |
+
const savePromise = token ?
|
| 2084 |
+
fetch('/dispatch_settings', {
|
| 2085 |
+
method: 'POST',
|
| 2086 |
+
headers: { 'Content-Type': 'application/json' },
|
| 2087 |
+
body: JSON.stringify({ bot_token: token })
|
| 2088 |
+
}) : Promise.resolve();
|
| 2089 |
+
|
| 2090 |
+
savePromise.then(() => {
|
| 2091 |
+
return fetch('/test_dispatch', { method: 'POST' });
|
| 2092 |
+
})
|
| 2093 |
+
.then(r => r.json())
|
| 2094 |
+
.then(data => {
|
| 2095 |
+
if (data.ok) {
|
| 2096 |
+
showDispatchToast('✅ Chat ID detected & test sent!');
|
| 2097 |
+
// Reload settings to show detected chat ID
|
| 2098 |
+
fetch('/dispatch_settings')
|
| 2099 |
+
.then(r => r.json())
|
| 2100 |
+
.then(s => {
|
| 2101 |
+
if (s.chat_id) document.getElementById('cfg-chat-id').value = s.chat_id;
|
| 2102 |
+
});
|
| 2103 |
+
} else {
|
| 2104 |
+
showDispatchToast('⚠ Could not detect chat ID. Send /start to the bot from Telegram, then try again.');
|
| 2105 |
+
}
|
| 2106 |
+
})
|
| 2107 |
+
.catch(() => showDispatchToast('⚠ Detection failed'));
|
| 2108 |
+
}
|
| 2109 |
+
|
| 2110 |
+
// ─── Toast Notification ───
|
| 2111 |
+
function showDispatchToast(message) {
|
| 2112 |
+
// Remove existing toast
|
| 2113 |
+
const existing = document.getElementById('dispatch-toast');
|
| 2114 |
+
if (existing) existing.remove();
|
| 2115 |
+
|
| 2116 |
+
const toast = document.createElement('div');
|
| 2117 |
+
toast.id = 'dispatch-toast';
|
| 2118 |
+
toast.textContent = message;
|
| 2119 |
+
toast.style.cssText = `
|
| 2120 |
+
position: fixed;
|
| 2121 |
+
bottom: 24px;
|
| 2122 |
+
right: 24px;
|
| 2123 |
+
background: var(--bg-panel);
|
| 2124 |
+
color: var(--text-primary);
|
| 2125 |
+
font-family: var(--font-mono);
|
| 2126 |
+
font-size: 0.75rem;
|
| 2127 |
+
padding: 12px 20px;
|
| 2128 |
+
border-radius: 8px;
|
| 2129 |
+
border: 1px solid var(--border-glow);
|
| 2130 |
+
box-shadow: 0 4px 30px rgba(0, 200, 255, 0.15);
|
| 2131 |
+
z-index: 12000;
|
| 2132 |
+
animation: fadeIn 0.3s ease;
|
| 2133 |
+
backdrop-filter: blur(10px);
|
| 2134 |
+
`;
|
| 2135 |
+
document.body.appendChild(toast);
|
| 2136 |
+
setTimeout(() => {
|
| 2137 |
+
toast.style.opacity = '0';
|
| 2138 |
+
toast.style.transition = 'opacity 0.3s';
|
| 2139 |
+
setTimeout(() => toast.remove(), 300);
|
| 2140 |
+
}, 3000);
|
| 2141 |
+
}
|
| 2142 |
+
|
| 2143 |
+
// ─── Intervals ───
|
| 2144 |
+
setInterval(updateStats, 1000);
|
| 2145 |
+
setInterval(refreshAuditLog, 5000);
|
| 2146 |
+
setInterval(updateDispatchCenter, 3000);
|
| 2147 |
+
|
| 2148 |
+
// Initial load
|
| 2149 |
+
setTimeout(refreshAuditLog, 1500);
|
| 2150 |
+
setTimeout(updateDispatchCenter, 1000);
|
| 2151 |
+
</script>
|
| 2152 |
+
</body>
|
| 2153 |
+
|
| 2154 |
+
</html>
|
templates/sentinel_dashboard_v15.html
ADDED
|
@@ -0,0 +1,2253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>SENTINEL V14 — Dispatch Integration</title>
|
| 8 |
+
<link
|
| 9 |
+
href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700;800;900&family=Rajdhani:wght@300;400;500;600;700&family=Share+Tech+Mono&display=swap"
|
| 10 |
+
rel="stylesheet">
|
| 11 |
+
<script src="https://unpkg.com/feather-icons"></script>
|
| 12 |
+
<style>
|
| 13 |
+
/* ═══════════════════════════════════════════════
|
| 14 |
+
CSS VARIABLES — SCI-FI PALETTE
|
| 15 |
+
═══════════════════════════════════════════════ */
|
| 16 |
+
:root {
|
| 17 |
+
--bg-deep: #05080f;
|
| 18 |
+
--bg-panel: rgba(8, 16, 32, 0.85);
|
| 19 |
+
--bg-card: rgba(10, 20, 40, 0.7);
|
| 20 |
+
--border-glow: rgba(0, 200, 255, 0.15);
|
| 21 |
+
--border-glow-hover: rgba(0, 200, 255, 0.4);
|
| 22 |
+
--cyan: #00d4ff;
|
| 23 |
+
--cyan-dim: #0090aa;
|
| 24 |
+
--neon-green: #00ff88;
|
| 25 |
+
--neon-red: #ff2040;
|
| 26 |
+
--neon-amber: #ffaa00;
|
| 27 |
+
--text-primary: #e0f0ff;
|
| 28 |
+
--text-secondary: #5a8aaa;
|
| 29 |
+
--text-dim: #2a4a5a;
|
| 30 |
+
--scanline-opacity: 0.03;
|
| 31 |
+
--font-display: 'Orbitron', sans-serif;
|
| 32 |
+
--font-body: 'Rajdhani', sans-serif;
|
| 33 |
+
--font-mono: 'Share Tech Mono', monospace;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/* ═══════════════════════════════════════════════
|
| 37 |
+
BASE RESET & BODY
|
| 38 |
+
═══════════════════════════════════════════════ */
|
| 39 |
+
*,
|
| 40 |
+
*::before,
|
| 41 |
+
*::after {
|
| 42 |
+
margin: 0;
|
| 43 |
+
padding: 0;
|
| 44 |
+
box-sizing: border-box;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
body {
|
| 48 |
+
font-family: var(--font-body);
|
| 49 |
+
background: var(--bg-deep);
|
| 50 |
+
color: var(--text-primary);
|
| 51 |
+
height: 100vh;
|
| 52 |
+
overflow: hidden;
|
| 53 |
+
display: flex;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/* Scanline overlay on body */
|
| 57 |
+
body::after {
|
| 58 |
+
content: '';
|
| 59 |
+
position: fixed;
|
| 60 |
+
top: 0;
|
| 61 |
+
left: 0;
|
| 62 |
+
right: 0;
|
| 63 |
+
bottom: 0;
|
| 64 |
+
background: repeating-linear-gradient(0deg,
|
| 65 |
+
transparent,
|
| 66 |
+
transparent 2px,
|
| 67 |
+
rgba(0, 200, 255, var(--scanline-opacity)) 2px,
|
| 68 |
+
rgba(0, 200, 255, var(--scanline-opacity)) 4px);
|
| 69 |
+
pointer-events: none;
|
| 70 |
+
z-index: 9999;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
/* ═══════════════════════════════════════════════
|
| 74 |
+
RED ALERT OVERLAY
|
| 75 |
+
═══════════════════════════════════════════════ */
|
| 76 |
+
.red-alert-overlay {
|
| 77 |
+
position: fixed;
|
| 78 |
+
top: 0;
|
| 79 |
+
left: 0;
|
| 80 |
+
right: 0;
|
| 81 |
+
bottom: 0;
|
| 82 |
+
pointer-events: none;
|
| 83 |
+
z-index: 9998;
|
| 84 |
+
opacity: 0;
|
| 85 |
+
transition: opacity 0.3s ease;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.red-alert-overlay.active {
|
| 89 |
+
opacity: 1;
|
| 90 |
+
animation: red-pulse-border 1s ease-in-out infinite;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.red-alert-overlay.active::before {
|
| 94 |
+
content: '';
|
| 95 |
+
position: absolute;
|
| 96 |
+
top: 0;
|
| 97 |
+
left: 0;
|
| 98 |
+
right: 0;
|
| 99 |
+
bottom: 0;
|
| 100 |
+
border: 4px solid var(--neon-red);
|
| 101 |
+
box-shadow: inset 0 0 80px rgba(255, 32, 64, 0.15),
|
| 102 |
+
0 0 40px rgba(255, 32, 64, 0.3);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.red-alert-banner {
|
| 106 |
+
position: fixed;
|
| 107 |
+
top: 0;
|
| 108 |
+
left: 50%;
|
| 109 |
+
transform: translateX(-50%);
|
| 110 |
+
background: rgba(255, 20, 40, 0.9);
|
| 111 |
+
color: #fff;
|
| 112 |
+
font-family: var(--font-display);
|
| 113 |
+
font-size: 0.85rem;
|
| 114 |
+
font-weight: 700;
|
| 115 |
+
letter-spacing: 4px;
|
| 116 |
+
padding: 8px 40px;
|
| 117 |
+
z-index: 10000;
|
| 118 |
+
border-bottom-left-radius: 8px;
|
| 119 |
+
border-bottom-right-radius: 8px;
|
| 120 |
+
box-shadow: 0 4px 30px rgba(255, 20, 40, 0.5);
|
| 121 |
+
display: none;
|
| 122 |
+
animation: alert-flash 0.8s ease-in-out infinite;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.red-alert-banner.active {
|
| 126 |
+
display: block;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
@keyframes red-pulse-border {
|
| 130 |
+
|
| 131 |
+
0%,
|
| 132 |
+
100% {
|
| 133 |
+
opacity: 0.6;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
50% {
|
| 137 |
+
opacity: 1;
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
@keyframes alert-flash {
|
| 142 |
+
|
| 143 |
+
0%,
|
| 144 |
+
100% {
|
| 145 |
+
opacity: 1;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
50% {
|
| 149 |
+
opacity: 0.6;
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
/* ═══════════════════════════════════════════════
|
| 154 |
+
SIDEBAR
|
| 155 |
+
═══════════════════════════════════════════════ */
|
| 156 |
+
.sidebar {
|
| 157 |
+
width: 260px;
|
| 158 |
+
min-width: 260px;
|
| 159 |
+
background: var(--bg-panel);
|
| 160 |
+
backdrop-filter: blur(20px);
|
| 161 |
+
-webkit-backdrop-filter: blur(20px);
|
| 162 |
+
border-right: 1px solid var(--border-glow);
|
| 163 |
+
display: flex;
|
| 164 |
+
flex-direction: column;
|
| 165 |
+
padding: 20px;
|
| 166 |
+
z-index: 100;
|
| 167 |
+
overflow-y: auto;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.logo {
|
| 171 |
+
font-family: var(--font-display);
|
| 172 |
+
font-size: 1rem;
|
| 173 |
+
font-weight: 700;
|
| 174 |
+
margin-bottom: 8px;
|
| 175 |
+
display: flex;
|
| 176 |
+
align-items: center;
|
| 177 |
+
gap: 10px;
|
| 178 |
+
color: var(--cyan);
|
| 179 |
+
letter-spacing: 2px;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.logo svg {
|
| 183 |
+
filter: drop-shadow(0 0 6px var(--cyan));
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
.version-tag {
|
| 187 |
+
font-family: var(--font-mono);
|
| 188 |
+
font-size: 0.65rem;
|
| 189 |
+
color: var(--text-dim);
|
| 190 |
+
margin-bottom: 24px;
|
| 191 |
+
letter-spacing: 1px;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.nav-group {
|
| 195 |
+
margin-bottom: 20px;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.nav-label {
|
| 199 |
+
font-family: var(--font-display);
|
| 200 |
+
font-size: 0.6rem;
|
| 201 |
+
font-weight: 600;
|
| 202 |
+
color: var(--text-dim);
|
| 203 |
+
margin-bottom: 8px;
|
| 204 |
+
text-transform: uppercase;
|
| 205 |
+
letter-spacing: 2px;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
.nav-item {
|
| 209 |
+
padding: 10px 12px;
|
| 210 |
+
margin-bottom: 3px;
|
| 211 |
+
border-radius: 6px;
|
| 212 |
+
cursor: pointer;
|
| 213 |
+
transition: all 0.25s ease;
|
| 214 |
+
display: flex;
|
| 215 |
+
align-items: center;
|
| 216 |
+
gap: 10px;
|
| 217 |
+
color: var(--text-secondary);
|
| 218 |
+
font-size: 0.9rem;
|
| 219 |
+
font-weight: 500;
|
| 220 |
+
border: 1px solid transparent;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.nav-item:hover {
|
| 224 |
+
background: rgba(0, 200, 255, 0.05);
|
| 225 |
+
color: var(--text-primary);
|
| 226 |
+
border-color: var(--border-glow);
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.nav-item.active {
|
| 230 |
+
background: rgba(0, 200, 255, 0.1);
|
| 231 |
+
color: var(--cyan);
|
| 232 |
+
border-color: rgba(0, 200, 255, 0.3);
|
| 233 |
+
box-shadow: 0 0 15px rgba(0, 200, 255, 0.05);
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
.nav-item svg {
|
| 237 |
+
width: 16px;
|
| 238 |
+
height: 16px;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
/* Audit Log Panel in sidebar */
|
| 242 |
+
.audit-section {
|
| 243 |
+
margin-top: auto;
|
| 244 |
+
flex-shrink: 0;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
.audit-log-container {
|
| 248 |
+
max-height: 200px;
|
| 249 |
+
overflow-y: auto;
|
| 250 |
+
scrollbar-width: thin;
|
| 251 |
+
scrollbar-color: var(--cyan-dim) transparent;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
.audit-log-container::-webkit-scrollbar {
|
| 255 |
+
width: 4px;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.audit-log-container::-webkit-scrollbar-track {
|
| 259 |
+
background: transparent;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.audit-log-container::-webkit-scrollbar-thumb {
|
| 263 |
+
background: var(--cyan-dim);
|
| 264 |
+
border-radius: 2px;
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
.audit-entry {
|
| 268 |
+
font-family: var(--font-mono);
|
| 269 |
+
font-size: 0.65rem;
|
| 270 |
+
padding: 6px 8px;
|
| 271 |
+
margin-bottom: 2px;
|
| 272 |
+
border-radius: 4px;
|
| 273 |
+
background: rgba(0, 0, 0, 0.3);
|
| 274 |
+
border-left: 2px solid var(--cyan-dim);
|
| 275 |
+
line-height: 1.4;
|
| 276 |
+
color: var(--text-secondary);
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
.audit-entry.severity-WARNING {
|
| 280 |
+
border-left-color: var(--neon-amber);
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
.audit-entry.severity-CRITICAL {
|
| 284 |
+
border-left-color: var(--neon-red);
|
| 285 |
+
color: var(--neon-red);
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
.audit-time {
|
| 289 |
+
color: var(--text-dim);
|
| 290 |
+
font-size: 0.6rem;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
/* ═══════════════════════════════════════════════
|
| 294 |
+
MAIN CONTENT AREA
|
| 295 |
+
═══════════════════════════════════════════════ */
|
| 296 |
+
.main-content {
|
| 297 |
+
flex: 1;
|
| 298 |
+
padding: 16px;
|
| 299 |
+
display: grid;
|
| 300 |
+
grid-template-columns: 1fr 320px;
|
| 301 |
+
gap: 16px;
|
| 302 |
+
overflow: hidden;
|
| 303 |
+
background: radial-gradient(ellipse at 20% 0%, rgba(0, 60, 100, 0.15) 0%, transparent 60%),
|
| 304 |
+
radial-gradient(ellipse at 80% 100%, rgba(0, 40, 80, 0.1) 0%, transparent 60%),
|
| 305 |
+
var(--bg-deep);
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
/* ═════════════════════════════════════════��═════
|
| 309 |
+
MULTI-CAMERA GRID
|
| 310 |
+
═══════════════════════════════════════════════ */
|
| 311 |
+
.camera-grid {
|
| 312 |
+
display: grid;
|
| 313 |
+
grid-template-columns: 1fr 1fr;
|
| 314 |
+
grid-template-rows: 1fr 1fr;
|
| 315 |
+
gap: 8px;
|
| 316 |
+
height: 100%;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
.camera-grid.single-view {
|
| 320 |
+
grid-template-columns: 1fr;
|
| 321 |
+
grid-template-rows: 1fr;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.camera-grid.single-view .feed-cell:not(.expanded) {
|
| 325 |
+
display: none;
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
.feed-cell {
|
| 329 |
+
background: var(--bg-card);
|
| 330 |
+
border-radius: 10px;
|
| 331 |
+
border: 1px solid var(--border-glow);
|
| 332 |
+
overflow: hidden;
|
| 333 |
+
position: relative;
|
| 334 |
+
cursor: pointer;
|
| 335 |
+
transition: all 0.3s ease;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.feed-cell:hover {
|
| 339 |
+
border-color: var(--border-glow-hover);
|
| 340 |
+
box-shadow: 0 0 20px rgba(0, 200, 255, 0.1);
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
.feed-cell.red-alert-active {
|
| 344 |
+
border-color: var(--neon-red) !important;
|
| 345 |
+
box-shadow: 0 0 30px rgba(255, 32, 64, 0.3) !important;
|
| 346 |
+
animation: cell-red-pulse 1.5s ease-in-out infinite;
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
@keyframes cell-red-pulse {
|
| 350 |
+
|
| 351 |
+
0%,
|
| 352 |
+
100% {
|
| 353 |
+
box-shadow: 0 0 20px rgba(255, 32, 64, 0.2);
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
50% {
|
| 357 |
+
box-shadow: 0 0 40px rgba(255, 32, 64, 0.5);
|
| 358 |
+
}
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
.feed-header {
|
| 362 |
+
position: absolute;
|
| 363 |
+
top: 8px;
|
| 364 |
+
left: 10px;
|
| 365 |
+
right: 10px;
|
| 366 |
+
display: flex;
|
| 367 |
+
justify-content: space-between;
|
| 368 |
+
align-items: center;
|
| 369 |
+
z-index: 5;
|
| 370 |
+
pointer-events: none;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
.feed-badge {
|
| 374 |
+
background: rgba(0, 0, 0, 0.7);
|
| 375 |
+
backdrop-filter: blur(8px);
|
| 376 |
+
padding: 4px 10px;
|
| 377 |
+
border-radius: 4px;
|
| 378 |
+
font-family: var(--font-mono);
|
| 379 |
+
font-size: 0.65rem;
|
| 380 |
+
font-weight: 400;
|
| 381 |
+
border: 1px solid var(--border-glow);
|
| 382 |
+
display: flex;
|
| 383 |
+
align-items: center;
|
| 384 |
+
gap: 6px;
|
| 385 |
+
color: var(--cyan);
|
| 386 |
+
letter-spacing: 1px;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
.live-dot {
|
| 390 |
+
width: 6px;
|
| 391 |
+
height: 6px;
|
| 392 |
+
background: var(--neon-red);
|
| 393 |
+
border-radius: 50%;
|
| 394 |
+
box-shadow: 0 0 8px var(--neon-red);
|
| 395 |
+
animation: pulse-dot 2s infinite;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
@keyframes pulse-dot {
|
| 399 |
+
|
| 400 |
+
0%,
|
| 401 |
+
100% {
|
| 402 |
+
opacity: 1;
|
| 403 |
+
transform: scale(1);
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
50% {
|
| 407 |
+
opacity: 0.4;
|
| 408 |
+
transform: scale(1.3);
|
| 409 |
+
}
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
.feed-status {
|
| 413 |
+
font-family: var(--font-mono);
|
| 414 |
+
font-size: 0.6rem;
|
| 415 |
+
color: var(--neon-green);
|
| 416 |
+
background: rgba(0, 0, 0, 0.6);
|
| 417 |
+
padding: 3px 8px;
|
| 418 |
+
border-radius: 4px;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
.feed-stream {
|
| 422 |
+
width: 100%;
|
| 423 |
+
height: 100%;
|
| 424 |
+
object-fit: contain;
|
| 425 |
+
background: #000;
|
| 426 |
+
display: block;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.feed-offline {
|
| 430 |
+
display: flex;
|
| 431 |
+
flex-direction: column;
|
| 432 |
+
align-items: center;
|
| 433 |
+
justify-content: center;
|
| 434 |
+
height: 100%;
|
| 435 |
+
color: var(--text-dim);
|
| 436 |
+
font-family: var(--font-mono);
|
| 437 |
+
font-size: 0.75rem;
|
| 438 |
+
letter-spacing: 1px;
|
| 439 |
+
gap: 8px;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
.feed-offline svg {
|
| 443 |
+
width: 24px;
|
| 444 |
+
height: 24px;
|
| 445 |
+
color: var(--text-dim);
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
/* Fullscreen expand button */
|
| 449 |
+
.expand-btn {
|
| 450 |
+
position: absolute;
|
| 451 |
+
bottom: 8px;
|
| 452 |
+
right: 8px;
|
| 453 |
+
background: rgba(0, 0, 0, 0.6);
|
| 454 |
+
border: 1px solid var(--border-glow);
|
| 455 |
+
color: var(--cyan);
|
| 456 |
+
width: 28px;
|
| 457 |
+
height: 28px;
|
| 458 |
+
border-radius: 4px;
|
| 459 |
+
cursor: pointer;
|
| 460 |
+
display: flex;
|
| 461 |
+
align-items: center;
|
| 462 |
+
justify-content: center;
|
| 463 |
+
transition: all 0.2s;
|
| 464 |
+
z-index: 5;
|
| 465 |
+
pointer-events: all;
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
.expand-btn:hover {
|
| 469 |
+
background: rgba(0, 200, 255, 0.15);
|
| 470 |
+
border-color: var(--cyan);
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
.expand-btn svg {
|
| 474 |
+
width: 14px;
|
| 475 |
+
height: 14px;
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
/* ═══════════════════════════════════════════════
|
| 479 |
+
INTEL PANEL (Right Column)
|
| 480 |
+
═══════════════════════════════════════════════ */
|
| 481 |
+
.intel-panel {
|
| 482 |
+
display: flex;
|
| 483 |
+
flex-direction: column;
|
| 484 |
+
gap: 12px;
|
| 485 |
+
overflow-y: auto;
|
| 486 |
+
scrollbar-width: thin;
|
| 487 |
+
scrollbar-color: var(--cyan-dim) transparent;
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
.intel-panel::-webkit-scrollbar {
|
| 491 |
+
width: 4px;
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
.intel-panel::-webkit-scrollbar-thumb {
|
| 495 |
+
background: var(--cyan-dim);
|
| 496 |
+
border-radius: 2px;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
.card {
|
| 500 |
+
background: var(--bg-card);
|
| 501 |
+
backdrop-filter: blur(15px);
|
| 502 |
+
border-radius: 10px;
|
| 503 |
+
padding: 18px;
|
| 504 |
+
border: 1px solid var(--border-glow);
|
| 505 |
+
transition: border-color 0.3s;
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
.card:hover {
|
| 509 |
+
border-color: var(--border-glow-hover);
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
.card-header {
|
| 513 |
+
display: flex;
|
| 514 |
+
justify-content: space-between;
|
| 515 |
+
align-items: center;
|
| 516 |
+
margin-bottom: 14px;
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
.card-title {
|
| 520 |
+
font-family: var(--font-display);
|
| 521 |
+
font-size: 0.65rem;
|
| 522 |
+
font-weight: 600;
|
| 523 |
+
color: var(--cyan);
|
| 524 |
+
text-transform: uppercase;
|
| 525 |
+
letter-spacing: 2px;
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
.card-icon {
|
| 529 |
+
color: var(--text-dim);
|
| 530 |
+
width: 16px;
|
| 531 |
+
height: 16px;
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
/* Threat gauge */
|
| 535 |
+
.threat-gauge {
|
| 536 |
+
display: flex;
|
| 537 |
+
flex-direction: column;
|
| 538 |
+
align-items: center;
|
| 539 |
+
padding: 10px 0;
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
.score-ring {
|
| 543 |
+
position: relative;
|
| 544 |
+
width: 130px;
|
| 545 |
+
height: 130px;
|
| 546 |
+
margin-bottom: 12px;
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
.score-ring svg {
|
| 550 |
+
width: 130px;
|
| 551 |
+
height: 130px;
|
| 552 |
+
transform: rotate(-90deg);
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
.score-ring-bg {
|
| 556 |
+
fill: none;
|
| 557 |
+
stroke: rgba(255, 255, 255, 0.05);
|
| 558 |
+
stroke-width: 6;
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
.score-ring-fill {
|
| 562 |
+
fill: none;
|
| 563 |
+
stroke: var(--neon-green);
|
| 564 |
+
stroke-width: 6;
|
| 565 |
+
stroke-linecap: round;
|
| 566 |
+
stroke-dasharray: 377;
|
| 567 |
+
stroke-dashoffset: 377;
|
| 568 |
+
transition: stroke-dashoffset 0.8s cubic-bezier(0.4, 0, 0.2, 1),
|
| 569 |
+
stroke 0.5s ease;
|
| 570 |
+
filter: drop-shadow(0 0 8px var(--neon-green));
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
.score-text {
|
| 574 |
+
position: absolute;
|
| 575 |
+
top: 50%;
|
| 576 |
+
left: 50%;
|
| 577 |
+
transform: translate(-50%, -50%);
|
| 578 |
+
text-align: center;
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
.score-value {
|
| 582 |
+
font-family: var(--font-display);
|
| 583 |
+
font-size: 2.2rem;
|
| 584 |
+
font-weight: 800;
|
| 585 |
+
line-height: 1;
|
| 586 |
+
color: var(--text-primary);
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
.score-label {
|
| 590 |
+
font-family: var(--font-mono);
|
| 591 |
+
font-size: 0.55rem;
|
| 592 |
+
color: var(--text-dim);
|
| 593 |
+
letter-spacing: 2px;
|
| 594 |
+
margin-top: 4px;
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
.status-text {
|
| 598 |
+
font-family: var(--font-display);
|
| 599 |
+
font-size: 0.85rem;
|
| 600 |
+
font-weight: 700;
|
| 601 |
+
color: var(--neon-green);
|
| 602 |
+
letter-spacing: 3px;
|
| 603 |
+
text-shadow: 0 0 20px rgba(0, 255, 136, 0.3);
|
| 604 |
+
transition: all 0.5s ease;
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
/* Stats */
|
| 608 |
+
.stats-list {
|
| 609 |
+
display: flex;
|
| 610 |
+
flex-direction: column;
|
| 611 |
+
gap: 6px;
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
.stat-row {
|
| 615 |
+
display: flex;
|
| 616 |
+
justify-content: space-between;
|
| 617 |
+
align-items: center;
|
| 618 |
+
padding: 10px 12px;
|
| 619 |
+
background: rgba(0, 200, 255, 0.03);
|
| 620 |
+
border-radius: 6px;
|
| 621 |
+
border: 1px solid rgba(0, 200, 255, 0.05);
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
.stat-name {
|
| 625 |
+
font-size: 0.8rem;
|
| 626 |
+
color: var(--text-secondary);
|
| 627 |
+
font-weight: 500;
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
.stat-val {
|
| 631 |
+
font-family: var(--font-mono);
|
| 632 |
+
font-size: 0.85rem;
|
| 633 |
+
font-weight: 600;
|
| 634 |
+
color: var(--text-primary);
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
/* Mode indicator */
|
| 638 |
+
.mode-indicator {
|
| 639 |
+
display: flex;
|
| 640 |
+
align-items: center;
|
| 641 |
+
gap: 8px;
|
| 642 |
+
padding: 10px 14px;
|
| 643 |
+
background: rgba(0, 200, 255, 0.05);
|
| 644 |
+
border-radius: 6px;
|
| 645 |
+
border: 1px solid var(--border-glow);
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
.mode-dot {
|
| 649 |
+
width: 8px;
|
| 650 |
+
height: 8px;
|
| 651 |
+
background: var(--cyan);
|
| 652 |
+
border-radius: 50%;
|
| 653 |
+
box-shadow: 0 0 10px var(--cyan);
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
.mode-name {
|
| 657 |
+
font-family: var(--font-display);
|
| 658 |
+
font-size: 0.7rem;
|
| 659 |
+
font-weight: 600;
|
| 660 |
+
letter-spacing: 2px;
|
| 661 |
+
color: var(--cyan);
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
/* Buttons */
|
| 665 |
+
.btn {
|
| 666 |
+
width: 100%;
|
| 667 |
+
padding: 12px;
|
| 668 |
+
border: 1px solid var(--border-glow);
|
| 669 |
+
border-radius: 6px;
|
| 670 |
+
font-size: 0.8rem;
|
| 671 |
+
font-weight: 600;
|
| 672 |
+
cursor: pointer;
|
| 673 |
+
transition: all 0.25s;
|
| 674 |
+
font-family: var(--font-display);
|
| 675 |
+
letter-spacing: 1px;
|
| 676 |
+
text-transform: uppercase;
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
.btn-primary {
|
| 680 |
+
background: linear-gradient(135deg, rgba(0, 200, 255, 0.2), rgba(0, 100, 200, 0.1));
|
| 681 |
+
color: var(--cyan);
|
| 682 |
+
border-color: rgba(0, 200, 255, 0.3);
|
| 683 |
+
}
|
| 684 |
+
|
| 685 |
+
.btn-primary:hover {
|
| 686 |
+
background: linear-gradient(135deg, rgba(0, 200, 255, 0.3), rgba(0, 100, 200, 0.2));
|
| 687 |
+
box-shadow: 0 0 20px rgba(0, 200, 255, 0.15);
|
| 688 |
+
transform: translateY(-1px);
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
+
.btn-secondary {
|
| 692 |
+
background: rgba(255, 255, 255, 0.03);
|
| 693 |
+
color: var(--text-secondary);
|
| 694 |
+
}
|
| 695 |
+
|
| 696 |
+
.btn-secondary:hover {
|
| 697 |
+
background: rgba(255, 255, 255, 0.08);
|
| 698 |
+
color: var(--text-primary);
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
/* ═══════════════════════════════════════════════
|
| 702 |
+
MODAL
|
| 703 |
+
═══════════════════════════════════════════════ */
|
| 704 |
+
.modal-overlay {
|
| 705 |
+
position: fixed;
|
| 706 |
+
top: 0;
|
| 707 |
+
left: 0;
|
| 708 |
+
right: 0;
|
| 709 |
+
bottom: 0;
|
| 710 |
+
background: rgba(0, 0, 0, 0.8);
|
| 711 |
+
backdrop-filter: blur(10px);
|
| 712 |
+
display: none;
|
| 713 |
+
justify-content: center;
|
| 714 |
+
align-items: center;
|
| 715 |
+
z-index: 11000;
|
| 716 |
+
opacity: 0;
|
| 717 |
+
transition: opacity 0.3s ease;
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
.modal-overlay.show {
|
| 721 |
+
display: flex;
|
| 722 |
+
opacity: 1;
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
.modal-card {
|
| 726 |
+
background: var(--bg-panel);
|
| 727 |
+
width: 520px;
|
| 728 |
+
max-width: 90vw;
|
| 729 |
+
border-radius: 12px;
|
| 730 |
+
padding: 28px;
|
| 731 |
+
border: 1px solid var(--border-glow);
|
| 732 |
+
box-shadow: 0 0 60px rgba(0, 200, 255, 0.1);
|
| 733 |
+
transform: scale(0.95);
|
| 734 |
+
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
+
.modal-overlay.show .modal-card {
|
| 738 |
+
transform: scale(1);
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
.modal-title {
|
| 742 |
+
font-family: var(--font-display);
|
| 743 |
+
font-size: 1rem;
|
| 744 |
+
font-weight: 700;
|
| 745 |
+
color: var(--cyan);
|
| 746 |
+
letter-spacing: 2px;
|
| 747 |
+
margin-bottom: 6px;
|
| 748 |
+
}
|
| 749 |
+
|
| 750 |
+
.modal-subtitle {
|
| 751 |
+
font-family: var(--font-mono);
|
| 752 |
+
font-size: 0.7rem;
|
| 753 |
+
color: var(--text-dim);
|
| 754 |
+
margin-bottom: 16px;
|
| 755 |
+
}
|
| 756 |
+
|
| 757 |
+
.report-content {
|
| 758 |
+
background: rgba(0, 0, 0, 0.5);
|
| 759 |
+
padding: 16px;
|
| 760 |
+
border-radius: 8px;
|
| 761 |
+
font-family: var(--font-mono);
|
| 762 |
+
font-size: 0.75rem;
|
| 763 |
+
color: var(--neon-green);
|
| 764 |
+
margin-bottom: 16px;
|
| 765 |
+
line-height: 1.6;
|
| 766 |
+
border: 1px solid var(--border-glow);
|
| 767 |
+
max-height: 300px;
|
| 768 |
+
overflow-y: auto;
|
| 769 |
+
white-space: pre-wrap;
|
| 770 |
+
}
|
| 771 |
+
|
| 772 |
+
/* ═══════════════════════════════════════════════
|
| 773 |
+
MOBILE RESPONSIVE
|
| 774 |
+
═══════════════════════════════════════════════ */
|
| 775 |
+
@media (max-width: 1024px) {
|
| 776 |
+
.main-content {
|
| 777 |
+
grid-template-columns: 1fr 280px;
|
| 778 |
+
}
|
| 779 |
+
}
|
| 780 |
+
|
| 781 |
+
@media (max-width: 768px) {
|
| 782 |
+
body {
|
| 783 |
+
flex-direction: column;
|
| 784 |
+
overflow-y: auto;
|
| 785 |
+
}
|
| 786 |
+
|
| 787 |
+
.sidebar {
|
| 788 |
+
width: 100%;
|
| 789 |
+
min-width: 100%;
|
| 790 |
+
flex-direction: row;
|
| 791 |
+
flex-wrap: wrap;
|
| 792 |
+
padding: 10px 16px;
|
| 793 |
+
gap: 6px;
|
| 794 |
+
order: 2;
|
| 795 |
+
border-right: none;
|
| 796 |
+
border-top: 1px solid var(--border-glow);
|
| 797 |
+
overflow-y: visible;
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
.logo {
|
| 801 |
+
width: 100%;
|
| 802 |
+
margin-bottom: 6px;
|
| 803 |
+
font-size: 0.85rem;
|
| 804 |
+
}
|
| 805 |
+
|
| 806 |
+
.version-tag {
|
| 807 |
+
display: none;
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
+
.nav-group {
|
| 811 |
+
display: flex;
|
| 812 |
+
flex-wrap: wrap;
|
| 813 |
+
gap: 4px;
|
| 814 |
+
margin-bottom: 0;
|
| 815 |
+
}
|
| 816 |
+
|
| 817 |
+
.nav-label {
|
| 818 |
+
width: 100%;
|
| 819 |
+
margin-bottom: 4px;
|
| 820 |
+
}
|
| 821 |
+
|
| 822 |
+
.nav-item {
|
| 823 |
+
flex: none;
|
| 824 |
+
padding: 8px 10px;
|
| 825 |
+
font-size: 0.75rem;
|
| 826 |
+
}
|
| 827 |
+
|
| 828 |
+
.audit-section {
|
| 829 |
+
display: none;
|
| 830 |
+
}
|
| 831 |
+
|
| 832 |
+
.main-content {
|
| 833 |
+
grid-template-columns: 1fr;
|
| 834 |
+
grid-template-rows: auto auto;
|
| 835 |
+
order: 1;
|
| 836 |
+
overflow-y: auto;
|
| 837 |
+
padding: 10px;
|
| 838 |
+
}
|
| 839 |
+
|
| 840 |
+
.camera-grid {
|
| 841 |
+
grid-template-columns: 1fr;
|
| 842 |
+
grid-template-rows: auto;
|
| 843 |
+
min-height: 250px;
|
| 844 |
+
}
|
| 845 |
+
|
| 846 |
+
.feed-cell:not(:first-child) {
|
| 847 |
+
display: none;
|
| 848 |
+
}
|
| 849 |
+
|
| 850 |
+
.intel-panel {
|
| 851 |
+
overflow-y: visible;
|
| 852 |
+
}
|
| 853 |
+
}
|
| 854 |
+
|
| 855 |
+
@media (max-width: 480px) {
|
| 856 |
+
.sidebar .nav-item {
|
| 857 |
+
font-size: 0.7rem;
|
| 858 |
+
padding: 6px 8px;
|
| 859 |
+
gap: 6px;
|
| 860 |
+
}
|
| 861 |
+
|
| 862 |
+
.sidebar .nav-item svg {
|
| 863 |
+
width: 14px;
|
| 864 |
+
height: 14px;
|
| 865 |
+
}
|
| 866 |
+
|
| 867 |
+
.score-ring {
|
| 868 |
+
width: 100px;
|
| 869 |
+
height: 100px;
|
| 870 |
+
}
|
| 871 |
+
|
| 872 |
+
.score-ring svg {
|
| 873 |
+
width: 100px;
|
| 874 |
+
height: 100px;
|
| 875 |
+
}
|
| 876 |
+
|
| 877 |
+
.score-value {
|
| 878 |
+
font-size: 1.6rem;
|
| 879 |
+
}
|
| 880 |
+
}
|
| 881 |
+
|
| 882 |
+
/* ═══════════════════════════════════════════════
|
| 883 |
+
DISPATCH CENTER STYLES
|
| 884 |
+
═══════════════════════════════════════════════ */
|
| 885 |
+
.dispatch-badge {
|
| 886 |
+
display: inline-block;
|
| 887 |
+
padding: 2px 8px;
|
| 888 |
+
border-radius: 3px;
|
| 889 |
+
font-family: var(--font-mono);
|
| 890 |
+
font-size: 0.55rem;
|
| 891 |
+
font-weight: 600;
|
| 892 |
+
letter-spacing: 1px;
|
| 893 |
+
text-transform: uppercase;
|
| 894 |
+
}
|
| 895 |
+
|
| 896 |
+
.dispatch-badge.sent {
|
| 897 |
+
background: rgba(0, 255, 136, 0.15);
|
| 898 |
+
color: var(--neon-green);
|
| 899 |
+
border: 1px solid rgba(0, 255, 136, 0.3);
|
| 900 |
+
}
|
| 901 |
+
|
| 902 |
+
.dispatch-badge.pending {
|
| 903 |
+
background: rgba(255, 170, 0, 0.15);
|
| 904 |
+
color: var(--neon-amber);
|
| 905 |
+
border: 1px solid rgba(255, 170, 0, 0.3);
|
| 906 |
+
animation: pulse-dot 2s infinite;
|
| 907 |
+
}
|
| 908 |
+
|
| 909 |
+
.dispatch-badge.rejected {
|
| 910 |
+
background: rgba(255, 32, 64, 0.15);
|
| 911 |
+
color: var(--neon-red);
|
| 912 |
+
border: 1px solid rgba(255, 32, 64, 0.3);
|
| 913 |
+
}
|
| 914 |
+
|
| 915 |
+
.dispatch-badge.failed {
|
| 916 |
+
background: rgba(255, 32, 64, 0.1);
|
| 917 |
+
color: #ff6080;
|
| 918 |
+
border: 1px solid rgba(255, 96, 128, 0.3);
|
| 919 |
+
}
|
| 920 |
+
|
| 921 |
+
.dispatch-entry {
|
| 922 |
+
font-family: var(--font-mono);
|
| 923 |
+
font-size: 0.65rem;
|
| 924 |
+
padding: 8px 10px;
|
| 925 |
+
margin-bottom: 4px;
|
| 926 |
+
border-radius: 4px;
|
| 927 |
+
background: rgba(0, 0, 0, 0.3);
|
| 928 |
+
border-left: 2px solid var(--cyan-dim);
|
| 929 |
+
line-height: 1.5;
|
| 930 |
+
color: var(--text-secondary);
|
| 931 |
+
display: flex;
|
| 932 |
+
justify-content: space-between;
|
| 933 |
+
align-items: flex-start;
|
| 934 |
+
gap: 8px;
|
| 935 |
+
}
|
| 936 |
+
|
| 937 |
+
.dispatch-entry.pending-entry {
|
| 938 |
+
border-left-color: var(--neon-amber);
|
| 939 |
+
background: rgba(255, 170, 0, 0.05);
|
| 940 |
+
}
|
| 941 |
+
|
| 942 |
+
.dispatch-entry .dispatch-info {
|
| 943 |
+
flex: 1;
|
| 944 |
+
}
|
| 945 |
+
|
| 946 |
+
.dispatch-entry .dispatch-score {
|
| 947 |
+
font-family: var(--font-display);
|
| 948 |
+
font-size: 0.8rem;
|
| 949 |
+
font-weight: 700;
|
| 950 |
+
min-width: 30px;
|
| 951 |
+
text-align: right;
|
| 952 |
+
}
|
| 953 |
+
|
| 954 |
+
.dispatch-actions {
|
| 955 |
+
display: flex;
|
| 956 |
+
gap: 4px;
|
| 957 |
+
margin-top: 6px;
|
| 958 |
+
}
|
| 959 |
+
|
| 960 |
+
.dispatch-actions button {
|
| 961 |
+
padding: 4px 12px;
|
| 962 |
+
border-radius: 4px;
|
| 963 |
+
font-family: var(--font-display);
|
| 964 |
+
font-size: 0.6rem;
|
| 965 |
+
font-weight: 600;
|
| 966 |
+
letter-spacing: 1px;
|
| 967 |
+
cursor: pointer;
|
| 968 |
+
transition: all 0.2s;
|
| 969 |
+
border: 1px solid;
|
| 970 |
+
}
|
| 971 |
+
|
| 972 |
+
.btn-approve {
|
| 973 |
+
background: rgba(0, 255, 136, 0.1);
|
| 974 |
+
border-color: rgba(0, 255, 136, 0.4) !important;
|
| 975 |
+
color: var(--neon-green);
|
| 976 |
+
}
|
| 977 |
+
|
| 978 |
+
.btn-approve:hover {
|
| 979 |
+
background: rgba(0, 255, 136, 0.25);
|
| 980 |
+
box-shadow: 0 0 12px rgba(0, 255, 136, 0.2);
|
| 981 |
+
}
|
| 982 |
+
|
| 983 |
+
.btn-reject {
|
| 984 |
+
background: rgba(255, 32, 64, 0.1);
|
| 985 |
+
border-color: rgba(255, 32, 64, 0.4) !important;
|
| 986 |
+
color: var(--neon-red);
|
| 987 |
+
}
|
| 988 |
+
|
| 989 |
+
.btn-reject:hover {
|
| 990 |
+
background: rgba(255, 32, 64, 0.25);
|
| 991 |
+
box-shadow: 0 0 12px rgba(255, 32, 64, 0.2);
|
| 992 |
+
}
|
| 993 |
+
|
| 994 |
+
.dispatch-config-row {
|
| 995 |
+
display: flex;
|
| 996 |
+
justify-content: space-between;
|
| 997 |
+
align-items: center;
|
| 998 |
+
padding: 10px 0;
|
| 999 |
+
border-bottom: 1px solid rgba(0, 200, 255, 0.05);
|
| 1000 |
+
}
|
| 1001 |
+
|
| 1002 |
+
.dispatch-config-row:last-child {
|
| 1003 |
+
border-bottom: none;
|
| 1004 |
+
}
|
| 1005 |
+
|
| 1006 |
+
.dispatch-config-label {
|
| 1007 |
+
font-size: 0.8rem;
|
| 1008 |
+
color: var(--text-secondary);
|
| 1009 |
+
font-weight: 500;
|
| 1010 |
+
}
|
| 1011 |
+
|
| 1012 |
+
.toggle-switch {
|
| 1013 |
+
position: relative;
|
| 1014 |
+
width: 42px;
|
| 1015 |
+
height: 22px;
|
| 1016 |
+
cursor: pointer;
|
| 1017 |
+
}
|
| 1018 |
+
|
| 1019 |
+
.toggle-switch input {
|
| 1020 |
+
opacity: 0;
|
| 1021 |
+
width: 0;
|
| 1022 |
+
height: 0;
|
| 1023 |
+
}
|
| 1024 |
+
|
| 1025 |
+
.toggle-slider {
|
| 1026 |
+
position: absolute;
|
| 1027 |
+
top: 0;
|
| 1028 |
+
left: 0;
|
| 1029 |
+
right: 0;
|
| 1030 |
+
bottom: 0;
|
| 1031 |
+
background: rgba(255, 255, 255, 0.1);
|
| 1032 |
+
border-radius: 22px;
|
| 1033 |
+
transition: 0.3s;
|
| 1034 |
+
border: 1px solid rgba(255, 255, 255, 0.15);
|
| 1035 |
+
}
|
| 1036 |
+
|
| 1037 |
+
.toggle-slider::before {
|
| 1038 |
+
content: '';
|
| 1039 |
+
position: absolute;
|
| 1040 |
+
width: 16px;
|
| 1041 |
+
height: 16px;
|
| 1042 |
+
left: 2px;
|
| 1043 |
+
bottom: 2px;
|
| 1044 |
+
background: var(--text-secondary);
|
| 1045 |
+
border-radius: 50%;
|
| 1046 |
+
transition: 0.3s;
|
| 1047 |
+
}
|
| 1048 |
+
|
| 1049 |
+
.toggle-switch input:checked+.toggle-slider {
|
| 1050 |
+
background: rgba(0, 200, 255, 0.3);
|
| 1051 |
+
border-color: var(--cyan);
|
| 1052 |
+
}
|
| 1053 |
+
|
| 1054 |
+
.toggle-switch input:checked+.toggle-slider::before {
|
| 1055 |
+
transform: translateX(20px);
|
| 1056 |
+
background: var(--cyan);
|
| 1057 |
+
}
|
| 1058 |
+
|
| 1059 |
+
.config-input {
|
| 1060 |
+
background: rgba(0, 0, 0, 0.4);
|
| 1061 |
+
border: 1px solid var(--border-glow);
|
| 1062 |
+
border-radius: 4px;
|
| 1063 |
+
color: var(--text-primary);
|
| 1064 |
+
font-family: var(--font-mono);
|
| 1065 |
+
font-size: 0.75rem;
|
| 1066 |
+
padding: 6px 10px;
|
| 1067 |
+
width: 100%;
|
| 1068 |
+
transition: border-color 0.3s;
|
| 1069 |
+
}
|
| 1070 |
+
|
| 1071 |
+
.config-input:focus {
|
| 1072 |
+
outline: none;
|
| 1073 |
+
border-color: var(--cyan);
|
| 1074 |
+
box-shadow: 0 0 8px rgba(0, 200, 255, 0.15);
|
| 1075 |
+
}
|
| 1076 |
+
|
| 1077 |
+
.config-input::placeholder {
|
| 1078 |
+
color: var(--text-dim);
|
| 1079 |
+
}
|
| 1080 |
+
|
| 1081 |
+
.cooldown-slider {
|
| 1082 |
+
-webkit-appearance: none;
|
| 1083 |
+
appearance: none;
|
| 1084 |
+
width: 100%;
|
| 1085 |
+
height: 4px;
|
| 1086 |
+
border-radius: 2px;
|
| 1087 |
+
background: rgba(255, 255, 255, 0.1);
|
| 1088 |
+
outline: none;
|
| 1089 |
+
margin: 8px 0;
|
| 1090 |
+
}
|
| 1091 |
+
|
| 1092 |
+
.cooldown-slider::-webkit-slider-thumb {
|
| 1093 |
+
-webkit-appearance: none;
|
| 1094 |
+
width: 14px;
|
| 1095 |
+
height: 14px;
|
| 1096 |
+
border-radius: 50%;
|
| 1097 |
+
background: var(--cyan);
|
| 1098 |
+
cursor: pointer;
|
| 1099 |
+
box-shadow: 0 0 8px rgba(0, 200, 255, 0.4);
|
| 1100 |
+
}
|
| 1101 |
+
|
| 1102 |
+
.pending-count-badge {
|
| 1103 |
+
background: var(--neon-amber);
|
| 1104 |
+
color: #000;
|
| 1105 |
+
font-family: var(--font-mono);
|
| 1106 |
+
font-size: 0.55rem;
|
| 1107 |
+
font-weight: 700;
|
| 1108 |
+
width: 16px;
|
| 1109 |
+
height: 16px;
|
| 1110 |
+
border-radius: 50%;
|
| 1111 |
+
display: none;
|
| 1112 |
+
align-items: center;
|
| 1113 |
+
justify-content: center;
|
| 1114 |
+
margin-left: auto;
|
| 1115 |
+
}
|
| 1116 |
+
|
| 1117 |
+
.pending-count-badge.visible {
|
| 1118 |
+
display: flex;
|
| 1119 |
+
}
|
| 1120 |
+
|
| 1121 |
+
.dispatch-status-bar {
|
| 1122 |
+
display: flex;
|
| 1123 |
+
align-items: center;
|
| 1124 |
+
gap: 6px;
|
| 1125 |
+
padding: 6px 10px;
|
| 1126 |
+
border-radius: 4px;
|
| 1127 |
+
font-family: var(--font-mono);
|
| 1128 |
+
font-size: 0.6rem;
|
| 1129 |
+
margin-bottom: 8px;
|
| 1130 |
+
}
|
| 1131 |
+
|
| 1132 |
+
.dispatch-status-bar.configured {
|
| 1133 |
+
background: rgba(0, 255, 136, 0.05);
|
| 1134 |
+
border: 1px solid rgba(0, 255, 136, 0.15);
|
| 1135 |
+
color: var(--neon-green);
|
| 1136 |
+
}
|
| 1137 |
+
|
| 1138 |
+
.dispatch-status-bar.not-configured {
|
| 1139 |
+
background: rgba(255, 170, 0, 0.05);
|
| 1140 |
+
border: 1px solid rgba(255, 170, 0, 0.15);
|
| 1141 |
+
color: var(--neon-amber);
|
| 1142 |
+
}
|
| 1143 |
+
|
| 1144 |
+
.dispatch-dot {
|
| 1145 |
+
width: 6px;
|
| 1146 |
+
height: 6px;
|
| 1147 |
+
border-radius: 50%;
|
| 1148 |
+
}
|
| 1149 |
+
|
| 1150 |
+
.dispatch-dot.online {
|
| 1151 |
+
background: var(--neon-green);
|
| 1152 |
+
box-shadow: 0 0 6px var(--neon-green);
|
| 1153 |
+
}
|
| 1154 |
+
|
| 1155 |
+
.dispatch-dot.offline {
|
| 1156 |
+
background: var(--neon-amber);
|
| 1157 |
+
box-shadow: 0 0 6px var(--neon-amber);
|
| 1158 |
+
}
|
| 1159 |
+
</style>
|
| 1160 |
+
</head>
|
| 1161 |
+
|
| 1162 |
+
<body>
|
| 1163 |
+
<!-- Red Alert Overlay -->
|
| 1164 |
+
<div class="red-alert-overlay" id="red-alert-overlay"></div>
|
| 1165 |
+
<div class="red-alert-banner" id="red-alert-banner">
|
| 1166 |
+
⚠ RED ALERT — CRITICAL THREAT DETECTED ⚠
|
| 1167 |
+
<button class="btn btn-secondary" onclick="resetSystem()"
|
| 1168 |
+
style="width: auto; margin-top: 10px; background: rgba(0,0,0,0.5); border: 1px solid #ff2040; color: #ff2040;">RESET
|
| 1169 |
+
SYSTEM</button>
|
| 1170 |
+
</div>
|
| 1171 |
+
|
| 1172 |
+
<!-- SIDEBAR -->
|
| 1173 |
+
<div class="sidebar">
|
| 1174 |
+
<div class="logo">
|
| 1175 |
+
<i data-feather="shield"></i>
|
| 1176 |
+
SENTINEL
|
| 1177 |
+
</div>
|
| 1178 |
+
<div class="version-tag">V15.0 // AI + ETHICS INTEGRATION</div>
|
| 1179 |
+
|
| 1180 |
+
<div class="nav-group">
|
| 1181 |
+
<div class="nav-label">Detection Modules</div>
|
| 1182 |
+
<div class="nav-item active" onclick="toggleModule('movement', this)" id="nav-movement">
|
| 1183 |
+
<i data-feather="activity"></i> Movement
|
| 1184 |
+
</div>
|
| 1185 |
+
<div class="nav-item" onclick="toggleModule('facemask', this)" id="nav-facemask">
|
| 1186 |
+
<i data-feather="eye"></i> Facemask
|
| 1187 |
+
</div>
|
| 1188 |
+
<div class="nav-item" onclick="toggleModule('weapon', this)" id="nav-weapon">
|
| 1189 |
+
<i data-feather="crosshair"></i> Weapon
|
| 1190 |
+
</div>
|
| 1191 |
+
<div class="nav-item" onclick="toggleModule('public_safety', this)" id="nav-public_safety">
|
| 1192 |
+
<i data-feather="users"></i> Public Safety
|
| 1193 |
+
</div>
|
| 1194 |
+
<div class="nav-item" onclick="toggleModule('suspect_journey', this)" id="nav-suspect_journey"
|
| 1195 |
+
style="border-color: rgba(255,255,0,0.3);">
|
| 1196 |
+
<i data-feather="map-pin"></i> Suspect Journey
|
| 1197 |
+
</div>
|
| 1198 |
+
</div>
|
| 1199 |
+
|
| 1200 |
+
<div class="nav-group">
|
| 1201 |
+
<div class="nav-label">Dispatch</div>
|
| 1202 |
+
<div class="nav-item" onclick="openDispatchSettings()" id="nav-dispatch">
|
| 1203 |
+
<i data-feather="bell"></i> Dispatch Settings
|
| 1204 |
+
<span class="pending-count-badge" id="pending-badge">0</span>
|
| 1205 |
+
</div>
|
| 1206 |
+
</div>
|
| 1207 |
+
|
| 1208 |
+
<div class="nav-group">
|
| 1209 |
+
<div class="nav-label">Ethics & AI</div>
|
| 1210 |
+
<div class="nav-item" onclick="openPrivacySettings()">
|
| 1211 |
+
<i data-feather="lock"></i> Privacy Controls
|
| 1212 |
+
</div>
|
| 1213 |
+
<div class="nav-item" onclick="openFairnessMetrics()">
|
| 1214 |
+
<i data-feather="pie-chart"></i> Fairness Metrics
|
| 1215 |
+
</div>
|
| 1216 |
+
</div>
|
| 1217 |
+
|
| 1218 |
+
<div class="nav-group">
|
| 1219 |
+
<div class="nav-label">Grid Layout</div>
|
| 1220 |
+
<div class="nav-item" onclick="setGridLayout('quad')">
|
| 1221 |
+
<i data-feather="grid"></i> 2×2 Grid
|
| 1222 |
+
</div>
|
| 1223 |
+
<div class="nav-item" onclick="setGridLayout('single')">
|
| 1224 |
+
<i data-feather="maximize-2"></i> Single View
|
| 1225 |
+
</div>
|
| 1226 |
+
</div>
|
| 1227 |
+
|
| 1228 |
+
<!-- Audit Log -->
|
| 1229 |
+
<div class="audit-section">
|
| 1230 |
+
<div class="nav-label" style="margin-bottom: 8px;">Operator Log</div>
|
| 1231 |
+
<div class="audit-log-container" id="audit-log-container">
|
| 1232 |
+
<div class="audit-entry">
|
| 1233 |
+
<div class="audit-time">--:--:--</div>
|
| 1234 |
+
SYSTEM STANDBY
|
| 1235 |
+
</div>
|
| 1236 |
+
</div>
|
| 1237 |
+
</div>
|
| 1238 |
+
</div>
|
| 1239 |
+
|
| 1240 |
+
<!-- MAIN CONTENT -->
|
| 1241 |
+
<div class="main-content">
|
| 1242 |
+
<!-- Multi-Camera Grid -->
|
| 1243 |
+
<div class="camera-grid" id="camera-grid">
|
| 1244 |
+
<!-- Feed 0 — Primary -->
|
| 1245 |
+
<div class="feed-cell expanded" id="feed-0" onclick="expandFeed(0)">
|
| 1246 |
+
<div class="feed-header">
|
| 1247 |
+
<div class="feed-badge">
|
| 1248 |
+
<div class="live-dot"></div>
|
| 1249 |
+
<span>FEED 01 // PRIMARY</span>
|
| 1250 |
+
</div>
|
| 1251 |
+
<div class="feed-status" id="feed-0-status">ACTIVE</div>
|
| 1252 |
+
</div>
|
| 1253 |
+
<img class="feed-stream" id="stream-0" src="/video_feed/0" alt="Feed 0">
|
| 1254 |
+
<button class="expand-btn" onclick="event.stopPropagation(); expandFeed(0)" title="Expand">
|
| 1255 |
+
<i data-feather="maximize-2"></i>
|
| 1256 |
+
</button>
|
| 1257 |
+
<button class="expand-btn"
|
| 1258 |
+
style="right:40px; background: rgba(0,200,255,0.2); border-color: var(--cyan);"
|
| 1259 |
+
onclick="event.stopPropagation(); triggerFeedUpload(0)" title="Upload to this feed">
|
| 1260 |
+
<i data-feather="upload-cloud"></i>
|
| 1261 |
+
</button>
|
| 1262 |
+
</div>
|
| 1263 |
+
|
| 1264 |
+
<!-- Feed 1 -->
|
| 1265 |
+
<div class="feed-cell" id="feed-1">
|
| 1266 |
+
<div class="feed-header">
|
| 1267 |
+
<div class="feed-badge">
|
| 1268 |
+
<div class="live-dot" style="background: var(--text-dim); box-shadow: none;"></div>
|
| 1269 |
+
<span>FEED 02</span>
|
| 1270 |
+
</div>
|
| 1271 |
+
<div class="feed-status" id="feed-1-status" style="color: var(--text-dim);">STANDBY</div>
|
| 1272 |
+
</div>
|
| 1273 |
+
<div class="feed-offline" id="offline-1">
|
| 1274 |
+
<i data-feather="video-off"></i>
|
| 1275 |
+
<span>NO SIGNAL</span>
|
| 1276 |
+
<button class="btn btn-primary"
|
| 1277 |
+
style="width:auto; margin-top:10px; padding:8px 16px; font-size:0.7rem;"
|
| 1278 |
+
onclick="event.stopPropagation(); triggerFeedUpload(1)">
|
| 1279 |
+
<i data-feather="upload-cloud" style="width:14px;height:14px;"></i> UPLOAD CLIP
|
| 1280 |
+
</button>
|
| 1281 |
+
</div>
|
| 1282 |
+
</div>
|
| 1283 |
+
|
| 1284 |
+
<!-- Feed 2 -->
|
| 1285 |
+
<div class="feed-cell" id="feed-2">
|
| 1286 |
+
<div class="feed-header">
|
| 1287 |
+
<div class="feed-badge">
|
| 1288 |
+
<div class="live-dot" style="background: var(--text-dim); box-shadow: none;"></div>
|
| 1289 |
+
<span>FEED 03</span>
|
| 1290 |
+
</div>
|
| 1291 |
+
<div class="feed-status" id="feed-2-status" style="color: var(--text-dim);">STANDBY</div>
|
| 1292 |
+
</div>
|
| 1293 |
+
<div class="feed-offline" id="offline-2">
|
| 1294 |
+
<i data-feather="video-off"></i>
|
| 1295 |
+
<span>NO SIGNAL</span>
|
| 1296 |
+
<button class="btn btn-primary"
|
| 1297 |
+
style="width:auto; margin-top:10px; padding:8px 16px; font-size:0.7rem;"
|
| 1298 |
+
onclick="event.stopPropagation(); triggerFeedUpload(2)">
|
| 1299 |
+
<i data-feather="upload-cloud" style="width:14px;height:14px;"></i> UPLOAD CLIP
|
| 1300 |
+
</button>
|
| 1301 |
+
</div>
|
| 1302 |
+
</div>
|
| 1303 |
+
|
| 1304 |
+
<!-- Feed 3 -->
|
| 1305 |
+
<div class="feed-cell" id="feed-3">
|
| 1306 |
+
<div class="feed-header">
|
| 1307 |
+
<div class="feed-badge">
|
| 1308 |
+
<div class="live-dot" style="background: var(--text-dim); box-shadow: none;"></div>
|
| 1309 |
+
<span>FEED 04</span>
|
| 1310 |
+
</div>
|
| 1311 |
+
<div class="feed-status" id="feed-3-status" style="color: var(--text-dim);">STANDBY</div>
|
| 1312 |
+
</div>
|
| 1313 |
+
<div class="feed-offline" id="offline-3">
|
| 1314 |
+
<i data-feather="video-off"></i>
|
| 1315 |
+
<span>NO SIGNAL</span>
|
| 1316 |
+
<button class="btn btn-primary"
|
| 1317 |
+
style="width:auto; margin-top:10px; padding:8px 16px; font-size:0.7rem;"
|
| 1318 |
+
onclick="event.stopPropagation(); triggerFeedUpload(3)">
|
| 1319 |
+
<i data-feather="upload-cloud" style="width:14px;height:14px;"></i> UPLOAD CLIP
|
| 1320 |
+
</button>
|
| 1321 |
+
</div>
|
| 1322 |
+
</div>
|
| 1323 |
+
</div>
|
| 1324 |
+
|
| 1325 |
+
<!-- Hidden file input for per-feed upload -->
|
| 1326 |
+
<input type="file" id="feed-upload-input" style="display: none" accept="video/*,.gif">
|
| 1327 |
+
|
| 1328 |
+
|
| 1329 |
+
<!-- Intel Panel -->
|
| 1330 |
+
<div class="intel-panel">
|
| 1331 |
+
<!-- Suspect Journey -->
|
| 1332 |
+
<div class="card" id="journey-card">
|
| 1333 |
+
<div class="card-header">
|
| 1334 |
+
<div class="card-title">Suspect Journey</div>
|
| 1335 |
+
<i data-feather="map" class="card-icon"></i>
|
| 1336 |
+
</div>
|
| 1337 |
+
<div class="audit-log-container" id="journey-container" style="max-height: 250px;">
|
| 1338 |
+
<div class="audit-entry">
|
| 1339 |
+
<div class="audit-time">--:--:--</div>
|
| 1340 |
+
ACTIVATE 'SUSPECT JOURNEY' MODULE TO BEGIN
|
| 1341 |
+
</div>
|
| 1342 |
+
</div>
|
| 1343 |
+
</div>
|
| 1344 |
+
|
| 1345 |
+
<!-- V15 AI Situational Assessment -->
|
| 1346 |
+
<div class="card" id="ai-assessment-card" style="border-color: var(--cyan);">
|
| 1347 |
+
<div class="card-header">
|
| 1348 |
+
<div class="card-title">AI Assessment <span id="ai-severity" class="dispatch-badge" style="margin-left:8px;">LOW</span></div>
|
| 1349 |
+
<i data-feather="cpu" class="card-icon" style="color: var(--cyan);"></i>
|
| 1350 |
+
</div>
|
| 1351 |
+
<div style="font-size: 0.8rem; color: var(--text-primary); margin-bottom: 8px;" id="ai-summary">
|
| 1352 |
+
System initializing — awaiting first AI assessment.
|
| 1353 |
+
</div>
|
| 1354 |
+
<div style="font-family: var(--font-mono); font-size: 0.65rem; color: var(--text-dim); margin-bottom: 8px;">
|
| 1355 |
+
<div id="ai-patterns"></div>
|
| 1356 |
+
</div>
|
| 1357 |
+
<div style="padding: 6px; background: rgba(0,200,255,0.1); border-left: 2px solid var(--cyan); font-size: 0.75rem; color: var(--text-primary);" id="ai-action">
|
| 1358 |
+
Monitor feeds and enable detection modules.
|
| 1359 |
+
</div>
|
| 1360 |
+
<div style="font-family: var(--font-mono); font-size: 0.6rem; color: var(--text-dim); text-align: right; margin-top: 6px;" id="ai-metadata">
|
| 1361 |
+
Confidence: 0% | Last updated: --:--:--
|
| 1362 |
+
</div>
|
| 1363 |
+
</div>
|
| 1364 |
+
|
| 1365 |
+
<!-- Active Mode -->
|
| 1366 |
+
<div class="card">
|
| 1367 |
+
<div class="card-header">
|
| 1368 |
+
<div class="card-title">Active Mode</div>
|
| 1369 |
+
</div>
|
| 1370 |
+
<div class="mode-indicator">
|
| 1371 |
+
<div class="mode-dot"></div>
|
| 1372 |
+
<div class="mode-name" id="mode-title">1 MODULE ACTIVE</div>
|
| 1373 |
+
</div>
|
| 1374 |
+
</div>
|
| 1375 |
+
|
| 1376 |
+
<!-- Threat Assessment -->
|
| 1377 |
+
<div class="card" id="threat-card">
|
| 1378 |
+
<div class="card-header">
|
| 1379 |
+
<div class="card-title">Threat Assessment</div>
|
| 1380 |
+
<i data-feather="alert-triangle" class="card-icon"></i>
|
| 1381 |
+
</div>
|
| 1382 |
+
<div class="threat-gauge">
|
| 1383 |
+
<div class="score-ring">
|
| 1384 |
+
<svg viewBox="0 0 130 130">
|
| 1385 |
+
<circle class="score-ring-bg" cx="65" cy="65" r="60"></circle>
|
| 1386 |
+
<circle class="score-ring-fill" id="score-ring-fill" cx="65" cy="65" r="60"></circle>
|
| 1387 |
+
</svg>
|
| 1388 |
+
<div class="score-text">
|
| 1389 |
+
<div class="score-value" id="threat-score">0</div>
|
| 1390 |
+
<div class="score-label">THREAT LEVEL</div>
|
| 1391 |
+
</div>
|
| 1392 |
+
</div>
|
| 1393 |
+
<div class="status-text" id="status-text">SECURE</div>
|
| 1394 |
+
</div>
|
| 1395 |
+
</div>
|
| 1396 |
+
|
| 1397 |
+
<!-- V15 Explainability Panel -->
|
| 1398 |
+
<div class="card" id="explain-card">
|
| 1399 |
+
<div class="card-header">
|
| 1400 |
+
<div class="card-title">Threat Explanation</div>
|
| 1401 |
+
<i data-feather="info" class="card-icon"></i>
|
| 1402 |
+
</div>
|
| 1403 |
+
<div class="audit-log-container" id="explain-container" style="max-height: 150px; background: rgba(0,0,0,0.2);">
|
| 1404 |
+
<div class="audit-entry">
|
| 1405 |
+
<span style="color:var(--text-dim);">No active threats contributing to score.</span>
|
| 1406 |
+
</div>
|
| 1407 |
+
</div>
|
| 1408 |
+
</div>
|
| 1409 |
+
|
| 1410 |
+
<!-- Live Metrics -->
|
| 1411 |
+
<div class="card">
|
| 1412 |
+
<div class="card-header">
|
| 1413 |
+
<div class="card-title">Live Metrics</div>
|
| 1414 |
+
<i data-feather="bar-chart-2" class="card-icon"></i>
|
| 1415 |
+
</div>
|
| 1416 |
+
<div class="stats-list" id="stats-container">
|
| 1417 |
+
<div class="stat-row">
|
| 1418 |
+
<span class="stat-name">System Status</span>
|
| 1419 |
+
<span class="stat-val">Initializing</span>
|
| 1420 |
+
</div>
|
| 1421 |
+
</div>
|
| 1422 |
+
</div>
|
| 1423 |
+
|
| 1424 |
+
<!-- Dispatch Center -->
|
| 1425 |
+
<div class="card" id="dispatch-card">
|
| 1426 |
+
<div class="card-header">
|
| 1427 |
+
<div class="card-title">Dispatch Center</div>
|
| 1428 |
+
<i data-feather="send" class="card-icon"></i>
|
| 1429 |
+
</div>
|
| 1430 |
+
<div class="dispatch-status-bar not-configured" id="dispatch-status-bar">
|
| 1431 |
+
<div class="dispatch-dot offline" id="dispatch-dot"></div>
|
| 1432 |
+
<span id="dispatch-status-text">TELEGRAM NOT CONFIGURED</span>
|
| 1433 |
+
</div>
|
| 1434 |
+
|
| 1435 |
+
<!-- Pending Approvals -->
|
| 1436 |
+
<div id="pending-section" style="display:none; margin-bottom: 10px;">
|
| 1437 |
+
<div
|
| 1438 |
+
style="font-family: var(--font-display); font-size: 0.6rem; color: var(--neon-amber); letter-spacing: 1px; margin-bottom: 6px;">
|
| 1439 |
+
⏳ PENDING APPROVAL</div>
|
| 1440 |
+
<div id="pending-container"></div>
|
| 1441 |
+
</div>
|
| 1442 |
+
|
| 1443 |
+
<!-- Dispatch Log -->
|
| 1444 |
+
<div class="audit-log-container" id="dispatch-log-container" style="max-height: 180px;">
|
| 1445 |
+
<div class="dispatch-entry">
|
| 1446 |
+
<div class="dispatch-info">
|
| 1447 |
+
<div class="audit-time">--:--:--</div>
|
| 1448 |
+
NO DISPATCH HISTORY
|
| 1449 |
+
</div>
|
| 1450 |
+
</div>
|
| 1451 |
+
</div>
|
| 1452 |
+
|
| 1453 |
+
<!-- Quick Actions -->
|
| 1454 |
+
<div style="display: flex; gap: 6px; margin-top: 10px;">
|
| 1455 |
+
<button class="btn btn-primary" onclick="manualDispatch()"
|
| 1456 |
+
style="font-size: 0.65rem; padding: 8px;">📡 SEND ALERT</button>
|
| 1457 |
+
<button class="btn btn-secondary" onclick="testDispatch()"
|
| 1458 |
+
style="font-size: 0.65rem; padding: 8px;">🧪 TEST</button>
|
| 1459 |
+
<button class="btn btn-secondary" onclick="openDispatchSettings()"
|
| 1460 |
+
style="font-size: 0.65rem; padding: 8px;">⚙</button>
|
| 1461 |
+
</div>
|
| 1462 |
+
</div>
|
| 1463 |
+
|
| 1464 |
+
<!-- Actions -->
|
| 1465 |
+
<div class="card">
|
| 1466 |
+
<div class="card-header">
|
| 1467 |
+
<div class="card-title">Actions</div>
|
| 1468 |
+
</div>
|
| 1469 |
+
<button class="btn btn-primary" onclick="generateReport()">Generate Incident Report</button>
|
| 1470 |
+
<button class="btn btn-secondary" onclick="refreshAuditLog()" style="margin-top: 8px;">Refresh Audit
|
| 1471 |
+
Log</button>
|
| 1472 |
+
<button class="btn btn-secondary" onclick="resetSystem()"
|
| 1473 |
+
style="margin-top: 8px; border-color: #ff2040; color: #ff2040;">⚠ SYSTEM RESET</button>
|
| 1474 |
+
</div>
|
| 1475 |
+
</div>
|
| 1476 |
+
</div>
|
| 1477 |
+
|
| 1478 |
+
<!-- Report Modal -->
|
| 1479 |
+
<div id="report-modal" class="modal-overlay">
|
| 1480 |
+
<div class="modal-card">
|
| 1481 |
+
<div class="modal-title">Incident Report</div>
|
| 1482 |
+
<div class="modal-subtitle">Generated by Sentinel AI Agent // Gemini 1.5</div>
|
| 1483 |
+
<div class="report-content" id="report-content">Analyzing data stream...</div>
|
| 1484 |
+
<button class="btn btn-secondary" onclick="closeModal()">Dismiss</button>
|
| 1485 |
+
</div>
|
| 1486 |
+
</div>
|
| 1487 |
+
|
| 1488 |
+
<!-- V15 Privacy Settings Modal -->
|
| 1489 |
+
<div id="privacy-modal" class="modal-overlay">
|
| 1490 |
+
<div class="modal-card" style="width: 450px;">
|
| 1491 |
+
<div class="modal-title">🛡 Privacy & Security (D1)</div>
|
| 1492 |
+
<div class="modal-subtitle">Data Protection & Retention Controls</div>
|
| 1493 |
+
|
| 1494 |
+
<div class="dispatch-config-row">
|
| 1495 |
+
<span class="dispatch-config-label">Auto-Redact Faces (Blur)</span>
|
| 1496 |
+
<label class="toggle-switch">
|
| 1497 |
+
<input type="checkbox" id="priv-redact" onchange="savePrivacySettings()">
|
| 1498 |
+
<span class="toggle-slider"></span>
|
| 1499 |
+
</label>
|
| 1500 |
+
</div>
|
| 1501 |
+
|
| 1502 |
+
<div class="dispatch-config-row" style="flex-direction: column; align-items: flex-start; gap: 6px;">
|
| 1503 |
+
<span class="dispatch-config-label">Data Retention: <span id="retention-display" style="color: var(--cyan);">24h</span></span>
|
| 1504 |
+
<input type="range" class="cooldown-slider" id="priv-retention" min="1" max="168" value="24"
|
| 1505 |
+
oninput="document.getElementById('retention-display').textContent = this.value + 'h'"
|
| 1506 |
+
onchange="savePrivacySettings()">
|
| 1507 |
+
</div>
|
| 1508 |
+
|
| 1509 |
+
<div style="margin-top: 15px; padding: 10px; background: rgba(0,255,136,0.05); border-radius: 4px; font-size: 0.65rem; color: var(--text-dim);">
|
| 1510 |
+
<i data-feather="terminal" style="width:12px; height:12px; vertical-align: middle; margin-right:4px;"></i>
|
| 1511 |
+
Encryption: AES-256 enabled for all audit logs.
|
| 1512 |
+
<br>
|
| 1513 |
+
Purge: Background cleanup runs every 5 minutes.
|
| 1514 |
+
</div>
|
| 1515 |
+
|
| 1516 |
+
<button class="btn btn-secondary" onclick="document.getElementById('privacy-modal').classList.remove('show')" style="margin-top: 15px;">Close</button>
|
| 1517 |
+
</div>
|
| 1518 |
+
</div>
|
| 1519 |
+
|
| 1520 |
+
<!-- V15 Fairness Audit Modal -->
|
| 1521 |
+
<div id="fairness-modal" class="modal-overlay">
|
| 1522 |
+
<div class="modal-card" style="width: 500px;">
|
| 1523 |
+
<div class="modal-title">⚖ Fairness & Bias Audit (D3)</div>
|
| 1524 |
+
<div class="modal-subtitle">Systemic Detection Analysis // Threshold Tuning</div>
|
| 1525 |
+
|
| 1526 |
+
<div id="fairness-stats-container">
|
| 1527 |
+
<!-- Populated by JS -->
|
| 1528 |
+
</div>
|
| 1529 |
+
|
| 1530 |
+
<div style="margin-top: 15px; padding: 10px; background: rgba(0,200,255,0.05); border-radius: 4px; font-size: 0.65rem; color: var(--text-dim);">
|
| 1531 |
+
<i data-feather="info" style="width:12px; height:12px; vertical-align: middle; margin-right:4px;"></i>
|
| 1532 |
+
Thresholds define minimum model confidence required to trigger a detection.
|
| 1533 |
+
Higher values reduce false positives (Bias Mitigation).
|
| 1534 |
+
</div>
|
| 1535 |
+
|
| 1536 |
+
<button class="btn btn-secondary" onclick="document.getElementById('fairness-modal').classList.remove('show')" style="margin-top: 15px;">Close</button>
|
| 1537 |
+
</div>
|
| 1538 |
+
</div>
|
| 1539 |
+
|
| 1540 |
+
<!-- Dispatch Settings Modal -->
|
| 1541 |
+
<div id="dispatch-settings-modal" class="modal-overlay">
|
| 1542 |
+
<div class="modal-card" style="width: 480px;">
|
| 1543 |
+
<div class="modal-title">⚙ Dispatch Settings</div>
|
| 1544 |
+
<div class="modal-subtitle">Telegram Bot Integration // Automated Alert System</div>
|
| 1545 |
+
|
| 1546 |
+
<div style="margin-bottom: 16px;">
|
| 1547 |
+
<div class="dispatch-config-row">
|
| 1548 |
+
<span class="dispatch-config-label">Master Switch</span>
|
| 1549 |
+
<label class="toggle-switch">
|
| 1550 |
+
<input type="checkbox" id="cfg-enabled" checked onchange="saveDispatchSettings()">
|
| 1551 |
+
<span class="toggle-slider"></span>
|
| 1552 |
+
</label>
|
| 1553 |
+
</div>
|
| 1554 |
+
|
| 1555 |
+
<div class="dispatch-config-row">
|
| 1556 |
+
<span class="dispatch-config-label">Auto-Dispatch (skip approval)</span>
|
| 1557 |
+
<label class="toggle-switch">
|
| 1558 |
+
<input type="checkbox" id="cfg-auto-dispatch" onchange="saveDispatchSettings()">
|
| 1559 |
+
<span class="toggle-slider"></span>
|
| 1560 |
+
</label>
|
| 1561 |
+
</div>
|
| 1562 |
+
|
| 1563 |
+
<div class="dispatch-config-row" style="flex-direction: column; align-items: flex-start; gap: 6px;">
|
| 1564 |
+
<span class="dispatch-config-label">Cooldown: <span id="cooldown-display"
|
| 1565 |
+
style="color: var(--cyan);">60s</span></span>
|
| 1566 |
+
<input type="range" class="cooldown-slider" id="cfg-cooldown" min="10" max="300" value="60"
|
| 1567 |
+
oninput="document.getElementById('cooldown-display').textContent = this.value + 's'"
|
| 1568 |
+
onchange="saveDispatchSettings()">
|
| 1569 |
+
</div>
|
| 1570 |
+
</div>
|
| 1571 |
+
|
| 1572 |
+
<div
|
| 1573 |
+
style="font-family: var(--font-display); font-size: 0.6rem; color: var(--cyan); letter-spacing: 1px; margin-bottom: 10px;">
|
| 1574 |
+
TELEGRAM CONFIGURATION</div>
|
| 1575 |
+
|
| 1576 |
+
<div style="margin-bottom: 10px;">
|
| 1577 |
+
<label style="font-size: 0.7rem; color: var(--text-secondary); display: block; margin-bottom: 4px;">Bot
|
| 1578 |
+
Token</label>
|
| 1579 |
+
<input type="text" class="config-input" id="cfg-bot-token" placeholder="e.g. 8659917680:AAFHai-..."
|
| 1580 |
+
style="margin-bottom: 8px;">
|
| 1581 |
+
|
| 1582 |
+
<label style="font-size: 0.7rem; color: var(--text-secondary); display: block; margin-bottom: 4px;">Chat
|
| 1583 |
+
ID <span style="color: var(--text-dim);">(send /start to your bot, then click
|
| 1584 |
+
Auto-Detect)</span></label>
|
| 1585 |
+
<div style="display: flex; gap: 6px;">
|
| 1586 |
+
<input type="text" class="config-input" id="cfg-chat-id" placeholder="e.g. 123456789"
|
| 1587 |
+
style="flex: 1;">
|
| 1588 |
+
<button class="btn btn-secondary" onclick="autoDetectChatId()"
|
| 1589 |
+
style="width: auto; padding: 6px 12px; font-size: 0.65rem;">AUTO-DETECT</button>
|
| 1590 |
+
</div>
|
| 1591 |
+
</div>
|
| 1592 |
+
|
| 1593 |
+
<div style="display: flex; gap: 8px; margin-top: 16px;">
|
| 1594 |
+
<button class="btn btn-primary" onclick="saveTelegramConfig()" style="flex: 1;">SAVE CONFIG</button>
|
| 1595 |
+
<button class="btn btn-primary" onclick="testDispatch()"
|
| 1596 |
+
style="flex: 1; background: rgba(0, 255, 136, 0.1); border-color: rgba(0, 255, 136, 0.3); color: var(--neon-green);">🧪
|
| 1597 |
+
SEND TEST</button>
|
| 1598 |
+
</div>
|
| 1599 |
+
<button class="btn btn-secondary" onclick="closeDispatchSettings()" style="margin-top: 8px;">Close</button>
|
| 1600 |
+
</div>
|
| 1601 |
+
</div>
|
| 1602 |
+
|
| 1603 |
+
<!-- ═══════════════════════════════════════════════
|
| 1604 |
+
JAVASCRIPT
|
| 1605 |
+
═══════════════════════════════════════════════ -->
|
| 1606 |
+
<script>
|
| 1607 |
+
feather.replace();
|
| 1608 |
+
|
| 1609 |
+
// ─── State ───
|
| 1610 |
+
let currentLayout = 'quad'; // 'quad' or 'single'
|
| 1611 |
+
let expandedFeed = 0;
|
| 1612 |
+
let isRedAlert = false;
|
| 1613 |
+
|
| 1614 |
+
// ─── Module Toggling ───
|
| 1615 |
+
const modeTitles = {
|
| 1616 |
+
'movement': 'MOVEMENT ANALYSIS',
|
| 1617 |
+
'facemask': 'FACEMASK DETECTION',
|
| 1618 |
+
'weapon': 'WEAPON DETECTION',
|
| 1619 |
+
'public_safety': 'PUBLIC SAFETY',
|
| 1620 |
+
'suspect_journey': 'SUSPECT JOURNEY'
|
| 1621 |
+
};
|
| 1622 |
+
|
| 1623 |
+
function toggleModule(module, buttonElement) {
|
| 1624 |
+
buttonElement.classList.toggle('active');
|
| 1625 |
+
|
| 1626 |
+
fetch('/toggle_module', {
|
| 1627 |
+
method: 'POST',
|
| 1628 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1629 |
+
body: JSON.stringify({ module: module })
|
| 1630 |
+
})
|
| 1631 |
+
.then(r => r.json())
|
| 1632 |
+
.then(data => {
|
| 1633 |
+
updateActiveModulesDisplay(data.active_modules);
|
| 1634 |
+
});
|
| 1635 |
+
}
|
| 1636 |
+
|
| 1637 |
+
function updateActiveModulesDisplay(activeModules) {
|
| 1638 |
+
const modeTitle = document.getElementById('mode-title');
|
| 1639 |
+
const count = activeModules.length;
|
| 1640 |
+
|
| 1641 |
+
if (count === 0) {
|
| 1642 |
+
modeTitle.textContent = 'NO MODULES ACTIVE';
|
| 1643 |
+
} else if (count === 1) {
|
| 1644 |
+
modeTitle.textContent = modeTitles[activeModules[0]] || activeModules[0].toUpperCase();
|
| 1645 |
+
} else {
|
| 1646 |
+
modeTitle.textContent = `${count} MODULES ACTIVE`;
|
| 1647 |
+
}
|
| 1648 |
+
}
|
| 1649 |
+
|
| 1650 |
+
|
| 1651 |
+
// ── Per-Feed Upload ──
|
| 1652 |
+
let uploadTargetFeed = 0;
|
| 1653 |
+
|
| 1654 |
+
function triggerFeedUpload(feedId) {
|
| 1655 |
+
uploadTargetFeed = feedId;
|
| 1656 |
+
document.getElementById('feed-upload-input').click();
|
| 1657 |
+
}
|
| 1658 |
+
|
| 1659 |
+
document.addEventListener('DOMContentLoaded', function () {
|
| 1660 |
+
const feedInput = document.getElementById('feed-upload-input');
|
| 1661 |
+
if (feedInput) {
|
| 1662 |
+
feedInput.addEventListener('change', function () {
|
| 1663 |
+
if (this.files[0]) {
|
| 1664 |
+
const feedId = uploadTargetFeed;
|
| 1665 |
+
const formData = new FormData();
|
| 1666 |
+
formData.append('file', this.files[0]);
|
| 1667 |
+
|
| 1668 |
+
const statusEl = document.getElementById('feed-' + feedId + '-status');
|
| 1669 |
+
if (statusEl) statusEl.textContent = 'UPLOADING...';
|
| 1670 |
+
|
| 1671 |
+
fetch('/upload_video/' + feedId, { method: 'POST', body: formData })
|
| 1672 |
+
.then(r => r.json())
|
| 1673 |
+
.then(data => {
|
| 1674 |
+
if (data.success) {
|
| 1675 |
+
activateFeedUI(feedId);
|
| 1676 |
+
if (statusEl) statusEl.textContent = 'FILE FEED';
|
| 1677 |
+
}
|
| 1678 |
+
})
|
| 1679 |
+
.catch(() => {
|
| 1680 |
+
if (statusEl) statusEl.textContent = 'UPLOAD FAILED';
|
| 1681 |
+
});
|
| 1682 |
+
|
| 1683 |
+
this.value = '';
|
| 1684 |
+
}
|
| 1685 |
+
});
|
| 1686 |
+
}
|
| 1687 |
+
});
|
| 1688 |
+
|
| 1689 |
+
function activateFeedUI(feedId) {
|
| 1690 |
+
// Hide the "NO SIGNAL" overlay and show the stream
|
| 1691 |
+
const offline = document.getElementById('offline-' + feedId);
|
| 1692 |
+
if (offline) offline.style.display = 'none';
|
| 1693 |
+
|
| 1694 |
+
const cell = document.getElementById('feed-' + feedId);
|
| 1695 |
+
// If no stream img exists yet, create one
|
| 1696 |
+
let img = document.getElementById('stream-' + feedId);
|
| 1697 |
+
if (!img) {
|
| 1698 |
+
img = document.createElement('img');
|
| 1699 |
+
img.className = 'feed-stream';
|
| 1700 |
+
img.id = 'stream-' + feedId;
|
| 1701 |
+
img.alt = 'Feed ' + feedId;
|
| 1702 |
+
cell.appendChild(img);
|
| 1703 |
+
}
|
| 1704 |
+
|
| 1705 |
+
// Force refresh
|
| 1706 |
+
img.src = '';
|
| 1707 |
+
setTimeout(() => {
|
| 1708 |
+
img.src = '/video_feed/' + feedId + '?t=' + Date.now();
|
| 1709 |
+
}, 400);
|
| 1710 |
+
|
| 1711 |
+
// Update the live dot
|
| 1712 |
+
const badge = cell.querySelector('.live-dot');
|
| 1713 |
+
if (badge) {
|
| 1714 |
+
badge.style.background = '';
|
| 1715 |
+
badge.style.boxShadow = '';
|
| 1716 |
+
}
|
| 1717 |
+
|
| 1718 |
+
feather.replace();
|
| 1719 |
+
}
|
| 1720 |
+
|
| 1721 |
+
/**
|
| 1722 |
+
* Force-refresh an MJPEG stream by setting a new src with a cache-buster.
|
| 1723 |
+
* This drops the old HTTP connection and establishes a fresh one.
|
| 1724 |
+
*/
|
| 1725 |
+
function refreshFeedStream(feedId) {
|
| 1726 |
+
const img = document.getElementById('stream-' + feedId);
|
| 1727 |
+
if (img) {
|
| 1728 |
+
// Brief blank to visually signal the switch
|
| 1729 |
+
img.src = '';
|
| 1730 |
+
// Small delay lets the backend fully initialize the new feed
|
| 1731 |
+
setTimeout(() => {
|
| 1732 |
+
img.src = '/video_feed/' + feedId + '?t=' + Date.now();
|
| 1733 |
+
}, 300);
|
| 1734 |
+
}
|
| 1735 |
+
}
|
| 1736 |
+
|
| 1737 |
+
// ─── Grid Layout ───
|
| 1738 |
+
function setGridLayout(layout) {
|
| 1739 |
+
const grid = document.getElementById('camera-grid');
|
| 1740 |
+
currentLayout = layout;
|
| 1741 |
+
|
| 1742 |
+
if (layout === 'single') {
|
| 1743 |
+
grid.classList.add('single-view');
|
| 1744 |
+
// Show only the expanded feed
|
| 1745 |
+
document.querySelectorAll('.feed-cell').forEach((cell, i) => {
|
| 1746 |
+
cell.classList.toggle('expanded', i === expandedFeed);
|
| 1747 |
+
});
|
| 1748 |
+
} else {
|
| 1749 |
+
grid.classList.remove('single-view');
|
| 1750 |
+
document.querySelectorAll('.feed-cell').forEach(cell => {
|
| 1751 |
+
cell.classList.remove('expanded');
|
| 1752 |
+
});
|
| 1753 |
+
}
|
| 1754 |
+
}
|
| 1755 |
+
|
| 1756 |
+
function expandFeed(feedId) {
|
| 1757 |
+
expandedFeed = feedId;
|
| 1758 |
+
if (currentLayout === 'single') {
|
| 1759 |
+
document.querySelectorAll('.feed-cell').forEach((cell, i) => {
|
| 1760 |
+
cell.classList.toggle('expanded', i === feedId);
|
| 1761 |
+
});
|
| 1762 |
+
}
|
| 1763 |
+
}
|
| 1764 |
+
|
| 1765 |
+
// ─── Stats & Red Alert Updates ───
|
| 1766 |
+
function updateStats() {
|
| 1767 |
+
fetch('/stats')
|
| 1768 |
+
.then(r => r.json())
|
| 1769 |
+
.then(data => {
|
| 1770 |
+
const score = data.threat_score;
|
| 1771 |
+
const scoreEl = document.getElementById('threat-score');
|
| 1772 |
+
const statusEl = document.getElementById('status-text');
|
| 1773 |
+
const ringFill = document.getElementById('score-ring-fill');
|
| 1774 |
+
const threatCard = document.getElementById('threat-card');
|
| 1775 |
+
|
| 1776 |
+
scoreEl.textContent = score;
|
| 1777 |
+
|
| 1778 |
+
// Compute ring dash offset (circumference = 2 * π * 60 ≈ 377)
|
| 1779 |
+
const circumference = 377;
|
| 1780 |
+
const offset = circumference - (circumference * score / 100);
|
| 1781 |
+
ringFill.style.strokeDashoffset = offset;
|
| 1782 |
+
|
| 1783 |
+
// Color based on score
|
| 1784 |
+
let color, status, glow;
|
| 1785 |
+
if (score >= 80) {
|
| 1786 |
+
color = '#ff2040';
|
| 1787 |
+
status = 'CRITICAL';
|
| 1788 |
+
glow = 'rgba(255, 32, 64, 0.4)';
|
| 1789 |
+
} else if (score >= 50) {
|
| 1790 |
+
color = '#ffaa00';
|
| 1791 |
+
status = 'ELEVATED';
|
| 1792 |
+
glow = 'rgba(255, 170, 0, 0.3)';
|
| 1793 |
+
} else if (score >= 25) {
|
| 1794 |
+
color = '#00d4ff';
|
| 1795 |
+
status = 'GUARDED';
|
| 1796 |
+
glow = 'rgba(0, 200, 255, 0.3)';
|
| 1797 |
+
} else {
|
| 1798 |
+
color = '#00ff88';
|
| 1799 |
+
status = 'SECURE';
|
| 1800 |
+
glow = 'rgba(0, 255, 136, 0.3)';
|
| 1801 |
+
}
|
| 1802 |
+
|
| 1803 |
+
statusEl.textContent = status;
|
| 1804 |
+
statusEl.style.color = color;
|
| 1805 |
+
statusEl.style.textShadow = `0 0 20px ${glow}`;
|
| 1806 |
+
ringFill.style.stroke = color;
|
| 1807 |
+
ringFill.style.filter = `drop-shadow(0 0 8px ${glow})`;
|
| 1808 |
+
|
| 1809 |
+
// V15 Updates
|
| 1810 |
+
updateAIAssessment();
|
| 1811 |
+
updateExplainability();
|
| 1812 |
+
|
| 1813 |
+
updateJourneyData();
|
| 1814 |
+
|
| 1815 |
+
// Red Alert state
|
| 1816 |
+
const alertOverlay = document.getElementById('red-alert-overlay');
|
| 1817 |
+
const alertBanner = document.getElementById('red-alert-banner');
|
| 1818 |
+
const feedCells = document.querySelectorAll('.feed-cell');
|
| 1819 |
+
|
| 1820 |
+
if (data.red_alert) {
|
| 1821 |
+
alertOverlay.classList.add('active');
|
| 1822 |
+
alertBanner.classList.add('active');
|
| 1823 |
+
feedCells.forEach(cell => cell.classList.add('red-alert-active'));
|
| 1824 |
+
threatCard.style.borderColor = 'rgba(255, 32, 64, 0.5)';
|
| 1825 |
+
|
| 1826 |
+
if (!isRedAlert) {
|
| 1827 |
+
playAlertTone();
|
| 1828 |
+
isRedAlert = true;
|
| 1829 |
+
}
|
| 1830 |
+
} else {
|
| 1831 |
+
alertOverlay.classList.remove('active');
|
| 1832 |
+
alertBanner.classList.remove('active');
|
| 1833 |
+
feedCells.forEach(cell => cell.classList.remove('red-alert-active'));
|
| 1834 |
+
threatCard.style.borderColor = '';
|
| 1835 |
+
isRedAlert = false;
|
| 1836 |
+
}
|
| 1837 |
+
|
| 1838 |
+
// Update mode display
|
| 1839 |
+
if (data.active_modules) {
|
| 1840 |
+
updateActiveModulesDisplay(data.active_modules);
|
| 1841 |
+
}
|
| 1842 |
+
|
| 1843 |
+
// Update live metrics
|
| 1844 |
+
const container = document.getElementById('stats-container');
|
| 1845 |
+
container.innerHTML = '';
|
| 1846 |
+
|
| 1847 |
+
if (!data.details || Object.keys(data.details).length === 0) {
|
| 1848 |
+
container.innerHTML = '<div class="stat-row"><span class="stat-name">System Status</span><span class="stat-val">Standby</span></div>';
|
| 1849 |
+
} else {
|
| 1850 |
+
for (const [key, value] of Object.entries(data.details)) {
|
| 1851 |
+
const div = document.createElement('div');
|
| 1852 |
+
div.className = 'stat-row';
|
| 1853 |
+
const formattedKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
| 1854 |
+
let displayVal = value;
|
| 1855 |
+
if (typeof value === 'boolean') {
|
| 1856 |
+
displayVal = value ? '⚠ YES' : 'No';
|
| 1857 |
+
}
|
| 1858 |
+
div.innerHTML = `<span class="stat-name">${formattedKey}</span><span class="stat-val">${displayVal}</span>`;
|
| 1859 |
+
container.appendChild(div);
|
| 1860 |
+
}
|
| 1861 |
+
}
|
| 1862 |
+
})
|
| 1863 |
+
.catch(() => { });
|
| 1864 |
+
}
|
| 1865 |
+
|
| 1866 |
+
// ─── Alert Tone (Web Audio API) ───
|
| 1867 |
+
function playAlertTone() {
|
| 1868 |
+
try {
|
| 1869 |
+
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
| 1870 |
+
const oscillator = audioCtx.createOscillator();
|
| 1871 |
+
const gainNode = audioCtx.createGain();
|
| 1872 |
+
|
| 1873 |
+
oscillator.connect(gainNode);
|
| 1874 |
+
gainNode.connect(audioCtx.destination);
|
| 1875 |
+
|
| 1876 |
+
oscillator.type = 'square';
|
| 1877 |
+
oscillator.frequency.setValueAtTime(880, audioCtx.currentTime);
|
| 1878 |
+
oscillator.frequency.setValueAtTime(660, audioCtx.currentTime + 0.15);
|
| 1879 |
+
oscillator.frequency.setValueAtTime(880, audioCtx.currentTime + 0.3);
|
| 1880 |
+
|
| 1881 |
+
gainNode.gain.setValueAtTime(0.08, audioCtx.currentTime);
|
| 1882 |
+
gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.5);
|
| 1883 |
+
|
| 1884 |
+
oscillator.start(audioCtx.currentTime);
|
| 1885 |
+
oscillator.stop(audioCtx.currentTime + 0.5);
|
| 1886 |
+
} catch (e) {
|
| 1887 |
+
// Audio not available — silent fallback
|
| 1888 |
+
}
|
| 1889 |
+
}
|
| 1890 |
+
|
| 1891 |
+
// ─── AI Report ───
|
| 1892 |
+
function generateReport() {
|
| 1893 |
+
const modal = document.getElementById('report-modal');
|
| 1894 |
+
modal.classList.add('show');
|
| 1895 |
+
document.getElementById('report-content').textContent = 'ANALYZING DATA STREAM...';
|
| 1896 |
+
|
| 1897 |
+
fetch('/generate_report', { method: 'POST' })
|
| 1898 |
+
.then(r => r.json())
|
| 1899 |
+
.then(data => {
|
| 1900 |
+
document.getElementById('report-content').textContent = data.report;
|
| 1901 |
+
})
|
| 1902 |
+
.catch(() => {
|
| 1903 |
+
document.getElementById('report-content').textContent = 'ERROR: Connection to AI Agent lost.';
|
| 1904 |
+
});
|
| 1905 |
+
}
|
| 1906 |
+
|
| 1907 |
+
function closeModal() {
|
| 1908 |
+
document.getElementById('report-modal').classList.remove('show');
|
| 1909 |
+
}
|
| 1910 |
+
|
| 1911 |
+
// ─── Audit Log Refresh ───
|
| 1912 |
+
function refreshAuditLog() {
|
| 1913 |
+
fetch('/audit_log')
|
| 1914 |
+
.then(r => r.json())
|
| 1915 |
+
.then(data => {
|
| 1916 |
+
const container = document.getElementById('audit-log-container');
|
| 1917 |
+
container.innerHTML = '';
|
| 1918 |
+
|
| 1919 |
+
if (data.log.length === 0) {
|
| 1920 |
+
container.innerHTML = '<div class="audit-entry"><div class="audit-time">--:--:--</div>NO ENTRIES</div>';
|
| 1921 |
+
return;
|
| 1922 |
+
}
|
| 1923 |
+
|
| 1924 |
+
data.log.slice(0, 30).forEach(entry => {
|
| 1925 |
+
const div = document.createElement('div');
|
| 1926 |
+
div.className = `audit-entry severity-${entry.severity}`;
|
| 1927 |
+
div.innerHTML = `
|
| 1928 |
+
<div class="audit-time">${entry.timestamp.split(' ')[1] || entry.timestamp}</div>
|
| 1929 |
+
${entry.action}: ${entry.details}
|
| 1930 |
+
`;
|
| 1931 |
+
container.appendChild(div);
|
| 1932 |
+
});
|
| 1933 |
+
})
|
| 1934 |
+
.catch(() => { });
|
| 1935 |
+
}
|
| 1936 |
+
|
| 1937 |
+
// ─── System Reset ───
|
| 1938 |
+
function resetSystem() {
|
| 1939 |
+
if (!confirm("⚠ CONFIRM SYSTEM RESET ⚠\n\nThis will clear all tracking history, threat scores, and active alerts.\nThe system will revert to default state.")) {
|
| 1940 |
+
return;
|
| 1941 |
+
}
|
| 1942 |
+
|
| 1943 |
+
fetch('/reset_system', { method: 'POST' })
|
| 1944 |
+
.then(r => r.json())
|
| 1945 |
+
.then(data => {
|
| 1946 |
+
if (data.success) {
|
| 1947 |
+
// Reload page to refresh all UI states cleanly
|
| 1948 |
+
window.location.reload();
|
| 1949 |
+
}
|
| 1950 |
+
});
|
| 1951 |
+
}
|
| 1952 |
+
|
| 1953 |
+
function updateJourneyData() {
|
| 1954 |
+
fetch('/journey_data')
|
| 1955 |
+
.then(r => r.json())
|
| 1956 |
+
.then(data => {
|
| 1957 |
+
const container = document.getElementById('journey-container');
|
| 1958 |
+
if (data.length === 0) {
|
| 1959 |
+
container.innerHTML = '<div class="audit-entry"><div class="audit-time">--:--:--</div>SCANNING...</div>';
|
| 1960 |
+
return;
|
| 1961 |
+
}
|
| 1962 |
+
|
| 1963 |
+
let html = '';
|
| 1964 |
+
data.forEach(subject => {
|
| 1965 |
+
const historyHtml = subject.history.map(h => `F${h.feed}`).join(' → ');
|
| 1966 |
+
html += `
|
| 1967 |
+
<div class="audit-entry">
|
| 1968 |
+
<div class="audit-time">${subject.last_seen}</div>
|
| 1969 |
+
<span style="color:var(--cyan)">SUBJ #${subject.id}</span> [FEED ${subject.last_feed}]
|
| 1970 |
+
<div style="font-size: 0.6rem; color: var(--text-dim); margin-top:2px;">${historyHtml}</div>
|
| 1971 |
+
</div>
|
| 1972 |
+
`;
|
| 1973 |
+
});
|
| 1974 |
+
container.innerHTML = html;
|
| 1975 |
+
});
|
| 1976 |
+
}
|
| 1977 |
+
|
| 1978 |
+
// ═══════════════════════════════════════════════════
|
| 1979 |
+
// DISPATCH CENTER
|
| 1980 |
+
// ═══════════════════════════════════════════════════
|
| 1981 |
+
|
| 1982 |
+
function updateDispatchCenter() {
|
| 1983 |
+
fetch('/dispatch_log')
|
| 1984 |
+
.then(r => r.json())
|
| 1985 |
+
.then(data => {
|
| 1986 |
+
// Update status bar
|
| 1987 |
+
const statusBar = document.getElementById('dispatch-status-bar');
|
| 1988 |
+
const statusDot = document.getElementById('dispatch-dot');
|
| 1989 |
+
const statusText = document.getElementById('dispatch-status-text');
|
| 1990 |
+
|
| 1991 |
+
if (data.settings.telegram_configured) {
|
| 1992 |
+
statusBar.className = 'dispatch-status-bar configured';
|
| 1993 |
+
statusDot.className = 'dispatch-dot online';
|
| 1994 |
+
const mode = data.settings.auto_dispatch ? 'AUTO' : 'MANUAL';
|
| 1995 |
+
statusText.textContent = `TELEGRAM ONLINE // ${mode} MODE`;
|
| 1996 |
+
} else {
|
| 1997 |
+
statusBar.className = 'dispatch-status-bar not-configured';
|
| 1998 |
+
statusDot.className = 'dispatch-dot offline';
|
| 1999 |
+
statusText.textContent = 'TELEGRAM NOT CONFIGURED';
|
| 2000 |
+
}
|
| 2001 |
+
|
| 2002 |
+
// Update pending approvals
|
| 2003 |
+
const pendingSection = document.getElementById('pending-section');
|
| 2004 |
+
const pendingContainer = document.getElementById('pending-container');
|
| 2005 |
+
const pendingBadge = document.getElementById('pending-badge');
|
| 2006 |
+
|
| 2007 |
+
if (data.pending && data.pending.length > 0) {
|
| 2008 |
+
pendingSection.style.display = 'block';
|
| 2009 |
+
pendingBadge.textContent = data.pending.length;
|
| 2010 |
+
pendingBadge.classList.add('visible');
|
| 2011 |
+
|
| 2012 |
+
let pendingHtml = '';
|
| 2013 |
+
data.pending.forEach(evt => {
|
| 2014 |
+
const scoreColor = evt.threat_score >= 80 ? 'var(--neon-red)' :
|
| 2015 |
+
evt.threat_score >= 50 ? 'var(--neon-amber)' : 'var(--cyan)';
|
| 2016 |
+
pendingHtml += `
|
| 2017 |
+
<div class="dispatch-entry pending-entry">
|
| 2018 |
+
<div class="dispatch-info">
|
| 2019 |
+
<div class="audit-time">${evt.timestamp}</div>
|
| 2020 |
+
<span class="dispatch-badge pending">PENDING</span>
|
| 2021 |
+
Threat Score: ${evt.threat_score} // ${evt.active_modules.join(', ').toUpperCase()}
|
| 2022 |
+
<div class="dispatch-actions">
|
| 2023 |
+
<button class="btn-approve" onclick="approveDispatch('${evt.id}')">✓ APPROVE</button>
|
| 2024 |
+
<button class="btn-reject" onclick="rejectDispatch('${evt.id}')">✗ REJECT</button>
|
| 2025 |
+
</div>
|
| 2026 |
+
</div>
|
| 2027 |
+
<div class="dispatch-score" style="color: ${scoreColor};">${evt.threat_score}</div>
|
| 2028 |
+
</div>
|
| 2029 |
+
`;
|
| 2030 |
+
});
|
| 2031 |
+
pendingContainer.innerHTML = pendingHtml;
|
| 2032 |
+
} else {
|
| 2033 |
+
pendingSection.style.display = 'none';
|
| 2034 |
+
pendingBadge.classList.remove('visible');
|
| 2035 |
+
}
|
| 2036 |
+
|
| 2037 |
+
// Update dispatch log
|
| 2038 |
+
const logContainer = document.getElementById('dispatch-log-container');
|
| 2039 |
+
if (data.log.length === 0 && (!data.pending || data.pending.length === 0)) {
|
| 2040 |
+
logContainer.innerHTML = '<div class="dispatch-entry"><div class="dispatch-info"><div class="audit-time">--:--:--</div>NO DISPATCH HISTORY</div></div>';
|
| 2041 |
+
} else {
|
| 2042 |
+
let logHtml = '';
|
| 2043 |
+
data.log.slice(0, 15).forEach(evt => {
|
| 2044 |
+
const badgeClass = evt.status;
|
| 2045 |
+
const statusLabel = evt.status.toUpperCase();
|
| 2046 |
+
const scoreColor = evt.threat_score >= 80 ? 'var(--neon-red)' :
|
| 2047 |
+
evt.threat_score >= 50 ? 'var(--neon-amber)' : 'var(--cyan)';
|
| 2048 |
+
logHtml += `
|
| 2049 |
+
<div class="dispatch-entry">
|
| 2050 |
+
<div class="dispatch-info">
|
| 2051 |
+
<div class="audit-time">${evt.timestamp}</div>
|
| 2052 |
+
<span class="dispatch-badge ${badgeClass}">${statusLabel}</span>
|
| 2053 |
+
Score: ${evt.threat_score} // ${(evt.active_modules || []).join(', ').toUpperCase() || 'N/A'}
|
| 2054 |
+
</div>
|
| 2055 |
+
<div class="dispatch-score" style="color: ${scoreColor};">${evt.threat_score}</div>
|
| 2056 |
+
</div>
|
| 2057 |
+
`;
|
| 2058 |
+
});
|
| 2059 |
+
logContainer.innerHTML = logHtml;
|
| 2060 |
+
}
|
| 2061 |
+
})
|
| 2062 |
+
.catch(() => { });
|
| 2063 |
+
}
|
| 2064 |
+
|
| 2065 |
+
function approveDispatch(eventId) {
|
| 2066 |
+
fetch('/approve_dispatch/' + eventId, { method: 'POST' })
|
| 2067 |
+
.then(r => r.json())
|
| 2068 |
+
.then(data => {
|
| 2069 |
+
updateDispatchCenter();
|
| 2070 |
+
if (data.status === 'sent') {
|
| 2071 |
+
showDispatchToast('✅ Alert dispatched via Telegram');
|
| 2072 |
+
} else {
|
| 2073 |
+
showDispatchToast('⚠ Dispatch failed — check Telegram config');
|
| 2074 |
+
}
|
| 2075 |
+
});
|
| 2076 |
+
}
|
| 2077 |
+
|
| 2078 |
+
function rejectDispatch(eventId) {
|
| 2079 |
+
fetch('/reject_dispatch/' + eventId, { method: 'POST' })
|
| 2080 |
+
.then(r => r.json())
|
| 2081 |
+
.then(() => {
|
| 2082 |
+
updateDispatchCenter();
|
| 2083 |
+
showDispatchToast('🔴 Alert rejected');
|
| 2084 |
+
});
|
| 2085 |
+
}
|
| 2086 |
+
|
| 2087 |
+
function manualDispatch() {
|
| 2088 |
+
if (!confirm('📡 DISPATCH ALERT NOW?\n\nThis will send the current threat status to Telegram immediately.')) return;
|
| 2089 |
+
fetch('/dispatch_alert', { method: 'POST' })
|
| 2090 |
+
.then(r => r.json())
|
| 2091 |
+
.then(data => {
|
| 2092 |
+
if (data.success) {
|
| 2093 |
+
showDispatchToast('✅ Alert dispatched via Telegram');
|
| 2094 |
+
} else {
|
| 2095 |
+
showDispatchToast('⚠ ' + (data.error || 'Dispatch failed'));
|
| 2096 |
+
}
|
| 2097 |
+
updateDispatchCenter();
|
| 2098 |
+
})
|
| 2099 |
+
.catch(() => showDispatchToast('⚠ Network error'));
|
| 2100 |
+
}
|
| 2101 |
+
|
| 2102 |
+
function testDispatch() {
|
| 2103 |
+
showDispatchToast('🧪 Sending test message...');
|
| 2104 |
+
fetch('/test_dispatch', { method: 'POST' })
|
| 2105 |
+
.then(r => r.json())
|
| 2106 |
+
.then(data => {
|
| 2107 |
+
if (data.ok) {
|
| 2108 |
+
showDispatchToast('✅ Test message sent to Telegram!');
|
| 2109 |
+
updateDispatchCenter();
|
| 2110 |
+
} else {
|
| 2111 |
+
showDispatchToast('⚠ ' + (data.error || 'Test failed'));
|
| 2112 |
+
}
|
| 2113 |
+
})
|
| 2114 |
+
.catch(() => showDispatchToast('⚠ Connection error'));
|
| 2115 |
+
}
|
| 2116 |
+
|
| 2117 |
+
// ─── Dispatch Settings Modal ───
|
| 2118 |
+
function openDispatchSettings() {
|
| 2119 |
+
const modal = document.getElementById('dispatch-settings-modal');
|
| 2120 |
+
modal.classList.add('show');
|
| 2121 |
+
|
| 2122 |
+
// Load current settings
|
| 2123 |
+
fetch('/dispatch_settings')
|
| 2124 |
+
.then(r => r.json())
|
| 2125 |
+
.then(data => {
|
| 2126 |
+
document.getElementById('cfg-enabled').checked = data.enabled;
|
| 2127 |
+
document.getElementById('cfg-auto-dispatch').checked = data.auto_dispatch;
|
| 2128 |
+
document.getElementById('cfg-cooldown').value = data.cooldown_seconds;
|
| 2129 |
+
document.getElementById('cooldown-display').textContent = data.cooldown_seconds + 's';
|
| 2130 |
+
if (data.chat_id) document.getElementById('cfg-chat-id').value = data.chat_id;
|
| 2131 |
+
// Don't populate bot token for security (show masked version in placeholder)
|
| 2132 |
+
const tokenInput = document.getElementById('cfg-bot-token');
|
| 2133 |
+
if (data.bot_token) tokenInput.placeholder = data.bot_token;
|
| 2134 |
+
});
|
| 2135 |
+
}
|
| 2136 |
+
|
| 2137 |
+
function closeDispatchSettings() {
|
| 2138 |
+
document.getElementById('dispatch-settings-modal').classList.remove('show');
|
| 2139 |
+
}
|
| 2140 |
+
|
| 2141 |
+
function saveDispatchSettings() {
|
| 2142 |
+
fetch('/dispatch_settings', {
|
| 2143 |
+
method: 'POST',
|
| 2144 |
+
headers: { 'Content-Type': 'application/json' },
|
| 2145 |
+
body: JSON.stringify({
|
| 2146 |
+
enabled: document.getElementById('cfg-enabled').checked,
|
| 2147 |
+
auto_dispatch: document.getElementById('cfg-auto-dispatch').checked,
|
| 2148 |
+
cooldown_seconds: parseInt(document.getElementById('cfg-cooldown').value)
|
| 2149 |
+
})
|
| 2150 |
+
}).then(() => updateDispatchCenter());
|
| 2151 |
+
}
|
| 2152 |
+
|
| 2153 |
+
function saveTelegramConfig() {
|
| 2154 |
+
const token = document.getElementById('cfg-bot-token').value.trim();
|
| 2155 |
+
const chatId = document.getElementById('cfg-chat-id').value.trim();
|
| 2156 |
+
|
| 2157 |
+
if (!token && !chatId) {
|
| 2158 |
+
showDispatchToast('⚠ Please enter bot token and/or chat ID');
|
| 2159 |
+
return;
|
| 2160 |
+
}
|
| 2161 |
+
|
| 2162 |
+
const payload = {};
|
| 2163 |
+
if (token) payload.bot_token = token;
|
| 2164 |
+
if (chatId) payload.chat_id = chatId;
|
| 2165 |
+
|
| 2166 |
+
fetch('/dispatch_settings', {
|
| 2167 |
+
method: 'POST',
|
| 2168 |
+
headers: { 'Content-Type': 'application/json' },
|
| 2169 |
+
body: JSON.stringify(payload)
|
| 2170 |
+
})
|
| 2171 |
+
.then(r => r.json())
|
| 2172 |
+
.then(() => {
|
| 2173 |
+
showDispatchToast('✅ Telegram config saved!');
|
| 2174 |
+
updateDispatchCenter();
|
| 2175 |
+
});
|
| 2176 |
+
}
|
| 2177 |
+
|
| 2178 |
+
function autoDetectChatId() {
|
| 2179 |
+
showDispatchToast('🔍 Scanning for chat ID...');
|
| 2180 |
+
// First save any token that was entered
|
| 2181 |
+
const token = document.getElementById('cfg-bot-token').value.trim();
|
| 2182 |
+
const savePromise = token ?
|
| 2183 |
+
fetch('/dispatch_settings', {
|
| 2184 |
+
method: 'POST',
|
| 2185 |
+
headers: { 'Content-Type': 'application/json' },
|
| 2186 |
+
body: JSON.stringify({ bot_token: token })
|
| 2187 |
+
}) : Promise.resolve();
|
| 2188 |
+
|
| 2189 |
+
savePromise.then(() => {
|
| 2190 |
+
return fetch('/test_dispatch', { method: 'POST' });
|
| 2191 |
+
})
|
| 2192 |
+
.then(r => r.json())
|
| 2193 |
+
.then(data => {
|
| 2194 |
+
if (data.ok) {
|
| 2195 |
+
showDispatchToast('✅ Chat ID detected & test sent!');
|
| 2196 |
+
// Reload settings to show detected chat ID
|
| 2197 |
+
fetch('/dispatch_settings')
|
| 2198 |
+
.then(r => r.json())
|
| 2199 |
+
.then(s => {
|
| 2200 |
+
if (s.chat_id) document.getElementById('cfg-chat-id').value = s.chat_id;
|
| 2201 |
+
});
|
| 2202 |
+
} else {
|
| 2203 |
+
showDispatchToast('⚠ Could not detect chat ID. Send /start to the bot from Telegram, then try again.');
|
| 2204 |
+
}
|
| 2205 |
+
})
|
| 2206 |
+
.catch(() => showDispatchToast('⚠ Detection failed'));
|
| 2207 |
+
}
|
| 2208 |
+
|
| 2209 |
+
// ─── Toast Notification ───
|
| 2210 |
+
function showDispatchToast(message) {
|
| 2211 |
+
// Remove existing toast
|
| 2212 |
+
const existing = document.getElementById('dispatch-toast');
|
| 2213 |
+
if (existing) existing.remove();
|
| 2214 |
+
|
| 2215 |
+
const toast = document.createElement('div');
|
| 2216 |
+
toast.id = 'dispatch-toast';
|
| 2217 |
+
toast.textContent = message;
|
| 2218 |
+
toast.style.cssText = `
|
| 2219 |
+
position: fixed;
|
| 2220 |
+
bottom: 24px;
|
| 2221 |
+
right: 24px;
|
| 2222 |
+
background: var(--bg-panel);
|
| 2223 |
+
color: var(--text-primary);
|
| 2224 |
+
font-family: var(--font-mono);
|
| 2225 |
+
font-size: 0.75rem;
|
| 2226 |
+
padding: 12px 20px;
|
| 2227 |
+
border-radius: 8px;
|
| 2228 |
+
border: 1px solid var(--border-glow);
|
| 2229 |
+
box-shadow: 0 4px 30px rgba(0, 200, 255, 0.15);
|
| 2230 |
+
z-index: 12000;
|
| 2231 |
+
animation: fadeIn 0.3s ease;
|
| 2232 |
+
backdrop-filter: blur(10px);
|
| 2233 |
+
`;
|
| 2234 |
+
document.body.appendChild(toast);
|
| 2235 |
+
setTimeout(() => {
|
| 2236 |
+
toast.style.opacity = '0';
|
| 2237 |
+
toast.style.transition = 'opacity 0.3s';
|
| 2238 |
+
setTimeout(() => toast.remove(), 300);
|
| 2239 |
+
}, 3000);
|
| 2240 |
+
}
|
| 2241 |
+
|
| 2242 |
+
// ─── Intervals ───
|
| 2243 |
+
setInterval(updateStats, 1000);
|
| 2244 |
+
setInterval(refreshAuditLog, 5000);
|
| 2245 |
+
setInterval(updateDispatchCenter, 3000);
|
| 2246 |
+
|
| 2247 |
+
// Initial load
|
| 2248 |
+
setTimeout(refreshAuditLog, 1500);
|
| 2249 |
+
setTimeout(updateDispatchCenter, 1000);
|
| 2250 |
+
</script>
|
| 2251 |
+
</body>
|
| 2252 |
+
|
| 2253 |
+
</html>
|
templates/sentinel_dashboard_v2.html
ADDED
|
@@ -0,0 +1,624 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>SENTINEL - Security Intelligence Platform</title>
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
|
| 9 |
+
rel="stylesheet">
|
| 10 |
+
<style>
|
| 11 |
+
* {
|
| 12 |
+
margin: 0;
|
| 13 |
+
padding: 0;
|
| 14 |
+
box-sizing: border-box;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
:root {
|
| 18 |
+
--primary: #007AFF;
|
| 19 |
+
--primary-hover: #0051D5;
|
| 20 |
+
--accent: #FF6B00;
|
| 21 |
+
--bg-primary: #000000;
|
| 22 |
+
--bg-secondary: #0A0A0A;
|
| 23 |
+
--bg-tertiary: #161616;
|
| 24 |
+
--surface: #1C1C1E;
|
| 25 |
+
--surface-elevated: #2C2C2E;
|
| 26 |
+
--text-primary: #FFFFFF;
|
| 27 |
+
--text-secondary: #98989D;
|
| 28 |
+
--text-tertiary: #636366;
|
| 29 |
+
--border: rgba(255, 255, 255, 0.08);
|
| 30 |
+
--border-hover: rgba(255, 255, 255, 0.15);
|
| 31 |
+
--danger: #FF453A;
|
| 32 |
+
--warning: #FF9F0A;
|
| 33 |
+
--success: #30D158;
|
| 34 |
+
--shadow: rgba(0, 0, 0, 0.5);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
body {
|
| 38 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
| 39 |
+
background: var(--bg-primary);
|
| 40 |
+
color: var(--text-primary);
|
| 41 |
+
height: 100vh;
|
| 42 |
+
overflow: hidden;
|
| 43 |
+
display: flex;
|
| 44 |
+
-webkit-font-smoothing: antialiased;
|
| 45 |
+
-moz-osx-font-smoothing: grayscale;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
/* Sidebar */
|
| 49 |
+
.sidebar {
|
| 50 |
+
width: 260px;
|
| 51 |
+
background: var(--bg-secondary);
|
| 52 |
+
border-right: 1px solid var(--border);
|
| 53 |
+
display: flex;
|
| 54 |
+
flex-direction: column;
|
| 55 |
+
padding: 24px 16px;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.logo {
|
| 59 |
+
font-size: 20px;
|
| 60 |
+
font-weight: 700;
|
| 61 |
+
margin-bottom: 36px;
|
| 62 |
+
padding: 0 12px;
|
| 63 |
+
letter-spacing: -0.5px;
|
| 64 |
+
color: var(--text-primary);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.logo span {
|
| 68 |
+
color: var(--accent);
|
| 69 |
+
font-weight: 800;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.nav-section {
|
| 73 |
+
margin-bottom: 24px;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.nav-label {
|
| 77 |
+
font-size: 11px;
|
| 78 |
+
font-weight: 600;
|
| 79 |
+
text-transform: uppercase;
|
| 80 |
+
letter-spacing: 0.8px;
|
| 81 |
+
color: var(--text-tertiary);
|
| 82 |
+
padding: 0 12px;
|
| 83 |
+
margin-bottom: 8px;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.nav-item {
|
| 87 |
+
padding: 10px 12px;
|
| 88 |
+
margin-bottom: 2px;
|
| 89 |
+
border-radius: 8px;
|
| 90 |
+
cursor: pointer;
|
| 91 |
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
| 92 |
+
display: flex;
|
| 93 |
+
align-items: center;
|
| 94 |
+
gap: 12px;
|
| 95 |
+
color: var(--text-secondary);
|
| 96 |
+
font-size: 14px;
|
| 97 |
+
font-weight: 500;
|
| 98 |
+
position: relative;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.nav-item:hover {
|
| 102 |
+
background: var(--surface);
|
| 103 |
+
color: var(--text-primary);
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.nav-item.active {
|
| 107 |
+
background: var(--surface);
|
| 108 |
+
color: var(--primary);
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.nav-item.active::before {
|
| 112 |
+
content: '';
|
| 113 |
+
position: absolute;
|
| 114 |
+
left: 0;
|
| 115 |
+
top: 50%;
|
| 116 |
+
transform: translateY(-50%);
|
| 117 |
+
width: 3px;
|
| 118 |
+
height: 20px;
|
| 119 |
+
background: var(--primary);
|
| 120 |
+
border-radius: 0 2px 2px 0;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.nav-icon {
|
| 124 |
+
width: 20px;
|
| 125 |
+
height: 20px;
|
| 126 |
+
display: flex;
|
| 127 |
+
align-items: center;
|
| 128 |
+
justify-content: center;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.nav-icon svg {
|
| 132 |
+
width: 18px;
|
| 133 |
+
height: 18px;
|
| 134 |
+
stroke: currentColor;
|
| 135 |
+
fill: none;
|
| 136 |
+
stroke-width: 2;
|
| 137 |
+
stroke-linecap: round;
|
| 138 |
+
stroke-linejoin: round;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.spacer {
|
| 142 |
+
flex: 1;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
/* Main Content */
|
| 146 |
+
.main-content {
|
| 147 |
+
flex: 1;
|
| 148 |
+
padding: 24px;
|
| 149 |
+
display: grid;
|
| 150 |
+
grid-template-columns: 1fr 340px;
|
| 151 |
+
gap: 24px;
|
| 152 |
+
overflow: hidden;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
/* Video Panel */
|
| 156 |
+
.video-container {
|
| 157 |
+
background: var(--bg-secondary);
|
| 158 |
+
border-radius: 12px;
|
| 159 |
+
border: 1px solid var(--border);
|
| 160 |
+
overflow: hidden;
|
| 161 |
+
display: flex;
|
| 162 |
+
flex-direction: column;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
.panel-header {
|
| 166 |
+
padding: 16px 20px;
|
| 167 |
+
border-bottom: 1px solid var(--border);
|
| 168 |
+
display: flex;
|
| 169 |
+
justify-content: space-between;
|
| 170 |
+
align-items: center;
|
| 171 |
+
background: var(--bg-secondary);
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.panel-title {
|
| 175 |
+
font-size: 15px;
|
| 176 |
+
font-weight: 600;
|
| 177 |
+
color: var(--text-primary);
|
| 178 |
+
letter-spacing: -0.2px;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
.status-badge {
|
| 182 |
+
display: flex;
|
| 183 |
+
align-items: center;
|
| 184 |
+
gap: 6px;
|
| 185 |
+
padding: 5px 10px;
|
| 186 |
+
border-radius: 6px;
|
| 187 |
+
font-size: 12px;
|
| 188 |
+
font-weight: 600;
|
| 189 |
+
background: rgba(255, 69, 58, 0.15);
|
| 190 |
+
color: var(--danger);
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
.status-dot {
|
| 194 |
+
width: 6px;
|
| 195 |
+
height: 6px;
|
| 196 |
+
border-radius: 50%;
|
| 197 |
+
background: var(--danger);
|
| 198 |
+
animation: pulse 2s ease-in-out infinite;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
@keyframes pulse {
|
| 202 |
+
0%, 100% { opacity: 1; }
|
| 203 |
+
50% { opacity: 0.4; }
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
.video-wrapper {
|
| 207 |
+
flex: 1;
|
| 208 |
+
background: #000;
|
| 209 |
+
display: flex;
|
| 210 |
+
align-items: center;
|
| 211 |
+
justify-content: center;
|
| 212 |
+
position: relative;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
#video-stream {
|
| 216 |
+
max-width: 100%;
|
| 217 |
+
max-height: 100%;
|
| 218 |
+
object-fit: contain;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
/* Sidebar Panel */
|
| 222 |
+
.intel-panel {
|
| 223 |
+
display: flex;
|
| 224 |
+
flex-direction: column;
|
| 225 |
+
gap: 16px;
|
| 226 |
+
overflow-y: auto;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.intel-panel::-webkit-scrollbar {
|
| 230 |
+
width: 6px;
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
.intel-panel::-webkit-scrollbar-track {
|
| 234 |
+
background: transparent;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.intel-panel::-webkit-scrollbar-thumb {
|
| 238 |
+
background: var(--surface);
|
| 239 |
+
border-radius: 3px;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
.card {
|
| 243 |
+
background: var(--bg-secondary);
|
| 244 |
+
border-radius: 12px;
|
| 245 |
+
padding: 20px;
|
| 246 |
+
border: 1px solid var(--border);
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
.card-header {
|
| 250 |
+
font-size: 12px;
|
| 251 |
+
font-weight: 600;
|
| 252 |
+
text-transform: uppercase;
|
| 253 |
+
letter-spacing: 0.5px;
|
| 254 |
+
color: var(--text-tertiary);
|
| 255 |
+
margin-bottom: 16px;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
/* Threat Score */
|
| 259 |
+
.threat-score {
|
| 260 |
+
text-align: center;
|
| 261 |
+
padding: 12px 0;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
.score-value {
|
| 265 |
+
font-size: 72px;
|
| 266 |
+
font-weight: 700;
|
| 267 |
+
line-height: 1;
|
| 268 |
+
letter-spacing: -2px;
|
| 269 |
+
background: linear-gradient(135deg, var(--success), #00C853);
|
| 270 |
+
-webkit-background-clip: text;
|
| 271 |
+
-webkit-text-fill-color: transparent;
|
| 272 |
+
background-clip: text;
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
.score-value.warning {
|
| 276 |
+
background: linear-gradient(135deg, var(--warning), #FF6D00);
|
| 277 |
+
-webkit-background-clip: text;
|
| 278 |
+
-webkit-text-fill-color: transparent;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
.score-value.danger {
|
| 282 |
+
background: linear-gradient(135deg, var(--danger), #D50000);
|
| 283 |
+
-webkit-background-clip: text;
|
| 284 |
+
-webkit-text-fill-color: transparent;
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
.score-label {
|
| 288 |
+
margin-top: 8px;
|
| 289 |
+
font-size: 13px;
|
| 290 |
+
font-weight: 600;
|
| 291 |
+
color: var(--text-secondary);
|
| 292 |
+
letter-spacing: 0.5px;
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
/* Stats */
|
| 296 |
+
.stats-grid {
|
| 297 |
+
display: grid;
|
| 298 |
+
grid-template-columns: 1fr 1fr;
|
| 299 |
+
gap: 10px;
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
.stat-item {
|
| 303 |
+
background: var(--surface);
|
| 304 |
+
padding: 14px;
|
| 305 |
+
border-radius: 8px;
|
| 306 |
+
transition: all 0.2s ease;
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
.stat-item:hover {
|
| 310 |
+
background: var(--surface-elevated);
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
.stat-num {
|
| 314 |
+
font-size: 24px;
|
| 315 |
+
font-weight: 700;
|
| 316 |
+
letter-spacing: -0.5px;
|
| 317 |
+
color: var(--text-primary);
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
.stat-desc {
|
| 321 |
+
font-size: 11px;
|
| 322 |
+
font-weight: 500;
|
| 323 |
+
color: var(--text-secondary);
|
| 324 |
+
margin-top: 4px;
|
| 325 |
+
text-transform: uppercase;
|
| 326 |
+
letter-spacing: 0.5px;
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
/* Buttons */
|
| 330 |
+
.btn {
|
| 331 |
+
width: 100%;
|
| 332 |
+
padding: 11px 16px;
|
| 333 |
+
border: none;
|
| 334 |
+
border-radius: 8px;
|
| 335 |
+
font-weight: 600;
|
| 336 |
+
font-size: 14px;
|
| 337 |
+
cursor: pointer;
|
| 338 |
+
font-family: 'Inter', sans-serif;
|
| 339 |
+
transition: all 0.2s ease;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
.btn-primary {
|
| 343 |
+
background: var(--primary);
|
| 344 |
+
color: white;
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
.btn-primary:hover {
|
| 348 |
+
background: var(--primary-hover);
|
| 349 |
+
transform: translateY(-1px);
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
.btn-primary:active {
|
| 353 |
+
transform: translateY(0);
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
.btn-secondary {
|
| 357 |
+
background: var(--surface);
|
| 358 |
+
color: var(--text-primary);
|
| 359 |
+
border: 1px solid var(--border);
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
.btn-secondary:hover {
|
| 363 |
+
background: var(--surface-elevated);
|
| 364 |
+
border-color: var(--border-hover);
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
.card-description {
|
| 368 |
+
font-size: 13px;
|
| 369 |
+
line-height: 1.5;
|
| 370 |
+
color: var(--text-secondary);
|
| 371 |
+
margin-bottom: 16px;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
/* Modal */
|
| 375 |
+
.modal {
|
| 376 |
+
display: none;
|
| 377 |
+
position: fixed;
|
| 378 |
+
top: 0;
|
| 379 |
+
left: 0;
|
| 380 |
+
width: 100%;
|
| 381 |
+
height: 100%;
|
| 382 |
+
background: rgba(0, 0, 0, 0.75);
|
| 383 |
+
backdrop-filter: blur(8px);
|
| 384 |
+
z-index: 1000;
|
| 385 |
+
justify-content: center;
|
| 386 |
+
align-items: center;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
.modal-content {
|
| 390 |
+
background: var(--bg-secondary);
|
| 391 |
+
width: 500px;
|
| 392 |
+
max-width: 90%;
|
| 393 |
+
padding: 28px;
|
| 394 |
+
border-radius: 16px;
|
| 395 |
+
border: 1px solid var(--border);
|
| 396 |
+
box-shadow: 0 20px 60px var(--shadow);
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
.modal-title {
|
| 400 |
+
font-size: 20px;
|
| 401 |
+
font-weight: 700;
|
| 402 |
+
margin-bottom: 20px;
|
| 403 |
+
letter-spacing: -0.5px;
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
.report-text {
|
| 407 |
+
white-space: pre-wrap;
|
| 408 |
+
font-family: 'SF Mono', 'Monaco', 'Courier New', monospace;
|
| 409 |
+
font-size: 13px;
|
| 410 |
+
color: var(--success);
|
| 411 |
+
background: var(--surface);
|
| 412 |
+
padding: 16px;
|
| 413 |
+
border-radius: 8px;
|
| 414 |
+
margin-bottom: 20px;
|
| 415 |
+
max-height: 300px;
|
| 416 |
+
overflow-y: auto;
|
| 417 |
+
line-height: 1.6;
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
.report-text::-webkit-scrollbar {
|
| 421 |
+
width: 6px;
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
.report-text::-webkit-scrollbar-track {
|
| 425 |
+
background: transparent;
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
.report-text::-webkit-scrollbar-thumb {
|
| 429 |
+
background: var(--surface-elevated);
|
| 430 |
+
border-radius: 3px;
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
input[type="file"] {
|
| 434 |
+
display: none;
|
| 435 |
+
}
|
| 436 |
+
</style>
|
| 437 |
+
</head>
|
| 438 |
+
|
| 439 |
+
<body>
|
| 440 |
+
<div class="sidebar">
|
| 441 |
+
<div class="logo">PROJECT <span>SENTINEL</span></div>
|
| 442 |
+
|
| 443 |
+
<div class="nav-section">
|
| 444 |
+
<div class="nav-label">Detection Modes</div>
|
| 445 |
+
<div class="nav-item active" onclick="setMode('movement', this)">
|
| 446 |
+
<div class="nav-icon">
|
| 447 |
+
<svg viewBox="0 0 24 24"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
|
| 448 |
+
</div>
|
| 449 |
+
<span>Movement Analysis</span>
|
| 450 |
+
</div>
|
| 451 |
+
<div class="nav-item" onclick="setMode('facemask', this)">
|
| 452 |
+
<div class="nav-icon">
|
| 453 |
+
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/></svg>
|
| 454 |
+
</div>
|
| 455 |
+
<span>Facemask Detection</span>
|
| 456 |
+
</div>
|
| 457 |
+
<div class="nav-item" onclick="setMode('weapon', this)">
|
| 458 |
+
<div class="nav-icon">
|
| 459 |
+
<svg viewBox="0 0 24 24"><path d="M12 2v20M2 12h20"/></svg>
|
| 460 |
+
</div>
|
| 461 |
+
<span>Weapon Detection</span>
|
| 462 |
+
</div>
|
| 463 |
+
</div>
|
| 464 |
+
|
| 465 |
+
<div class="spacer"></div>
|
| 466 |
+
|
| 467 |
+
<div class="nav-section">
|
| 468 |
+
<div class="nav-label">Video Source</div>
|
| 469 |
+
<div class="nav-item" onclick="document.getElementById('upload-input').click()">
|
| 470 |
+
<div class="nav-icon">
|
| 471 |
+
<svg viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg>
|
| 472 |
+
</div>
|
| 473 |
+
<span>Upload Video</span>
|
| 474 |
+
</div>
|
| 475 |
+
<input type="file" id="upload-input" accept="video/*" onchange="handleFileUpload(this)">
|
| 476 |
+
<div class="nav-item" onclick="setSource('camera')">
|
| 477 |
+
<div class="nav-icon">
|
| 478 |
+
<svg viewBox="0 0 24 24"><path d="M23 7l-7 5 7 5V7z"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>
|
| 479 |
+
</div>
|
| 480 |
+
<span>Live Camera</span>
|
| 481 |
+
</div>
|
| 482 |
+
</div>
|
| 483 |
+
</div>
|
| 484 |
+
|
| 485 |
+
<div class="main-content">
|
| 486 |
+
<div class="video-container">
|
| 487 |
+
<div class="panel-header">
|
| 488 |
+
<span class="panel-title" id="mode-title">Movement Analysis Feed</span>
|
| 489 |
+
<div class="status-badge">
|
| 490 |
+
<span class="status-dot"></span>
|
| 491 |
+
<span>LIVE</span>
|
| 492 |
+
</div>
|
| 493 |
+
</div>
|
| 494 |
+
<div class="video-wrapper">
|
| 495 |
+
<img id="video-stream" src="{{ url_for('video_feed') }}" alt="Video Feed">
|
| 496 |
+
</div>
|
| 497 |
+
</div>
|
| 498 |
+
|
| 499 |
+
<div class="intel-panel">
|
| 500 |
+
<div class="card">
|
| 501 |
+
<div class="card-header">Threat Assessment</div>
|
| 502 |
+
<div class="threat-score">
|
| 503 |
+
<div class="score-value" id="threat-score">0</div>
|
| 504 |
+
<div class="score-label" id="threat-label">Low Risk</div>
|
| 505 |
+
</div>
|
| 506 |
+
</div>
|
| 507 |
+
|
| 508 |
+
<div class="card">
|
| 509 |
+
<div class="card-header">Live Intelligence</div>
|
| 510 |
+
<div class="stats-grid" id="stats-container">
|
| 511 |
+
<!-- Populated by JavaScript -->
|
| 512 |
+
</div>
|
| 513 |
+
</div>
|
| 514 |
+
|
| 515 |
+
<div class="card">
|
| 516 |
+
<div class="card-header">Report Generation</div>
|
| 517 |
+
<p class="card-description">
|
| 518 |
+
Generate an automated incident report based on current threat assessment data.
|
| 519 |
+
</p>
|
| 520 |
+
<button class="btn btn-primary" onclick="generateReport()">Generate Report</button>
|
| 521 |
+
</div>
|
| 522 |
+
</div>
|
| 523 |
+
</div>
|
| 524 |
+
|
| 525 |
+
<div id="report-modal" class="modal">
|
| 526 |
+
<div class="modal-content">
|
| 527 |
+
<h2 class="modal-title">Incident Report</h2>
|
| 528 |
+
<div class="report-text" id="report-content">Generating...</div>
|
| 529 |
+
<button class="btn btn-secondary"
|
| 530 |
+
onclick="document.getElementById('report-modal').style.display='none'">Close</button>
|
| 531 |
+
</div>
|
| 532 |
+
</div>
|
| 533 |
+
|
| 534 |
+
<script>
|
| 535 |
+
function setMode(mode, element) {
|
| 536 |
+
// Update UI
|
| 537 |
+
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
|
| 538 |
+
element.classList.add('active');
|
| 539 |
+
|
| 540 |
+
const titles = {
|
| 541 |
+
'movement': 'Movement Analysis Feed',
|
| 542 |
+
'facemask': 'Facemask Detection Feed',
|
| 543 |
+
'weapon': 'Weapon Detection Feed'
|
| 544 |
+
};
|
| 545 |
+
document.getElementById('mode-title').textContent = titles[mode];
|
| 546 |
+
|
| 547 |
+
// Call Backend
|
| 548 |
+
fetch('/set_mode', {
|
| 549 |
+
method: 'POST',
|
| 550 |
+
headers: { 'Content-Type': 'application/json' },
|
| 551 |
+
body: JSON.stringify({ mode: mode })
|
| 552 |
+
});
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
function setSource(source) {
|
| 556 |
+
fetch('/set_source', {
|
| 557 |
+
method: 'POST',
|
| 558 |
+
headers: { 'Content-Type': 'application/json' },
|
| 559 |
+
body: JSON.stringify({ source: source })
|
| 560 |
+
});
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
function handleFileUpload(input) {
|
| 564 |
+
if (input.files[0]) {
|
| 565 |
+
const formData = new FormData();
|
| 566 |
+
formData.append('file', input.files[0]);
|
| 567 |
+
fetch('/upload_video', { method: 'POST', body: formData });
|
| 568 |
+
}
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
function generateReport() {
|
| 572 |
+
document.getElementById('report-modal').style.display = 'flex';
|
| 573 |
+
document.getElementById('report-content').textContent = "Analyzing data...";
|
| 574 |
+
|
| 575 |
+
fetch('/generate_report', { method: 'POST' })
|
| 576 |
+
.then(r => r.json())
|
| 577 |
+
.then(data => {
|
| 578 |
+
document.getElementById('report-content').textContent = data.report;
|
| 579 |
+
});
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
function updateStats() {
|
| 583 |
+
fetch('/stats')
|
| 584 |
+
.then(r => r.json())
|
| 585 |
+
.then(data => {
|
| 586 |
+
// Update Score
|
| 587 |
+
const score = data.threat_score;
|
| 588 |
+
const scoreEl = document.getElementById('threat-score');
|
| 589 |
+
const labelEl = document.getElementById('threat-label');
|
| 590 |
+
|
| 591 |
+
scoreEl.textContent = score;
|
| 592 |
+
scoreEl.className = 'score-value';
|
| 593 |
+
|
| 594 |
+
if (score > 75) {
|
| 595 |
+
scoreEl.classList.add('danger');
|
| 596 |
+
labelEl.textContent = 'Critical';
|
| 597 |
+
} else if (score > 40) {
|
| 598 |
+
scoreEl.classList.add('warning');
|
| 599 |
+
labelEl.textContent = 'Elevated';
|
| 600 |
+
} else {
|
| 601 |
+
labelEl.textContent = 'Low Risk';
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
// Update Stats Grid
|
| 605 |
+
const container = document.getElementById('stats-container');
|
| 606 |
+
container.innerHTML = '';
|
| 607 |
+
|
| 608 |
+
for (const [key, value] of Object.entries(data.details)) {
|
| 609 |
+
const div = document.createElement('div');
|
| 610 |
+
div.className = 'stat-item';
|
| 611 |
+
const label = key.replace(/_/g, ' ').split(' ').map(w =>
|
| 612 |
+
w.charAt(0).toUpperCase() + w.slice(1)
|
| 613 |
+
).join(' ');
|
| 614 |
+
div.innerHTML = `<div class="stat-num">${value}</div><div class="stat-desc">${label}</div>`;
|
| 615 |
+
container.appendChild(div);
|
| 616 |
+
}
|
| 617 |
+
});
|
| 618 |
+
}
|
| 619 |
+
|
| 620 |
+
setInterval(updateStats, 1000);
|
| 621 |
+
</script>
|
| 622 |
+
</body>
|
| 623 |
+
|
| 624 |
+
</html>
|
templates/sentinel_dashboard_v3.html
ADDED
|
@@ -0,0 +1,573 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>Sentinel V3</title>
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
| 9 |
+
<script src="https://unpkg.com/feather-icons"></script>
|
| 10 |
+
<style>
|
| 11 |
+
:root {
|
| 12 |
+
--bg-color: #000000;
|
| 13 |
+
--sidebar-bg: rgba(28, 28, 30, 0.8);
|
| 14 |
+
--card-bg: rgba(28, 28, 30, 0.6);
|
| 15 |
+
--border-color: rgba(255, 255, 255, 0.1);
|
| 16 |
+
--text-primary: #F5F5F7;
|
| 17 |
+
--text-secondary: #86868B;
|
| 18 |
+
--accent-blue: #0A84FF;
|
| 19 |
+
--accent-red: #FF453A;
|
| 20 |
+
--accent-green: #30D158;
|
| 21 |
+
--accent-orange: #FF9F0A;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
* {
|
| 25 |
+
margin: 0;
|
| 26 |
+
padding: 0;
|
| 27 |
+
box-sizing: border-box;
|
| 28 |
+
-webkit-font-smoothing: antialiased;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
body {
|
| 32 |
+
font-family: -apple-system, BlinkMacSystemFont, "Inter", sans-serif;
|
| 33 |
+
background-color: var(--bg-color);
|
| 34 |
+
color: var(--text-primary);
|
| 35 |
+
height: 100vh;
|
| 36 |
+
overflow: hidden;
|
| 37 |
+
display: flex;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/* Sidebar */
|
| 41 |
+
.sidebar {
|
| 42 |
+
width: 260px;
|
| 43 |
+
background-color: var(--sidebar-bg);
|
| 44 |
+
backdrop-filter: blur(20px);
|
| 45 |
+
-webkit-backdrop-filter: blur(20px);
|
| 46 |
+
border-right: 1px solid var(--border-color);
|
| 47 |
+
display: flex;
|
| 48 |
+
flex-direction: column;
|
| 49 |
+
padding: 24px;
|
| 50 |
+
z-index: 10;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.logo {
|
| 54 |
+
font-size: 1.2rem;
|
| 55 |
+
font-weight: 600;
|
| 56 |
+
margin-bottom: 40px;
|
| 57 |
+
display: flex;
|
| 58 |
+
align-items: center;
|
| 59 |
+
gap: 10px;
|
| 60 |
+
color: var(--text-primary);
|
| 61 |
+
letter-spacing: -0.5px;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.nav-group {
|
| 65 |
+
margin-bottom: 30px;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.nav-label {
|
| 69 |
+
font-size: 0.75rem;
|
| 70 |
+
font-weight: 600;
|
| 71 |
+
color: var(--text-secondary);
|
| 72 |
+
margin-bottom: 10px;
|
| 73 |
+
text-transform: uppercase;
|
| 74 |
+
letter-spacing: 0.5px;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.nav-item {
|
| 78 |
+
padding: 10px 12px;
|
| 79 |
+
margin-bottom: 4px;
|
| 80 |
+
border-radius: 8px;
|
| 81 |
+
cursor: pointer;
|
| 82 |
+
transition: all 0.2s ease;
|
| 83 |
+
display: flex;
|
| 84 |
+
align-items: center;
|
| 85 |
+
gap: 12px;
|
| 86 |
+
color: var(--text-secondary);
|
| 87 |
+
font-size: 0.9rem;
|
| 88 |
+
font-weight: 500;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.nav-item:hover {
|
| 92 |
+
background-color: rgba(255, 255, 255, 0.05);
|
| 93 |
+
color: var(--text-primary);
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.nav-item.active {
|
| 97 |
+
background-color: rgba(10, 132, 255, 0.15);
|
| 98 |
+
color: var(--accent-blue);
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.nav-item svg {
|
| 102 |
+
width: 18px;
|
| 103 |
+
height: 18px;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
/* Main Content */
|
| 107 |
+
.main-content {
|
| 108 |
+
flex: 1;
|
| 109 |
+
padding: 24px;
|
| 110 |
+
display: grid;
|
| 111 |
+
grid-template-columns: 1fr 360px;
|
| 112 |
+
gap: 24px;
|
| 113 |
+
background: radial-gradient(circle at top right, #1a1a1a 0%, #000000 100%);
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
/* Video Feed */
|
| 117 |
+
.video-container {
|
| 118 |
+
background-color: var(--card-bg);
|
| 119 |
+
border-radius: 18px;
|
| 120 |
+
border: 1px solid var(--border-color);
|
| 121 |
+
overflow: hidden;
|
| 122 |
+
display: flex;
|
| 123 |
+
flex-direction: column;
|
| 124 |
+
position: relative;
|
| 125 |
+
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
.video-header {
|
| 129 |
+
position: absolute;
|
| 130 |
+
top: 20px;
|
| 131 |
+
left: 20px;
|
| 132 |
+
right: 20px;
|
| 133 |
+
display: flex;
|
| 134 |
+
justify-content: space-between;
|
| 135 |
+
align-items: center;
|
| 136 |
+
z-index: 5;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.mode-badge {
|
| 140 |
+
background: rgba(0, 0, 0, 0.6);
|
| 141 |
+
backdrop-filter: blur(10px);
|
| 142 |
+
-webkit-backdrop-filter: blur(10px);
|
| 143 |
+
padding: 6px 12px;
|
| 144 |
+
border-radius: 20px;
|
| 145 |
+
font-size: 0.8rem;
|
| 146 |
+
font-weight: 600;
|
| 147 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 148 |
+
display: flex;
|
| 149 |
+
align-items: center;
|
| 150 |
+
gap: 6px;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.live-indicator {
|
| 154 |
+
width: 8px;
|
| 155 |
+
height: 8px;
|
| 156 |
+
background-color: var(--accent-red);
|
| 157 |
+
border-radius: 50%;
|
| 158 |
+
box-shadow: 0 0 10px var(--accent-red);
|
| 159 |
+
animation: pulse 2s infinite;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
#video-stream {
|
| 163 |
+
width: 100%;
|
| 164 |
+
height: 100%;
|
| 165 |
+
object-fit: contain;
|
| 166 |
+
background: #000;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
/* Intelligence Panel */
|
| 170 |
+
.intel-panel {
|
| 171 |
+
display: flex;
|
| 172 |
+
flex-direction: column;
|
| 173 |
+
gap: 24px;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
.card {
|
| 177 |
+
background-color: var(--card-bg);
|
| 178 |
+
backdrop-filter: blur(20px);
|
| 179 |
+
-webkit-backdrop-filter: blur(20px);
|
| 180 |
+
border-radius: 18px;
|
| 181 |
+
padding: 24px;
|
| 182 |
+
border: 1px solid var(--border-color);
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.card-header {
|
| 186 |
+
display: flex;
|
| 187 |
+
justify-content: space-between;
|
| 188 |
+
align-items: center;
|
| 189 |
+
margin-bottom: 20px;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.card-title {
|
| 193 |
+
font-size: 0.95rem;
|
| 194 |
+
font-weight: 600;
|
| 195 |
+
color: var(--text-primary);
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.threat-meter {
|
| 199 |
+
display: flex;
|
| 200 |
+
flex-direction: column;
|
| 201 |
+
align-items: center;
|
| 202 |
+
padding: 10px 0;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
.score-circle {
|
| 206 |
+
width: 140px;
|
| 207 |
+
height: 140px;
|
| 208 |
+
border-radius: 50%;
|
| 209 |
+
border: 4px solid rgba(255, 255, 255, 0.1);
|
| 210 |
+
display: flex;
|
| 211 |
+
flex-direction: column;
|
| 212 |
+
justify-content: center;
|
| 213 |
+
align-items: center;
|
| 214 |
+
margin-bottom: 16px;
|
| 215 |
+
position: relative;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
.score-circle::after {
|
| 219 |
+
content: '';
|
| 220 |
+
position: absolute;
|
| 221 |
+
top: -4px;
|
| 222 |
+
left: -4px;
|
| 223 |
+
right: -4px;
|
| 224 |
+
bottom: -4px;
|
| 225 |
+
border-radius: 50%;
|
| 226 |
+
border: 4px solid var(--accent-green);
|
| 227 |
+
border-top-color: transparent;
|
| 228 |
+
border-left-color: transparent;
|
| 229 |
+
transform: rotate(-45deg);
|
| 230 |
+
transition: all 0.5s ease;
|
| 231 |
+
filter: drop-shadow(0 0 8px rgba(48, 209, 88, 0.3));
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
.score-value {
|
| 235 |
+
font-size: 3rem;
|
| 236 |
+
font-weight: 700;
|
| 237 |
+
line-height: 1;
|
| 238 |
+
letter-spacing: -1px;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
.score-label {
|
| 242 |
+
font-size: 0.8rem;
|
| 243 |
+
color: var(--text-secondary);
|
| 244 |
+
margin-top: 4px;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
.status-text {
|
| 248 |
+
font-size: 1.1rem;
|
| 249 |
+
font-weight: 600;
|
| 250 |
+
color: var(--accent-green);
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
.stats-list {
|
| 254 |
+
display: flex;
|
| 255 |
+
flex-direction: column;
|
| 256 |
+
gap: 12px;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
.stat-row {
|
| 260 |
+
display: flex;
|
| 261 |
+
justify-content: space-between;
|
| 262 |
+
align-items: center;
|
| 263 |
+
padding: 12px;
|
| 264 |
+
background: rgba(255, 255, 255, 0.03);
|
| 265 |
+
border-radius: 12px;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
.stat-name {
|
| 269 |
+
font-size: 0.9rem;
|
| 270 |
+
color: var(--text-secondary);
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
.stat-val {
|
| 274 |
+
font-size: 1rem;
|
| 275 |
+
font-weight: 600;
|
| 276 |
+
color: var(--text-primary);
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
.btn {
|
| 280 |
+
width: 100%;
|
| 281 |
+
padding: 14px;
|
| 282 |
+
border: none;
|
| 283 |
+
border-radius: 12px;
|
| 284 |
+
font-size: 0.95rem;
|
| 285 |
+
font-weight: 600;
|
| 286 |
+
cursor: pointer;
|
| 287 |
+
transition: all 0.2s;
|
| 288 |
+
font-family: inherit;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
.btn-primary {
|
| 292 |
+
background-color: var(--accent-blue);
|
| 293 |
+
color: white;
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
.btn-primary:hover {
|
| 297 |
+
background-color: #0071e3;
|
| 298 |
+
transform: scale(1.02);
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
.btn-secondary {
|
| 302 |
+
background-color: rgba(255, 255, 255, 0.1);
|
| 303 |
+
color: var(--text-primary);
|
| 304 |
+
margin-top: 10px;
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
.btn-secondary:hover {
|
| 308 |
+
background-color: rgba(255, 255, 255, 0.15);
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
/* Modal */
|
| 312 |
+
.modal-overlay {
|
| 313 |
+
position: fixed;
|
| 314 |
+
top: 0;
|
| 315 |
+
left: 0;
|
| 316 |
+
right: 0;
|
| 317 |
+
bottom: 0;
|
| 318 |
+
background: rgba(0, 0, 0, 0.7);
|
| 319 |
+
backdrop-filter: blur(5px);
|
| 320 |
+
display: none;
|
| 321 |
+
justify-content: center;
|
| 322 |
+
align-items: center;
|
| 323 |
+
z-index: 1000;
|
| 324 |
+
opacity: 0;
|
| 325 |
+
transition: opacity 0.3s ease;
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
.modal-overlay.show {
|
| 329 |
+
display: flex;
|
| 330 |
+
opacity: 1;
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
.modal-card {
|
| 334 |
+
background: #1c1c1e;
|
| 335 |
+
width: 480px;
|
| 336 |
+
border-radius: 20px;
|
| 337 |
+
padding: 30px;
|
| 338 |
+
border: 1px solid var(--border-color);
|
| 339 |
+
transform: scale(0.95);
|
| 340 |
+
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
.modal-overlay.show .modal-card {
|
| 344 |
+
transform: scale(1);
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
.report-content {
|
| 348 |
+
background: #000;
|
| 349 |
+
padding: 16px;
|
| 350 |
+
border-radius: 12px;
|
| 351 |
+
font-family: 'SF Mono', 'Menlo', monospace;
|
| 352 |
+
font-size: 0.85rem;
|
| 353 |
+
color: var(--accent-green);
|
| 354 |
+
margin: 20px 0;
|
| 355 |
+
line-height: 1.5;
|
| 356 |
+
border: 1px solid var(--border-color);
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
@keyframes pulse {
|
| 360 |
+
0% {
|
| 361 |
+
opacity: 1;
|
| 362 |
+
transform: scale(1);
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
50% {
|
| 366 |
+
opacity: 0.5;
|
| 367 |
+
transform: scale(1.2);
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
100% {
|
| 371 |
+
opacity: 1;
|
| 372 |
+
transform: scale(1);
|
| 373 |
+
}
|
| 374 |
+
}
|
| 375 |
+
</style>
|
| 376 |
+
</head>
|
| 377 |
+
|
| 378 |
+
<body>
|
| 379 |
+
<div class="sidebar">
|
| 380 |
+
<div class="logo">
|
| 381 |
+
<i data-feather="shield"></i>
|
| 382 |
+
SENTINEL V3
|
| 383 |
+
</div>
|
| 384 |
+
|
| 385 |
+
<div class="nav-group">
|
| 386 |
+
<div class="nav-label">Detection Modes</div>
|
| 387 |
+
<div class="nav-item active" onclick="setMode('movement', this)">
|
| 388 |
+
<i data-feather="activity"></i> Movement
|
| 389 |
+
</div>
|
| 390 |
+
<div class="nav-item" onclick="setMode('facemask', this)">
|
| 391 |
+
<i data-feather="user"></i> Facemask
|
| 392 |
+
</div>
|
| 393 |
+
<div class="nav-item" onclick="setMode('weapon', this)">
|
| 394 |
+
<i data-feather="target"></i> Weapon
|
| 395 |
+
</div>
|
| 396 |
+
<div class="nav-item" onclick="setMode('public_safety', this)">
|
| 397 |
+
<i data-feather="users"></i> Public Safety
|
| 398 |
+
</div>
|
| 399 |
+
</div>
|
| 400 |
+
|
| 401 |
+
<div class="nav-group" style="margin-top: auto;">
|
| 402 |
+
<div class="nav-label">Input Source</div>
|
| 403 |
+
<div class="nav-item" onclick="setSource('camera')">
|
| 404 |
+
<i data-feather="camera"></i> Live Camera
|
| 405 |
+
</div>
|
| 406 |
+
<div class="nav-item" onclick="document.getElementById('upload-input').click()">
|
| 407 |
+
<i data-feather="upload-cloud"></i> Upload Video
|
| 408 |
+
</div>
|
| 409 |
+
<input type="file" id="upload-input" style="display: none" accept="video/*"
|
| 410 |
+
onchange="handleFileUpload(this)">
|
| 411 |
+
</div>
|
| 412 |
+
</div>
|
| 413 |
+
|
| 414 |
+
<div class="main-content">
|
| 415 |
+
<div class="video-container">
|
| 416 |
+
<div class="video-header">
|
| 417 |
+
<div class="mode-badge">
|
| 418 |
+
<div class="live-indicator"></div>
|
| 419 |
+
<span id="mode-title">MOVEMENT ANALYSIS</span>
|
| 420 |
+
</div>
|
| 421 |
+
</div>
|
| 422 |
+
<img id="video-stream" src="{{ url_for('video_feed') }}">
|
| 423 |
+
</div>
|
| 424 |
+
|
| 425 |
+
<div class="intel-panel">
|
| 426 |
+
<div class="card">
|
| 427 |
+
<div class="card-header">
|
| 428 |
+
<div class="card-title">Threat Assessment</div>
|
| 429 |
+
<i data-feather="alert-circle" style="width: 18px; color: var(--text-secondary);"></i>
|
| 430 |
+
</div>
|
| 431 |
+
<div class="threat-meter">
|
| 432 |
+
<div class="score-circle" id="score-circle">
|
| 433 |
+
<div class="score-value" id="threat-score">0</div>
|
| 434 |
+
<div class="score-label">RISK LEVEL</div>
|
| 435 |
+
</div>
|
| 436 |
+
<div class="status-text" id="status-text">SECURE</div>
|
| 437 |
+
</div>
|
| 438 |
+
</div>
|
| 439 |
+
|
| 440 |
+
<div class="card">
|
| 441 |
+
<div class="card-header">
|
| 442 |
+
<div class="card-title">Live Metrics</div>
|
| 443 |
+
<i data-feather="bar-chart-2" style="width: 18px; color: var(--text-secondary);"></i>
|
| 444 |
+
</div>
|
| 445 |
+
<div class="stats-list" id="stats-container">
|
| 446 |
+
<!-- Populated by JS -->
|
| 447 |
+
</div>
|
| 448 |
+
</div>
|
| 449 |
+
|
| 450 |
+
<div class="card">
|
| 451 |
+
<div class="card-header">
|
| 452 |
+
<div class="card-title">Actions</div>
|
| 453 |
+
</div>
|
| 454 |
+
<button class="btn btn-primary" onclick="generateReport()">Generate Incident Report</button>
|
| 455 |
+
</div>
|
| 456 |
+
</div>
|
| 457 |
+
</div>
|
| 458 |
+
|
| 459 |
+
<div id="report-modal" class="modal-overlay">
|
| 460 |
+
<div class="modal-card">
|
| 461 |
+
<h2 style="font-size: 1.5rem; font-weight: 700; margin-bottom: 10px;">Incident Report</h2>
|
| 462 |
+
<p style="color: var(--text-secondary); font-size: 0.9rem;">Generated by Sentinel AI Agent</p>
|
| 463 |
+
<div class="report-content" id="report-content">Analyzing data stream...</div>
|
| 464 |
+
<button class="btn btn-secondary" onclick="closeModal()">Dismiss</button>
|
| 465 |
+
</div>
|
| 466 |
+
</div>
|
| 467 |
+
|
| 468 |
+
<script>
|
| 469 |
+
feather.replace();
|
| 470 |
+
|
| 471 |
+
function setMode(mode, element) {
|
| 472 |
+
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
|
| 473 |
+
element.classList.add('active');
|
| 474 |
+
|
| 475 |
+
const titles = {
|
| 476 |
+
'movement': 'MOVEMENT ANALYSIS',
|
| 477 |
+
'facemask': 'FACEMASK DETECTION',
|
| 478 |
+
'weapon': 'WEAPON DETECTION',
|
| 479 |
+
'public_safety': 'PUBLIC SAFETY MONITORING'
|
| 480 |
+
};
|
| 481 |
+
document.getElementById('mode-title').textContent = titles[mode];
|
| 482 |
+
|
| 483 |
+
fetch('/set_mode', {
|
| 484 |
+
method: 'POST',
|
| 485 |
+
headers: { 'Content-Type': 'application/json' },
|
| 486 |
+
body: JSON.stringify({ mode: mode })
|
| 487 |
+
});
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
function setSource(source) {
|
| 491 |
+
fetch('/set_source', {
|
| 492 |
+
method: 'POST',
|
| 493 |
+
headers: { 'Content-Type': 'application/json' },
|
| 494 |
+
body: JSON.stringify({ source: source })
|
| 495 |
+
});
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
function handleFileUpload(input) {
|
| 499 |
+
if (input.files[0]) {
|
| 500 |
+
const formData = new FormData();
|
| 501 |
+
formData.append('file', input.files[0]);
|
| 502 |
+
fetch('/upload_video', { method: 'POST', body: formData });
|
| 503 |
+
}
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
function generateReport() {
|
| 507 |
+
const modal = document.getElementById('report-modal');
|
| 508 |
+
modal.classList.add('show');
|
| 509 |
+
document.getElementById('report-content').textContent = "Analyzing data stream...";
|
| 510 |
+
|
| 511 |
+
fetch('/generate_report', { method: 'POST' })
|
| 512 |
+
.then(r => r.json())
|
| 513 |
+
.then(data => {
|
| 514 |
+
document.getElementById('report-content').textContent = data.report;
|
| 515 |
+
});
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
function closeModal() {
|
| 519 |
+
document.getElementById('report-modal').classList.remove('show');
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
function updateStats() {
|
| 523 |
+
fetch('/stats')
|
| 524 |
+
.then(r => r.json())
|
| 525 |
+
.then(data => {
|
| 526 |
+
const score = data.threat_score;
|
| 527 |
+
const scoreEl = document.getElementById('threat-score');
|
| 528 |
+
const statusEl = document.getElementById('status-text');
|
| 529 |
+
const circle = document.getElementById('score-circle');
|
| 530 |
+
|
| 531 |
+
scoreEl.textContent = score;
|
| 532 |
+
|
| 533 |
+
let color = '#30D158'; // Green
|
| 534 |
+
let status = 'SECURE';
|
| 535 |
+
|
| 536 |
+
if (score > 75) {
|
| 537 |
+
color = '#FF453A'; // Red
|
| 538 |
+
status = 'CRITICAL THREAT';
|
| 539 |
+
} else if (score > 40) {
|
| 540 |
+
color = '#FF9F0A'; // Orange
|
| 541 |
+
status = 'ELEVATED RISK';
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
statusEl.textContent = status;
|
| 545 |
+
statusEl.style.color = color;
|
| 546 |
+
circle.style.setProperty('--accent-green', color); // Hack to update pseudo-element color via variable if I used one, but here I need to update the style rule or use a variable.
|
| 547 |
+
// Actually, let's fix the CSS to use a variable for the border color
|
| 548 |
+
// The CSS uses var(--accent-green) for the pseudo element. So I can just set that variable on the element.
|
| 549 |
+
circle.style.setProperty('--accent-green', color);
|
| 550 |
+
|
| 551 |
+
const container = document.getElementById('stats-container');
|
| 552 |
+
container.innerHTML = '';
|
| 553 |
+
|
| 554 |
+
if (Object.keys(data.details).length === 0) {
|
| 555 |
+
container.innerHTML = '<div class="stat-row"><span class="stat-name">System Status</span><span class="stat-val">Standby</span></div>';
|
| 556 |
+
} else {
|
| 557 |
+
for (const [key, value] of Object.entries(data.details)) {
|
| 558 |
+
const div = document.createElement('div');
|
| 559 |
+
div.className = 'stat-row';
|
| 560 |
+
// Format key: replace underscores with spaces and capitalize words
|
| 561 |
+
const formattedKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
| 562 |
+
div.innerHTML = `<span class="stat-name">${formattedKey}</span><span class="stat-val">${value}</span>`;
|
| 563 |
+
container.appendChild(div);
|
| 564 |
+
}
|
| 565 |
+
}
|
| 566 |
+
});
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
setInterval(updateStats, 1000);
|
| 570 |
+
</script>
|
| 571 |
+
</body>
|
| 572 |
+
|
| 573 |
+
</html>
|
templates/sentinel_dashboard_v4.html
ADDED
|
@@ -0,0 +1,573 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>Sentinel V4</title>
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
| 9 |
+
<script src="https://unpkg.com/feather-icons"></script>
|
| 10 |
+
<style>
|
| 11 |
+
:root {
|
| 12 |
+
--bg-color: #000000;
|
| 13 |
+
--sidebar-bg: rgba(28, 28, 30, 0.8);
|
| 14 |
+
--card-bg: rgba(28, 28, 30, 0.6);
|
| 15 |
+
--border-color: rgba(255, 255, 255, 0.1);
|
| 16 |
+
--text-primary: #F5F5F7;
|
| 17 |
+
--text-secondary: #86868B;
|
| 18 |
+
--accent-blue: #0A84FF;
|
| 19 |
+
--accent-red: #FF453A;
|
| 20 |
+
--accent-green: #30D158;
|
| 21 |
+
--accent-orange: #FF9F0A;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
* {
|
| 25 |
+
margin: 0;
|
| 26 |
+
padding: 0;
|
| 27 |
+
box-sizing: border-box;
|
| 28 |
+
-webkit-font-smoothing: antialiased;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
body {
|
| 32 |
+
font-family: -apple-system, BlinkMacSystemFont, "Inter", sans-serif;
|
| 33 |
+
background-color: var(--bg-color);
|
| 34 |
+
color: var(--text-primary);
|
| 35 |
+
height: 100vh;
|
| 36 |
+
overflow: hidden;
|
| 37 |
+
display: flex;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/* Sidebar */
|
| 41 |
+
.sidebar {
|
| 42 |
+
width: 260px;
|
| 43 |
+
background-color: var(--sidebar-bg);
|
| 44 |
+
backdrop-filter: blur(20px);
|
| 45 |
+
-webkit-backdrop-filter: blur(20px);
|
| 46 |
+
border-right: 1px solid var(--border-color);
|
| 47 |
+
display: flex;
|
| 48 |
+
flex-direction: column;
|
| 49 |
+
padding: 24px;
|
| 50 |
+
z-index: 10;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.logo {
|
| 54 |
+
font-size: 1.2rem;
|
| 55 |
+
font-weight: 600;
|
| 56 |
+
margin-bottom: 40px;
|
| 57 |
+
display: flex;
|
| 58 |
+
align-items: center;
|
| 59 |
+
gap: 10px;
|
| 60 |
+
color: var(--text-primary);
|
| 61 |
+
letter-spacing: -0.5px;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.nav-group {
|
| 65 |
+
margin-bottom: 30px;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.nav-label {
|
| 69 |
+
font-size: 0.75rem;
|
| 70 |
+
font-weight: 600;
|
| 71 |
+
color: var(--text-secondary);
|
| 72 |
+
margin-bottom: 10px;
|
| 73 |
+
text-transform: uppercase;
|
| 74 |
+
letter-spacing: 0.5px;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.nav-item {
|
| 78 |
+
padding: 10px 12px;
|
| 79 |
+
margin-bottom: 4px;
|
| 80 |
+
border-radius: 8px;
|
| 81 |
+
cursor: pointer;
|
| 82 |
+
transition: all 0.2s ease;
|
| 83 |
+
display: flex;
|
| 84 |
+
align-items: center;
|
| 85 |
+
gap: 12px;
|
| 86 |
+
color: var(--text-secondary);
|
| 87 |
+
font-size: 0.9rem;
|
| 88 |
+
font-weight: 500;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.nav-item:hover {
|
| 92 |
+
background-color: rgba(255, 255, 255, 0.05);
|
| 93 |
+
color: var(--text-primary);
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.nav-item.active {
|
| 97 |
+
background-color: rgba(10, 132, 255, 0.15);
|
| 98 |
+
color: var(--accent-blue);
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.nav-item svg {
|
| 102 |
+
width: 18px;
|
| 103 |
+
height: 18px;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
/* Main Content */
|
| 107 |
+
.main-content {
|
| 108 |
+
flex: 1;
|
| 109 |
+
padding: 24px;
|
| 110 |
+
display: grid;
|
| 111 |
+
grid-template-columns: 1fr 360px;
|
| 112 |
+
gap: 24px;
|
| 113 |
+
background: radial-gradient(circle at top right, #1a1a1a 0%, #000000 100%);
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
/* Video Feed */
|
| 117 |
+
.video-container {
|
| 118 |
+
background-color: var(--card-bg);
|
| 119 |
+
border-radius: 18px;
|
| 120 |
+
border: 1px solid var(--border-color);
|
| 121 |
+
overflow: hidden;
|
| 122 |
+
display: flex;
|
| 123 |
+
flex-direction: column;
|
| 124 |
+
position: relative;
|
| 125 |
+
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
.video-header {
|
| 129 |
+
position: absolute;
|
| 130 |
+
top: 20px;
|
| 131 |
+
left: 20px;
|
| 132 |
+
right: 20px;
|
| 133 |
+
display: flex;
|
| 134 |
+
justify-content: space-between;
|
| 135 |
+
align-items: center;
|
| 136 |
+
z-index: 5;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.mode-badge {
|
| 140 |
+
background: rgba(0, 0, 0, 0.6);
|
| 141 |
+
backdrop-filter: blur(10px);
|
| 142 |
+
-webkit-backdrop-filter: blur(10px);
|
| 143 |
+
padding: 6px 12px;
|
| 144 |
+
border-radius: 20px;
|
| 145 |
+
font-size: 0.8rem;
|
| 146 |
+
font-weight: 600;
|
| 147 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 148 |
+
display: flex;
|
| 149 |
+
align-items: center;
|
| 150 |
+
gap: 6px;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.live-indicator {
|
| 154 |
+
width: 8px;
|
| 155 |
+
height: 8px;
|
| 156 |
+
background-color: var(--accent-red);
|
| 157 |
+
border-radius: 50%;
|
| 158 |
+
box-shadow: 0 0 10px var(--accent-red);
|
| 159 |
+
animation: pulse 2s infinite;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
#video-stream {
|
| 163 |
+
width: 100%;
|
| 164 |
+
height: 100%;
|
| 165 |
+
object-fit: contain;
|
| 166 |
+
background: #000;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
/* Intelligence Panel */
|
| 170 |
+
.intel-panel {
|
| 171 |
+
display: flex;
|
| 172 |
+
flex-direction: column;
|
| 173 |
+
gap: 24px;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
.card {
|
| 177 |
+
background-color: var(--card-bg);
|
| 178 |
+
backdrop-filter: blur(20px);
|
| 179 |
+
-webkit-backdrop-filter: blur(20px);
|
| 180 |
+
border-radius: 18px;
|
| 181 |
+
padding: 24px;
|
| 182 |
+
border: 1px solid var(--border-color);
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.card-header {
|
| 186 |
+
display: flex;
|
| 187 |
+
justify-content: space-between;
|
| 188 |
+
align-items: center;
|
| 189 |
+
margin-bottom: 20px;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.card-title {
|
| 193 |
+
font-size: 0.95rem;
|
| 194 |
+
font-weight: 600;
|
| 195 |
+
color: var(--text-primary);
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.threat-meter {
|
| 199 |
+
display: flex;
|
| 200 |
+
flex-direction: column;
|
| 201 |
+
align-items: center;
|
| 202 |
+
padding: 10px 0;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
.score-circle {
|
| 206 |
+
width: 140px;
|
| 207 |
+
height: 140px;
|
| 208 |
+
border-radius: 50%;
|
| 209 |
+
border: 4px solid rgba(255, 255, 255, 0.1);
|
| 210 |
+
display: flex;
|
| 211 |
+
flex-direction: column;
|
| 212 |
+
justify-content: center;
|
| 213 |
+
align-items: center;
|
| 214 |
+
margin-bottom: 16px;
|
| 215 |
+
position: relative;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
.score-circle::after {
|
| 219 |
+
content: '';
|
| 220 |
+
position: absolute;
|
| 221 |
+
top: -4px;
|
| 222 |
+
left: -4px;
|
| 223 |
+
right: -4px;
|
| 224 |
+
bottom: -4px;
|
| 225 |
+
border-radius: 50%;
|
| 226 |
+
border: 4px solid var(--accent-green);
|
| 227 |
+
border-top-color: transparent;
|
| 228 |
+
border-left-color: transparent;
|
| 229 |
+
transform: rotate(-45deg);
|
| 230 |
+
transition: all 0.5s ease;
|
| 231 |
+
filter: drop-shadow(0 0 8px rgba(48, 209, 88, 0.3));
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
.score-value {
|
| 235 |
+
font-size: 3rem;
|
| 236 |
+
font-weight: 700;
|
| 237 |
+
line-height: 1;
|
| 238 |
+
letter-spacing: -1px;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
.score-label {
|
| 242 |
+
font-size: 0.8rem;
|
| 243 |
+
color: var(--text-secondary);
|
| 244 |
+
margin-top: 4px;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
.status-text {
|
| 248 |
+
font-size: 1.1rem;
|
| 249 |
+
font-weight: 600;
|
| 250 |
+
color: var(--accent-green);
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
.stats-list {
|
| 254 |
+
display: flex;
|
| 255 |
+
flex-direction: column;
|
| 256 |
+
gap: 12px;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
.stat-row {
|
| 260 |
+
display: flex;
|
| 261 |
+
justify-content: space-between;
|
| 262 |
+
align-items: center;
|
| 263 |
+
padding: 12px;
|
| 264 |
+
background: rgba(255, 255, 255, 0.03);
|
| 265 |
+
border-radius: 12px;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
.stat-name {
|
| 269 |
+
font-size: 0.9rem;
|
| 270 |
+
color: var(--text-secondary);
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
.stat-val {
|
| 274 |
+
font-size: 1rem;
|
| 275 |
+
font-weight: 600;
|
| 276 |
+
color: var(--text-primary);
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
.btn {
|
| 280 |
+
width: 100%;
|
| 281 |
+
padding: 14px;
|
| 282 |
+
border: none;
|
| 283 |
+
border-radius: 12px;
|
| 284 |
+
font-size: 0.95rem;
|
| 285 |
+
font-weight: 600;
|
| 286 |
+
cursor: pointer;
|
| 287 |
+
transition: all 0.2s;
|
| 288 |
+
font-family: inherit;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
.btn-primary {
|
| 292 |
+
background-color: var(--accent-blue);
|
| 293 |
+
color: white;
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
.btn-primary:hover {
|
| 297 |
+
background-color: #0071e3;
|
| 298 |
+
transform: scale(1.02);
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
.btn-secondary {
|
| 302 |
+
background-color: rgba(255, 255, 255, 0.1);
|
| 303 |
+
color: var(--text-primary);
|
| 304 |
+
margin-top: 10px;
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
.btn-secondary:hover {
|
| 308 |
+
background-color: rgba(255, 255, 255, 0.15);
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
/* Modal */
|
| 312 |
+
.modal-overlay {
|
| 313 |
+
position: fixed;
|
| 314 |
+
top: 0;
|
| 315 |
+
left: 0;
|
| 316 |
+
right: 0;
|
| 317 |
+
bottom: 0;
|
| 318 |
+
background: rgba(0, 0, 0, 0.7);
|
| 319 |
+
backdrop-filter: blur(5px);
|
| 320 |
+
display: none;
|
| 321 |
+
justify-content: center;
|
| 322 |
+
align-items: center;
|
| 323 |
+
z-index: 1000;
|
| 324 |
+
opacity: 0;
|
| 325 |
+
transition: opacity 0.3s ease;
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
.modal-overlay.show {
|
| 329 |
+
display: flex;
|
| 330 |
+
opacity: 1;
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
.modal-card {
|
| 334 |
+
background: #1c1c1e;
|
| 335 |
+
width: 480px;
|
| 336 |
+
border-radius: 20px;
|
| 337 |
+
padding: 30px;
|
| 338 |
+
border: 1px solid var(--border-color);
|
| 339 |
+
transform: scale(0.95);
|
| 340 |
+
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
.modal-overlay.show .modal-card {
|
| 344 |
+
transform: scale(1);
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
.report-content {
|
| 348 |
+
background: #000;
|
| 349 |
+
padding: 16px;
|
| 350 |
+
border-radius: 12px;
|
| 351 |
+
font-family: 'SF Mono', 'Menlo', monospace;
|
| 352 |
+
font-size: 0.85rem;
|
| 353 |
+
color: var(--accent-green);
|
| 354 |
+
margin: 20px 0;
|
| 355 |
+
line-height: 1.5;
|
| 356 |
+
border: 1px solid var(--border-color);
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
@keyframes pulse {
|
| 360 |
+
0% {
|
| 361 |
+
opacity: 1;
|
| 362 |
+
transform: scale(1);
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
50% {
|
| 366 |
+
opacity: 0.5;
|
| 367 |
+
transform: scale(1.2);
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
100% {
|
| 371 |
+
opacity: 1;
|
| 372 |
+
transform: scale(1);
|
| 373 |
+
}
|
| 374 |
+
}
|
| 375 |
+
</style>
|
| 376 |
+
</head>
|
| 377 |
+
|
| 378 |
+
<body>
|
| 379 |
+
<div class="sidebar">
|
| 380 |
+
<div class="logo">
|
| 381 |
+
<i data-feather="shield"></i>
|
| 382 |
+
SENTINEL V4
|
| 383 |
+
</div>
|
| 384 |
+
|
| 385 |
+
<div class="nav-group">
|
| 386 |
+
<div class="nav-label">Detection Modes</div>
|
| 387 |
+
<div class="nav-item active" onclick="setMode('movement', this)">
|
| 388 |
+
<i data-feather="activity"></i> Movement
|
| 389 |
+
</div>
|
| 390 |
+
<div class="nav-item" onclick="setMode('facemask', this)">
|
| 391 |
+
<i data-feather="user"></i> Facemask
|
| 392 |
+
</div>
|
| 393 |
+
<div class="nav-item" onclick="setMode('weapon', this)">
|
| 394 |
+
<i data-feather="target"></i> Weapon
|
| 395 |
+
</div>
|
| 396 |
+
<div class="nav-item" onclick="setMode('public_safety', this)">
|
| 397 |
+
<i data-feather="users"></i> Public Safety
|
| 398 |
+
</div>
|
| 399 |
+
</div>
|
| 400 |
+
|
| 401 |
+
<div class="nav-group" style="margin-top: auto;">
|
| 402 |
+
<div class="nav-label">Input Source</div>
|
| 403 |
+
<div class="nav-item" onclick="setSource('camera')">
|
| 404 |
+
<i data-feather="camera"></i> Live Camera
|
| 405 |
+
</div>
|
| 406 |
+
<div class="nav-item" onclick="document.getElementById('upload-input').click()">
|
| 407 |
+
<i data-feather="upload-cloud"></i> Upload Video
|
| 408 |
+
</div>
|
| 409 |
+
<input type="file" id="upload-input" style="display: none" accept="video/*"
|
| 410 |
+
onchange="handleFileUpload(this)">
|
| 411 |
+
</div>
|
| 412 |
+
</div>
|
| 413 |
+
|
| 414 |
+
<div class="main-content">
|
| 415 |
+
<div class="video-container">
|
| 416 |
+
<div class="video-header">
|
| 417 |
+
<div class="mode-badge">
|
| 418 |
+
<div class="live-indicator"></div>
|
| 419 |
+
<span id="mode-title">MOVEMENT ANALYSIS</span>
|
| 420 |
+
</div>
|
| 421 |
+
</div>
|
| 422 |
+
<img id="video-stream" src="{{ url_for('video_feed') }}">
|
| 423 |
+
</div>
|
| 424 |
+
|
| 425 |
+
<div class="intel-panel">
|
| 426 |
+
<div class="card">
|
| 427 |
+
<div class="card-header">
|
| 428 |
+
<div class="card-title">Threat Assessment</div>
|
| 429 |
+
<i data-feather="alert-circle" style="width: 18px; color: var(--text-secondary);"></i>
|
| 430 |
+
</div>
|
| 431 |
+
<div class="threat-meter">
|
| 432 |
+
<div class="score-circle" id="score-circle">
|
| 433 |
+
<div class="score-value" id="threat-score">0</div>
|
| 434 |
+
<div class="score-label">RISK LEVEL</div>
|
| 435 |
+
</div>
|
| 436 |
+
<div class="status-text" id="status-text">SECURE</div>
|
| 437 |
+
</div>
|
| 438 |
+
</div>
|
| 439 |
+
|
| 440 |
+
<div class="card">
|
| 441 |
+
<div class="card-header">
|
| 442 |
+
<div class="card-title">Live Metrics</div>
|
| 443 |
+
<i data-feather="bar-chart-2" style="width: 18px; color: var(--text-secondary);"></i>
|
| 444 |
+
</div>
|
| 445 |
+
<div class="stats-list" id="stats-container">
|
| 446 |
+
<!-- Populated by JS -->
|
| 447 |
+
</div>
|
| 448 |
+
</div>
|
| 449 |
+
|
| 450 |
+
<div class="card">
|
| 451 |
+
<div class="card-header">
|
| 452 |
+
<div class="card-title">Actions</div>
|
| 453 |
+
</div>
|
| 454 |
+
<button class="btn btn-primary" onclick="generateReport()">Generate Incident Report</button>
|
| 455 |
+
</div>
|
| 456 |
+
</div>
|
| 457 |
+
</div>
|
| 458 |
+
|
| 459 |
+
<div id="report-modal" class="modal-overlay">
|
| 460 |
+
<div class="modal-card">
|
| 461 |
+
<h2 style="font-size: 1.5rem; font-weight: 700; margin-bottom: 10px;">Incident Report</h2>
|
| 462 |
+
<p style="color: var(--text-secondary); font-size: 0.9rem;">Generated by Sentinel AI Agent</p>
|
| 463 |
+
<div class="report-content" id="report-content">Analyzing data stream...</div>
|
| 464 |
+
<button class="btn btn-secondary" onclick="closeModal()">Dismiss</button>
|
| 465 |
+
</div>
|
| 466 |
+
</div>
|
| 467 |
+
|
| 468 |
+
<script>
|
| 469 |
+
feather.replace();
|
| 470 |
+
|
| 471 |
+
function setMode(mode, element) {
|
| 472 |
+
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
|
| 473 |
+
element.classList.add('active');
|
| 474 |
+
|
| 475 |
+
const titles = {
|
| 476 |
+
'movement': 'MOVEMENT ANALYSIS',
|
| 477 |
+
'facemask': 'FACEMASK DETECTION',
|
| 478 |
+
'weapon': 'WEAPON DETECTION',
|
| 479 |
+
'public_safety': 'PUBLIC SAFETY MONITORING'
|
| 480 |
+
};
|
| 481 |
+
document.getElementById('mode-title').textContent = titles[mode];
|
| 482 |
+
|
| 483 |
+
fetch('/set_mode', {
|
| 484 |
+
method: 'POST',
|
| 485 |
+
headers: { 'Content-Type': 'application/json' },
|
| 486 |
+
body: JSON.stringify({ mode: mode })
|
| 487 |
+
});
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
function setSource(source) {
|
| 491 |
+
fetch('/set_source', {
|
| 492 |
+
method: 'POST',
|
| 493 |
+
headers: { 'Content-Type': 'application/json' },
|
| 494 |
+
body: JSON.stringify({ source: source })
|
| 495 |
+
});
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
function handleFileUpload(input) {
|
| 499 |
+
if (input.files[0]) {
|
| 500 |
+
const formData = new FormData();
|
| 501 |
+
formData.append('file', input.files[0]);
|
| 502 |
+
fetch('/upload_video', { method: 'POST', body: formData });
|
| 503 |
+
}
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
function generateReport() {
|
| 507 |
+
const modal = document.getElementById('report-modal');
|
| 508 |
+
modal.classList.add('show');
|
| 509 |
+
document.getElementById('report-content').textContent = "Analyzing data stream...";
|
| 510 |
+
|
| 511 |
+
fetch('/generate_report', { method: 'POST' })
|
| 512 |
+
.then(r => r.json())
|
| 513 |
+
.then(data => {
|
| 514 |
+
document.getElementById('report-content').textContent = data.report;
|
| 515 |
+
});
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
function closeModal() {
|
| 519 |
+
document.getElementById('report-modal').classList.remove('show');
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
function updateStats() {
|
| 523 |
+
fetch('/stats')
|
| 524 |
+
.then(r => r.json())
|
| 525 |
+
.then(data => {
|
| 526 |
+
const score = data.threat_score;
|
| 527 |
+
const scoreEl = document.getElementById('threat-score');
|
| 528 |
+
const statusEl = document.getElementById('status-text');
|
| 529 |
+
const circle = document.getElementById('score-circle');
|
| 530 |
+
|
| 531 |
+
scoreEl.textContent = score;
|
| 532 |
+
|
| 533 |
+
let color = '#30D158'; // Green
|
| 534 |
+
let status = 'SECURE';
|
| 535 |
+
|
| 536 |
+
if (score > 75) {
|
| 537 |
+
color = '#FF453A'; // Red
|
| 538 |
+
status = 'CRITICAL THREAT';
|
| 539 |
+
} else if (score > 40) {
|
| 540 |
+
color = '#FF9F0A'; // Orange
|
| 541 |
+
status = 'ELEVATED RISK';
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
statusEl.textContent = status;
|
| 545 |
+
statusEl.style.color = color;
|
| 546 |
+
circle.style.setProperty('--accent-green', color); // Hack to update pseudo-element color via variable if I used one, but here I need to update the style rule or use a variable.
|
| 547 |
+
// Actually, let's fix the CSS to use a variable for the border color
|
| 548 |
+
// The CSS uses var(--accent-green) for the pseudo element. So I can just set that variable on the element.
|
| 549 |
+
circle.style.setProperty('--accent-green', color);
|
| 550 |
+
|
| 551 |
+
const container = document.getElementById('stats-container');
|
| 552 |
+
container.innerHTML = '';
|
| 553 |
+
|
| 554 |
+
if (Object.keys(data.details).length === 0) {
|
| 555 |
+
container.innerHTML = '<div class="stat-row"><span class="stat-name">System Status</span><span class="stat-val">Standby</span></div>';
|
| 556 |
+
} else {
|
| 557 |
+
for (const [key, value] of Object.entries(data.details)) {
|
| 558 |
+
const div = document.createElement('div');
|
| 559 |
+
div.className = 'stat-row';
|
| 560 |
+
// Format key: replace underscores with spaces and capitalize words
|
| 561 |
+
const formattedKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
| 562 |
+
div.innerHTML = `<span class="stat-name">${formattedKey}</span><span class="stat-val">${value}</span>`;
|
| 563 |
+
container.appendChild(div);
|
| 564 |
+
}
|
| 565 |
+
}
|
| 566 |
+
});
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
setInterval(updateStats, 1000);
|
| 570 |
+
</script>
|
| 571 |
+
</body>
|
| 572 |
+
|
| 573 |
+
</html>
|
templates/sentinel_dashboard_v7.html
ADDED
|
@@ -0,0 +1,1386 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>SENTINEL V7 — Intelligence Grid</title>
|
| 8 |
+
<link
|
| 9 |
+
href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700;800;900&family=Rajdhani:wght@300;400;500;600;700&family=Share+Tech+Mono&display=swap"
|
| 10 |
+
rel="stylesheet">
|
| 11 |
+
<script src="https://unpkg.com/feather-icons"></script>
|
| 12 |
+
<style>
|
| 13 |
+
/* ═══════════════════════════════════════════════
|
| 14 |
+
CSS VARIABLES — SCI-FI PALETTE
|
| 15 |
+
═══════════════════════════════════════════════ */
|
| 16 |
+
:root {
|
| 17 |
+
--bg-deep: #05080f;
|
| 18 |
+
--bg-panel: rgba(8, 16, 32, 0.85);
|
| 19 |
+
--bg-card: rgba(10, 20, 40, 0.7);
|
| 20 |
+
--border-glow: rgba(0, 200, 255, 0.15);
|
| 21 |
+
--border-glow-hover: rgba(0, 200, 255, 0.4);
|
| 22 |
+
--cyan: #00d4ff;
|
| 23 |
+
--cyan-dim: #0090aa;
|
| 24 |
+
--neon-green: #00ff88;
|
| 25 |
+
--neon-red: #ff2040;
|
| 26 |
+
--neon-amber: #ffaa00;
|
| 27 |
+
--text-primary: #e0f0ff;
|
| 28 |
+
--text-secondary: #5a8aaa;
|
| 29 |
+
--text-dim: #2a4a5a;
|
| 30 |
+
--scanline-opacity: 0.03;
|
| 31 |
+
--font-display: 'Orbitron', sans-serif;
|
| 32 |
+
--font-body: 'Rajdhani', sans-serif;
|
| 33 |
+
--font-mono: 'Share Tech Mono', monospace;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/* ═══════════════════════════════════════════════
|
| 37 |
+
BASE RESET & BODY
|
| 38 |
+
═══════════════════════════════════════════════ */
|
| 39 |
+
*,
|
| 40 |
+
*::before,
|
| 41 |
+
*::after {
|
| 42 |
+
margin: 0;
|
| 43 |
+
padding: 0;
|
| 44 |
+
box-sizing: border-box;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
body {
|
| 48 |
+
font-family: var(--font-body);
|
| 49 |
+
background: var(--bg-deep);
|
| 50 |
+
color: var(--text-primary);
|
| 51 |
+
height: 100vh;
|
| 52 |
+
overflow: hidden;
|
| 53 |
+
display: flex;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/* Scanline overlay on body */
|
| 57 |
+
body::after {
|
| 58 |
+
content: '';
|
| 59 |
+
position: fixed;
|
| 60 |
+
top: 0;
|
| 61 |
+
left: 0;
|
| 62 |
+
right: 0;
|
| 63 |
+
bottom: 0;
|
| 64 |
+
background: repeating-linear-gradient(0deg,
|
| 65 |
+
transparent,
|
| 66 |
+
transparent 2px,
|
| 67 |
+
rgba(0, 200, 255, var(--scanline-opacity)) 2px,
|
| 68 |
+
rgba(0, 200, 255, var(--scanline-opacity)) 4px);
|
| 69 |
+
pointer-events: none;
|
| 70 |
+
z-index: 9999;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
/* ═══════════════════════════════════════════════
|
| 74 |
+
RED ALERT OVERLAY
|
| 75 |
+
═══════════════════════════════════════════════ */
|
| 76 |
+
.red-alert-overlay {
|
| 77 |
+
position: fixed;
|
| 78 |
+
top: 0;
|
| 79 |
+
left: 0;
|
| 80 |
+
right: 0;
|
| 81 |
+
bottom: 0;
|
| 82 |
+
pointer-events: none;
|
| 83 |
+
z-index: 9998;
|
| 84 |
+
opacity: 0;
|
| 85 |
+
transition: opacity 0.3s ease;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.red-alert-overlay.active {
|
| 89 |
+
opacity: 1;
|
| 90 |
+
animation: red-pulse-border 1s ease-in-out infinite;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.red-alert-overlay.active::before {
|
| 94 |
+
content: '';
|
| 95 |
+
position: absolute;
|
| 96 |
+
top: 0;
|
| 97 |
+
left: 0;
|
| 98 |
+
right: 0;
|
| 99 |
+
bottom: 0;
|
| 100 |
+
border: 4px solid var(--neon-red);
|
| 101 |
+
box-shadow: inset 0 0 80px rgba(255, 32, 64, 0.15),
|
| 102 |
+
0 0 40px rgba(255, 32, 64, 0.3);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.red-alert-banner {
|
| 106 |
+
position: fixed;
|
| 107 |
+
top: 0;
|
| 108 |
+
left: 50%;
|
| 109 |
+
transform: translateX(-50%);
|
| 110 |
+
background: rgba(255, 20, 40, 0.9);
|
| 111 |
+
color: #fff;
|
| 112 |
+
font-family: var(--font-display);
|
| 113 |
+
font-size: 0.85rem;
|
| 114 |
+
font-weight: 700;
|
| 115 |
+
letter-spacing: 4px;
|
| 116 |
+
padding: 8px 40px;
|
| 117 |
+
z-index: 10000;
|
| 118 |
+
border-bottom-left-radius: 8px;
|
| 119 |
+
border-bottom-right-radius: 8px;
|
| 120 |
+
box-shadow: 0 4px 30px rgba(255, 20, 40, 0.5);
|
| 121 |
+
display: none;
|
| 122 |
+
animation: alert-flash 0.8s ease-in-out infinite;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.red-alert-banner.active {
|
| 126 |
+
display: block;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
@keyframes red-pulse-border {
|
| 130 |
+
|
| 131 |
+
0%,
|
| 132 |
+
100% {
|
| 133 |
+
opacity: 0.6;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
50% {
|
| 137 |
+
opacity: 1;
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
@keyframes alert-flash {
|
| 142 |
+
|
| 143 |
+
0%,
|
| 144 |
+
100% {
|
| 145 |
+
opacity: 1;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
50% {
|
| 149 |
+
opacity: 0.6;
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
/* ═══════════════════════════════════════════════
|
| 154 |
+
SIDEBAR
|
| 155 |
+
═══════════════════════════════════════════════ */
|
| 156 |
+
.sidebar {
|
| 157 |
+
width: 260px;
|
| 158 |
+
min-width: 260px;
|
| 159 |
+
background: var(--bg-panel);
|
| 160 |
+
backdrop-filter: blur(20px);
|
| 161 |
+
-webkit-backdrop-filter: blur(20px);
|
| 162 |
+
border-right: 1px solid var(--border-glow);
|
| 163 |
+
display: flex;
|
| 164 |
+
flex-direction: column;
|
| 165 |
+
padding: 20px;
|
| 166 |
+
z-index: 100;
|
| 167 |
+
overflow-y: auto;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.logo {
|
| 171 |
+
font-family: var(--font-display);
|
| 172 |
+
font-size: 1rem;
|
| 173 |
+
font-weight: 700;
|
| 174 |
+
margin-bottom: 8px;
|
| 175 |
+
display: flex;
|
| 176 |
+
align-items: center;
|
| 177 |
+
gap: 10px;
|
| 178 |
+
color: var(--cyan);
|
| 179 |
+
letter-spacing: 2px;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.logo svg {
|
| 183 |
+
filter: drop-shadow(0 0 6px var(--cyan));
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
.version-tag {
|
| 187 |
+
font-family: var(--font-mono);
|
| 188 |
+
font-size: 0.65rem;
|
| 189 |
+
color: var(--text-dim);
|
| 190 |
+
margin-bottom: 24px;
|
| 191 |
+
letter-spacing: 1px;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.nav-group {
|
| 195 |
+
margin-bottom: 20px;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.nav-label {
|
| 199 |
+
font-family: var(--font-display);
|
| 200 |
+
font-size: 0.6rem;
|
| 201 |
+
font-weight: 600;
|
| 202 |
+
color: var(--text-dim);
|
| 203 |
+
margin-bottom: 8px;
|
| 204 |
+
text-transform: uppercase;
|
| 205 |
+
letter-spacing: 2px;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
.nav-item {
|
| 209 |
+
padding: 10px 12px;
|
| 210 |
+
margin-bottom: 3px;
|
| 211 |
+
border-radius: 6px;
|
| 212 |
+
cursor: pointer;
|
| 213 |
+
transition: all 0.25s ease;
|
| 214 |
+
display: flex;
|
| 215 |
+
align-items: center;
|
| 216 |
+
gap: 10px;
|
| 217 |
+
color: var(--text-secondary);
|
| 218 |
+
font-size: 0.9rem;
|
| 219 |
+
font-weight: 500;
|
| 220 |
+
border: 1px solid transparent;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.nav-item:hover {
|
| 224 |
+
background: rgba(0, 200, 255, 0.05);
|
| 225 |
+
color: var(--text-primary);
|
| 226 |
+
border-color: var(--border-glow);
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.nav-item.active {
|
| 230 |
+
background: rgba(0, 200, 255, 0.1);
|
| 231 |
+
color: var(--cyan);
|
| 232 |
+
border-color: rgba(0, 200, 255, 0.3);
|
| 233 |
+
box-shadow: 0 0 15px rgba(0, 200, 255, 0.05);
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
.nav-item svg {
|
| 237 |
+
width: 16px;
|
| 238 |
+
height: 16px;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
/* Audit Log Panel in sidebar */
|
| 242 |
+
.audit-section {
|
| 243 |
+
margin-top: auto;
|
| 244 |
+
flex-shrink: 0;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
.audit-log-container {
|
| 248 |
+
max-height: 200px;
|
| 249 |
+
overflow-y: auto;
|
| 250 |
+
scrollbar-width: thin;
|
| 251 |
+
scrollbar-color: var(--cyan-dim) transparent;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
.audit-log-container::-webkit-scrollbar {
|
| 255 |
+
width: 4px;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.audit-log-container::-webkit-scrollbar-track {
|
| 259 |
+
background: transparent;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.audit-log-container::-webkit-scrollbar-thumb {
|
| 263 |
+
background: var(--cyan-dim);
|
| 264 |
+
border-radius: 2px;
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
.audit-entry {
|
| 268 |
+
font-family: var(--font-mono);
|
| 269 |
+
font-size: 0.65rem;
|
| 270 |
+
padding: 6px 8px;
|
| 271 |
+
margin-bottom: 2px;
|
| 272 |
+
border-radius: 4px;
|
| 273 |
+
background: rgba(0, 0, 0, 0.3);
|
| 274 |
+
border-left: 2px solid var(--cyan-dim);
|
| 275 |
+
line-height: 1.4;
|
| 276 |
+
color: var(--text-secondary);
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
.audit-entry.severity-WARNING {
|
| 280 |
+
border-left-color: var(--neon-amber);
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
.audit-entry.severity-CRITICAL {
|
| 284 |
+
border-left-color: var(--neon-red);
|
| 285 |
+
color: var(--neon-red);
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
.audit-time {
|
| 289 |
+
color: var(--text-dim);
|
| 290 |
+
font-size: 0.6rem;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
/* ═══════════════════════════════════════════════
|
| 294 |
+
MAIN CONTENT AREA
|
| 295 |
+
═══════════════════════════════════════════════ */
|
| 296 |
+
.main-content {
|
| 297 |
+
flex: 1;
|
| 298 |
+
padding: 16px;
|
| 299 |
+
display: grid;
|
| 300 |
+
grid-template-columns: 1fr 320px;
|
| 301 |
+
gap: 16px;
|
| 302 |
+
overflow: hidden;
|
| 303 |
+
background: radial-gradient(ellipse at 20% 0%, rgba(0, 60, 100, 0.15) 0%, transparent 60%),
|
| 304 |
+
radial-gradient(ellipse at 80% 100%, rgba(0, 40, 80, 0.1) 0%, transparent 60%),
|
| 305 |
+
var(--bg-deep);
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
/* ═══════════════════════════════════════════════
|
| 309 |
+
MULTI-CAMERA GRID
|
| 310 |
+
═══════════════════════════════════════════════ */
|
| 311 |
+
.camera-grid {
|
| 312 |
+
display: grid;
|
| 313 |
+
grid-template-columns: 1fr 1fr;
|
| 314 |
+
grid-template-rows: 1fr 1fr;
|
| 315 |
+
gap: 8px;
|
| 316 |
+
height: 100%;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
.camera-grid.single-view {
|
| 320 |
+
grid-template-columns: 1fr;
|
| 321 |
+
grid-template-rows: 1fr;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.camera-grid.single-view .feed-cell:not(.expanded) {
|
| 325 |
+
display: none;
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
.feed-cell {
|
| 329 |
+
background: var(--bg-card);
|
| 330 |
+
border-radius: 10px;
|
| 331 |
+
border: 1px solid var(--border-glow);
|
| 332 |
+
overflow: hidden;
|
| 333 |
+
position: relative;
|
| 334 |
+
cursor: pointer;
|
| 335 |
+
transition: all 0.3s ease;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.feed-cell:hover {
|
| 339 |
+
border-color: var(--border-glow-hover);
|
| 340 |
+
box-shadow: 0 0 20px rgba(0, 200, 255, 0.1);
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
.feed-cell.red-alert-active {
|
| 344 |
+
border-color: var(--neon-red) !important;
|
| 345 |
+
box-shadow: 0 0 30px rgba(255, 32, 64, 0.3) !important;
|
| 346 |
+
animation: cell-red-pulse 1.5s ease-in-out infinite;
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
@keyframes cell-red-pulse {
|
| 350 |
+
|
| 351 |
+
0%,
|
| 352 |
+
100% {
|
| 353 |
+
box-shadow: 0 0 20px rgba(255, 32, 64, 0.2);
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
50% {
|
| 357 |
+
box-shadow: 0 0 40px rgba(255, 32, 64, 0.5);
|
| 358 |
+
}
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
.feed-header {
|
| 362 |
+
position: absolute;
|
| 363 |
+
top: 8px;
|
| 364 |
+
left: 10px;
|
| 365 |
+
right: 10px;
|
| 366 |
+
display: flex;
|
| 367 |
+
justify-content: space-between;
|
| 368 |
+
align-items: center;
|
| 369 |
+
z-index: 5;
|
| 370 |
+
pointer-events: none;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
.feed-badge {
|
| 374 |
+
background: rgba(0, 0, 0, 0.7);
|
| 375 |
+
backdrop-filter: blur(8px);
|
| 376 |
+
padding: 4px 10px;
|
| 377 |
+
border-radius: 4px;
|
| 378 |
+
font-family: var(--font-mono);
|
| 379 |
+
font-size: 0.65rem;
|
| 380 |
+
font-weight: 400;
|
| 381 |
+
border: 1px solid var(--border-glow);
|
| 382 |
+
display: flex;
|
| 383 |
+
align-items: center;
|
| 384 |
+
gap: 6px;
|
| 385 |
+
color: var(--cyan);
|
| 386 |
+
letter-spacing: 1px;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
.live-dot {
|
| 390 |
+
width: 6px;
|
| 391 |
+
height: 6px;
|
| 392 |
+
background: var(--neon-red);
|
| 393 |
+
border-radius: 50%;
|
| 394 |
+
box-shadow: 0 0 8px var(--neon-red);
|
| 395 |
+
animation: pulse-dot 2s infinite;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
@keyframes pulse-dot {
|
| 399 |
+
|
| 400 |
+
0%,
|
| 401 |
+
100% {
|
| 402 |
+
opacity: 1;
|
| 403 |
+
transform: scale(1);
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
50% {
|
| 407 |
+
opacity: 0.4;
|
| 408 |
+
transform: scale(1.3);
|
| 409 |
+
}
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
.feed-status {
|
| 413 |
+
font-family: var(--font-mono);
|
| 414 |
+
font-size: 0.6rem;
|
| 415 |
+
color: var(--neon-green);
|
| 416 |
+
background: rgba(0, 0, 0, 0.6);
|
| 417 |
+
padding: 3px 8px;
|
| 418 |
+
border-radius: 4px;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
.feed-stream {
|
| 422 |
+
width: 100%;
|
| 423 |
+
height: 100%;
|
| 424 |
+
object-fit: contain;
|
| 425 |
+
background: #000;
|
| 426 |
+
display: block;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.feed-offline {
|
| 430 |
+
display: flex;
|
| 431 |
+
flex-direction: column;
|
| 432 |
+
align-items: center;
|
| 433 |
+
justify-content: center;
|
| 434 |
+
height: 100%;
|
| 435 |
+
color: var(--text-dim);
|
| 436 |
+
font-family: var(--font-mono);
|
| 437 |
+
font-size: 0.75rem;
|
| 438 |
+
letter-spacing: 1px;
|
| 439 |
+
gap: 8px;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
.feed-offline svg {
|
| 443 |
+
width: 24px;
|
| 444 |
+
height: 24px;
|
| 445 |
+
color: var(--text-dim);
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
/* Fullscreen expand button */
|
| 449 |
+
.expand-btn {
|
| 450 |
+
position: absolute;
|
| 451 |
+
bottom: 8px;
|
| 452 |
+
right: 8px;
|
| 453 |
+
background: rgba(0, 0, 0, 0.6);
|
| 454 |
+
border: 1px solid var(--border-glow);
|
| 455 |
+
color: var(--cyan);
|
| 456 |
+
width: 28px;
|
| 457 |
+
height: 28px;
|
| 458 |
+
border-radius: 4px;
|
| 459 |
+
cursor: pointer;
|
| 460 |
+
display: flex;
|
| 461 |
+
align-items: center;
|
| 462 |
+
justify-content: center;
|
| 463 |
+
transition: all 0.2s;
|
| 464 |
+
z-index: 5;
|
| 465 |
+
pointer-events: all;
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
.expand-btn:hover {
|
| 469 |
+
background: rgba(0, 200, 255, 0.15);
|
| 470 |
+
border-color: var(--cyan);
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
.expand-btn svg {
|
| 474 |
+
width: 14px;
|
| 475 |
+
height: 14px;
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
/* ═══════════════════════════════════════════════
|
| 479 |
+
INTEL PANEL (Right Column)
|
| 480 |
+
═══════════════════════════════════════════════ */
|
| 481 |
+
.intel-panel {
|
| 482 |
+
display: flex;
|
| 483 |
+
flex-direction: column;
|
| 484 |
+
gap: 12px;
|
| 485 |
+
overflow-y: auto;
|
| 486 |
+
scrollbar-width: thin;
|
| 487 |
+
scrollbar-color: var(--cyan-dim) transparent;
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
.intel-panel::-webkit-scrollbar {
|
| 491 |
+
width: 4px;
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
.intel-panel::-webkit-scrollbar-thumb {
|
| 495 |
+
background: var(--cyan-dim);
|
| 496 |
+
border-radius: 2px;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
.card {
|
| 500 |
+
background: var(--bg-card);
|
| 501 |
+
backdrop-filter: blur(15px);
|
| 502 |
+
border-radius: 10px;
|
| 503 |
+
padding: 18px;
|
| 504 |
+
border: 1px solid var(--border-glow);
|
| 505 |
+
transition: border-color 0.3s;
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
.card:hover {
|
| 509 |
+
border-color: var(--border-glow-hover);
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
.card-header {
|
| 513 |
+
display: flex;
|
| 514 |
+
justify-content: space-between;
|
| 515 |
+
align-items: center;
|
| 516 |
+
margin-bottom: 14px;
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
.card-title {
|
| 520 |
+
font-family: var(--font-display);
|
| 521 |
+
font-size: 0.65rem;
|
| 522 |
+
font-weight: 600;
|
| 523 |
+
color: var(--cyan);
|
| 524 |
+
text-transform: uppercase;
|
| 525 |
+
letter-spacing: 2px;
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
.card-icon {
|
| 529 |
+
color: var(--text-dim);
|
| 530 |
+
width: 16px;
|
| 531 |
+
height: 16px;
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
/* Threat gauge */
|
| 535 |
+
.threat-gauge {
|
| 536 |
+
display: flex;
|
| 537 |
+
flex-direction: column;
|
| 538 |
+
align-items: center;
|
| 539 |
+
padding: 10px 0;
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
.score-ring {
|
| 543 |
+
position: relative;
|
| 544 |
+
width: 130px;
|
| 545 |
+
height: 130px;
|
| 546 |
+
margin-bottom: 12px;
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
.score-ring svg {
|
| 550 |
+
width: 130px;
|
| 551 |
+
height: 130px;
|
| 552 |
+
transform: rotate(-90deg);
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
.score-ring-bg {
|
| 556 |
+
fill: none;
|
| 557 |
+
stroke: rgba(255, 255, 255, 0.05);
|
| 558 |
+
stroke-width: 6;
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
.score-ring-fill {
|
| 562 |
+
fill: none;
|
| 563 |
+
stroke: var(--neon-green);
|
| 564 |
+
stroke-width: 6;
|
| 565 |
+
stroke-linecap: round;
|
| 566 |
+
stroke-dasharray: 377;
|
| 567 |
+
stroke-dashoffset: 377;
|
| 568 |
+
transition: stroke-dashoffset 0.8s cubic-bezier(0.4, 0, 0.2, 1),
|
| 569 |
+
stroke 0.5s ease;
|
| 570 |
+
filter: drop-shadow(0 0 8px var(--neon-green));
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
.score-text {
|
| 574 |
+
position: absolute;
|
| 575 |
+
top: 50%;
|
| 576 |
+
left: 50%;
|
| 577 |
+
transform: translate(-50%, -50%);
|
| 578 |
+
text-align: center;
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
.score-value {
|
| 582 |
+
font-family: var(--font-display);
|
| 583 |
+
font-size: 2.2rem;
|
| 584 |
+
font-weight: 800;
|
| 585 |
+
line-height: 1;
|
| 586 |
+
color: var(--text-primary);
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
.score-label {
|
| 590 |
+
font-family: var(--font-mono);
|
| 591 |
+
font-size: 0.55rem;
|
| 592 |
+
color: var(--text-dim);
|
| 593 |
+
letter-spacing: 2px;
|
| 594 |
+
margin-top: 4px;
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
.status-text {
|
| 598 |
+
font-family: var(--font-display);
|
| 599 |
+
font-size: 0.85rem;
|
| 600 |
+
font-weight: 700;
|
| 601 |
+
color: var(--neon-green);
|
| 602 |
+
letter-spacing: 3px;
|
| 603 |
+
text-shadow: 0 0 20px rgba(0, 255, 136, 0.3);
|
| 604 |
+
transition: all 0.5s ease;
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
/* Stats */
|
| 608 |
+
.stats-list {
|
| 609 |
+
display: flex;
|
| 610 |
+
flex-direction: column;
|
| 611 |
+
gap: 6px;
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
.stat-row {
|
| 615 |
+
display: flex;
|
| 616 |
+
justify-content: space-between;
|
| 617 |
+
align-items: center;
|
| 618 |
+
padding: 10px 12px;
|
| 619 |
+
background: rgba(0, 200, 255, 0.03);
|
| 620 |
+
border-radius: 6px;
|
| 621 |
+
border: 1px solid rgba(0, 200, 255, 0.05);
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
.stat-name {
|
| 625 |
+
font-size: 0.8rem;
|
| 626 |
+
color: var(--text-secondary);
|
| 627 |
+
font-weight: 500;
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
.stat-val {
|
| 631 |
+
font-family: var(--font-mono);
|
| 632 |
+
font-size: 0.85rem;
|
| 633 |
+
font-weight: 600;
|
| 634 |
+
color: var(--text-primary);
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
/* Mode indicator */
|
| 638 |
+
.mode-indicator {
|
| 639 |
+
display: flex;
|
| 640 |
+
align-items: center;
|
| 641 |
+
gap: 8px;
|
| 642 |
+
padding: 10px 14px;
|
| 643 |
+
background: rgba(0, 200, 255, 0.05);
|
| 644 |
+
border-radius: 6px;
|
| 645 |
+
border: 1px solid var(--border-glow);
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
.mode-dot {
|
| 649 |
+
width: 8px;
|
| 650 |
+
height: 8px;
|
| 651 |
+
background: var(--cyan);
|
| 652 |
+
border-radius: 50%;
|
| 653 |
+
box-shadow: 0 0 10px var(--cyan);
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
.mode-name {
|
| 657 |
+
font-family: var(--font-display);
|
| 658 |
+
font-size: 0.7rem;
|
| 659 |
+
font-weight: 600;
|
| 660 |
+
letter-spacing: 2px;
|
| 661 |
+
color: var(--cyan);
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
/* Buttons */
|
| 665 |
+
.btn {
|
| 666 |
+
width: 100%;
|
| 667 |
+
padding: 12px;
|
| 668 |
+
border: 1px solid var(--border-glow);
|
| 669 |
+
border-radius: 6px;
|
| 670 |
+
font-size: 0.8rem;
|
| 671 |
+
font-weight: 600;
|
| 672 |
+
cursor: pointer;
|
| 673 |
+
transition: all 0.25s;
|
| 674 |
+
font-family: var(--font-display);
|
| 675 |
+
letter-spacing: 1px;
|
| 676 |
+
text-transform: uppercase;
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
.btn-primary {
|
| 680 |
+
background: linear-gradient(135deg, rgba(0, 200, 255, 0.2), rgba(0, 100, 200, 0.1));
|
| 681 |
+
color: var(--cyan);
|
| 682 |
+
border-color: rgba(0, 200, 255, 0.3);
|
| 683 |
+
}
|
| 684 |
+
|
| 685 |
+
.btn-primary:hover {
|
| 686 |
+
background: linear-gradient(135deg, rgba(0, 200, 255, 0.3), rgba(0, 100, 200, 0.2));
|
| 687 |
+
box-shadow: 0 0 20px rgba(0, 200, 255, 0.15);
|
| 688 |
+
transform: translateY(-1px);
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
+
.btn-secondary {
|
| 692 |
+
background: rgba(255, 255, 255, 0.03);
|
| 693 |
+
color: var(--text-secondary);
|
| 694 |
+
}
|
| 695 |
+
|
| 696 |
+
.btn-secondary:hover {
|
| 697 |
+
background: rgba(255, 255, 255, 0.08);
|
| 698 |
+
color: var(--text-primary);
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
/* ═══════════════════════════════════════════════
|
| 702 |
+
MODAL
|
| 703 |
+
═══════════════════════════════════════════════ */
|
| 704 |
+
.modal-overlay {
|
| 705 |
+
position: fixed;
|
| 706 |
+
top: 0;
|
| 707 |
+
left: 0;
|
| 708 |
+
right: 0;
|
| 709 |
+
bottom: 0;
|
| 710 |
+
background: rgba(0, 0, 0, 0.8);
|
| 711 |
+
backdrop-filter: blur(10px);
|
| 712 |
+
display: none;
|
| 713 |
+
justify-content: center;
|
| 714 |
+
align-items: center;
|
| 715 |
+
z-index: 11000;
|
| 716 |
+
opacity: 0;
|
| 717 |
+
transition: opacity 0.3s ease;
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
.modal-overlay.show {
|
| 721 |
+
display: flex;
|
| 722 |
+
opacity: 1;
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
.modal-card {
|
| 726 |
+
background: var(--bg-panel);
|
| 727 |
+
width: 520px;
|
| 728 |
+
max-width: 90vw;
|
| 729 |
+
border-radius: 12px;
|
| 730 |
+
padding: 28px;
|
| 731 |
+
border: 1px solid var(--border-glow);
|
| 732 |
+
box-shadow: 0 0 60px rgba(0, 200, 255, 0.1);
|
| 733 |
+
transform: scale(0.95);
|
| 734 |
+
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
+
.modal-overlay.show .modal-card {
|
| 738 |
+
transform: scale(1);
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
.modal-title {
|
| 742 |
+
font-family: var(--font-display);
|
| 743 |
+
font-size: 1rem;
|
| 744 |
+
font-weight: 700;
|
| 745 |
+
color: var(--cyan);
|
| 746 |
+
letter-spacing: 2px;
|
| 747 |
+
margin-bottom: 6px;
|
| 748 |
+
}
|
| 749 |
+
|
| 750 |
+
.modal-subtitle {
|
| 751 |
+
font-family: var(--font-mono);
|
| 752 |
+
font-size: 0.7rem;
|
| 753 |
+
color: var(--text-dim);
|
| 754 |
+
margin-bottom: 16px;
|
| 755 |
+
}
|
| 756 |
+
|
| 757 |
+
.report-content {
|
| 758 |
+
background: rgba(0, 0, 0, 0.5);
|
| 759 |
+
padding: 16px;
|
| 760 |
+
border-radius: 8px;
|
| 761 |
+
font-family: var(--font-mono);
|
| 762 |
+
font-size: 0.75rem;
|
| 763 |
+
color: var(--neon-green);
|
| 764 |
+
margin-bottom: 16px;
|
| 765 |
+
line-height: 1.6;
|
| 766 |
+
border: 1px solid var(--border-glow);
|
| 767 |
+
max-height: 300px;
|
| 768 |
+
overflow-y: auto;
|
| 769 |
+
white-space: pre-wrap;
|
| 770 |
+
}
|
| 771 |
+
|
| 772 |
+
/* ═══════════════════════════════════════════════
|
| 773 |
+
MOBILE RESPONSIVE
|
| 774 |
+
═══════════════════════════════════════════════ */
|
| 775 |
+
@media (max-width: 1024px) {
|
| 776 |
+
.main-content {
|
| 777 |
+
grid-template-columns: 1fr 280px;
|
| 778 |
+
}
|
| 779 |
+
}
|
| 780 |
+
|
| 781 |
+
@media (max-width: 768px) {
|
| 782 |
+
body {
|
| 783 |
+
flex-direction: column;
|
| 784 |
+
overflow-y: auto;
|
| 785 |
+
}
|
| 786 |
+
|
| 787 |
+
.sidebar {
|
| 788 |
+
width: 100%;
|
| 789 |
+
min-width: 100%;
|
| 790 |
+
flex-direction: row;
|
| 791 |
+
flex-wrap: wrap;
|
| 792 |
+
padding: 10px 16px;
|
| 793 |
+
gap: 6px;
|
| 794 |
+
order: 2;
|
| 795 |
+
border-right: none;
|
| 796 |
+
border-top: 1px solid var(--border-glow);
|
| 797 |
+
overflow-y: visible;
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
.logo {
|
| 801 |
+
width: 100%;
|
| 802 |
+
margin-bottom: 6px;
|
| 803 |
+
font-size: 0.85rem;
|
| 804 |
+
}
|
| 805 |
+
|
| 806 |
+
.version-tag {
|
| 807 |
+
display: none;
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
+
.nav-group {
|
| 811 |
+
display: flex;
|
| 812 |
+
flex-wrap: wrap;
|
| 813 |
+
gap: 4px;
|
| 814 |
+
margin-bottom: 0;
|
| 815 |
+
}
|
| 816 |
+
|
| 817 |
+
.nav-label {
|
| 818 |
+
width: 100%;
|
| 819 |
+
margin-bottom: 4px;
|
| 820 |
+
}
|
| 821 |
+
|
| 822 |
+
.nav-item {
|
| 823 |
+
flex: none;
|
| 824 |
+
padding: 8px 10px;
|
| 825 |
+
font-size: 0.75rem;
|
| 826 |
+
}
|
| 827 |
+
|
| 828 |
+
.audit-section {
|
| 829 |
+
display: none;
|
| 830 |
+
}
|
| 831 |
+
|
| 832 |
+
.main-content {
|
| 833 |
+
grid-template-columns: 1fr;
|
| 834 |
+
grid-template-rows: auto auto;
|
| 835 |
+
order: 1;
|
| 836 |
+
overflow-y: auto;
|
| 837 |
+
padding: 10px;
|
| 838 |
+
}
|
| 839 |
+
|
| 840 |
+
.camera-grid {
|
| 841 |
+
grid-template-columns: 1fr;
|
| 842 |
+
grid-template-rows: auto;
|
| 843 |
+
min-height: 250px;
|
| 844 |
+
}
|
| 845 |
+
|
| 846 |
+
.feed-cell:not(:first-child) {
|
| 847 |
+
display: none;
|
| 848 |
+
}
|
| 849 |
+
|
| 850 |
+
.intel-panel {
|
| 851 |
+
overflow-y: visible;
|
| 852 |
+
}
|
| 853 |
+
}
|
| 854 |
+
|
| 855 |
+
@media (max-width: 480px) {
|
| 856 |
+
.sidebar .nav-item {
|
| 857 |
+
font-size: 0.7rem;
|
| 858 |
+
padding: 6px 8px;
|
| 859 |
+
gap: 6px;
|
| 860 |
+
}
|
| 861 |
+
|
| 862 |
+
.sidebar .nav-item svg {
|
| 863 |
+
width: 14px;
|
| 864 |
+
height: 14px;
|
| 865 |
+
}
|
| 866 |
+
|
| 867 |
+
.score-ring {
|
| 868 |
+
width: 100px;
|
| 869 |
+
height: 100px;
|
| 870 |
+
}
|
| 871 |
+
|
| 872 |
+
.score-ring svg {
|
| 873 |
+
width: 100px;
|
| 874 |
+
height: 100px;
|
| 875 |
+
}
|
| 876 |
+
|
| 877 |
+
.score-value {
|
| 878 |
+
font-size: 1.6rem;
|
| 879 |
+
}
|
| 880 |
+
}
|
| 881 |
+
</style>
|
| 882 |
+
</head>
|
| 883 |
+
|
| 884 |
+
<body>
|
| 885 |
+
<!-- Red Alert Overlay -->
|
| 886 |
+
<div class="red-alert-overlay" id="red-alert-overlay"></div>
|
| 887 |
+
<div class="red-alert-banner" id="red-alert-banner">⚠ RED ALERT — CRITICAL THREAT DETECTED ⚠</div>
|
| 888 |
+
|
| 889 |
+
<!-- SIDEBAR -->
|
| 890 |
+
<div class="sidebar">
|
| 891 |
+
<div class="logo">
|
| 892 |
+
<i data-feather="shield"></i>
|
| 893 |
+
SENTINEL
|
| 894 |
+
</div>
|
| 895 |
+
<div class="version-tag">V7.0 // MULTI-CAMERA GRID</div>
|
| 896 |
+
|
| 897 |
+
<div class="nav-group">
|
| 898 |
+
<div class="nav-label">Detection Modules</div>
|
| 899 |
+
<div class="nav-item active" onclick="setMode('movement', this)" id="nav-movement">
|
| 900 |
+
<i data-feather="activity"></i> Movement
|
| 901 |
+
</div>
|
| 902 |
+
<div class="nav-item" onclick="setMode('facemask', this)" id="nav-facemask">
|
| 903 |
+
<i data-feather="eye"></i> Facemask
|
| 904 |
+
</div>
|
| 905 |
+
<div class="nav-item" onclick="setMode('weapon', this)" id="nav-weapon">
|
| 906 |
+
<i data-feather="crosshair"></i> Weapon
|
| 907 |
+
</div>
|
| 908 |
+
<div class="nav-item" onclick="setMode('public_safety', this)" id="nav-public_safety">
|
| 909 |
+
<i data-feather="users"></i> Public Safety
|
| 910 |
+
</div>
|
| 911 |
+
</div>
|
| 912 |
+
|
| 913 |
+
<div class="nav-group">
|
| 914 |
+
<div class="nav-label">Input Source</div>
|
| 915 |
+
<div class="nav-item" onclick="setSource('camera')">
|
| 916 |
+
<i data-feather="video"></i> Live Camera
|
| 917 |
+
</div>
|
| 918 |
+
<div class="nav-item" onclick="document.getElementById('upload-input').click()">
|
| 919 |
+
<i data-feather="upload-cloud"></i> Upload Video
|
| 920 |
+
</div>
|
| 921 |
+
<input type="file" id="upload-input" style="display: none" accept="video/*"
|
| 922 |
+
onchange="handleFileUpload(this)">
|
| 923 |
+
</div>
|
| 924 |
+
|
| 925 |
+
<div class="nav-group">
|
| 926 |
+
<div class="nav-label">Grid Layout</div>
|
| 927 |
+
<div class="nav-item" onclick="setGridLayout('quad')">
|
| 928 |
+
<i data-feather="grid"></i> 2×2 Grid
|
| 929 |
+
</div>
|
| 930 |
+
<div class="nav-item" onclick="setGridLayout('single')">
|
| 931 |
+
<i data-feather="maximize-2"></i> Single View
|
| 932 |
+
</div>
|
| 933 |
+
</div>
|
| 934 |
+
|
| 935 |
+
<!-- Audit Log -->
|
| 936 |
+
<div class="audit-section">
|
| 937 |
+
<div class="nav-label" style="margin-bottom: 8px;">Operator Log</div>
|
| 938 |
+
<div class="audit-log-container" id="audit-log-container">
|
| 939 |
+
<div class="audit-entry">
|
| 940 |
+
<div class="audit-time">--:--:--</div>
|
| 941 |
+
SYSTEM STANDBY
|
| 942 |
+
</div>
|
| 943 |
+
</div>
|
| 944 |
+
</div>
|
| 945 |
+
</div>
|
| 946 |
+
|
| 947 |
+
<!-- MAIN CONTENT -->
|
| 948 |
+
<div class="main-content">
|
| 949 |
+
<!-- Multi-Camera Grid -->
|
| 950 |
+
<div class="camera-grid" id="camera-grid">
|
| 951 |
+
<!-- Feed 0 — Primary -->
|
| 952 |
+
<div class="feed-cell expanded" id="feed-0" onclick="expandFeed(0)">
|
| 953 |
+
<div class="feed-header">
|
| 954 |
+
<div class="feed-badge">
|
| 955 |
+
<div class="live-dot"></div>
|
| 956 |
+
<span>FEED 01 // PRIMARY</span>
|
| 957 |
+
</div>
|
| 958 |
+
<div class="feed-status" id="feed-0-status">ACTIVE</div>
|
| 959 |
+
</div>
|
| 960 |
+
<img class="feed-stream" id="stream-0" src="/video_feed/0" alt="Feed 0">
|
| 961 |
+
<button class="expand-btn" onclick="event.stopPropagation(); expandFeed(0)" title="Expand">
|
| 962 |
+
<i data-feather="maximize-2"></i>
|
| 963 |
+
</button>
|
| 964 |
+
</div>
|
| 965 |
+
|
| 966 |
+
<!-- Feed 1 -->
|
| 967 |
+
<div class="feed-cell" id="feed-1">
|
| 968 |
+
<div class="feed-header">
|
| 969 |
+
<div class="feed-badge">
|
| 970 |
+
<div class="live-dot" style="background: var(--text-dim); box-shadow: none;"></div>
|
| 971 |
+
<span>FEED 02</span>
|
| 972 |
+
</div>
|
| 973 |
+
<div class="feed-status" id="feed-1-status" style="color: var(--text-dim);">STANDBY</div>
|
| 974 |
+
</div>
|
| 975 |
+
<div class="feed-offline" id="offline-1">
|
| 976 |
+
<i data-feather="video-off"></i>
|
| 977 |
+
NO SIGNAL
|
| 978 |
+
</div>
|
| 979 |
+
</div>
|
| 980 |
+
|
| 981 |
+
<!-- Feed 2 -->
|
| 982 |
+
<div class="feed-cell" id="feed-2">
|
| 983 |
+
<div class="feed-header">
|
| 984 |
+
<div class="feed-badge">
|
| 985 |
+
<div class="live-dot" style="background: var(--text-dim); box-shadow: none;"></div>
|
| 986 |
+
<span>FEED 03</span>
|
| 987 |
+
</div>
|
| 988 |
+
<div class="feed-status" id="feed-2-status" style="color: var(--text-dim);">STANDBY</div>
|
| 989 |
+
</div>
|
| 990 |
+
<div class="feed-offline" id="offline-2">
|
| 991 |
+
<i data-feather="video-off"></i>
|
| 992 |
+
NO SIGNAL
|
| 993 |
+
</div>
|
| 994 |
+
</div>
|
| 995 |
+
|
| 996 |
+
<!-- Feed 3 -->
|
| 997 |
+
<div class="feed-cell" id="feed-3">
|
| 998 |
+
<div class="feed-header">
|
| 999 |
+
<div class="feed-badge">
|
| 1000 |
+
<div class="live-dot" style="background: var(--text-dim); box-shadow: none;"></div>
|
| 1001 |
+
<span>FEED 04</span>
|
| 1002 |
+
</div>
|
| 1003 |
+
<div class="feed-status" id="feed-3-status" style="color: var(--text-dim);">STANDBY</div>
|
| 1004 |
+
</div>
|
| 1005 |
+
<div class="feed-offline" id="offline-3">
|
| 1006 |
+
<i data-feather="video-off"></i>
|
| 1007 |
+
NO SIGNAL
|
| 1008 |
+
</div>
|
| 1009 |
+
</div>
|
| 1010 |
+
</div>
|
| 1011 |
+
|
| 1012 |
+
<!-- Intel Panel -->
|
| 1013 |
+
<div class="intel-panel">
|
| 1014 |
+
<!-- Active Mode -->
|
| 1015 |
+
<div class="card">
|
| 1016 |
+
<div class="card-header">
|
| 1017 |
+
<div class="card-title">Active Mode</div>
|
| 1018 |
+
</div>
|
| 1019 |
+
<div class="mode-indicator">
|
| 1020 |
+
<div class="mode-dot"></div>
|
| 1021 |
+
<div class="mode-name" id="mode-title">MOVEMENT ANALYSIS</div>
|
| 1022 |
+
</div>
|
| 1023 |
+
</div>
|
| 1024 |
+
|
| 1025 |
+
<!-- Threat Assessment -->
|
| 1026 |
+
<div class="card" id="threat-card">
|
| 1027 |
+
<div class="card-header">
|
| 1028 |
+
<div class="card-title">Threat Assessment</div>
|
| 1029 |
+
<i data-feather="alert-triangle" class="card-icon"></i>
|
| 1030 |
+
</div>
|
| 1031 |
+
<div class="threat-gauge">
|
| 1032 |
+
<div class="score-ring">
|
| 1033 |
+
<svg viewBox="0 0 130 130">
|
| 1034 |
+
<circle class="score-ring-bg" cx="65" cy="65" r="60"></circle>
|
| 1035 |
+
<circle class="score-ring-fill" id="score-ring-fill" cx="65" cy="65" r="60"></circle>
|
| 1036 |
+
</svg>
|
| 1037 |
+
<div class="score-text">
|
| 1038 |
+
<div class="score-value" id="threat-score">0</div>
|
| 1039 |
+
<div class="score-label">THREAT LEVEL</div>
|
| 1040 |
+
</div>
|
| 1041 |
+
</div>
|
| 1042 |
+
<div class="status-text" id="status-text">SECURE</div>
|
| 1043 |
+
</div>
|
| 1044 |
+
</div>
|
| 1045 |
+
|
| 1046 |
+
<!-- Live Metrics -->
|
| 1047 |
+
<div class="card">
|
| 1048 |
+
<div class="card-header">
|
| 1049 |
+
<div class="card-title">Live Metrics</div>
|
| 1050 |
+
<i data-feather="bar-chart-2" class="card-icon"></i>
|
| 1051 |
+
</div>
|
| 1052 |
+
<div class="stats-list" id="stats-container">
|
| 1053 |
+
<div class="stat-row">
|
| 1054 |
+
<span class="stat-name">System Status</span>
|
| 1055 |
+
<span class="stat-val">Initializing</span>
|
| 1056 |
+
</div>
|
| 1057 |
+
</div>
|
| 1058 |
+
</div>
|
| 1059 |
+
|
| 1060 |
+
<!-- Actions -->
|
| 1061 |
+
<div class="card">
|
| 1062 |
+
<div class="card-header">
|
| 1063 |
+
<div class="card-title">Actions</div>
|
| 1064 |
+
</div>
|
| 1065 |
+
<button class="btn btn-primary" onclick="generateReport()">Generate Incident Report</button>
|
| 1066 |
+
<button class="btn btn-secondary" onclick="refreshAuditLog()" style="margin-top: 8px;">Refresh Audit
|
| 1067 |
+
Log</button>
|
| 1068 |
+
</div>
|
| 1069 |
+
</div>
|
| 1070 |
+
</div>
|
| 1071 |
+
|
| 1072 |
+
<!-- Report Modal -->
|
| 1073 |
+
<div id="report-modal" class="modal-overlay">
|
| 1074 |
+
<div class="modal-card">
|
| 1075 |
+
<div class="modal-title">Incident Report</div>
|
| 1076 |
+
<div class="modal-subtitle">Generated by Sentinel AI Agent // Gemini 1.5</div>
|
| 1077 |
+
<div class="report-content" id="report-content">Analyzing data stream...</div>
|
| 1078 |
+
<button class="btn btn-secondary" onclick="closeModal()">Dismiss</button>
|
| 1079 |
+
</div>
|
| 1080 |
+
</div>
|
| 1081 |
+
|
| 1082 |
+
<!-- ═══════════════════════════════════════════════
|
| 1083 |
+
JAVASCRIPT
|
| 1084 |
+
═══════════════════════════════════════════════ -->
|
| 1085 |
+
<script>
|
| 1086 |
+
feather.replace();
|
| 1087 |
+
|
| 1088 |
+
// ─── State ───
|
| 1089 |
+
let currentLayout = 'quad'; // 'quad' or 'single'
|
| 1090 |
+
let expandedFeed = 0;
|
| 1091 |
+
let isRedAlert = false;
|
| 1092 |
+
|
| 1093 |
+
// ─── Mode Switching ───
|
| 1094 |
+
const modeTitles = {
|
| 1095 |
+
'movement': 'MOVEMENT ANALYSIS',
|
| 1096 |
+
'facemask': 'FACEMASK DETECTION',
|
| 1097 |
+
'weapon': 'WEAPON DETECTION',
|
| 1098 |
+
'public_safety': 'PUBLIC SAFETY'
|
| 1099 |
+
};
|
| 1100 |
+
|
| 1101 |
+
function setMode(mode, element) {
|
| 1102 |
+
// Check if already active
|
| 1103 |
+
const isActive = element.classList.contains('active');
|
| 1104 |
+
let targetMode = mode;
|
| 1105 |
+
|
| 1106 |
+
if (isActive) {
|
| 1107 |
+
// Toggle off -> Standby
|
| 1108 |
+
targetMode = 'standby';
|
| 1109 |
+
element.classList.remove('active');
|
| 1110 |
+
document.getElementById('mode-title').textContent = 'SYSTEM STANDBY';
|
| 1111 |
+
} else {
|
| 1112 |
+
// Activate new mode
|
| 1113 |
+
document.querySelectorAll('.nav-group:first-of-type .nav-item').forEach(el => el.classList.remove('active'));
|
| 1114 |
+
element.classList.add('active');
|
| 1115 |
+
document.getElementById('mode-title').textContent = modeTitles[mode] || mode.toUpperCase();
|
| 1116 |
+
}
|
| 1117 |
+
|
| 1118 |
+
fetch('/set_mode', {
|
| 1119 |
+
method: 'POST',
|
| 1120 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1121 |
+
body: JSON.stringify({ mode: targetMode })
|
| 1122 |
+
});
|
| 1123 |
+
}
|
| 1124 |
+
|
| 1125 |
+
function setSource(source) {
|
| 1126 |
+
fetch('/set_source', {
|
| 1127 |
+
method: 'POST',
|
| 1128 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1129 |
+
body: JSON.stringify({ source: source })
|
| 1130 |
+
})
|
| 1131 |
+
.then(r => r.json())
|
| 1132 |
+
.then(data => {
|
| 1133 |
+
if (data.success) {
|
| 1134 |
+
// Force the browser to reconnect to the restarted MJPEG stream
|
| 1135 |
+
refreshFeedStream(0);
|
| 1136 |
+
}
|
| 1137 |
+
});
|
| 1138 |
+
}
|
| 1139 |
+
|
| 1140 |
+
function handleFileUpload(input) {
|
| 1141 |
+
if (input.files[0]) {
|
| 1142 |
+
const formData = new FormData();
|
| 1143 |
+
formData.append('file', input.files[0]);
|
| 1144 |
+
|
| 1145 |
+
// Show uploading state
|
| 1146 |
+
const statusEl = document.getElementById('feed-0-status');
|
| 1147 |
+
if (statusEl) statusEl.textContent = 'UPLOADING...';
|
| 1148 |
+
|
| 1149 |
+
fetch('/upload_video', { method: 'POST', body: formData })
|
| 1150 |
+
.then(r => r.json())
|
| 1151 |
+
.then(data => {
|
| 1152 |
+
if (data.success) {
|
| 1153 |
+
// Force the browser to reconnect to the restarted MJPEG stream
|
| 1154 |
+
refreshFeedStream(0);
|
| 1155 |
+
if (statusEl) statusEl.textContent = 'FILE FEED';
|
| 1156 |
+
}
|
| 1157 |
+
})
|
| 1158 |
+
.catch(() => {
|
| 1159 |
+
if (statusEl) statusEl.textContent = 'UPLOAD FAILED';
|
| 1160 |
+
});
|
| 1161 |
+
|
| 1162 |
+
// Reset file input so the same file can be re-uploaded
|
| 1163 |
+
input.value = '';
|
| 1164 |
+
}
|
| 1165 |
+
}
|
| 1166 |
+
|
| 1167 |
+
/**
|
| 1168 |
+
* Force-refresh an MJPEG stream by setting a new src with a cache-buster.
|
| 1169 |
+
* This drops the old HTTP connection and establishes a fresh one.
|
| 1170 |
+
*/
|
| 1171 |
+
function refreshFeedStream(feedId) {
|
| 1172 |
+
const img = document.getElementById('stream-' + feedId);
|
| 1173 |
+
if (img) {
|
| 1174 |
+
// Brief blank to visually signal the switch
|
| 1175 |
+
img.src = '';
|
| 1176 |
+
// Small delay lets the backend fully initialize the new feed
|
| 1177 |
+
setTimeout(() => {
|
| 1178 |
+
img.src = '/video_feed/' + feedId + '?t=' + Date.now();
|
| 1179 |
+
}, 300);
|
| 1180 |
+
}
|
| 1181 |
+
}
|
| 1182 |
+
|
| 1183 |
+
// ─── Grid Layout ───
|
| 1184 |
+
function setGridLayout(layout) {
|
| 1185 |
+
const grid = document.getElementById('camera-grid');
|
| 1186 |
+
currentLayout = layout;
|
| 1187 |
+
|
| 1188 |
+
if (layout === 'single') {
|
| 1189 |
+
grid.classList.add('single-view');
|
| 1190 |
+
// Show only the expanded feed
|
| 1191 |
+
document.querySelectorAll('.feed-cell').forEach((cell, i) => {
|
| 1192 |
+
cell.classList.toggle('expanded', i === expandedFeed);
|
| 1193 |
+
});
|
| 1194 |
+
} else {
|
| 1195 |
+
grid.classList.remove('single-view');
|
| 1196 |
+
document.querySelectorAll('.feed-cell').forEach(cell => {
|
| 1197 |
+
cell.classList.remove('expanded');
|
| 1198 |
+
});
|
| 1199 |
+
}
|
| 1200 |
+
}
|
| 1201 |
+
|
| 1202 |
+
function expandFeed(feedId) {
|
| 1203 |
+
expandedFeed = feedId;
|
| 1204 |
+
if (currentLayout === 'single') {
|
| 1205 |
+
document.querySelectorAll('.feed-cell').forEach((cell, i) => {
|
| 1206 |
+
cell.classList.toggle('expanded', i === feedId);
|
| 1207 |
+
});
|
| 1208 |
+
}
|
| 1209 |
+
}
|
| 1210 |
+
|
| 1211 |
+
// ─── Stats & Red Alert Updates ───
|
| 1212 |
+
function updateStats() {
|
| 1213 |
+
fetch('/stats')
|
| 1214 |
+
.then(r => r.json())
|
| 1215 |
+
.then(data => {
|
| 1216 |
+
const score = data.threat_score;
|
| 1217 |
+
const scoreEl = document.getElementById('threat-score');
|
| 1218 |
+
const statusEl = document.getElementById('status-text');
|
| 1219 |
+
const ringFill = document.getElementById('score-ring-fill');
|
| 1220 |
+
const threatCard = document.getElementById('threat-card');
|
| 1221 |
+
|
| 1222 |
+
scoreEl.textContent = score;
|
| 1223 |
+
|
| 1224 |
+
// Compute ring dash offset (circumference = 2 * π * 60 ≈ 377)
|
| 1225 |
+
const circumference = 377;
|
| 1226 |
+
const offset = circumference - (circumference * score / 100);
|
| 1227 |
+
ringFill.style.strokeDashoffset = offset;
|
| 1228 |
+
|
| 1229 |
+
// Color based on score
|
| 1230 |
+
let color, status, glow;
|
| 1231 |
+
if (score >= 80) {
|
| 1232 |
+
color = '#ff2040';
|
| 1233 |
+
status = 'CRITICAL';
|
| 1234 |
+
glow = 'rgba(255, 32, 64, 0.4)';
|
| 1235 |
+
} else if (score >= 50) {
|
| 1236 |
+
color = '#ffaa00';
|
| 1237 |
+
status = 'ELEVATED';
|
| 1238 |
+
glow = 'rgba(255, 170, 0, 0.3)';
|
| 1239 |
+
} else if (score >= 25) {
|
| 1240 |
+
color = '#00d4ff';
|
| 1241 |
+
status = 'GUARDED';
|
| 1242 |
+
glow = 'rgba(0, 200, 255, 0.3)';
|
| 1243 |
+
} else {
|
| 1244 |
+
color = '#00ff88';
|
| 1245 |
+
status = 'SECURE';
|
| 1246 |
+
glow = 'rgba(0, 255, 136, 0.3)';
|
| 1247 |
+
}
|
| 1248 |
+
|
| 1249 |
+
statusEl.textContent = status;
|
| 1250 |
+
statusEl.style.color = color;
|
| 1251 |
+
statusEl.style.textShadow = `0 0 20px ${glow}`;
|
| 1252 |
+
ringFill.style.stroke = color;
|
| 1253 |
+
ringFill.style.filter = `drop-shadow(0 0 8px ${glow})`;
|
| 1254 |
+
|
| 1255 |
+
// Red Alert state
|
| 1256 |
+
const alertOverlay = document.getElementById('red-alert-overlay');
|
| 1257 |
+
const alertBanner = document.getElementById('red-alert-banner');
|
| 1258 |
+
const feedCells = document.querySelectorAll('.feed-cell');
|
| 1259 |
+
|
| 1260 |
+
if (data.red_alert) {
|
| 1261 |
+
alertOverlay.classList.add('active');
|
| 1262 |
+
alertBanner.classList.add('active');
|
| 1263 |
+
feedCells.forEach(cell => cell.classList.add('red-alert-active'));
|
| 1264 |
+
threatCard.style.borderColor = 'rgba(255, 32, 64, 0.5)';
|
| 1265 |
+
|
| 1266 |
+
if (!isRedAlert) {
|
| 1267 |
+
playAlertTone();
|
| 1268 |
+
isRedAlert = true;
|
| 1269 |
+
}
|
| 1270 |
+
} else {
|
| 1271 |
+
alertOverlay.classList.remove('active');
|
| 1272 |
+
alertBanner.classList.remove('active');
|
| 1273 |
+
feedCells.forEach(cell => cell.classList.remove('red-alert-active'));
|
| 1274 |
+
threatCard.style.borderColor = '';
|
| 1275 |
+
isRedAlert = false;
|
| 1276 |
+
}
|
| 1277 |
+
|
| 1278 |
+
// Update mode display
|
| 1279 |
+
if (data.mode) {
|
| 1280 |
+
document.getElementById('mode-title').textContent = modeTitles[data.mode] || data.mode.toUpperCase();
|
| 1281 |
+
}
|
| 1282 |
+
|
| 1283 |
+
// Update live metrics
|
| 1284 |
+
const container = document.getElementById('stats-container');
|
| 1285 |
+
container.innerHTML = '';
|
| 1286 |
+
|
| 1287 |
+
if (!data.details || Object.keys(data.details).length === 0) {
|
| 1288 |
+
container.innerHTML = '<div class="stat-row"><span class="stat-name">System Status</span><span class="stat-val">Standby</span></div>';
|
| 1289 |
+
} else {
|
| 1290 |
+
for (const [key, value] of Object.entries(data.details)) {
|
| 1291 |
+
const div = document.createElement('div');
|
| 1292 |
+
div.className = 'stat-row';
|
| 1293 |
+
const formattedKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
| 1294 |
+
let displayVal = value;
|
| 1295 |
+
if (typeof value === 'boolean') {
|
| 1296 |
+
displayVal = value ? '⚠ YES' : 'No';
|
| 1297 |
+
}
|
| 1298 |
+
div.innerHTML = `<span class="stat-name">${formattedKey}</span><span class="stat-val">${displayVal}</span>`;
|
| 1299 |
+
container.appendChild(div);
|
| 1300 |
+
}
|
| 1301 |
+
}
|
| 1302 |
+
})
|
| 1303 |
+
.catch(() => { });
|
| 1304 |
+
}
|
| 1305 |
+
|
| 1306 |
+
// ─── Alert Tone (Web Audio API) ───
|
| 1307 |
+
function playAlertTone() {
|
| 1308 |
+
try {
|
| 1309 |
+
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
| 1310 |
+
const oscillator = audioCtx.createOscillator();
|
| 1311 |
+
const gainNode = audioCtx.createGain();
|
| 1312 |
+
|
| 1313 |
+
oscillator.connect(gainNode);
|
| 1314 |
+
gainNode.connect(audioCtx.destination);
|
| 1315 |
+
|
| 1316 |
+
oscillator.type = 'square';
|
| 1317 |
+
oscillator.frequency.setValueAtTime(880, audioCtx.currentTime);
|
| 1318 |
+
oscillator.frequency.setValueAtTime(660, audioCtx.currentTime + 0.15);
|
| 1319 |
+
oscillator.frequency.setValueAtTime(880, audioCtx.currentTime + 0.3);
|
| 1320 |
+
|
| 1321 |
+
gainNode.gain.setValueAtTime(0.08, audioCtx.currentTime);
|
| 1322 |
+
gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.5);
|
| 1323 |
+
|
| 1324 |
+
oscillator.start(audioCtx.currentTime);
|
| 1325 |
+
oscillator.stop(audioCtx.currentTime + 0.5);
|
| 1326 |
+
} catch (e) {
|
| 1327 |
+
// Audio not available — silent fallback
|
| 1328 |
+
}
|
| 1329 |
+
}
|
| 1330 |
+
|
| 1331 |
+
// ─── AI Report ───
|
| 1332 |
+
function generateReport() {
|
| 1333 |
+
const modal = document.getElementById('report-modal');
|
| 1334 |
+
modal.classList.add('show');
|
| 1335 |
+
document.getElementById('report-content').textContent = 'ANALYZING DATA STREAM...';
|
| 1336 |
+
|
| 1337 |
+
fetch('/generate_report', { method: 'POST' })
|
| 1338 |
+
.then(r => r.json())
|
| 1339 |
+
.then(data => {
|
| 1340 |
+
document.getElementById('report-content').textContent = data.report;
|
| 1341 |
+
})
|
| 1342 |
+
.catch(() => {
|
| 1343 |
+
document.getElementById('report-content').textContent = 'ERROR: Connection to AI Agent lost.';
|
| 1344 |
+
});
|
| 1345 |
+
}
|
| 1346 |
+
|
| 1347 |
+
function closeModal() {
|
| 1348 |
+
document.getElementById('report-modal').classList.remove('show');
|
| 1349 |
+
}
|
| 1350 |
+
|
| 1351 |
+
// ─── Audit Log Refresh ───
|
| 1352 |
+
function refreshAuditLog() {
|
| 1353 |
+
fetch('/audit_log')
|
| 1354 |
+
.then(r => r.json())
|
| 1355 |
+
.then(data => {
|
| 1356 |
+
const container = document.getElementById('audit-log-container');
|
| 1357 |
+
container.innerHTML = '';
|
| 1358 |
+
|
| 1359 |
+
if (data.log.length === 0) {
|
| 1360 |
+
container.innerHTML = '<div class="audit-entry"><div class="audit-time">--:--:--</div>NO ENTRIES</div>';
|
| 1361 |
+
return;
|
| 1362 |
+
}
|
| 1363 |
+
|
| 1364 |
+
data.log.slice(0, 30).forEach(entry => {
|
| 1365 |
+
const div = document.createElement('div');
|
| 1366 |
+
div.className = `audit-entry severity-${entry.severity}`;
|
| 1367 |
+
div.innerHTML = `
|
| 1368 |
+
<div class="audit-time">${entry.timestamp.split(' ')[1] || entry.timestamp}</div>
|
| 1369 |
+
${entry.action}: ${entry.details}
|
| 1370 |
+
`;
|
| 1371 |
+
container.appendChild(div);
|
| 1372 |
+
});
|
| 1373 |
+
})
|
| 1374 |
+
.catch(() => { });
|
| 1375 |
+
}
|
| 1376 |
+
|
| 1377 |
+
// ─── Intervals ───
|
| 1378 |
+
setInterval(updateStats, 1000);
|
| 1379 |
+
setInterval(refreshAuditLog, 5000);
|
| 1380 |
+
|
| 1381 |
+
// Initial load
|
| 1382 |
+
setTimeout(refreshAuditLog, 1500);
|
| 1383 |
+
</script>
|
| 1384 |
+
</body>
|
| 1385 |
+
|
| 1386 |
+
</html>
|
templates/sentinel_dashboard_v9.html
ADDED
|
@@ -0,0 +1,1385 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>SENTINEL V9 — Intelligence Grid</title>
|
| 8 |
+
<link
|
| 9 |
+
href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700;800;900&family=Rajdhani:wght@300;400;500;600;700&family=Share+Tech+Mono&display=swap"
|
| 10 |
+
rel="stylesheet">
|
| 11 |
+
<script src="https://unpkg.com/feather-icons"></script>
|
| 12 |
+
<style>
|
| 13 |
+
/* ═══════════════════════════════════════════════
|
| 14 |
+
CSS VARIABLES — SCI-FI PALETTE
|
| 15 |
+
═══════════════════════════════════════════════ */
|
| 16 |
+
:root {
|
| 17 |
+
--bg-deep: #05080f;
|
| 18 |
+
--bg-panel: rgba(8, 16, 32, 0.85);
|
| 19 |
+
--bg-card: rgba(10, 20, 40, 0.7);
|
| 20 |
+
--border-glow: rgba(0, 200, 255, 0.15);
|
| 21 |
+
--border-glow-hover: rgba(0, 200, 255, 0.4);
|
| 22 |
+
--cyan: #00d4ff;
|
| 23 |
+
--cyan-dim: #0090aa;
|
| 24 |
+
--neon-green: #00ff88;
|
| 25 |
+
--neon-red: #ff2040;
|
| 26 |
+
--neon-amber: #ffaa00;
|
| 27 |
+
--text-primary: #e0f0ff;
|
| 28 |
+
--text-secondary: #5a8aaa;
|
| 29 |
+
--text-dim: #2a4a5a;
|
| 30 |
+
--scanline-opacity: 0.03;
|
| 31 |
+
--font-display: 'Orbitron', sans-serif;
|
| 32 |
+
--font-body: 'Rajdhani', sans-serif;
|
| 33 |
+
--font-mono: 'Share Tech Mono', monospace;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/* ═══════════════════════════════════════════════
|
| 37 |
+
BASE RESET & BODY
|
| 38 |
+
═══════════════════════════════════════════════ */
|
| 39 |
+
*,
|
| 40 |
+
*::before,
|
| 41 |
+
*::after {
|
| 42 |
+
margin: 0;
|
| 43 |
+
padding: 0;
|
| 44 |
+
box-sizing: border-box;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
body {
|
| 48 |
+
font-family: var(--font-body);
|
| 49 |
+
background: var(--bg-deep);
|
| 50 |
+
color: var(--text-primary);
|
| 51 |
+
height: 100vh;
|
| 52 |
+
overflow: hidden;
|
| 53 |
+
display: flex;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/* Scanline overlay on body */
|
| 57 |
+
body::after {
|
| 58 |
+
content: '';
|
| 59 |
+
position: fixed;
|
| 60 |
+
top: 0;
|
| 61 |
+
left: 0;
|
| 62 |
+
right: 0;
|
| 63 |
+
bottom: 0;
|
| 64 |
+
background: repeating-linear-gradient(0deg,
|
| 65 |
+
transparent,
|
| 66 |
+
transparent 2px,
|
| 67 |
+
rgba(0, 200, 255, var(--scanline-opacity)) 2px,
|
| 68 |
+
rgba(0, 200, 255, var(--scanline-opacity)) 4px);
|
| 69 |
+
pointer-events: none;
|
| 70 |
+
z-index: 9999;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
/* ═══════════════════════════════════════════════
|
| 74 |
+
RED ALERT OVERLAY
|
| 75 |
+
═══════════════════════════════════════════════ */
|
| 76 |
+
.red-alert-overlay {
|
| 77 |
+
position: fixed;
|
| 78 |
+
top: 0;
|
| 79 |
+
left: 0;
|
| 80 |
+
right: 0;
|
| 81 |
+
bottom: 0;
|
| 82 |
+
pointer-events: none;
|
| 83 |
+
z-index: 9998;
|
| 84 |
+
opacity: 0;
|
| 85 |
+
transition: opacity 0.3s ease;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.red-alert-overlay.active {
|
| 89 |
+
opacity: 1;
|
| 90 |
+
animation: red-pulse-border 1s ease-in-out infinite;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.red-alert-overlay.active::before {
|
| 94 |
+
content: '';
|
| 95 |
+
position: absolute;
|
| 96 |
+
top: 0;
|
| 97 |
+
left: 0;
|
| 98 |
+
right: 0;
|
| 99 |
+
bottom: 0;
|
| 100 |
+
border: 4px solid var(--neon-red);
|
| 101 |
+
box-shadow: inset 0 0 80px rgba(255, 32, 64, 0.15),
|
| 102 |
+
0 0 40px rgba(255, 32, 64, 0.3);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.red-alert-banner {
|
| 106 |
+
position: fixed;
|
| 107 |
+
top: 0;
|
| 108 |
+
left: 50%;
|
| 109 |
+
transform: translateX(-50%);
|
| 110 |
+
background: rgba(255, 20, 40, 0.9);
|
| 111 |
+
color: #fff;
|
| 112 |
+
font-family: var(--font-display);
|
| 113 |
+
font-size: 0.85rem;
|
| 114 |
+
font-weight: 700;
|
| 115 |
+
letter-spacing: 4px;
|
| 116 |
+
padding: 8px 40px;
|
| 117 |
+
z-index: 10000;
|
| 118 |
+
border-bottom-left-radius: 8px;
|
| 119 |
+
border-bottom-right-radius: 8px;
|
| 120 |
+
box-shadow: 0 4px 30px rgba(255, 20, 40, 0.5);
|
| 121 |
+
display: none;
|
| 122 |
+
animation: alert-flash 0.8s ease-in-out infinite;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.red-alert-banner.active {
|
| 126 |
+
display: block;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
@keyframes red-pulse-border {
|
| 130 |
+
|
| 131 |
+
0%,
|
| 132 |
+
100% {
|
| 133 |
+
opacity: 0.6;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
50% {
|
| 137 |
+
opacity: 1;
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
@keyframes alert-flash {
|
| 142 |
+
|
| 143 |
+
0%,
|
| 144 |
+
100% {
|
| 145 |
+
opacity: 1;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
50% {
|
| 149 |
+
opacity: 0.6;
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
/* ═══════════════════════════════════════════════
|
| 154 |
+
SIDEBAR
|
| 155 |
+
═══════════════════════════════════════════════ */
|
| 156 |
+
.sidebar {
|
| 157 |
+
width: 260px;
|
| 158 |
+
min-width: 260px;
|
| 159 |
+
background: var(--bg-panel);
|
| 160 |
+
backdrop-filter: blur(20px);
|
| 161 |
+
-webkit-backdrop-filter: blur(20px);
|
| 162 |
+
border-right: 1px solid var(--border-glow);
|
| 163 |
+
display: flex;
|
| 164 |
+
flex-direction: column;
|
| 165 |
+
padding: 20px;
|
| 166 |
+
z-index: 100;
|
| 167 |
+
overflow-y: auto;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.logo {
|
| 171 |
+
font-family: var(--font-display);
|
| 172 |
+
font-size: 1rem;
|
| 173 |
+
font-weight: 700;
|
| 174 |
+
margin-bottom: 8px;
|
| 175 |
+
display: flex;
|
| 176 |
+
align-items: center;
|
| 177 |
+
gap: 10px;
|
| 178 |
+
color: var(--cyan);
|
| 179 |
+
letter-spacing: 2px;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.logo svg {
|
| 183 |
+
filter: drop-shadow(0 0 6px var(--cyan));
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
.version-tag {
|
| 187 |
+
font-family: var(--font-mono);
|
| 188 |
+
font-size: 0.65rem;
|
| 189 |
+
color: var(--text-dim);
|
| 190 |
+
margin-bottom: 24px;
|
| 191 |
+
letter-spacing: 1px;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.nav-group {
|
| 195 |
+
margin-bottom: 20px;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.nav-label {
|
| 199 |
+
font-family: var(--font-display);
|
| 200 |
+
font-size: 0.6rem;
|
| 201 |
+
font-weight: 600;
|
| 202 |
+
color: var(--text-dim);
|
| 203 |
+
margin-bottom: 8px;
|
| 204 |
+
text-transform: uppercase;
|
| 205 |
+
letter-spacing: 2px;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
.nav-item {
|
| 209 |
+
padding: 10px 12px;
|
| 210 |
+
margin-bottom: 3px;
|
| 211 |
+
border-radius: 6px;
|
| 212 |
+
cursor: pointer;
|
| 213 |
+
transition: all 0.25s ease;
|
| 214 |
+
display: flex;
|
| 215 |
+
align-items: center;
|
| 216 |
+
gap: 10px;
|
| 217 |
+
color: var(--text-secondary);
|
| 218 |
+
font-size: 0.9rem;
|
| 219 |
+
font-weight: 500;
|
| 220 |
+
border: 1px solid transparent;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.nav-item:hover {
|
| 224 |
+
background: rgba(0, 200, 255, 0.05);
|
| 225 |
+
color: var(--text-primary);
|
| 226 |
+
border-color: var(--border-glow);
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.nav-item.active {
|
| 230 |
+
background: rgba(0, 200, 255, 0.1);
|
| 231 |
+
color: var(--cyan);
|
| 232 |
+
border-color: rgba(0, 200, 255, 0.3);
|
| 233 |
+
box-shadow: 0 0 15px rgba(0, 200, 255, 0.05);
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
.nav-item svg {
|
| 237 |
+
width: 16px;
|
| 238 |
+
height: 16px;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
/* Audit Log Panel in sidebar */
|
| 242 |
+
.audit-section {
|
| 243 |
+
margin-top: auto;
|
| 244 |
+
flex-shrink: 0;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
.audit-log-container {
|
| 248 |
+
max-height: 200px;
|
| 249 |
+
overflow-y: auto;
|
| 250 |
+
scrollbar-width: thin;
|
| 251 |
+
scrollbar-color: var(--cyan-dim) transparent;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
.audit-log-container::-webkit-scrollbar {
|
| 255 |
+
width: 4px;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.audit-log-container::-webkit-scrollbar-track {
|
| 259 |
+
background: transparent;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.audit-log-container::-webkit-scrollbar-thumb {
|
| 263 |
+
background: var(--cyan-dim);
|
| 264 |
+
border-radius: 2px;
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
.audit-entry {
|
| 268 |
+
font-family: var(--font-mono);
|
| 269 |
+
font-size: 0.65rem;
|
| 270 |
+
padding: 6px 8px;
|
| 271 |
+
margin-bottom: 2px;
|
| 272 |
+
border-radius: 4px;
|
| 273 |
+
background: rgba(0, 0, 0, 0.3);
|
| 274 |
+
border-left: 2px solid var(--cyan-dim);
|
| 275 |
+
line-height: 1.4;
|
| 276 |
+
color: var(--text-secondary);
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
.audit-entry.severity-WARNING {
|
| 280 |
+
border-left-color: var(--neon-amber);
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
.audit-entry.severity-CRITICAL {
|
| 284 |
+
border-left-color: var(--neon-red);
|
| 285 |
+
color: var(--neon-red);
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
.audit-time {
|
| 289 |
+
color: var(--text-dim);
|
| 290 |
+
font-size: 0.6rem;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
/* ═══════════════════════════════════════════════
|
| 294 |
+
MAIN CONTENT AREA
|
| 295 |
+
═══════════════════════════════════════════════ */
|
| 296 |
+
.main-content {
|
| 297 |
+
flex: 1;
|
| 298 |
+
padding: 16px;
|
| 299 |
+
display: grid;
|
| 300 |
+
grid-template-columns: 1fr 320px;
|
| 301 |
+
gap: 16px;
|
| 302 |
+
overflow: hidden;
|
| 303 |
+
background: radial-gradient(ellipse at 20% 0%, rgba(0, 60, 100, 0.15) 0%, transparent 60%),
|
| 304 |
+
radial-gradient(ellipse at 80% 100%, rgba(0, 40, 80, 0.1) 0%, transparent 60%),
|
| 305 |
+
var(--bg-deep);
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
/* ═══════════════════════════════════════════════
|
| 309 |
+
MULTI-CAMERA GRID
|
| 310 |
+
═══════════════════════════════════════════════ */
|
| 311 |
+
.camera-grid {
|
| 312 |
+
display: grid;
|
| 313 |
+
grid-template-columns: 1fr 1fr;
|
| 314 |
+
grid-template-rows: 1fr 1fr;
|
| 315 |
+
gap: 8px;
|
| 316 |
+
height: 100%;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
.camera-grid.single-view {
|
| 320 |
+
grid-template-columns: 1fr;
|
| 321 |
+
grid-template-rows: 1fr;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.camera-grid.single-view .feed-cell:not(.expanded) {
|
| 325 |
+
display: none;
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
.feed-cell {
|
| 329 |
+
background: var(--bg-card);
|
| 330 |
+
border-radius: 10px;
|
| 331 |
+
border: 1px solid var(--border-glow);
|
| 332 |
+
overflow: hidden;
|
| 333 |
+
position: relative;
|
| 334 |
+
cursor: pointer;
|
| 335 |
+
transition: all 0.3s ease;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.feed-cell:hover {
|
| 339 |
+
border-color: var(--border-glow-hover);
|
| 340 |
+
box-shadow: 0 0 20px rgba(0, 200, 255, 0.1);
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
.feed-cell.red-alert-active {
|
| 344 |
+
border-color: var(--neon-red) !important;
|
| 345 |
+
box-shadow: 0 0 30px rgba(255, 32, 64, 0.3) !important;
|
| 346 |
+
animation: cell-red-pulse 1.5s ease-in-out infinite;
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
@keyframes cell-red-pulse {
|
| 350 |
+
|
| 351 |
+
0%,
|
| 352 |
+
100% {
|
| 353 |
+
box-shadow: 0 0 20px rgba(255, 32, 64, 0.2);
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
50% {
|
| 357 |
+
box-shadow: 0 0 40px rgba(255, 32, 64, 0.5);
|
| 358 |
+
}
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
.feed-header {
|
| 362 |
+
position: absolute;
|
| 363 |
+
top: 8px;
|
| 364 |
+
left: 10px;
|
| 365 |
+
right: 10px;
|
| 366 |
+
display: flex;
|
| 367 |
+
justify-content: space-between;
|
| 368 |
+
align-items: center;
|
| 369 |
+
z-index: 5;
|
| 370 |
+
pointer-events: none;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
.feed-badge {
|
| 374 |
+
background: rgba(0, 0, 0, 0.7);
|
| 375 |
+
backdrop-filter: blur(8px);
|
| 376 |
+
padding: 4px 10px;
|
| 377 |
+
border-radius: 4px;
|
| 378 |
+
font-family: var(--font-mono);
|
| 379 |
+
font-size: 0.65rem;
|
| 380 |
+
font-weight: 400;
|
| 381 |
+
border: 1px solid var(--border-glow);
|
| 382 |
+
display: flex;
|
| 383 |
+
align-items: center;
|
| 384 |
+
gap: 6px;
|
| 385 |
+
color: var(--cyan);
|
| 386 |
+
letter-spacing: 1px;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
.live-dot {
|
| 390 |
+
width: 6px;
|
| 391 |
+
height: 6px;
|
| 392 |
+
background: var(--neon-red);
|
| 393 |
+
border-radius: 50%;
|
| 394 |
+
box-shadow: 0 0 8px var(--neon-red);
|
| 395 |
+
animation: pulse-dot 2s infinite;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
@keyframes pulse-dot {
|
| 399 |
+
|
| 400 |
+
0%,
|
| 401 |
+
100% {
|
| 402 |
+
opacity: 1;
|
| 403 |
+
transform: scale(1);
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
50% {
|
| 407 |
+
opacity: 0.4;
|
| 408 |
+
transform: scale(1.3);
|
| 409 |
+
}
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
.feed-status {
|
| 413 |
+
font-family: var(--font-mono);
|
| 414 |
+
font-size: 0.6rem;
|
| 415 |
+
color: var(--neon-green);
|
| 416 |
+
background: rgba(0, 0, 0, 0.6);
|
| 417 |
+
padding: 3px 8px;
|
| 418 |
+
border-radius: 4px;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
.feed-stream {
|
| 422 |
+
width: 100%;
|
| 423 |
+
height: 100%;
|
| 424 |
+
object-fit: contain;
|
| 425 |
+
background: #000;
|
| 426 |
+
display: block;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.feed-offline {
|
| 430 |
+
display: flex;
|
| 431 |
+
flex-direction: column;
|
| 432 |
+
align-items: center;
|
| 433 |
+
justify-content: center;
|
| 434 |
+
height: 100%;
|
| 435 |
+
color: var(--text-dim);
|
| 436 |
+
font-family: var(--font-mono);
|
| 437 |
+
font-size: 0.75rem;
|
| 438 |
+
letter-spacing: 1px;
|
| 439 |
+
gap: 8px;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
.feed-offline svg {
|
| 443 |
+
width: 24px;
|
| 444 |
+
height: 24px;
|
| 445 |
+
color: var(--text-dim);
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
/* Fullscreen expand button */
|
| 449 |
+
.expand-btn {
|
| 450 |
+
position: absolute;
|
| 451 |
+
bottom: 8px;
|
| 452 |
+
right: 8px;
|
| 453 |
+
background: rgba(0, 0, 0, 0.6);
|
| 454 |
+
border: 1px solid var(--border-glow);
|
| 455 |
+
color: var(--cyan);
|
| 456 |
+
width: 28px;
|
| 457 |
+
height: 28px;
|
| 458 |
+
border-radius: 4px;
|
| 459 |
+
cursor: pointer;
|
| 460 |
+
display: flex;
|
| 461 |
+
align-items: center;
|
| 462 |
+
justify-content: center;
|
| 463 |
+
transition: all 0.2s;
|
| 464 |
+
z-index: 5;
|
| 465 |
+
pointer-events: all;
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
.expand-btn:hover {
|
| 469 |
+
background: rgba(0, 200, 255, 0.15);
|
| 470 |
+
border-color: var(--cyan);
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
.expand-btn svg {
|
| 474 |
+
width: 14px;
|
| 475 |
+
height: 14px;
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
/* ═══════════════════════════════════════════════
|
| 479 |
+
INTEL PANEL (Right Column)
|
| 480 |
+
═══════════════════════════════════════════════ */
|
| 481 |
+
.intel-panel {
|
| 482 |
+
display: flex;
|
| 483 |
+
flex-direction: column;
|
| 484 |
+
gap: 12px;
|
| 485 |
+
overflow-y: auto;
|
| 486 |
+
scrollbar-width: thin;
|
| 487 |
+
scrollbar-color: var(--cyan-dim) transparent;
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
.intel-panel::-webkit-scrollbar {
|
| 491 |
+
width: 4px;
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
.intel-panel::-webkit-scrollbar-thumb {
|
| 495 |
+
background: var(--cyan-dim);
|
| 496 |
+
border-radius: 2px;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
.card {
|
| 500 |
+
background: var(--bg-card);
|
| 501 |
+
backdrop-filter: blur(15px);
|
| 502 |
+
border-radius: 10px;
|
| 503 |
+
padding: 18px;
|
| 504 |
+
border: 1px solid var(--border-glow);
|
| 505 |
+
transition: border-color 0.3s;
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
.card:hover {
|
| 509 |
+
border-color: var(--border-glow-hover);
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
.card-header {
|
| 513 |
+
display: flex;
|
| 514 |
+
justify-content: space-between;
|
| 515 |
+
align-items: center;
|
| 516 |
+
margin-bottom: 14px;
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
.card-title {
|
| 520 |
+
font-family: var(--font-display);
|
| 521 |
+
font-size: 0.65rem;
|
| 522 |
+
font-weight: 600;
|
| 523 |
+
color: var(--cyan);
|
| 524 |
+
text-transform: uppercase;
|
| 525 |
+
letter-spacing: 2px;
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
.card-icon {
|
| 529 |
+
color: var(--text-dim);
|
| 530 |
+
width: 16px;
|
| 531 |
+
height: 16px;
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
/* Threat gauge */
|
| 535 |
+
.threat-gauge {
|
| 536 |
+
display: flex;
|
| 537 |
+
flex-direction: column;
|
| 538 |
+
align-items: center;
|
| 539 |
+
padding: 10px 0;
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
.score-ring {
|
| 543 |
+
position: relative;
|
| 544 |
+
width: 130px;
|
| 545 |
+
height: 130px;
|
| 546 |
+
margin-bottom: 12px;
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
.score-ring svg {
|
| 550 |
+
width: 130px;
|
| 551 |
+
height: 130px;
|
| 552 |
+
transform: rotate(-90deg);
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
.score-ring-bg {
|
| 556 |
+
fill: none;
|
| 557 |
+
stroke: rgba(255, 255, 255, 0.05);
|
| 558 |
+
stroke-width: 6;
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
.score-ring-fill {
|
| 562 |
+
fill: none;
|
| 563 |
+
stroke: var(--neon-green);
|
| 564 |
+
stroke-width: 6;
|
| 565 |
+
stroke-linecap: round;
|
| 566 |
+
stroke-dasharray: 377;
|
| 567 |
+
stroke-dashoffset: 377;
|
| 568 |
+
transition: stroke-dashoffset 0.8s cubic-bezier(0.4, 0, 0.2, 1),
|
| 569 |
+
stroke 0.5s ease;
|
| 570 |
+
filter: drop-shadow(0 0 8px var(--neon-green));
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
.score-text {
|
| 574 |
+
position: absolute;
|
| 575 |
+
top: 50%;
|
| 576 |
+
left: 50%;
|
| 577 |
+
transform: translate(-50%, -50%);
|
| 578 |
+
text-align: center;
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
.score-value {
|
| 582 |
+
font-family: var(--font-display);
|
| 583 |
+
font-size: 2.2rem;
|
| 584 |
+
font-weight: 800;
|
| 585 |
+
line-height: 1;
|
| 586 |
+
color: var(--text-primary);
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
.score-label {
|
| 590 |
+
font-family: var(--font-mono);
|
| 591 |
+
font-size: 0.55rem;
|
| 592 |
+
color: var(--text-dim);
|
| 593 |
+
letter-spacing: 2px;
|
| 594 |
+
margin-top: 4px;
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
.status-text {
|
| 598 |
+
font-family: var(--font-display);
|
| 599 |
+
font-size: 0.85rem;
|
| 600 |
+
font-weight: 700;
|
| 601 |
+
color: var(--neon-green);
|
| 602 |
+
letter-spacing: 3px;
|
| 603 |
+
text-shadow: 0 0 20px rgba(0, 255, 136, 0.3);
|
| 604 |
+
transition: all 0.5s ease;
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
/* Stats */
|
| 608 |
+
.stats-list {
|
| 609 |
+
display: flex;
|
| 610 |
+
flex-direction: column;
|
| 611 |
+
gap: 6px;
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
.stat-row {
|
| 615 |
+
display: flex;
|
| 616 |
+
justify-content: space-between;
|
| 617 |
+
align-items: center;
|
| 618 |
+
padding: 10px 12px;
|
| 619 |
+
background: rgba(0, 200, 255, 0.03);
|
| 620 |
+
border-radius: 6px;
|
| 621 |
+
border: 1px solid rgba(0, 200, 255, 0.05);
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
.stat-name {
|
| 625 |
+
font-size: 0.8rem;
|
| 626 |
+
color: var(--text-secondary);
|
| 627 |
+
font-weight: 500;
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
.stat-val {
|
| 631 |
+
font-family: var(--font-mono);
|
| 632 |
+
font-size: 0.85rem;
|
| 633 |
+
font-weight: 600;
|
| 634 |
+
color: var(--text-primary);
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
/* Mode indicator */
|
| 638 |
+
.mode-indicator {
|
| 639 |
+
display: flex;
|
| 640 |
+
align-items: center;
|
| 641 |
+
gap: 8px;
|
| 642 |
+
padding: 10px 14px;
|
| 643 |
+
background: rgba(0, 200, 255, 0.05);
|
| 644 |
+
border-radius: 6px;
|
| 645 |
+
border: 1px solid var(--border-glow);
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
.mode-dot {
|
| 649 |
+
width: 8px;
|
| 650 |
+
height: 8px;
|
| 651 |
+
background: var(--cyan);
|
| 652 |
+
border-radius: 50%;
|
| 653 |
+
box-shadow: 0 0 10px var(--cyan);
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
.mode-name {
|
| 657 |
+
font-family: var(--font-display);
|
| 658 |
+
font-size: 0.7rem;
|
| 659 |
+
font-weight: 600;
|
| 660 |
+
letter-spacing: 2px;
|
| 661 |
+
color: var(--cyan);
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
/* Buttons */
|
| 665 |
+
.btn {
|
| 666 |
+
width: 100%;
|
| 667 |
+
padding: 12px;
|
| 668 |
+
border: 1px solid var(--border-glow);
|
| 669 |
+
border-radius: 6px;
|
| 670 |
+
font-size: 0.8rem;
|
| 671 |
+
font-weight: 600;
|
| 672 |
+
cursor: pointer;
|
| 673 |
+
transition: all 0.25s;
|
| 674 |
+
font-family: var(--font-display);
|
| 675 |
+
letter-spacing: 1px;
|
| 676 |
+
text-transform: uppercase;
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
.btn-primary {
|
| 680 |
+
background: linear-gradient(135deg, rgba(0, 200, 255, 0.2), rgba(0, 100, 200, 0.1));
|
| 681 |
+
color: var(--cyan);
|
| 682 |
+
border-color: rgba(0, 200, 255, 0.3);
|
| 683 |
+
}
|
| 684 |
+
|
| 685 |
+
.btn-primary:hover {
|
| 686 |
+
background: linear-gradient(135deg, rgba(0, 200, 255, 0.3), rgba(0, 100, 200, 0.2));
|
| 687 |
+
box-shadow: 0 0 20px rgba(0, 200, 255, 0.15);
|
| 688 |
+
transform: translateY(-1px);
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
+
.btn-secondary {
|
| 692 |
+
background: rgba(255, 255, 255, 0.03);
|
| 693 |
+
color: var(--text-secondary);
|
| 694 |
+
}
|
| 695 |
+
|
| 696 |
+
.btn-secondary:hover {
|
| 697 |
+
background: rgba(255, 255, 255, 0.08);
|
| 698 |
+
color: var(--text-primary);
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
/* ═══════════════════════════════════════════════
|
| 702 |
+
MODAL
|
| 703 |
+
═══════════════════════════════════════════════ */
|
| 704 |
+
.modal-overlay {
|
| 705 |
+
position: fixed;
|
| 706 |
+
top: 0;
|
| 707 |
+
left: 0;
|
| 708 |
+
right: 0;
|
| 709 |
+
bottom: 0;
|
| 710 |
+
background: rgba(0, 0, 0, 0.8);
|
| 711 |
+
backdrop-filter: blur(10px);
|
| 712 |
+
display: none;
|
| 713 |
+
justify-content: center;
|
| 714 |
+
align-items: center;
|
| 715 |
+
z-index: 11000;
|
| 716 |
+
opacity: 0;
|
| 717 |
+
transition: opacity 0.3s ease;
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
.modal-overlay.show {
|
| 721 |
+
display: flex;
|
| 722 |
+
opacity: 1;
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
.modal-card {
|
| 726 |
+
background: var(--bg-panel);
|
| 727 |
+
width: 520px;
|
| 728 |
+
max-width: 90vw;
|
| 729 |
+
border-radius: 12px;
|
| 730 |
+
padding: 28px;
|
| 731 |
+
border: 1px solid var(--border-glow);
|
| 732 |
+
box-shadow: 0 0 60px rgba(0, 200, 255, 0.1);
|
| 733 |
+
transform: scale(0.95);
|
| 734 |
+
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
+
.modal-overlay.show .modal-card {
|
| 738 |
+
transform: scale(1);
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
.modal-title {
|
| 742 |
+
font-family: var(--font-display);
|
| 743 |
+
font-size: 1rem;
|
| 744 |
+
font-weight: 700;
|
| 745 |
+
color: var(--cyan);
|
| 746 |
+
letter-spacing: 2px;
|
| 747 |
+
margin-bottom: 6px;
|
| 748 |
+
}
|
| 749 |
+
|
| 750 |
+
.modal-subtitle {
|
| 751 |
+
font-family: var(--font-mono);
|
| 752 |
+
font-size: 0.7rem;
|
| 753 |
+
color: var(--text-dim);
|
| 754 |
+
margin-bottom: 16px;
|
| 755 |
+
}
|
| 756 |
+
|
| 757 |
+
.report-content {
|
| 758 |
+
background: rgba(0, 0, 0, 0.5);
|
| 759 |
+
padding: 16px;
|
| 760 |
+
border-radius: 8px;
|
| 761 |
+
font-family: var(--font-mono);
|
| 762 |
+
font-size: 0.75rem;
|
| 763 |
+
color: var(--neon-green);
|
| 764 |
+
margin-bottom: 16px;
|
| 765 |
+
line-height: 1.6;
|
| 766 |
+
border: 1px solid var(--border-glow);
|
| 767 |
+
max-height: 300px;
|
| 768 |
+
overflow-y: auto;
|
| 769 |
+
white-space: pre-wrap;
|
| 770 |
+
}
|
| 771 |
+
|
| 772 |
+
/* ═══════════════════════════════════════════════
|
| 773 |
+
MOBILE RESPONSIVE
|
| 774 |
+
═══════════════════════════════════════════════ */
|
| 775 |
+
@media (max-width: 1024px) {
|
| 776 |
+
.main-content {
|
| 777 |
+
grid-template-columns: 1fr 280px;
|
| 778 |
+
}
|
| 779 |
+
}
|
| 780 |
+
|
| 781 |
+
@media (max-width: 768px) {
|
| 782 |
+
body {
|
| 783 |
+
flex-direction: column;
|
| 784 |
+
overflow-y: auto;
|
| 785 |
+
}
|
| 786 |
+
|
| 787 |
+
.sidebar {
|
| 788 |
+
width: 100%;
|
| 789 |
+
min-width: 100%;
|
| 790 |
+
flex-direction: row;
|
| 791 |
+
flex-wrap: wrap;
|
| 792 |
+
padding: 10px 16px;
|
| 793 |
+
gap: 6px;
|
| 794 |
+
order: 2;
|
| 795 |
+
border-right: none;
|
| 796 |
+
border-top: 1px solid var(--border-glow);
|
| 797 |
+
overflow-y: visible;
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
.logo {
|
| 801 |
+
width: 100%;
|
| 802 |
+
margin-bottom: 6px;
|
| 803 |
+
font-size: 0.85rem;
|
| 804 |
+
}
|
| 805 |
+
|
| 806 |
+
.version-tag {
|
| 807 |
+
display: none;
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
+
.nav-group {
|
| 811 |
+
display: flex;
|
| 812 |
+
flex-wrap: wrap;
|
| 813 |
+
gap: 4px;
|
| 814 |
+
margin-bottom: 0;
|
| 815 |
+
}
|
| 816 |
+
|
| 817 |
+
.nav-label {
|
| 818 |
+
width: 100%;
|
| 819 |
+
margin-bottom: 4px;
|
| 820 |
+
}
|
| 821 |
+
|
| 822 |
+
.nav-item {
|
| 823 |
+
flex: none;
|
| 824 |
+
padding: 8px 10px;
|
| 825 |
+
font-size: 0.75rem;
|
| 826 |
+
}
|
| 827 |
+
|
| 828 |
+
.audit-section {
|
| 829 |
+
display: none;
|
| 830 |
+
}
|
| 831 |
+
|
| 832 |
+
.main-content {
|
| 833 |
+
grid-template-columns: 1fr;
|
| 834 |
+
grid-template-rows: auto auto;
|
| 835 |
+
order: 1;
|
| 836 |
+
overflow-y: auto;
|
| 837 |
+
padding: 10px;
|
| 838 |
+
}
|
| 839 |
+
|
| 840 |
+
.camera-grid {
|
| 841 |
+
grid-template-columns: 1fr;
|
| 842 |
+
grid-template-rows: auto;
|
| 843 |
+
min-height: 250px;
|
| 844 |
+
}
|
| 845 |
+
|
| 846 |
+
.feed-cell:not(:first-child) {
|
| 847 |
+
display: none;
|
| 848 |
+
}
|
| 849 |
+
|
| 850 |
+
.intel-panel {
|
| 851 |
+
overflow-y: visible;
|
| 852 |
+
}
|
| 853 |
+
}
|
| 854 |
+
|
| 855 |
+
@media (max-width: 480px) {
|
| 856 |
+
.sidebar .nav-item {
|
| 857 |
+
font-size: 0.7rem;
|
| 858 |
+
padding: 6px 8px;
|
| 859 |
+
gap: 6px;
|
| 860 |
+
}
|
| 861 |
+
|
| 862 |
+
.sidebar .nav-item svg {
|
| 863 |
+
width: 14px;
|
| 864 |
+
height: 14px;
|
| 865 |
+
}
|
| 866 |
+
|
| 867 |
+
.score-ring {
|
| 868 |
+
width: 100px;
|
| 869 |
+
height: 100px;
|
| 870 |
+
}
|
| 871 |
+
|
| 872 |
+
.score-ring svg {
|
| 873 |
+
width: 100px;
|
| 874 |
+
height: 100px;
|
| 875 |
+
}
|
| 876 |
+
|
| 877 |
+
.score-value {
|
| 878 |
+
font-size: 1.6rem;
|
| 879 |
+
}
|
| 880 |
+
}
|
| 881 |
+
</style>
|
| 882 |
+
</head>
|
| 883 |
+
|
| 884 |
+
<body>
|
| 885 |
+
<!-- Red Alert Overlay -->
|
| 886 |
+
<div class="red-alert-overlay" id="red-alert-overlay"></div>
|
| 887 |
+
<div class="red-alert-banner" id="red-alert-banner">⚠ RED ALERT — CRITICAL THREAT DETECTED ⚠</div>
|
| 888 |
+
|
| 889 |
+
<!-- SIDEBAR -->
|
| 890 |
+
<div class="sidebar">
|
| 891 |
+
<div class="logo">
|
| 892 |
+
<i data-feather="shield"></i>
|
| 893 |
+
SENTINEL
|
| 894 |
+
</div>
|
| 895 |
+
<div class="version-tag">V9.0 // IMPROVED UI</div>
|
| 896 |
+
|
| 897 |
+
<div class="nav-group">
|
| 898 |
+
<div class="nav-label">Detection Modules</div>
|
| 899 |
+
<div class="nav-item" onclick="setMode('standby', this)" id="nav-standby">
|
| 900 |
+
<i data-feather="pause-circle"></i> Standby
|
| 901 |
+
</div>
|
| 902 |
+
<div class="nav-item active" onclick="setMode('movement', this)" id="nav-movement">
|
| 903 |
+
<i data-feather="activity"></i> Movement
|
| 904 |
+
</div>
|
| 905 |
+
<div class="nav-item" onclick="setMode('facemask', this)" id="nav-facemask">
|
| 906 |
+
<i data-feather="eye"></i> Facemask
|
| 907 |
+
</div>
|
| 908 |
+
<div class="nav-item" onclick="setMode('weapon', this)" id="nav-weapon">
|
| 909 |
+
<i data-feather="crosshair"></i> Weapon
|
| 910 |
+
</div>
|
| 911 |
+
<div class="nav-item" onclick="setMode('public_safety', this)" id="nav-public_safety">
|
| 912 |
+
<i data-feather="users"></i> Public Safety
|
| 913 |
+
</div>
|
| 914 |
+
</div>
|
| 915 |
+
|
| 916 |
+
<div class="nav-group">
|
| 917 |
+
<div class="nav-label">Input Source</div>
|
| 918 |
+
<div class="nav-item" onclick="setSource('camera')">
|
| 919 |
+
<i data-feather="video"></i> Live Camera
|
| 920 |
+
</div>
|
| 921 |
+
<div class="nav-item" onclick="document.getElementById('upload-input').click()">
|
| 922 |
+
<i data-feather="upload-cloud"></i> Upload Video
|
| 923 |
+
</div>
|
| 924 |
+
<input type="file" id="upload-input" style="display: none" accept="video/*"
|
| 925 |
+
onchange="handleFileUpload(this)">
|
| 926 |
+
</div>
|
| 927 |
+
|
| 928 |
+
<div class="nav-group">
|
| 929 |
+
<div class="nav-label">Grid Layout</div>
|
| 930 |
+
<div class="nav-item" onclick="setGridLayout('quad')">
|
| 931 |
+
<i data-feather="grid"></i> 2×2 Grid
|
| 932 |
+
</div>
|
| 933 |
+
<div class="nav-item" onclick="setGridLayout('single')">
|
| 934 |
+
<i data-feather="maximize-2"></i> Single View
|
| 935 |
+
</div>
|
| 936 |
+
</div>
|
| 937 |
+
|
| 938 |
+
<!-- Audit Log -->
|
| 939 |
+
<div class="audit-section">
|
| 940 |
+
<div class="nav-label" style="margin-bottom: 8px;">Operator Log</div>
|
| 941 |
+
<div class="audit-log-container" id="audit-log-container">
|
| 942 |
+
<div class="audit-entry">
|
| 943 |
+
<div class="audit-time">--:--:--</div>
|
| 944 |
+
SYSTEM STANDBY
|
| 945 |
+
</div>
|
| 946 |
+
</div>
|
| 947 |
+
</div>
|
| 948 |
+
</div>
|
| 949 |
+
|
| 950 |
+
<!-- MAIN CONTENT -->
|
| 951 |
+
<div class="main-content">
|
| 952 |
+
<!-- Multi-Camera Grid -->
|
| 953 |
+
<div class="camera-grid" id="camera-grid">
|
| 954 |
+
<!-- Feed 0 — Primary -->
|
| 955 |
+
<div class="feed-cell expanded" id="feed-0" onclick="expandFeed(0)">
|
| 956 |
+
<div class="feed-header">
|
| 957 |
+
<div class="feed-badge">
|
| 958 |
+
<div class="live-dot"></div>
|
| 959 |
+
<span>FEED 01 // PRIMARY</span>
|
| 960 |
+
</div>
|
| 961 |
+
<div class="feed-status" id="feed-0-status">ACTIVE</div>
|
| 962 |
+
</div>
|
| 963 |
+
<img class="feed-stream" id="stream-0" src="/video_feed/0" alt="Feed 0">
|
| 964 |
+
<button class="expand-btn" onclick="event.stopPropagation(); expandFeed(0)" title="Expand">
|
| 965 |
+
<i data-feather="maximize-2"></i>
|
| 966 |
+
</button>
|
| 967 |
+
</div>
|
| 968 |
+
|
| 969 |
+
<!-- Feed 1 -->
|
| 970 |
+
<div class="feed-cell" id="feed-1">
|
| 971 |
+
<div class="feed-header">
|
| 972 |
+
<div class="feed-badge">
|
| 973 |
+
<div class="live-dot" style="background: var(--text-dim); box-shadow: none;"></div>
|
| 974 |
+
<span>FEED 02</span>
|
| 975 |
+
</div>
|
| 976 |
+
<div class="feed-status" id="feed-1-status" style="color: var(--text-dim);">STANDBY</div>
|
| 977 |
+
</div>
|
| 978 |
+
<div class="feed-offline" id="offline-1">
|
| 979 |
+
<i data-feather="video-off"></i>
|
| 980 |
+
NO SIGNAL
|
| 981 |
+
</div>
|
| 982 |
+
</div>
|
| 983 |
+
|
| 984 |
+
<!-- Feed 2 -->
|
| 985 |
+
<div class="feed-cell" id="feed-2">
|
| 986 |
+
<div class="feed-header">
|
| 987 |
+
<div class="feed-badge">
|
| 988 |
+
<div class="live-dot" style="background: var(--text-dim); box-shadow: none;"></div>
|
| 989 |
+
<span>FEED 03</span>
|
| 990 |
+
</div>
|
| 991 |
+
<div class="feed-status" id="feed-2-status" style="color: var(--text-dim);">STANDBY</div>
|
| 992 |
+
</div>
|
| 993 |
+
<div class="feed-offline" id="offline-2">
|
| 994 |
+
<i data-feather="video-off"></i>
|
| 995 |
+
NO SIGNAL
|
| 996 |
+
</div>
|
| 997 |
+
</div>
|
| 998 |
+
|
| 999 |
+
<!-- Feed 3 -->
|
| 1000 |
+
<div class="feed-cell" id="feed-3">
|
| 1001 |
+
<div class="feed-header">
|
| 1002 |
+
<div class="feed-badge">
|
| 1003 |
+
<div class="live-dot" style="background: var(--text-dim); box-shadow: none;"></div>
|
| 1004 |
+
<span>FEED 04</span>
|
| 1005 |
+
</div>
|
| 1006 |
+
<div class="feed-status" id="feed-3-status" style="color: var(--text-dim);">STANDBY</div>
|
| 1007 |
+
</div>
|
| 1008 |
+
<div class="feed-offline" id="offline-3">
|
| 1009 |
+
<i data-feather="video-off"></i>
|
| 1010 |
+
NO SIGNAL
|
| 1011 |
+
</div>
|
| 1012 |
+
</div>
|
| 1013 |
+
</div>
|
| 1014 |
+
|
| 1015 |
+
<!-- Intel Panel -->
|
| 1016 |
+
<div class="intel-panel">
|
| 1017 |
+
<!-- Active Mode -->
|
| 1018 |
+
<div class="card">
|
| 1019 |
+
<div class="card-header">
|
| 1020 |
+
<div class="card-title">Active Mode</div>
|
| 1021 |
+
</div>
|
| 1022 |
+
<div class="mode-indicator">
|
| 1023 |
+
<div class="mode-dot"></div>
|
| 1024 |
+
<div class="mode-name" id="mode-title">MOVEMENT ANALYSIS</div>
|
| 1025 |
+
</div>
|
| 1026 |
+
</div>
|
| 1027 |
+
|
| 1028 |
+
<!-- Threat Assessment -->
|
| 1029 |
+
<div class="card" id="threat-card">
|
| 1030 |
+
<div class="card-header">
|
| 1031 |
+
<div class="card-title">Threat Assessment</div>
|
| 1032 |
+
<i data-feather="alert-triangle" class="card-icon"></i>
|
| 1033 |
+
</div>
|
| 1034 |
+
<div class="threat-gauge">
|
| 1035 |
+
<div class="score-ring">
|
| 1036 |
+
<svg viewBox="0 0 130 130">
|
| 1037 |
+
<circle class="score-ring-bg" cx="65" cy="65" r="60"></circle>
|
| 1038 |
+
<circle class="score-ring-fill" id="score-ring-fill" cx="65" cy="65" r="60"></circle>
|
| 1039 |
+
</svg>
|
| 1040 |
+
<div class="score-text">
|
| 1041 |
+
<div class="score-value" id="threat-score">0</div>
|
| 1042 |
+
<div class="score-label">THREAT LEVEL</div>
|
| 1043 |
+
</div>
|
| 1044 |
+
</div>
|
| 1045 |
+
<div class="status-text" id="status-text">SECURE</div>
|
| 1046 |
+
</div>
|
| 1047 |
+
</div>
|
| 1048 |
+
|
| 1049 |
+
<!-- Live Metrics -->
|
| 1050 |
+
<div class="card">
|
| 1051 |
+
<div class="card-header">
|
| 1052 |
+
<div class="card-title">Live Metrics</div>
|
| 1053 |
+
<i data-feather="bar-chart-2" class="card-icon"></i>
|
| 1054 |
+
</div>
|
| 1055 |
+
<div class="stats-list" id="stats-container">
|
| 1056 |
+
<div class="stat-row">
|
| 1057 |
+
<span class="stat-name">System Status</span>
|
| 1058 |
+
<span class="stat-val">Initializing</span>
|
| 1059 |
+
</div>
|
| 1060 |
+
</div>
|
| 1061 |
+
</div>
|
| 1062 |
+
|
| 1063 |
+
<!-- Actions -->
|
| 1064 |
+
<div class="card">
|
| 1065 |
+
<div class="card-header">
|
| 1066 |
+
<div class="card-title">Actions</div>
|
| 1067 |
+
</div>
|
| 1068 |
+
<button class="btn btn-primary" onclick="generateReport()">Generate Incident Report</button>
|
| 1069 |
+
<button class="btn btn-secondary" onclick="refreshAuditLog()" style="margin-top: 8px;">Refresh Audit
|
| 1070 |
+
Log</button>
|
| 1071 |
+
</div>
|
| 1072 |
+
</div>
|
| 1073 |
+
</div>
|
| 1074 |
+
|
| 1075 |
+
<!-- Report Modal -->
|
| 1076 |
+
<div id="report-modal" class="modal-overlay">
|
| 1077 |
+
<div class="modal-card">
|
| 1078 |
+
<div class="modal-title">Incident Report</div>
|
| 1079 |
+
<div class="modal-subtitle">Generated by Sentinel AI Agent // Gemini 1.5</div>
|
| 1080 |
+
<div class="report-content" id="report-content">Analyzing data stream...</div>
|
| 1081 |
+
<button class="btn btn-secondary" onclick="closeModal()">Dismiss</button>
|
| 1082 |
+
</div>
|
| 1083 |
+
</div>
|
| 1084 |
+
|
| 1085 |
+
<!-- ═══════════════════════════════════════════════
|
| 1086 |
+
JAVASCRIPT
|
| 1087 |
+
═══════════════════════════════════════════════ -->
|
| 1088 |
+
<script>
|
| 1089 |
+
feather.replace();
|
| 1090 |
+
|
| 1091 |
+
// ─── State ───
|
| 1092 |
+
let currentLayout = 'quad'; // 'quad' or 'single'
|
| 1093 |
+
let expandedFeed = 0;
|
| 1094 |
+
let isRedAlert = false;
|
| 1095 |
+
|
| 1096 |
+
// ─── Mode Switching ───
|
| 1097 |
+
const modeTitles = {
|
| 1098 |
+
'standby': 'SYSTEM STANDBY',
|
| 1099 |
+
'movement': 'MOVEMENT ANALYSIS',
|
| 1100 |
+
'facemask': 'FACEMASK DETECTION',
|
| 1101 |
+
'weapon': 'WEAPON DETECTION',
|
| 1102 |
+
'public_safety': 'PUBLIC SAFETY'
|
| 1103 |
+
};
|
| 1104 |
+
|
| 1105 |
+
function setMode(mode, element) {
|
| 1106 |
+
// Radio-style selection: always activate the clicked mode
|
| 1107 |
+
// Remove active from all module buttons
|
| 1108 |
+
document.querySelectorAll('.nav-group:first-of-type .nav-item').forEach(el => el.classList.remove('active'));
|
| 1109 |
+
|
| 1110 |
+
// Add active to the clicked button
|
| 1111 |
+
element.classList.add('active');
|
| 1112 |
+
|
| 1113 |
+
// Update UI display
|
| 1114 |
+
document.getElementById('mode-title').textContent = modeTitles[mode] || mode.toUpperCase();
|
| 1115 |
+
|
| 1116 |
+
// Send to backend
|
| 1117 |
+
fetch('/set_mode', {
|
| 1118 |
+
method: 'POST',
|
| 1119 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1120 |
+
body: JSON.stringify({ mode: mode })
|
| 1121 |
+
});
|
| 1122 |
+
}
|
| 1123 |
+
|
| 1124 |
+
function setSource(source) {
|
| 1125 |
+
fetch('/set_source', {
|
| 1126 |
+
method: 'POST',
|
| 1127 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1128 |
+
body: JSON.stringify({ source: source })
|
| 1129 |
+
})
|
| 1130 |
+
.then(r => r.json())
|
| 1131 |
+
.then(data => {
|
| 1132 |
+
if (data.success) {
|
| 1133 |
+
// Force the browser to reconnect to the restarted MJPEG stream
|
| 1134 |
+
refreshFeedStream(0);
|
| 1135 |
+
}
|
| 1136 |
+
});
|
| 1137 |
+
}
|
| 1138 |
+
|
| 1139 |
+
function handleFileUpload(input) {
|
| 1140 |
+
if (input.files[0]) {
|
| 1141 |
+
const formData = new FormData();
|
| 1142 |
+
formData.append('file', input.files[0]);
|
| 1143 |
+
|
| 1144 |
+
// Show uploading state
|
| 1145 |
+
const statusEl = document.getElementById('feed-0-status');
|
| 1146 |
+
if (statusEl) statusEl.textContent = 'UPLOADING...';
|
| 1147 |
+
|
| 1148 |
+
fetch('/upload_video', { method: 'POST', body: formData })
|
| 1149 |
+
.then(r => r.json())
|
| 1150 |
+
.then(data => {
|
| 1151 |
+
if (data.success) {
|
| 1152 |
+
// Force the browser to reconnect to the restarted MJPEG stream
|
| 1153 |
+
refreshFeedStream(0);
|
| 1154 |
+
if (statusEl) statusEl.textContent = 'FILE FEED';
|
| 1155 |
+
}
|
| 1156 |
+
})
|
| 1157 |
+
.catch(() => {
|
| 1158 |
+
if (statusEl) statusEl.textContent = 'UPLOAD FAILED';
|
| 1159 |
+
});
|
| 1160 |
+
|
| 1161 |
+
// Reset file input so the same file can be re-uploaded
|
| 1162 |
+
input.value = '';
|
| 1163 |
+
}
|
| 1164 |
+
}
|
| 1165 |
+
|
| 1166 |
+
/**
|
| 1167 |
+
* Force-refresh an MJPEG stream by setting a new src with a cache-buster.
|
| 1168 |
+
* This drops the old HTTP connection and establishes a fresh one.
|
| 1169 |
+
*/
|
| 1170 |
+
function refreshFeedStream(feedId) {
|
| 1171 |
+
const img = document.getElementById('stream-' + feedId);
|
| 1172 |
+
if (img) {
|
| 1173 |
+
// Brief blank to visually signal the switch
|
| 1174 |
+
img.src = '';
|
| 1175 |
+
// Small delay lets the backend fully initialize the new feed
|
| 1176 |
+
setTimeout(() => {
|
| 1177 |
+
img.src = '/video_feed/' + feedId + '?t=' + Date.now();
|
| 1178 |
+
}, 300);
|
| 1179 |
+
}
|
| 1180 |
+
}
|
| 1181 |
+
|
| 1182 |
+
// ─── Grid Layout ───
|
| 1183 |
+
function setGridLayout(layout) {
|
| 1184 |
+
const grid = document.getElementById('camera-grid');
|
| 1185 |
+
currentLayout = layout;
|
| 1186 |
+
|
| 1187 |
+
if (layout === 'single') {
|
| 1188 |
+
grid.classList.add('single-view');
|
| 1189 |
+
// Show only the expanded feed
|
| 1190 |
+
document.querySelectorAll('.feed-cell').forEach((cell, i) => {
|
| 1191 |
+
cell.classList.toggle('expanded', i === expandedFeed);
|
| 1192 |
+
});
|
| 1193 |
+
} else {
|
| 1194 |
+
grid.classList.remove('single-view');
|
| 1195 |
+
document.querySelectorAll('.feed-cell').forEach(cell => {
|
| 1196 |
+
cell.classList.remove('expanded');
|
| 1197 |
+
});
|
| 1198 |
+
}
|
| 1199 |
+
}
|
| 1200 |
+
|
| 1201 |
+
function expandFeed(feedId) {
|
| 1202 |
+
expandedFeed = feedId;
|
| 1203 |
+
if (currentLayout === 'single') {
|
| 1204 |
+
document.querySelectorAll('.feed-cell').forEach((cell, i) => {
|
| 1205 |
+
cell.classList.toggle('expanded', i === feedId);
|
| 1206 |
+
});
|
| 1207 |
+
}
|
| 1208 |
+
}
|
| 1209 |
+
|
| 1210 |
+
// ─── Stats & Red Alert Updates ───
|
| 1211 |
+
function updateStats() {
|
| 1212 |
+
fetch('/stats')
|
| 1213 |
+
.then(r => r.json())
|
| 1214 |
+
.then(data => {
|
| 1215 |
+
const score = data.threat_score;
|
| 1216 |
+
const scoreEl = document.getElementById('threat-score');
|
| 1217 |
+
const statusEl = document.getElementById('status-text');
|
| 1218 |
+
const ringFill = document.getElementById('score-ring-fill');
|
| 1219 |
+
const threatCard = document.getElementById('threat-card');
|
| 1220 |
+
|
| 1221 |
+
scoreEl.textContent = score;
|
| 1222 |
+
|
| 1223 |
+
// Compute ring dash offset (circumference = 2 * π * 60 ≈ 377)
|
| 1224 |
+
const circumference = 377;
|
| 1225 |
+
const offset = circumference - (circumference * score / 100);
|
| 1226 |
+
ringFill.style.strokeDashoffset = offset;
|
| 1227 |
+
|
| 1228 |
+
// Color based on score
|
| 1229 |
+
let color, status, glow;
|
| 1230 |
+
if (score >= 80) {
|
| 1231 |
+
color = '#ff2040';
|
| 1232 |
+
status = 'CRITICAL';
|
| 1233 |
+
glow = 'rgba(255, 32, 64, 0.4)';
|
| 1234 |
+
} else if (score >= 50) {
|
| 1235 |
+
color = '#ffaa00';
|
| 1236 |
+
status = 'ELEVATED';
|
| 1237 |
+
glow = 'rgba(255, 170, 0, 0.3)';
|
| 1238 |
+
} else if (score >= 25) {
|
| 1239 |
+
color = '#00d4ff';
|
| 1240 |
+
status = 'GUARDED';
|
| 1241 |
+
glow = 'rgba(0, 200, 255, 0.3)';
|
| 1242 |
+
} else {
|
| 1243 |
+
color = '#00ff88';
|
| 1244 |
+
status = 'SECURE';
|
| 1245 |
+
glow = 'rgba(0, 255, 136, 0.3)';
|
| 1246 |
+
}
|
| 1247 |
+
|
| 1248 |
+
statusEl.textContent = status;
|
| 1249 |
+
statusEl.style.color = color;
|
| 1250 |
+
statusEl.style.textShadow = `0 0 20px ${glow}`;
|
| 1251 |
+
ringFill.style.stroke = color;
|
| 1252 |
+
ringFill.style.filter = `drop-shadow(0 0 8px ${glow})`;
|
| 1253 |
+
|
| 1254 |
+
// Red Alert state
|
| 1255 |
+
const alertOverlay = document.getElementById('red-alert-overlay');
|
| 1256 |
+
const alertBanner = document.getElementById('red-alert-banner');
|
| 1257 |
+
const feedCells = document.querySelectorAll('.feed-cell');
|
| 1258 |
+
|
| 1259 |
+
if (data.red_alert) {
|
| 1260 |
+
alertOverlay.classList.add('active');
|
| 1261 |
+
alertBanner.classList.add('active');
|
| 1262 |
+
feedCells.forEach(cell => cell.classList.add('red-alert-active'));
|
| 1263 |
+
threatCard.style.borderColor = 'rgba(255, 32, 64, 0.5)';
|
| 1264 |
+
|
| 1265 |
+
if (!isRedAlert) {
|
| 1266 |
+
playAlertTone();
|
| 1267 |
+
isRedAlert = true;
|
| 1268 |
+
}
|
| 1269 |
+
} else {
|
| 1270 |
+
alertOverlay.classList.remove('active');
|
| 1271 |
+
alertBanner.classList.remove('active');
|
| 1272 |
+
feedCells.forEach(cell => cell.classList.remove('red-alert-active'));
|
| 1273 |
+
threatCard.style.borderColor = '';
|
| 1274 |
+
isRedAlert = false;
|
| 1275 |
+
}
|
| 1276 |
+
|
| 1277 |
+
// Update mode display
|
| 1278 |
+
if (data.mode) {
|
| 1279 |
+
document.getElementById('mode-title').textContent = modeTitles[data.mode] || data.mode.toUpperCase();
|
| 1280 |
+
}
|
| 1281 |
+
|
| 1282 |
+
// Update live metrics
|
| 1283 |
+
const container = document.getElementById('stats-container');
|
| 1284 |
+
container.innerHTML = '';
|
| 1285 |
+
|
| 1286 |
+
if (!data.details || Object.keys(data.details).length === 0) {
|
| 1287 |
+
container.innerHTML = '<div class="stat-row"><span class="stat-name">System Status</span><span class="stat-val">Standby</span></div>';
|
| 1288 |
+
} else {
|
| 1289 |
+
for (const [key, value] of Object.entries(data.details)) {
|
| 1290 |
+
const div = document.createElement('div');
|
| 1291 |
+
div.className = 'stat-row';
|
| 1292 |
+
const formattedKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
| 1293 |
+
let displayVal = value;
|
| 1294 |
+
if (typeof value === 'boolean') {
|
| 1295 |
+
displayVal = value ? '⚠ YES' : 'No';
|
| 1296 |
+
}
|
| 1297 |
+
div.innerHTML = `<span class="stat-name">${formattedKey}</span><span class="stat-val">${displayVal}</span>`;
|
| 1298 |
+
container.appendChild(div);
|
| 1299 |
+
}
|
| 1300 |
+
}
|
| 1301 |
+
})
|
| 1302 |
+
.catch(() => { });
|
| 1303 |
+
}
|
| 1304 |
+
|
| 1305 |
+
// ─── Alert Tone (Web Audio API) ───
|
| 1306 |
+
function playAlertTone() {
|
| 1307 |
+
try {
|
| 1308 |
+
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
| 1309 |
+
const oscillator = audioCtx.createOscillator();
|
| 1310 |
+
const gainNode = audioCtx.createGain();
|
| 1311 |
+
|
| 1312 |
+
oscillator.connect(gainNode);
|
| 1313 |
+
gainNode.connect(audioCtx.destination);
|
| 1314 |
+
|
| 1315 |
+
oscillator.type = 'square';
|
| 1316 |
+
oscillator.frequency.setValueAtTime(880, audioCtx.currentTime);
|
| 1317 |
+
oscillator.frequency.setValueAtTime(660, audioCtx.currentTime + 0.15);
|
| 1318 |
+
oscillator.frequency.setValueAtTime(880, audioCtx.currentTime + 0.3);
|
| 1319 |
+
|
| 1320 |
+
gainNode.gain.setValueAtTime(0.08, audioCtx.currentTime);
|
| 1321 |
+
gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.5);
|
| 1322 |
+
|
| 1323 |
+
oscillator.start(audioCtx.currentTime);
|
| 1324 |
+
oscillator.stop(audioCtx.currentTime + 0.5);
|
| 1325 |
+
} catch (e) {
|
| 1326 |
+
// Audio not available — silent fallback
|
| 1327 |
+
}
|
| 1328 |
+
}
|
| 1329 |
+
|
| 1330 |
+
// ─── AI Report ───
|
| 1331 |
+
function generateReport() {
|
| 1332 |
+
const modal = document.getElementById('report-modal');
|
| 1333 |
+
modal.classList.add('show');
|
| 1334 |
+
document.getElementById('report-content').textContent = 'ANALYZING DATA STREAM...';
|
| 1335 |
+
|
| 1336 |
+
fetch('/generate_report', { method: 'POST' })
|
| 1337 |
+
.then(r => r.json())
|
| 1338 |
+
.then(data => {
|
| 1339 |
+
document.getElementById('report-content').textContent = data.report;
|
| 1340 |
+
})
|
| 1341 |
+
.catch(() => {
|
| 1342 |
+
document.getElementById('report-content').textContent = 'ERROR: Connection to AI Agent lost.';
|
| 1343 |
+
});
|
| 1344 |
+
}
|
| 1345 |
+
|
| 1346 |
+
function closeModal() {
|
| 1347 |
+
document.getElementById('report-modal').classList.remove('show');
|
| 1348 |
+
}
|
| 1349 |
+
|
| 1350 |
+
// ─── Audit Log Refresh ───
|
| 1351 |
+
function refreshAuditLog() {
|
| 1352 |
+
fetch('/audit_log')
|
| 1353 |
+
.then(r => r.json())
|
| 1354 |
+
.then(data => {
|
| 1355 |
+
const container = document.getElementById('audit-log-container');
|
| 1356 |
+
container.innerHTML = '';
|
| 1357 |
+
|
| 1358 |
+
if (data.log.length === 0) {
|
| 1359 |
+
container.innerHTML = '<div class="audit-entry"><div class="audit-time">--:--:--</div>NO ENTRIES</div>';
|
| 1360 |
+
return;
|
| 1361 |
+
}
|
| 1362 |
+
|
| 1363 |
+
data.log.slice(0, 30).forEach(entry => {
|
| 1364 |
+
const div = document.createElement('div');
|
| 1365 |
+
div.className = `audit-entry severity-${entry.severity}`;
|
| 1366 |
+
div.innerHTML = `
|
| 1367 |
+
<div class="audit-time">${entry.timestamp.split(' ')[1] || entry.timestamp}</div>
|
| 1368 |
+
${entry.action}: ${entry.details}
|
| 1369 |
+
`;
|
| 1370 |
+
container.appendChild(div);
|
| 1371 |
+
});
|
| 1372 |
+
})
|
| 1373 |
+
.catch(() => { });
|
| 1374 |
+
}
|
| 1375 |
+
|
| 1376 |
+
// ─── Intervals ───
|
| 1377 |
+
setInterval(updateStats, 1000);
|
| 1378 |
+
setInterval(refreshAuditLog, 5000);
|
| 1379 |
+
|
| 1380 |
+
// Initial load
|
| 1381 |
+
setTimeout(refreshAuditLog, 1500);
|
| 1382 |
+
</script>
|
| 1383 |
+
</body>
|
| 1384 |
+
|
| 1385 |
+
</html>
|
yolov8n.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f59b3d833e2ff32e194b5bb8e08d211dc7c5bdf144b90d2c8412c47ccfc83b36
|
| 3 |
+
size 6549796
|