Spaces:
Sleeping
Sleeping
Commit ·
dcfd6c3
1
Parent(s): 6cda601
Deploy TrackIQ v8
Browse files- Dockerfile +34 -0
- README.md +37 -5
- TRAFFIC_ROAD_APP/TRAFFIC_ROAD_APP/requirements_hf.txt +194 -0
- TRAFFIC_ROAD_APP/requirements_hf.txt +194 -0
- TRAFFIC_ROAD_APP/templates/dashboard.html +418 -0
- TRAFFIC_ROAD_APP/templates/history.html +295 -0
- TRAFFIC_ROAD_APP/templates/home.html +684 -0
- TRAFFIC_ROAD_APP/templates/index.html +486 -0
- TRAFFIC_ROAD_APP/templates/logs.html +415 -0
- app.py +596 -0
- models/.gitkeep +0 -0
- models/yolo11n.pt +3 -0
- requirements.txt +194 -0
- requirements_hf.txt +194 -0
- static/trackiq-api.js +202 -0
- templates/dashboard.html +418 -0
- templates/history.html +295 -0
- templates/home.html +684 -0
- templates/index.html +486 -0
- templates/logs.html +415 -0
Dockerfile
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
# Install system dependencies (OpenCV, ffmpeg, libGL)
|
| 4 |
+
RUN apt-get update && apt-get install -y \
|
| 5 |
+
libgl1-mesa-glx \
|
| 6 |
+
libglib2.0-0 \
|
| 7 |
+
libsm6 \
|
| 8 |
+
libxext6 \
|
| 9 |
+
libxrender-dev \
|
| 10 |
+
ffmpeg \
|
| 11 |
+
libopencv-dev \
|
| 12 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 13 |
+
|
| 14 |
+
WORKDIR /app
|
| 15 |
+
|
| 16 |
+
# Copy requirements first for layer caching
|
| 17 |
+
COPY requirements_hf.txt .
|
| 18 |
+
|
| 19 |
+
RUN pip install --no-cache-dir -r requirements_hf.txt
|
| 20 |
+
|
| 21 |
+
# Copy app source
|
| 22 |
+
COPY app.py .
|
| 23 |
+
COPY static/ ./static/
|
| 24 |
+
COPY templates/ ./templates/
|
| 25 |
+
|
| 26 |
+
# Create required directories
|
| 27 |
+
RUN mkdir -p models uploads outputs logs
|
| 28 |
+
|
| 29 |
+
# HuggingFace Spaces expose port 7860
|
| 30 |
+
ENV PORT=7860
|
| 31 |
+
EXPOSE 7860
|
| 32 |
+
|
| 33 |
+
# Start Flask on port 7860
|
| 34 |
+
CMD ["python", "app.py"]
|
README.md
CHANGED
|
@@ -1,11 +1,43 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: blue
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
-
|
| 9 |
---
|
| 10 |
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: TrackIQ — Traffic Road Detection
|
| 3 |
+
emoji: 🚦
|
| 4 |
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
+
app_port: 7860
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# 🚦 TrackIQ — Traffic Road Detection App
|
| 12 |
+
|
| 13 |
+
TrackIQ is a real-time traffic object detection and tracking application powered by YOLO.
|
| 14 |
+
|
| 15 |
+
## Features
|
| 16 |
+
|
| 17 |
+
- 🎥 Upload and analyze traffic videos
|
| 18 |
+
- 🚗 Detect vehicles, motorcycles, trucks, buses, pedestrians, bicycles, traffic lights, and road signs
|
| 19 |
+
- 📊 Live dashboard with detection statistics
|
| 20 |
+
- 📁 Session history and CSV log export
|
| 21 |
+
- ⚡ CPU-optimized inference with frame skipping
|
| 22 |
+
|
| 23 |
+
## How to Use
|
| 24 |
+
|
| 25 |
+
1. Go to the **Home** page and upload a video
|
| 26 |
+
2. Click **Run** to start detection
|
| 27 |
+
3. Watch detections stream live on the video
|
| 28 |
+
4. View stats on the **Dashboard**
|
| 29 |
+
5. Export logs from the **Logs** page
|
| 30 |
+
|
| 31 |
+
## Model
|
| 32 |
+
|
| 33 |
+
> ⚠️ **Upload your YOLO model** to the `models/` folder before running.
|
| 34 |
+
> Supported: `yolo11n.pt`, `yolov8n.pt`, etc.
|
| 35 |
+
|
| 36 |
+
Upload the model via the **Files** tab in your HuggingFace Space settings.
|
| 37 |
+
|
| 38 |
+
## Tech Stack
|
| 39 |
+
|
| 40 |
+
- Flask (Python backend)
|
| 41 |
+
- Ultralytics YOLO
|
| 42 |
+
- OpenCV
|
| 43 |
+
- HTML/CSS/JS frontend
|
TRAFFIC_ROAD_APP/TRAFFIC_ROAD_APP/requirements_hf.txt
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
absl-py==2.4.0
|
| 2 |
+
alabaster==1.0.0
|
| 3 |
+
altair==5.5.0
|
| 4 |
+
annotated-doc==0.0.4
|
| 5 |
+
annotated-types==0.7.0
|
| 6 |
+
anyio==4.13.0
|
| 7 |
+
asttokens==3.0.0
|
| 8 |
+
astunparse==1.6.3
|
| 9 |
+
attrs==25.4.0
|
| 10 |
+
babel==2.17.0
|
| 11 |
+
beautifulsoup4==4.14.2
|
| 12 |
+
bleach==6.3.0
|
| 13 |
+
blinker==1.9.0
|
| 14 |
+
cachetools==6.2.2
|
| 15 |
+
certifi==2025.11.12
|
| 16 |
+
cffi==2.0.0
|
| 17 |
+
charset-normalizer==3.4.4
|
| 18 |
+
clarabel==0.11.1
|
| 19 |
+
click==8.3.1
|
| 20 |
+
colorama==0.4.6
|
| 21 |
+
comm==0.2.3
|
| 22 |
+
contourpy==1.3.3
|
| 23 |
+
cvxpy==1.7.5
|
| 24 |
+
cycler==0.12.1
|
| 25 |
+
debugpy==1.8.17
|
| 26 |
+
decorator==5.2.1
|
| 27 |
+
defusedxml==0.7.1
|
| 28 |
+
dill==0.4.1
|
| 29 |
+
docutils==0.22.4
|
| 30 |
+
et_xmlfile==2.0.0
|
| 31 |
+
executing==2.2.1
|
| 32 |
+
fastjsonschema==2.21.2
|
| 33 |
+
fica==0.4.1
|
| 34 |
+
filelock==3.24.3
|
| 35 |
+
Flask==3.1.3
|
| 36 |
+
flask-cors==6.0.2
|
| 37 |
+
flatbuffers==25.12.19
|
| 38 |
+
fonttools==4.60.1
|
| 39 |
+
fsspec==2026.2.0
|
| 40 |
+
gast==0.7.0
|
| 41 |
+
geopandas==1.1.2
|
| 42 |
+
git-filter-repo==2.47.0
|
| 43 |
+
gitdb==4.0.12
|
| 44 |
+
GitPython==3.1.45
|
| 45 |
+
google-pasta==0.2.0
|
| 46 |
+
graphviz==0.21
|
| 47 |
+
greenlet==3.3.1
|
| 48 |
+
grpcio==1.80.0
|
| 49 |
+
h11==0.16.0
|
| 50 |
+
h5py==3.14.0
|
| 51 |
+
hf-xet==1.4.3
|
| 52 |
+
httpcore==1.0.9
|
| 53 |
+
httpx==0.28.1
|
| 54 |
+
huggingface_hub==1.10.2
|
| 55 |
+
idna==3.11
|
| 56 |
+
imagesize==1.4.1
|
| 57 |
+
ipykernel==7.1.0
|
| 58 |
+
ipylab==1.1.0
|
| 59 |
+
ipython==9.6.0
|
| 60 |
+
ipython_pygments_lexers==1.1.1
|
| 61 |
+
ipywidgets==8.1.8
|
| 62 |
+
itsdangerous==2.2.0
|
| 63 |
+
jedi==0.19.2
|
| 64 |
+
Jinja2==3.1.6
|
| 65 |
+
joblib==1.5.3
|
| 66 |
+
jsonschema==4.25.1
|
| 67 |
+
jsonschema-specifications==2025.9.1
|
| 68 |
+
jupyter_client==8.6.3
|
| 69 |
+
jupyter_core==5.9.1
|
| 70 |
+
jupyterlab_pygments==0.3.0
|
| 71 |
+
jupyterlab_widgets==3.0.16
|
| 72 |
+
jupytext==1.19.1
|
| 73 |
+
keras==3.14.0
|
| 74 |
+
kiwisolver==1.4.9
|
| 75 |
+
lap==0.5.13
|
| 76 |
+
lapx==0.9.4
|
| 77 |
+
libclang==18.1.1
|
| 78 |
+
lxml==6.0.2
|
| 79 |
+
markdown-it-py==4.0.0
|
| 80 |
+
MarkupSafe==3.0.3
|
| 81 |
+
matplotlib==3.10.7
|
| 82 |
+
matplotlib-inline==0.2.1
|
| 83 |
+
mdit-py-plugins==0.5.0
|
| 84 |
+
mdurl==0.1.2
|
| 85 |
+
mistune==3.2.0
|
| 86 |
+
ml_dtypes==0.5.4
|
| 87 |
+
mpmath==1.3.0
|
| 88 |
+
namex==0.1.0
|
| 89 |
+
narwhals==2.11.0
|
| 90 |
+
nbclient==0.10.4
|
| 91 |
+
nbconvert==7.17.0
|
| 92 |
+
nbformat==5.10.4
|
| 93 |
+
nest-asyncio==1.6.0
|
| 94 |
+
networkx==3.6.1
|
| 95 |
+
numpy==2.3.4
|
| 96 |
+
opencv-python==4.13.0.92
|
| 97 |
+
opencv-python-headless==4.13.0.92
|
| 98 |
+
openpyxl==3.1.5
|
| 99 |
+
opt_einsum==3.4.0
|
| 100 |
+
optree==0.19.0
|
| 101 |
+
osqp==1.0.5
|
| 102 |
+
otter-grader==6.1.6
|
| 103 |
+
outcome==1.3.0.post0
|
| 104 |
+
packaging==25.0
|
| 105 |
+
pandas==2.3.3
|
| 106 |
+
pandocfilters==1.5.1
|
| 107 |
+
parso==0.8.5
|
| 108 |
+
pillow==12.0.0
|
| 109 |
+
platformdirs==4.5.0
|
| 110 |
+
playwright==1.58.0
|
| 111 |
+
plotly==6.4.0
|
| 112 |
+
polars==1.40.0
|
| 113 |
+
polars-runtime-32==1.40.0
|
| 114 |
+
prompt_toolkit==3.0.52
|
| 115 |
+
protobuf==6.33.1
|
| 116 |
+
psutil==7.1.2
|
| 117 |
+
pure_eval==0.2.3
|
| 118 |
+
pyarrow==21.0.0
|
| 119 |
+
pycparser==2.23
|
| 120 |
+
pydantic==2.12.5
|
| 121 |
+
pydantic_core==2.41.5
|
| 122 |
+
pydeck==0.9.1
|
| 123 |
+
pyee==13.0.0
|
| 124 |
+
Pygments==2.19.2
|
| 125 |
+
pyogrio==0.12.1
|
| 126 |
+
pyparsing==3.2.5
|
| 127 |
+
pyproj==3.7.2
|
| 128 |
+
PySocks==1.7.1
|
| 129 |
+
python-dateutil==2.9.0.post0
|
| 130 |
+
python-dotenv==1.2.1
|
| 131 |
+
python-on-whales==0.80.0
|
| 132 |
+
pytz==2025.2
|
| 133 |
+
PyYAML==6.0.3
|
| 134 |
+
pyzmq==27.1.0
|
| 135 |
+
referencing==0.37.0
|
| 136 |
+
requests==2.32.5
|
| 137 |
+
rich==14.3.3
|
| 138 |
+
roman-numerals==4.1.0
|
| 139 |
+
rpds-py==0.28.0
|
| 140 |
+
scikit-learn==1.8.0
|
| 141 |
+
scipy==1.16.3
|
| 142 |
+
scs==3.2.11
|
| 143 |
+
seaborn==0.13.2
|
| 144 |
+
selenium==4.38.0
|
| 145 |
+
setuptools==80.9.0
|
| 146 |
+
shapely==2.1.2
|
| 147 |
+
shellingham==1.5.4
|
| 148 |
+
six==1.17.0
|
| 149 |
+
smmap==5.0.2
|
| 150 |
+
sniffio==1.3.1
|
| 151 |
+
snowballstemmer==3.0.1
|
| 152 |
+
sortedcontainers==2.4.0
|
| 153 |
+
soupsieve==2.8
|
| 154 |
+
Sphinx==9.1.0
|
| 155 |
+
sphinxcontrib-applehelp==2.0.0
|
| 156 |
+
sphinxcontrib-devhelp==2.0.0
|
| 157 |
+
sphinxcontrib-htmlhelp==2.1.0
|
| 158 |
+
sphinxcontrib-jsmath==1.0.1
|
| 159 |
+
sphinxcontrib-qthelp==2.0.0
|
| 160 |
+
sphinxcontrib-serializinghtml==2.0.0
|
| 161 |
+
stack-data==0.6.3
|
| 162 |
+
streamlit==1.51.0
|
| 163 |
+
sympy==1.14.0
|
| 164 |
+
tenacity==9.1.2
|
| 165 |
+
tensorflow==2.21.0
|
| 166 |
+
termcolor==3.3.0
|
| 167 |
+
threadpoolctl==3.6.0
|
| 168 |
+
tinycss2==1.4.0
|
| 169 |
+
toml==0.10.2
|
| 170 |
+
torch==2.11.0
|
| 171 |
+
torchvision==0.26.0
|
| 172 |
+
torchviz==0.0.3
|
| 173 |
+
tornado==6.5.2
|
| 174 |
+
tqdm==4.67.3
|
| 175 |
+
traitlets==5.14.3
|
| 176 |
+
trio==0.32.0
|
| 177 |
+
trio-websocket==0.12.2
|
| 178 |
+
typer==0.24.1
|
| 179 |
+
typing-inspection==0.4.2
|
| 180 |
+
typing_extensions==4.15.0
|
| 181 |
+
tzdata==2025.2
|
| 182 |
+
ultralytics==8.4.39
|
| 183 |
+
ultralytics-thop==2.0.18
|
| 184 |
+
urllib3==2.5.0
|
| 185 |
+
watchdog==6.0.0
|
| 186 |
+
wcwidth==0.2.14
|
| 187 |
+
webdriver-manager==4.0.2
|
| 188 |
+
webencodings==0.5.1
|
| 189 |
+
websocket-client==1.9.0
|
| 190 |
+
Werkzeug==3.1.8
|
| 191 |
+
wheel==0.46.3
|
| 192 |
+
widgetsnbextension==4.0.15
|
| 193 |
+
wrapt==1.17.3
|
| 194 |
+
wsproto==1.3.2
|
TRAFFIC_ROAD_APP/requirements_hf.txt
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
absl-py==2.4.0
|
| 2 |
+
alabaster==1.0.0
|
| 3 |
+
altair==5.5.0
|
| 4 |
+
annotated-doc==0.0.4
|
| 5 |
+
annotated-types==0.7.0
|
| 6 |
+
anyio==4.13.0
|
| 7 |
+
asttokens==3.0.0
|
| 8 |
+
astunparse==1.6.3
|
| 9 |
+
attrs==25.4.0
|
| 10 |
+
babel==2.17.0
|
| 11 |
+
beautifulsoup4==4.14.2
|
| 12 |
+
bleach==6.3.0
|
| 13 |
+
blinker==1.9.0
|
| 14 |
+
cachetools==6.2.2
|
| 15 |
+
certifi==2025.11.12
|
| 16 |
+
cffi==2.0.0
|
| 17 |
+
charset-normalizer==3.4.4
|
| 18 |
+
clarabel==0.11.1
|
| 19 |
+
click==8.3.1
|
| 20 |
+
colorama==0.4.6
|
| 21 |
+
comm==0.2.3
|
| 22 |
+
contourpy==1.3.3
|
| 23 |
+
cvxpy==1.7.5
|
| 24 |
+
cycler==0.12.1
|
| 25 |
+
debugpy==1.8.17
|
| 26 |
+
decorator==5.2.1
|
| 27 |
+
defusedxml==0.7.1
|
| 28 |
+
dill==0.4.1
|
| 29 |
+
docutils==0.22.4
|
| 30 |
+
et_xmlfile==2.0.0
|
| 31 |
+
executing==2.2.1
|
| 32 |
+
fastjsonschema==2.21.2
|
| 33 |
+
fica==0.4.1
|
| 34 |
+
filelock==3.24.3
|
| 35 |
+
Flask==3.1.3
|
| 36 |
+
flask-cors==6.0.2
|
| 37 |
+
flatbuffers==25.12.19
|
| 38 |
+
fonttools==4.60.1
|
| 39 |
+
fsspec==2026.2.0
|
| 40 |
+
gast==0.7.0
|
| 41 |
+
geopandas==1.1.2
|
| 42 |
+
git-filter-repo==2.47.0
|
| 43 |
+
gitdb==4.0.12
|
| 44 |
+
GitPython==3.1.45
|
| 45 |
+
google-pasta==0.2.0
|
| 46 |
+
graphviz==0.21
|
| 47 |
+
greenlet==3.3.1
|
| 48 |
+
grpcio==1.80.0
|
| 49 |
+
h11==0.16.0
|
| 50 |
+
h5py==3.14.0
|
| 51 |
+
hf-xet==1.4.3
|
| 52 |
+
httpcore==1.0.9
|
| 53 |
+
httpx==0.28.1
|
| 54 |
+
huggingface_hub==1.10.2
|
| 55 |
+
idna==3.11
|
| 56 |
+
imagesize==1.4.1
|
| 57 |
+
ipykernel==7.1.0
|
| 58 |
+
ipylab==1.1.0
|
| 59 |
+
ipython==9.6.0
|
| 60 |
+
ipython_pygments_lexers==1.1.1
|
| 61 |
+
ipywidgets==8.1.8
|
| 62 |
+
itsdangerous==2.2.0
|
| 63 |
+
jedi==0.19.2
|
| 64 |
+
Jinja2==3.1.6
|
| 65 |
+
joblib==1.5.3
|
| 66 |
+
jsonschema==4.25.1
|
| 67 |
+
jsonschema-specifications==2025.9.1
|
| 68 |
+
jupyter_client==8.6.3
|
| 69 |
+
jupyter_core==5.9.1
|
| 70 |
+
jupyterlab_pygments==0.3.0
|
| 71 |
+
jupyterlab_widgets==3.0.16
|
| 72 |
+
jupytext==1.19.1
|
| 73 |
+
keras==3.14.0
|
| 74 |
+
kiwisolver==1.4.9
|
| 75 |
+
lap==0.5.13
|
| 76 |
+
lapx==0.9.4
|
| 77 |
+
libclang==18.1.1
|
| 78 |
+
lxml==6.0.2
|
| 79 |
+
markdown-it-py==4.0.0
|
| 80 |
+
MarkupSafe==3.0.3
|
| 81 |
+
matplotlib==3.10.7
|
| 82 |
+
matplotlib-inline==0.2.1
|
| 83 |
+
mdit-py-plugins==0.5.0
|
| 84 |
+
mdurl==0.1.2
|
| 85 |
+
mistune==3.2.0
|
| 86 |
+
ml_dtypes==0.5.4
|
| 87 |
+
mpmath==1.3.0
|
| 88 |
+
namex==0.1.0
|
| 89 |
+
narwhals==2.11.0
|
| 90 |
+
nbclient==0.10.4
|
| 91 |
+
nbconvert==7.17.0
|
| 92 |
+
nbformat==5.10.4
|
| 93 |
+
nest-asyncio==1.6.0
|
| 94 |
+
networkx==3.6.1
|
| 95 |
+
numpy==2.3.4
|
| 96 |
+
opencv-python==4.13.0.92
|
| 97 |
+
opencv-python-headless==4.13.0.92
|
| 98 |
+
openpyxl==3.1.5
|
| 99 |
+
opt_einsum==3.4.0
|
| 100 |
+
optree==0.19.0
|
| 101 |
+
osqp==1.0.5
|
| 102 |
+
otter-grader==6.1.6
|
| 103 |
+
outcome==1.3.0.post0
|
| 104 |
+
packaging==25.0
|
| 105 |
+
pandas==2.3.3
|
| 106 |
+
pandocfilters==1.5.1
|
| 107 |
+
parso==0.8.5
|
| 108 |
+
pillow==12.0.0
|
| 109 |
+
platformdirs==4.5.0
|
| 110 |
+
playwright==1.58.0
|
| 111 |
+
plotly==6.4.0
|
| 112 |
+
polars==1.40.0
|
| 113 |
+
polars-runtime-32==1.40.0
|
| 114 |
+
prompt_toolkit==3.0.52
|
| 115 |
+
protobuf==6.33.1
|
| 116 |
+
psutil==7.1.2
|
| 117 |
+
pure_eval==0.2.3
|
| 118 |
+
pyarrow==21.0.0
|
| 119 |
+
pycparser==2.23
|
| 120 |
+
pydantic==2.12.5
|
| 121 |
+
pydantic_core==2.41.5
|
| 122 |
+
pydeck==0.9.1
|
| 123 |
+
pyee==13.0.0
|
| 124 |
+
Pygments==2.19.2
|
| 125 |
+
pyogrio==0.12.1
|
| 126 |
+
pyparsing==3.2.5
|
| 127 |
+
pyproj==3.7.2
|
| 128 |
+
PySocks==1.7.1
|
| 129 |
+
python-dateutil==2.9.0.post0
|
| 130 |
+
python-dotenv==1.2.1
|
| 131 |
+
python-on-whales==0.80.0
|
| 132 |
+
pytz==2025.2
|
| 133 |
+
PyYAML==6.0.3
|
| 134 |
+
pyzmq==27.1.0
|
| 135 |
+
referencing==0.37.0
|
| 136 |
+
requests==2.32.5
|
| 137 |
+
rich==14.3.3
|
| 138 |
+
roman-numerals==4.1.0
|
| 139 |
+
rpds-py==0.28.0
|
| 140 |
+
scikit-learn==1.8.0
|
| 141 |
+
scipy==1.16.3
|
| 142 |
+
scs==3.2.11
|
| 143 |
+
seaborn==0.13.2
|
| 144 |
+
selenium==4.38.0
|
| 145 |
+
setuptools==80.9.0
|
| 146 |
+
shapely==2.1.2
|
| 147 |
+
shellingham==1.5.4
|
| 148 |
+
six==1.17.0
|
| 149 |
+
smmap==5.0.2
|
| 150 |
+
sniffio==1.3.1
|
| 151 |
+
snowballstemmer==3.0.1
|
| 152 |
+
sortedcontainers==2.4.0
|
| 153 |
+
soupsieve==2.8
|
| 154 |
+
Sphinx==9.1.0
|
| 155 |
+
sphinxcontrib-applehelp==2.0.0
|
| 156 |
+
sphinxcontrib-devhelp==2.0.0
|
| 157 |
+
sphinxcontrib-htmlhelp==2.1.0
|
| 158 |
+
sphinxcontrib-jsmath==1.0.1
|
| 159 |
+
sphinxcontrib-qthelp==2.0.0
|
| 160 |
+
sphinxcontrib-serializinghtml==2.0.0
|
| 161 |
+
stack-data==0.6.3
|
| 162 |
+
streamlit==1.51.0
|
| 163 |
+
sympy==1.14.0
|
| 164 |
+
tenacity==9.1.2
|
| 165 |
+
tensorflow==2.21.0
|
| 166 |
+
termcolor==3.3.0
|
| 167 |
+
threadpoolctl==3.6.0
|
| 168 |
+
tinycss2==1.4.0
|
| 169 |
+
toml==0.10.2
|
| 170 |
+
torch==2.11.0
|
| 171 |
+
torchvision==0.26.0
|
| 172 |
+
torchviz==0.0.3
|
| 173 |
+
tornado==6.5.2
|
| 174 |
+
tqdm==4.67.3
|
| 175 |
+
traitlets==5.14.3
|
| 176 |
+
trio==0.32.0
|
| 177 |
+
trio-websocket==0.12.2
|
| 178 |
+
typer==0.24.1
|
| 179 |
+
typing-inspection==0.4.2
|
| 180 |
+
typing_extensions==4.15.0
|
| 181 |
+
tzdata==2025.2
|
| 182 |
+
ultralytics==8.4.39
|
| 183 |
+
ultralytics-thop==2.0.18
|
| 184 |
+
urllib3==2.5.0
|
| 185 |
+
watchdog==6.0.0
|
| 186 |
+
wcwidth==0.2.14
|
| 187 |
+
webdriver-manager==4.0.2
|
| 188 |
+
webencodings==0.5.1
|
| 189 |
+
websocket-client==1.9.0
|
| 190 |
+
Werkzeug==3.1.8
|
| 191 |
+
wheel==0.46.3
|
| 192 |
+
widgetsnbextension==4.0.15
|
| 193 |
+
wrapt==1.17.3
|
| 194 |
+
wsproto==1.3.2
|
TRAFFIC_ROAD_APP/templates/dashboard.html
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" data-theme="light">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>TrackIQ Dashboard</title>
|
| 7 |
+
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
|
| 8 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
| 9 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
| 10 |
+
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.2.0/dist/chartjs-plugin-datalabels.min.js"></script>
|
| 11 |
+
<style>
|
| 12 |
+
:root{--bg:#0c1117;--s:#151c25;--s2:#1c2531;--bd:rgba(255,255,255,0.07);--sh:0 2px 16px rgba(0,0,0,0.45);--shlg:0 8px 36px rgba(0,0,0,0.55);--t:#e8edf2;--t2:#7a8fa0;--t3:#3a4a5a;--ac:#880000;--ac2:#660000;--acl:rgba(136,0,0,0.12);--gr:#16a34a;--grb:#052e16;--bl:#3b82f6;--blb:#0f2040;--yw:#f59e0b;--ywb:#1a0f00;--re:#ef4444;--reb:#2d0000;--sw:60px;--r:14px;--rs:9px;}
|
| 13 |
+
[data-theme="light"]{--bg:#f0f4f8;--s:#fff;--s2:#f6f8fb;--bd:rgba(0,0,0,0.07);--sh:0 2px 16px rgba(0,0,0,0.09);--shlg:0 8px 36px rgba(0,0,0,0.13);--t:#0f1923;--t2:#5a6a7a;--t3:#9aaab8;--grb:#dcfce7;--blb:#dbeafe;--ywb:#fef9c3;--reb:#fee2e2;}
|
| 14 |
+
*{box-sizing:border-box;margin:0;padding:0}::-webkit-scrollbar{width:0;height:0}
|
| 15 |
+
body{font-family:'DM Sans',sans-serif;background:var(--bg);color:var(--t);font-size:13px;display:flex;height:100vh;overflow:hidden;transition:background .3s,color .3s}
|
| 16 |
+
.sb{width:var(--sw);background:var(--s);border-right:1px solid var(--bd);display:flex;flex-direction:column;align-items:center;padding:14px 0;gap:4px;flex-shrink:0}
|
| 17 |
+
.sbl{width:34px;height:34px;background:var(--ac);border-radius:8px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px;margin-bottom:10px;text-decoration:none}
|
| 18 |
+
.sbsep{width:26px;height:1px;background:var(--bd);margin:3px 0}
|
| 19 |
+
.sbb{width:38px;height:38px;border-radius:9px;display:flex;align-items:center;justify-content:center;color:var(--t3);font-size:14px;cursor:pointer;transition:all .2s;text-decoration:none;border:none;background:none;position:relative}
|
| 20 |
+
.sbb:hover{background:var(--s2);color:var(--t)}.sbb.on{background:var(--acl);color:var(--ac)}
|
| 21 |
+
.sbb .tip{position:absolute;left:44px;top:50%;transform:translateY(-50%);background:var(--t);color:var(--bg);font-size:10px;padding:3px 7px;border-radius:4px;white-space:nowrap;opacity:0;pointer-events:none;z-index:300}.sbb:hover .tip{opacity:1}
|
| 22 |
+
.sbsp{flex:1}.pw{flex:1;display:flex;flex-direction:column;overflow:hidden}
|
| 23 |
+
.topb{height:50px;background:var(--s);border-bottom:1px solid var(--bd);display:flex;align-items:center;justify-content:space-between;padding:0 18px;flex-shrink:0}
|
| 24 |
+
.topl,.topr{display:flex;align-items:center;gap:9px}.bn{font-size:14px;font-weight:600}.bn b{color:var(--ac)}
|
| 25 |
+
.pill{display:flex;align-items:center;gap:4px;font-size:10px;color:var(--gr);background:var(--grb);padding:2px 8px;border-radius:20px}
|
| 26 |
+
.ld{width:6px;height:6px;border-radius:50%;background:var(--gr);animation:blink 2s infinite}
|
| 27 |
+
@keyframes blink{0%,100%{opacity:1}50%{opacity:.3}}
|
| 28 |
+
.clk{font-size:10px;font-family:'DM Mono',monospace;color:var(--t2)}
|
| 29 |
+
.lg{display:flex;background:var(--s2);border-radius:5px;padding:2px;border:1px solid var(--bd)}
|
| 30 |
+
.lb{font-size:10px;font-weight:500;padding:2px 8px;border:none;background:none;cursor:pointer;border-radius:3px;color:var(--t2);font-family:'DM Sans',sans-serif;transition:all .2s}.lb.on{background:var(--ac);color:#fff}
|
| 31 |
+
.ib{width:28px;height:28px;border-radius:5px;border:1px solid var(--bd);background:var(--s);cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--t2);font-size:11px;position:relative;transition:all .2s}.ib:hover{background:var(--s2)}
|
| 32 |
+
.nd{position:absolute;top:3px;right:3px;width:6px;height:6px;border-radius:50%;background:var(--ac)}
|
| 33 |
+
main{flex:1;padding:12px 14px;display:grid;grid-template-columns:minmax(0,1.4fr) 360px;gap:12px;overflow:auto;min-height:0}
|
| 34 |
+
.left-col{display:grid;grid-template-rows:auto minmax(190px,1fr) minmax(190px,1fr);gap:10px;min-height:0}
|
| 35 |
+
.right-col{display:grid;grid-template-rows:minmax(300px,1.15fr) auto auto;gap:10px;min-height:0}
|
| 36 |
+
.mr{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
|
| 37 |
+
.mc,.cd{background:var(--s);border:1px solid var(--bd);box-shadow:var(--sh);overflow:hidden}
|
| 38 |
+
.mc{border-radius:var(--rs);padding:11px 13px;transition:transform .2s}.mc:hover{transform:translateY(-2px)}
|
| 39 |
+
.cd{border-radius:var(--r);padding:12px 14px;display:flex;flex-direction:column}.cd:hover{box-shadow:var(--shlg)}
|
| 40 |
+
.mct{display:flex;align-items:center;justify-content:space-between;margin-bottom:5px}
|
| 41 |
+
.mcl{font-size:10px;color:var(--t2)}.bx{font-size:9px;font-weight:500;padding:1px 5px;border-radius:4px}
|
| 42 |
+
.mv{font-size:20px;font-weight:300;letter-spacing:-.5px;line-height:1}.mu{font-size:10px;color:var(--t2)}.ms{font-size:9px;color:var(--t3);margin-top:2px}
|
| 43 |
+
.ct{font-size:11px;font-weight:500;margin-bottom:8px;display:flex;align-items:center;gap:5px;flex-shrink:0}.ct i{color:var(--ac);font-size:10px}
|
| 44 |
+
.chart-wrap{flex:1;min-height:0}
|
| 45 |
+
.stack{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
| 46 |
+
.hmcd{padding:0}
|
| 47 |
+
.hmhd{padding:10px 12px 6px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0}
|
| 48 |
+
.hmhd .ct{margin-bottom:0}
|
| 49 |
+
.hmbd{flex:1;position:relative;min-height:220px}
|
| 50 |
+
#hmc2{position:absolute;inset:0;width:100%!important;height:100%!important;display:block}
|
| 51 |
+
.stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:9px}
|
| 52 |
+
.statbox{background:var(--s2);border:1px solid var(--bd);border-radius:8px;padding:10px 11px}
|
| 53 |
+
.statbox .k{font-size:9px;color:var(--t2);text-transform:uppercase;letter-spacing:.04em}
|
| 54 |
+
.statbox .v{font-size:18px;font-weight:300;margin-top:4px}
|
| 55 |
+
.subline{font-size:10px;color:var(--t2)}
|
| 56 |
+
.listbox{display:flex;flex-direction:column;gap:6px}
|
| 57 |
+
.vr{display:flex;align-items:center;justify-content:space-between;background:var(--s2);border:1px solid var(--bd);border-radius:8px;padding:8px 10px}
|
| 58 |
+
.vrl{display:flex;align-items:center;gap:7px;font-size:11px}.vic{width:22px;height:22px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px}.vp{font-size:13px;font-weight:500}
|
| 59 |
+
.al{display:flex;align-items:flex-start;gap:8px;padding:7px 0;border-bottom:1px solid var(--bd)}.al:last-child{border-bottom:none}
|
| 60 |
+
.ai{width:22px;height:22px;border-radius:5px;display:flex;align-items:center;justify-content:center;font-size:9px;flex-shrink:0;margin-top:1px}
|
| 61 |
+
.at2{font-size:10px;font-weight:500;margin-bottom:1px}.as2{font-size:9px;color:var(--t2)}
|
| 62 |
+
.pill-row{display:flex;gap:6px;flex-wrap:wrap;margin-top:8px}.mini-pill{font-size:9px;padding:3px 7px;border-radius:999px;background:var(--s2);border:1px solid var(--bd);color:var(--t2)}
|
| 63 |
+
.empty-note{font-size:10px;color:var(--t3);padding-top:4px}
|
| 64 |
+
@media(max-width:1180px){main{grid-template-columns:1fr}.right-col{grid-template-rows:minmax(260px,auto) auto auto}.stack{grid-template-columns:1fr}.mr{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
| 65 |
+
</style>
|
| 66 |
+
</head>
|
| 67 |
+
<body>
|
| 68 |
+
<nav class="sb">
|
| 69 |
+
<a href="/" class="sbl"><i class="fas fa-satellite-dish"></i></a>
|
| 70 |
+
<div class="sbsep"></div>
|
| 71 |
+
<a href="/home" class="sbb"><i class="fas fa-play-circle"></i><span class="tip">Analyze</span></a>
|
| 72 |
+
<a href="/dashboard" class="sbb on"><i class="fas fa-chart-line"></i><span class="tip">Dashboard</span></a>
|
| 73 |
+
<a href="/logs" class="sbb"><i class="fas fa-list-ul"></i><span class="tip">Logs</span></a>
|
| 74 |
+
<a href="/history" class="sbb"><i class="fas fa-film"></i><span class="tip">History</span></a>
|
| 75 |
+
<div class="sbsp"></div>
|
| 76 |
+
<button class="sbb" onclick="toggleTheme()"><i class="fas fa-moon" id="ti"></i><span class="tip">Theme</span></button>
|
| 77 |
+
</nav>
|
| 78 |
+
|
| 79 |
+
<div class="pw">
|
| 80 |
+
<header class="topb">
|
| 81 |
+
<div class="topl">
|
| 82 |
+
<span class="bn">Track<b>IQ</b></span>
|
| 83 |
+
<span style="color:var(--bd);font-size:16px;margin:0 1px;">|</span>
|
| 84 |
+
<span style="font-size:12px;font-weight:500;" id="t-dash">Dashboard</span>
|
| 85 |
+
<div class="pill"><span class="ld"></span> <span id="liveBadge">Live</span></div>
|
| 86 |
+
</div>
|
| 87 |
+
<div class="topr">
|
| 88 |
+
<span class="clk" id="clk">--:--:--</span>
|
| 89 |
+
<div class="lg">
|
| 90 |
+
<button class="lb on" onclick="setLang('en',this)">EN</button>
|
| 91 |
+
<button class="lb" onclick="setLang('fr',this)">FR</button>
|
| 92 |
+
<button class="lb" onclick="setLang('wo',this)">WO</button>
|
| 93 |
+
</div>
|
| 94 |
+
<button class="ib" onclick="toggleNotifs()"><i class="fas fa-bell"></i><span class="nd" id="nd"></span></button>
|
| 95 |
+
<button class="ib" onclick="toggleTheme()"><i class="fas fa-moon" id="ti2"></i></button>
|
| 96 |
+
</div>
|
| 97 |
+
</header>
|
| 98 |
+
|
| 99 |
+
<div id="notifP" style="display:none;position:fixed;top:57px;right:65px;z-index:500;width:255px;background:var(--s);border:1px solid var(--bd);border-radius:var(--r);box-shadow:var(--shlg);padding:12px 14px;">
|
| 100 |
+
<div style="font-size:11px;font-weight:500;margin-bottom:8px;display:flex;justify-content:space-between;"><span id="t-notif">Notifications</span><button onclick="clearN()" style="font-size:10px;color:var(--ac);background:none;border:none;cursor:pointer;">Clear all</button></div>
|
| 101 |
+
<div id="nl">
|
| 102 |
+
<div style="padding:6px 0;border-bottom:1px solid var(--bd);"><div style="font-size:10px;font-weight:500;color:var(--re);" id="t-a1">Congestion Zone A</div><div style="font-size:9px;color:var(--t2);">4 min ago</div></div>
|
| 103 |
+
<div style="padding:6px 0;"><div style="font-size:10px;font-weight:500;" id="t-source-loaded">Source ready</div><div style="font-size:9px;color:var(--t2);">Just now</div></div>
|
| 104 |
+
</div>
|
| 105 |
+
</div>
|
| 106 |
+
|
| 107 |
+
<main>
|
| 108 |
+
<section class="left-col">
|
| 109 |
+
<div class="mr">
|
| 110 |
+
<div class="mc"><div class="mct"><span class="mcl" id="t-m1">Objects Today</span><span class="bx" style="background:var(--grb);color:var(--gr);" id="m1Trend">+0%</span></div><div><span class="mv" id="mvToday">0</span> <span class="mu" id="t-veh">obj.</span></div><div class="ms" id="m1Sub">Latest analyzed scenes</div></div>
|
| 111 |
+
<div class="mc"><div class="mct"><span class="mcl" id="t-m2">Current Flow</span><span class="bx" style="background:var(--grb);color:var(--gr);" id="flowBadge">Smooth</span></div><div><span class="mv" id="mvFlow">0</span> <span class="mu">obj/min</span></div><div class="ms" id="m2Sub">Based on recent activity</div></div>
|
| 112 |
+
<div class="mc"><div class="mct"><span class="mcl" id="t-m3">Avg FPS</span><span class="bx" style="background:var(--blb);color:var(--bl);" id="fpsBadge">Stable</span></div><div><span class="mv" id="mvFps">0</span> <span class="mu">fps</span></div><div class="ms" id="m3Sub">Last completed analysis</div></div>
|
| 113 |
+
<div class="mc"><div class="mct"><span class="mcl" id="t-m4">Latest clip</span><span class="bx" style="background:var(--ywb);color:var(--yw);" id="accBadge">Current</span></div><div><span class="mv" id="mvAcc">0</span> <span class="mu">s</span></div><div class="ms" id="m4Sub">Duration of latest processed video</div></div>
|
| 114 |
+
</div>
|
| 115 |
+
|
| 116 |
+
<div class="stack">
|
| 117 |
+
<div class="cd">
|
| 118 |
+
<div class="ct"><i class="fas fa-chart-area"></i><span id="t-c1">Flow & speed</span></div>
|
| 119 |
+
<div class="chart-wrap"><canvas id="fc"></canvas></div>
|
| 120 |
+
</div>
|
| 121 |
+
<div class="cd">
|
| 122 |
+
<div class="ct"><i class="fas fa-chart-bar"></i><span id="t-c2">Classification</span></div>
|
| 123 |
+
<div class="chart-wrap"><canvas id="tc"></canvas></div>
|
| 124 |
+
</div>
|
| 125 |
+
</div>
|
| 126 |
+
|
| 127 |
+
<div class="cd">
|
| 128 |
+
<div class="ct"><i class="fas fa-layer-group"></i><span id="t-scenes">Scene summary</span></div>
|
| 129 |
+
<div class="stats-grid">
|
| 130 |
+
<div class="statbox"><div class="k" id="t-live-source">Source</div><div class="v" id="liveSource">No source</div><div class="subline" id="liveSourceMeta">Ready after analysis</div></div>
|
| 131 |
+
<div class="statbox"><div class="k" id="t-live-objects">Objects</div><div class="v" id="liveObjects" id="liveObjects">0</div><div class="subline" id="liveObjectsMeta">Peak visible count</div></div>
|
| 132 |
+
<div class="statbox"><div class="k" id="t-live-fps">FPS</div><div class="v" id="liveFps" id="liveFps">0</div><div class="subline" id="liveFpsMeta">Captured stream rate</div></div>
|
| 133 |
+
<div class="statbox"><div class="k" id="t-live-duration">Duration</div><div class="v" id="liveDuration" id="liveDuration">0s</div><div class="subline" id="liveDurationMeta">Latest processed clip</div></div>
|
| 134 |
+
</div>
|
| 135 |
+
<div class="pill-row" id="liveClassPills"></div>
|
| 136 |
+
<div class="empty-note" id="liveNote">Current summary follows the latest processed analysis instead of the old floating video preview.</div>
|
| 137 |
+
<div id="sceneSummaryGrid" style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:10px;"></div>
|
| 138 |
+
</div>
|
| 139 |
+
</section>
|
| 140 |
+
|
| 141 |
+
<aside class="right-col">
|
| 142 |
+
<div class="cd hmcd">
|
| 143 |
+
<div class="hmhd"><div class="ct"><i class="fas fa-map-marker-alt"></i><span id="t-hm">Position heatmap</span></div><span style="font-size:9px;color:var(--t3);" id="t-hms">Object density</span></div>
|
| 144 |
+
<div class="hmbd"><canvas id="hmc2"></canvas></div>
|
| 145 |
+
</div>
|
| 146 |
+
|
| 147 |
+
<div class="stack">
|
| 148 |
+
<div class="cd" style="padding:11px 12px;">
|
| 149 |
+
<div class="ct">
|
| 150 |
+
<i class="fas fa-chart-pie"></i>
|
| 151 |
+
<span>Object Type Distribution</span>
|
| 152 |
+
</div>
|
| 153 |
+
<!-- Pie chart + légende -->
|
| 154 |
+
<div style="position:relative;display:flex;align-items:center;gap:10px;min-height:140px;">
|
| 155 |
+
<div style="width:145px;height:145px;flex-shrink:0;">
|
| 156 |
+
<canvas id="pieChart"></canvas>
|
| 157 |
+
|
| 158 |
+
</div>
|
| 159 |
+
<!-- Légende dynamique -->
|
| 160 |
+
<div id="pieLegend" style="flex:1;display:flex;flex-direction:column;gap:5px;">
|
| 161 |
+
<div style="font-size:10px;color:var(--t3);text-align:center;">No data yet</div>
|
| 162 |
+
</div>
|
| 163 |
+
</div>
|
| 164 |
+
</div>
|
| 165 |
+
<div class="cd" style="padding:11px 12px;">
|
| 166 |
+
<div class="ct"><i class="fas fa-wave-square"></i><span id="t-live-panel">Live stats</span></div>
|
| 167 |
+
<div class="stats-grid">
|
| 168 |
+
<div class="statbox"><div class="k" id="t-det">Detected</div><div class="v" id="sbdet">0</div></div>
|
| 169 |
+
<div class="statbox"><div class="k" id="t-acc">Latest FPS</div><div class="v" id="sbacc">0</div></div>
|
| 170 |
+
</div>
|
| 171 |
+
<div class="empty-note" id="livePanelNote">Heatmap and key stats stay visible at all times.</div>
|
| 172 |
+
</div>
|
| 173 |
+
</div>
|
| 174 |
+
</aside>
|
| 175 |
+
</main>
|
| 176 |
+
</div>
|
| 177 |
+
|
| 178 |
+
<script>
|
| 179 |
+
setInterval(()=>{document.getElementById('clk').textContent=new Date().toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit',second:'2-digit'});},1000);
|
| 180 |
+
|
| 181 |
+
function toggleTheme(){
|
| 182 |
+
const d=document.documentElement,dk=d.getAttribute('data-theme')==='dark';
|
| 183 |
+
d.setAttribute('data-theme',dk?'light':'dark');
|
| 184 |
+
['ti','ti2'].forEach(x=>{const e=document.getElementById(x);if(e)e.className=dk?'fas fa-moon':'fas fa-sun';});
|
| 185 |
+
rebuildCharts();
|
| 186 |
+
setTimeout(drawHm,50);
|
| 187 |
+
rebuildPie();
|
| 188 |
+
try{localStorage.setItem('tiq_theme',dk?'light':'dark');}catch(e){}
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
const TL={
|
| 192 |
+
en:{dash:'Dashboard',m1:'Objects Today',veh:'obj.',m2:'Current Flow',m3:'Avg FPS',m4:'Latest clip',c1:'Flow from video',c2:'Classification',hm:'Position heatmap',hms:'Detected positions',bk:'Breakdown',ca:'Cars',mo:'Moto',tr:'Trucks',al:'Alerts',notif:'Notifications',scenes:'Scene summary',livesource:'Source',liveobjects:'Objects',livefps:'FPS',liveduration:'Duration',livepanel:'Live stats',det:'Detected',acc:'Latest FPS'},
|
| 193 |
+
fr:{dash:'Tableau de bord',m1:'Objets du jour',veh:'obj.',m2:'Flux actuel',m3:'FPS moy.',m4:'Dernier clip',c1:'Flux vidéo',c2:'Classification',hm:'Carte de densité',hms:'Positions détectées',bk:'Répartition',ca:'Voitures',mo:'Motos',tr:'Camions',al:'Alertes',notif:'Notifications',scenes:'Résumé scène',livesource:'Source',liveobjects:'Objets',livefps:'FPS',liveduration:'Durée',livepanel:'Stats live',det:'Détectés',acc:'FPS récent'},
|
| 194 |
+
wo:{dash:'Dashboard',m1:'Objets tey',veh:'obj.',m2:'Yóbbu',m3:'FPS moy.',m4:'Clip mujj',c1:'Flux video',c2:'Wërsëg',hm:'Kàrt',hms:'Posision yi',bk:'Wërsëg',ca:'Cars',mo:'Moto',tr:'Trucks',al:'Yëgël',notif:'Yëgël yi',scenes:'Melo scène',livesource:'Source',liveobjects:'Objets',livefps:'FPS',liveduration:'Yoon',livepanel:'Stats live',det:'Yi réy',acc:'FPS mujj'}
|
| 195 |
+
};
|
| 196 |
+
|
| 197 |
+
function setLang(l,btn){
|
| 198 |
+
document.querySelectorAll('.lb').forEach(b=>b.classList.remove('on'));
|
| 199 |
+
btn.classList.add('on');
|
| 200 |
+
const t=TL[l];
|
| 201 |
+
const mp={dash:'t-dash',m1:'t-m1',veh:'t-veh',m2:'t-m2',m3:'t-m3',m4:'t-m4',c1:'t-c1',c2:'t-c2',hm:'t-hm',hms:'t-hms',bk:'t-bk',ca:'t-ca',mo:'t-mo',tr:'t-tr',al:'t-al',notif:'t-notif',scenes:'t-scenes',livesource:'t-live-source',liveobjects:'t-live-objects',livefps:'t-live-fps',liveduration:'t-live-duration',livepanel:'t-live-panel',det:'t-det',acc:'t-acc'};
|
| 202 |
+
Object.entries(mp).forEach(([k,id])=>{const el=document.getElementById(id);if(el&&t[k])el.textContent=t[k];});
|
| 203 |
+
try{localStorage.setItem('tiq_lang',l);}catch(e){}
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
let no=false;
|
| 207 |
+
function toggleNotifs(){no=!no;document.getElementById('notifP').style.display=no?'block':'none';if(no)document.getElementById('nd').style.display='none';}
|
| 208 |
+
function clearN(){document.getElementById('nl').innerHTML='<div style="font-size:10px;color:var(--t3);text-align:center;padding:10px;">No notifications</div>';no=false;document.getElementById('notifP').style.display='none';}
|
| 209 |
+
document.addEventListener('click',e=>{if(no&&!e.target.closest('#notifP')&&!e.target.closest('.ib'))toggleNotifs();});
|
| 210 |
+
|
| 211 |
+
window.fch=null;
|
| 212 |
+
window.tch=null;
|
| 213 |
+
window._tiqHeatPoints=[];
|
| 214 |
+
|
| 215 |
+
function gc(){
|
| 216 |
+
const dk=document.documentElement.getAttribute('data-theme')==='dark';
|
| 217 |
+
return{grid:dk?'rgba(255,255,255,0.05)':'rgba(0,0,0,0.06)',tick:dk?'#4a5a6a':'#94a3b8'};
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
/* ── Pie chart — Object Type Distribution ── */
|
| 221 |
+
window.pch = null;
|
| 222 |
+
|
| 223 |
+
function rebuildPie(labels=[], data=[], colors=[]){
|
| 224 |
+
// Enregistre datalabels seulement si dispo
|
| 225 |
+
if(window.ChartDataLabels) Chart.register(ChartDataLabels);
|
| 226 |
+
if(window.pch) window.pch.destroy();
|
| 227 |
+
const canvas = document.getElementById('pieChart');
|
| 228 |
+
if(!canvas) return;
|
| 229 |
+
const dk = document.documentElement.getAttribute('data-theme')==='dark';
|
| 230 |
+
|
| 231 |
+
const defaultColors = ['#3b82f6','#10b981','#f59e0b','#8b5cf6','#ec4899','#06b6d4','#ef4444','#f97316'];
|
| 232 |
+
|
| 233 |
+
window.pch = new Chart(canvas, {
|
| 234 |
+
type: 'pie',
|
| 235 |
+
data: {
|
| 236 |
+
labels: labels.length ? labels : ['No data'],
|
| 237 |
+
datasets: [{
|
| 238 |
+
data: data.length ? data : [1],
|
| 239 |
+
backgroundColor: colors.length ? colors : [dk?'#1c2531':'#e2e8f0'],
|
| 240 |
+
borderColor: dk ? '#151c25' : '#ffffff',
|
| 241 |
+
borderWidth: 2,
|
| 242 |
+
hoverOffset: 6,
|
| 243 |
+
}]
|
| 244 |
+
},
|
| 245 |
+
options: {
|
| 246 |
+
responsive: true,
|
| 247 |
+
maintainAspectRatio: false,
|
| 248 |
+
plugins: {
|
| 249 |
+
legend: { display: false },
|
| 250 |
+
tooltip: {
|
| 251 |
+
callbacks: {
|
| 252 |
+
label: ctx => {
|
| 253 |
+
const tot = ctx.dataset.data.reduce((a,b)=>a+b,0);
|
| 254 |
+
return ` ${ctx.label}: ${ctx.raw} (${Math.round(ctx.raw/tot*100)}%)`;
|
| 255 |
+
}
|
| 256 |
+
}
|
| 257 |
+
},
|
| 258 |
+
/* Affiche les % à l'intérieur de chaque part */
|
| 259 |
+
datalabels: {
|
| 260 |
+
color: '#fff',
|
| 261 |
+
font: { size: 11, weight: 'bold' },
|
| 262 |
+
formatter: (value, ctx) => {
|
| 263 |
+
const tot = ctx.dataset.data.reduce((a,b)=>a+b,0);
|
| 264 |
+
const pct = Math.round(value/tot*100);
|
| 265 |
+
return pct >= 5 ? pct+'%' : ''; // cache si trop petit
|
| 266 |
+
}
|
| 267 |
+
}
|
| 268 |
+
}
|
| 269 |
+
}
|
| 270 |
+
});
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
function updatePie(uc){
|
| 274 |
+
const COLORS = {
|
| 275 |
+
'Vehicle':'#3b82f6','Motorcycle':'#10b981','Truck':'#f59e0b',
|
| 276 |
+
'Bus':'#8b5cf6','Human':'#ec4899','Bicycle':'#06b6d4',
|
| 277 |
+
'Traffic light':'#ef4444','Road sign':'#f97316'
|
| 278 |
+
};
|
| 279 |
+
const EMO = {
|
| 280 |
+
'Vehicle':'🚗','Motorcycle':'🏍','Truck':'🚚','Bus':'🚌',
|
| 281 |
+
'Human':'🚶','Bicycle':'🚲','Traffic light':'🚦','Road sign':'🛑'
|
| 282 |
+
};
|
| 283 |
+
|
| 284 |
+
const labels = Object.keys(uc);
|
| 285 |
+
const data = Object.values(uc);
|
| 286 |
+
const colors = labels.map(l => COLORS[l] || '#888');
|
| 287 |
+
const total = data.reduce((a,b)=>a+b,0);
|
| 288 |
+
|
| 289 |
+
// Recrée le chart
|
| 290 |
+
rebuildPie(labels, data, colors);
|
| 291 |
+
|
| 292 |
+
// Met à jour le label central
|
| 293 |
+
const ptn = document.getElementById('pieTotalNum');
|
| 294 |
+
if(ptn) ptn.textContent = total;
|
| 295 |
+
|
| 296 |
+
// Légende dynamique
|
| 297 |
+
const leg = document.getElementById('pieLegend');
|
| 298 |
+
if(!leg) return;
|
| 299 |
+
leg.innerHTML = '';
|
| 300 |
+
labels.forEach((lbl, i) => {
|
| 301 |
+
const pct = Math.round(data[i]/Math.max(total,1)*100);
|
| 302 |
+
const row = document.createElement('div');
|
| 303 |
+
row.style.cssText = 'display:flex;align-items:center;justify-content:space-between;gap:6px;';
|
| 304 |
+
row.innerHTML = `
|
| 305 |
+
<div style="display:flex;align-items:center;gap:5px;font-size:11px;overflow:hidden;">
|
| 306 |
+
<span style="width:9px;height:9px;border-radius:2px;background:${colors[i]};flex-shrink:0;display:inline-block;"></span>
|
| 307 |
+
<span style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${EMO[lbl]||'📦'} ${lbl}</span>
|
| 308 |
+
</div>
|
| 309 |
+
<span style="font-size:10px;color:var(--t2);flex-shrink:0;">${data[i]}</span>
|
| 310 |
+
`;
|
| 311 |
+
leg.appendChild(row);
|
| 312 |
+
});
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
function rebuildCharts(){
|
| 316 |
+
if(window.fch)window.fch.destroy();
|
| 317 |
+
if(window.tch)window.tch.destroy();
|
| 318 |
+
const {grid,tick}=gc();
|
| 319 |
+
/* Flow chart : 2 datasets (Objects + FPS) — compatible avec trackiq-api.js */
|
| 320 |
+
window.fch=new Chart(document.getElementById('fc'),{
|
| 321 |
+
data:{labels:['—'],datasets:[
|
| 322 |
+
{type:'line',label:'Objects',data:[0],borderColor:'#3b82f6',backgroundColor:'rgba(59,130,246,0.08)',borderWidth:1.5,tension:0.45,pointRadius:0,fill:true,yAxisID:'y'},
|
| 323 |
+
{type:'line',label:'FPS',data:[0],borderColor:'#10b981',backgroundColor:'rgba(16,185,129,0.05)',borderWidth:1.5,tension:0.45,pointRadius:0,yAxisID:'y2'}
|
| 324 |
+
]},
|
| 325 |
+
options:{responsive:true,maintainAspectRatio:false,interaction:{mode:'index',intersect:false},plugins:{legend:{labels:{boxWidth:8,font:{size:9},color:tick}}},scales:{x:{ticks:{font:{size:9},color:tick,maxTicksLimit:8},grid:{color:grid},border:{display:false}},y:{position:'left',beginAtZero:true,ticks:{font:{size:9},color:tick},grid:{color:grid},border:{display:false}},y2:{position:'right',beginAtZero:true,ticks:{font:{size:9},color:tick},grid:{drawOnChartArea:false},border:{display:false}}}}
|
| 326 |
+
});
|
| 327 |
+
/* Classification chart : NON stacké, 1 dataset — compatible avec trackiq-api.js */
|
| 328 |
+
window.tch=new Chart(document.getElementById('tc'),{
|
| 329 |
+
type:'bar',
|
| 330 |
+
data:{labels:['—'],datasets:[
|
| 331 |
+
{label:'Count',data:[0],backgroundColor:'#3b82f6',borderRadius:3,borderSkipped:false}
|
| 332 |
+
]},
|
| 333 |
+
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false}},scales:{x:{ticks:{font:{size:9},color:tick},grid:{color:grid},border:{display:false}},y:{beginAtZero:true,ticks:{font:{size:9},color:tick},grid:{color:grid},border:{display:false}}}}
|
| 334 |
+
});
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
function drawHm(){
|
| 338 |
+
const c=document.getElementById('hmc2'),b=c.parentElement;
|
| 339 |
+
const w=b.offsetWidth,h=b.offsetHeight;
|
| 340 |
+
if(!w||!h)return;
|
| 341 |
+
c.width=w;c.height=h;
|
| 342 |
+
const ctx=c.getContext('2d');
|
| 343 |
+
const dk=document.documentElement.getAttribute('data-theme')==='dark';
|
| 344 |
+
ctx.fillStyle=dk?'#0c1219':'#e8eef5';
|
| 345 |
+
ctx.fillRect(0,0,w,h);
|
| 346 |
+
ctx.strokeStyle=dk?'rgba(255,255,255,0.07)':'rgba(0,0,0,0.07)';
|
| 347 |
+
ctx.lineWidth=1;
|
| 348 |
+
[0.25,0.5,0.75].forEach(r=>{ctx.beginPath();ctx.moveTo(0,h*r);ctx.lineTo(w,h*r);ctx.stroke();ctx.beginPath();ctx.moveTo(w*r,0);ctx.lineTo(w*r,h);ctx.stroke();});
|
| 349 |
+
const pts=Array.isArray(window._tiqHeatPoints)?window._tiqHeatPoints:[];
|
| 350 |
+
if(pts.length){
|
| 351 |
+
let mxR=0,mxH=0;
|
| 352 |
+
pts.forEach(p=>{if(p.r>mxR)mxR=p.r;if(p.h>mxH)mxH=p.h;});
|
| 353 |
+
mxR=Math.max(mxR,0.01);mxH=Math.max(mxH,0.01);
|
| 354 |
+
pts.forEach(p=>{
|
| 355 |
+
const px=(p.x/mxR)*w,py=(p.y/mxH)*h;
|
| 356 |
+
const rad=Math.max(10,Math.min(w,h)*0.015);
|
| 357 |
+
const grd=ctx.createRadialGradient(px,py,0,px,py,rad);
|
| 358 |
+
grd.addColorStop(0,'rgba(220,38,38,0.5)');
|
| 359 |
+
grd.addColorStop(0.4,'rgba(180,0,0,0.18)');
|
| 360 |
+
grd.addColorStop(1,'rgba(0,0,0,0)');
|
| 361 |
+
ctx.fillStyle=grd;ctx.fillRect(0,0,w,h);
|
| 362 |
+
});
|
| 363 |
+
ctx.font="9px 'DM Mono',monospace";
|
| 364 |
+
ctx.fillStyle=dk?'rgba(255,255,255,0.3)':'rgba(0,0,0,0.25)';
|
| 365 |
+
ctx.textAlign='left';ctx.fillText(pts.length+' points',8,13);
|
| 366 |
+
}else{
|
| 367 |
+
ctx.fillStyle=dk?'rgba(255,255,255,0.18)':'rgba(0,0,0,0.2)';
|
| 368 |
+
ctx.font="12px 'DM Sans',sans-serif";ctx.textAlign='center';
|
| 369 |
+
ctx.fillText('Run an analysis to build the heatmap',w/2,h/2);
|
| 370 |
+
ctx.textAlign='left';
|
| 371 |
+
}
|
| 372 |
+
ctx.font="10px 'DM Mono',monospace";
|
| 373 |
+
ctx.fillStyle=dk?'rgba(255,255,255,0.24)':'rgba(0,0,0,0.2)';
|
| 374 |
+
ctx.fillText('N',w/2-3,12);ctx.fillText('S',w/2-3,h-5);ctx.fillText('W',5,h/2+3);ctx.fillText('E',w-10,h/2+3);
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
async function loadHeatmap(){
|
| 378 |
+
try{
|
| 379 |
+
const r=await fetch('/api/logs/rows');
|
| 380 |
+
if(!r.ok)return;
|
| 381 |
+
const rows=await r.json();
|
| 382 |
+
if(!rows.length)return;
|
| 383 |
+
const pts=[];let mxX=0,mxY=0;
|
| 384 |
+
rows.slice(0,4000).forEach(r=>{
|
| 385 |
+
if(r.cx>0&&r.cy>0){pts.push({x:r.cx,y:r.cy,r:0,h:0});if(r.cx>mxX)mxX=r.cx;if(r.cy>mxY)mxY=r.cy;}
|
| 386 |
+
});
|
| 387 |
+
if(!pts.length)return;
|
| 388 |
+
pts.forEach(p=>{p.r=mxX;p.h=mxY;});
|
| 389 |
+
window._tiqHeatPoints=pts;
|
| 390 |
+
drawHm();
|
| 391 |
+
}catch(e){}
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
setTimeout(rebuildCharts,120);
|
| 395 |
+
setTimeout(rebuildPie,150);
|
| 396 |
+
setTimeout(drawHm,180);
|
| 397 |
+
setTimeout(loadHeatmap,600);
|
| 398 |
+
setInterval(loadHeatmap,20000);
|
| 399 |
+
window.addEventListener('resize',()=>setTimeout(drawHm,50));
|
| 400 |
+
|
| 401 |
+
(function(){
|
| 402 |
+
try{
|
| 403 |
+
const th=localStorage.getItem('tiq_theme');
|
| 404 |
+
if(th){
|
| 405 |
+
document.documentElement.setAttribute('data-theme',th);
|
| 406 |
+
if(th==='dark'){['ti','ti2'].forEach(x=>{const e=document.getElementById(x);if(e)e.className='fas fa-sun';});}
|
| 407 |
+
}
|
| 408 |
+
const lg=localStorage.getItem('tiq_lang');
|
| 409 |
+
if(lg&&lg!=='en'){
|
| 410 |
+
const btn=document.querySelector('.lb[onclick*="\''+lg+'\'"]');
|
| 411 |
+
if(btn)setLang(lg,btn);
|
| 412 |
+
}
|
| 413 |
+
}catch(e){}
|
| 414 |
+
})();
|
| 415 |
+
</script>
|
| 416 |
+
<script src="/static/trackiq-api.js"></script>
|
| 417 |
+
</body>
|
| 418 |
+
</html>
|
TRAFFIC_ROAD_APP/templates/history.html
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" data-theme="light">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>TrackIQ — History</title>
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
|
| 9 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
| 10 |
+
<style>
|
| 11 |
+
:root{--bg:#f0f4f8;--surface:#fff;--surface2:#f6f8fb;--border:rgba(0,0,0,0.07);--shadow:0 2px 16px rgba(0,0,0,0.09);--shadow-lg:0 8px 36px rgba(0,0,0,0.13);--text:#0f1923;--text2:#5a6a7a;--text3:#9aaab8;--accent:#880000;--accent2:#660000;--accent-light:rgba(136,0,0,0.07);--green:#16a34a;--green-bg:#dcfce7;--sidebar-w:60px;--radius:14px;--radius-sm:9px;}
|
| 12 |
+
[data-theme="dark"]{--bg:#0c1117;--surface:#151c25;--surface2:#1c2531;--border:rgba(255,255,255,0.07);--shadow:0 2px 16px rgba(0,0,0,0.45);--text:#e8edf2;--text2:#7a8fa0;--text3:#3a4a5a;--green-bg:#052e16;}
|
| 13 |
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
|
| 14 |
+
::-webkit-scrollbar{width:0;}
|
| 15 |
+
body{font-family:'DM Sans',sans-serif;background:var(--bg);color:var(--text);font-size:13px;display:flex;min-height:100vh;transition:background 0.3s,color 0.3s;}
|
| 16 |
+
|
| 17 |
+
.sidebar{width:var(--sidebar-w);background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;align-items:center;padding:16px 0;gap:4px;position:fixed;top:0;left:0;bottom:0;z-index:200;box-shadow:2px 0 12px rgba(0,0,0,0.04);}
|
| 18 |
+
.sb-logo{width:34px;height:34px;background:var(--accent);border-radius:8px;display:flex;align-items:center;justify-content:center;color:white;font-size:14px;margin-bottom:10px;text-decoration:none;}
|
| 19 |
+
.sb-sep{width:28px;height:1px;background:var(--border);margin:4px 0;}
|
| 20 |
+
.sb-btn{width:38px;height:38px;border-radius:9px;display:flex;align-items:center;justify-content:center;color:var(--text3);font-size:15px;cursor:pointer;transition:all 0.2s;text-decoration:none;border:none;background:none;position:relative;}
|
| 21 |
+
.sb-btn:hover{background:var(--surface2);color:var(--text);}
|
| 22 |
+
.sb-btn.active{background:var(--accent-light);color:var(--accent);}
|
| 23 |
+
.sb-btn .tip{position:absolute;left:46px;top:50%;transform:translateY(-50%);background:var(--text);color:var(--surface);font-size:11px;padding:3px 8px;border-radius:5px;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity 0.15s;z-index:300;}
|
| 24 |
+
.sb-btn:hover .tip{opacity:1;}
|
| 25 |
+
.sb-spacer{flex:1;}
|
| 26 |
+
|
| 27 |
+
.page-wrap{margin-left:var(--sidebar-w);flex:1;display:flex;flex-direction:column;}
|
| 28 |
+
.topbar{height:52px;background:var(--surface);border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;padding:0 22px;position:sticky;top:0;z-index:100;}
|
| 29 |
+
.topbar-l{display:flex;align-items:center;gap:10px;}
|
| 30 |
+
.brand-name{font-size:15px;font-weight:600;} .brand-name b{color:var(--accent);}
|
| 31 |
+
.sep{color:var(--border);font-size:18px;margin:0 2px;}
|
| 32 |
+
.topbar-r{display:flex;align-items:center;gap:7px;}
|
| 33 |
+
.lang-group{display:flex;background:var(--surface2);border-radius:6px;padding:2px;border:1px solid var(--border);}
|
| 34 |
+
.lang-btn{font-size:11px;font-weight:500;padding:3px 8px;border:none;background:none;cursor:pointer;border-radius:4px;color:var(--text2);font-family:'DM Sans',sans-serif;transition:all 0.2s;}
|
| 35 |
+
.lang-btn.active{background:var(--accent);color:white;}
|
| 36 |
+
.icon-btn{width:30px;height:30px;border-radius:6px;border:1px solid var(--border);background:var(--surface);cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--text2);font-size:12px;}
|
| 37 |
+
.icon-btn:hover{background:var(--surface2);}
|
| 38 |
+
|
| 39 |
+
main{flex:1;padding:18px 20px;max-width:1380px;width:100%;margin:0 auto;display:flex;flex-direction:column;gap:16px;}
|
| 40 |
+
|
| 41 |
+
/* PLAYER CARD — full width */
|
| 42 |
+
.player-card{grid-column:1/3;background:var(--surface);border-radius:var(--radius);border:1px solid var(--border);box-shadow:var(--shadow-lg);overflow:hidden;}
|
| 43 |
+
.player-head{padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;}
|
| 44 |
+
.player-title{font-size:13px;font-weight:500;display:flex;align-items:center;gap:7px;}
|
| 45 |
+
.player-title i{color:var(--accent);}
|
| 46 |
+
.player-body{position:relative;background:#000;}
|
| 47 |
+
.player-body video{width:100%;display:block;max-height:420px;object-fit:contain;}
|
| 48 |
+
.player-hud{position:absolute;top:11px;left:13px;color:white;font-family:'DM Mono',monospace;font-size:12px;line-height:1.8;text-shadow:1px 1px 4px rgba(0,0,0,0.98);pointer-events:none;}
|
| 49 |
+
.player-empty{padding:80px 20px;text-align:center;color:var(--text3);}
|
| 50 |
+
.player-empty i{font-size:44px;display:block;margin-bottom:12px;}
|
| 51 |
+
.player-empty p{font-size:13px;}
|
| 52 |
+
|
| 53 |
+
/* VIDEO GRID */
|
| 54 |
+
.vid-card{background:var(--surface);border-radius:var(--radius);border:1px solid var(--border);box-shadow:var(--shadow);overflow:hidden;cursor:pointer;transition:all 0.2s;}
|
| 55 |
+
.vid-card:hover{box-shadow:var(--shadow-lg);transform:translateY(-3px);}
|
| 56 |
+
.vid-card.selected{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent);}
|
| 57 |
+
.vid-thumb{position:relative;background:#0a0a0a;aspect-ratio:16/9;display:flex;align-items:center;justify-content:center;overflow:hidden;}
|
| 58 |
+
.vid-thumb video{width:100%;height:100%;object-fit:cover;display:block;}
|
| 59 |
+
.vid-thumb-ph{color:rgba(255,255,255,0.3);font-size:32px;}
|
| 60 |
+
.play-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.3);opacity:0;transition:opacity 0.2s;}
|
| 61 |
+
.play-overlay i{font-size:32px;color:white;}
|
| 62 |
+
.vid-card:hover .play-overlay{opacity:1;}
|
| 63 |
+
.vid-info{padding:11px 13px;}
|
| 64 |
+
.vid-name{font-size:12px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:3px;}
|
| 65 |
+
.vid-meta{font-size:10px;color:var(--text2);display:flex;align-items:center;justify-content:space-between;}
|
| 66 |
+
.vid-meta span{display:flex;align-items:center;gap:4px;}
|
| 67 |
+
.vid-actions{display:flex;gap:6px;margin-top:8px;}
|
| 68 |
+
.btn-vi{flex:1;padding:5px 8px;border-radius:6px;border:1px solid var(--border);background:var(--surface2);color:var(--text2);font-size:11px;cursor:pointer;font-family:'DM Sans',sans-serif;display:flex;align-items:center;justify-content:center;gap:4px;transition:all 0.2s;}
|
| 69 |
+
.btn-vi:hover{background:var(--accent);color:white;border-color:var(--accent);}
|
| 70 |
+
.btn-vi.danger:hover{background:#dc2626;border-color:#dc2626;}
|
| 71 |
+
|
| 72 |
+
/* EMPTY HISTORY */
|
| 73 |
+
.empty-hist{text-align:center;padding:60px 20px;color:var(--text3);}
|
| 74 |
+
.empty-hist i{font-size:48px;display:block;margin-bottom:14px;}
|
| 75 |
+
.empty-hist p{font-size:13px;margin-bottom:16px;}
|
| 76 |
+
.empty-hist a{display:inline-flex;align-items:center;gap:6px;background:var(--accent);color:white;padding:9px 18px;border-radius:9px;text-decoration:none;font-size:12px;}
|
| 77 |
+
|
| 78 |
+
/* BADGE */
|
| 79 |
+
.badge{font-size:10px;font-weight:500;padding:1px 6px;border-radius:4px;}
|
| 80 |
+
.bg{background:var(--green-bg);color:var(--green);}
|
| 81 |
+
</style>
|
| 82 |
+
</head>
|
| 83 |
+
<body>
|
| 84 |
+
|
| 85 |
+
<nav class="sidebar">
|
| 86 |
+
<a href="/" class="sb-logo"><i class="fas fa-satellite-dish"></i></a>
|
| 87 |
+
<div class="sb-sep"></div>
|
| 88 |
+
<a href="/home" class="sb-btn"><i class="fas fa-play-circle"></i><span class="tip" id="tip-analyze">Analyze</span></a>
|
| 89 |
+
<a href="/dashboard" class="sb-btn"><i class="fas fa-chart-line"></i><span class="tip" id="tip-dash">Dashboard</span></a>
|
| 90 |
+
<a href="/logs" class="sb-btn"><i class="fas fa-list-ul"></i><span class="tip" id="tip-logs">Logs</span></a>
|
| 91 |
+
<a href="/history" class="sb-btn active"><i class="fas fa-film"></i><span class="tip" id="tip-hist">History</span></a>
|
| 92 |
+
<div class="sb-spacer"></div>
|
| 93 |
+
<button class="sb-btn" onclick="toggleTheme()"><i class="fas fa-moon" id="themeIcon"></i><span class="tip">Theme</span></button>
|
| 94 |
+
</nav>
|
| 95 |
+
|
| 96 |
+
<div class="page-wrap">
|
| 97 |
+
<header class="topbar">
|
| 98 |
+
<div class="topbar-l">
|
| 99 |
+
<span class="brand-name">Track<b>IQ</b></span>
|
| 100 |
+
<span class="sep">|</span>
|
| 101 |
+
<span style="font-size:13px;font-weight:500;" id="t-title">Annotated History</span>
|
| 102 |
+
</div>
|
| 103 |
+
<div class="topbar-r">
|
| 104 |
+
<div class="lang-group">
|
| 105 |
+
<button class="lang-btn active" onclick="setLang('en',this)">EN</button>
|
| 106 |
+
<button class="lang-btn" onclick="setLang('fr',this)">FR</button>
|
| 107 |
+
<button class="lang-btn" onclick="setLang('wo',this)">WO</button>
|
| 108 |
+
</div>
|
| 109 |
+
<button class="icon-btn" onclick="toggleTheme()"><i class="fas fa-moon" id="themeIcon2"></i></button>
|
| 110 |
+
</div>
|
| 111 |
+
</header>
|
| 112 |
+
|
| 113 |
+
<main id="mainGrid">
|
| 114 |
+
|
| 115 |
+
<!-- Videos grid -->
|
| 116 |
+
<div id="videoGrid" style="display:grid;grid-template-columns:1fr 1fr;gap:16px;"></div>
|
| 117 |
+
|
| 118 |
+
</main>
|
| 119 |
+
</div>
|
| 120 |
+
|
| 121 |
+
<script>
|
| 122 |
+
/* THEME */
|
| 123 |
+
function toggleTheme(){
|
| 124 |
+
const d=document.documentElement,dark=d.getAttribute('data-theme')==='dark';
|
| 125 |
+
d.setAttribute('data-theme',dark?'light':'dark');
|
| 126 |
+
['themeIcon','themeIcon2'].forEach(id=>{const el=document.getElementById(id);if(el)el.className=dark?'fas fa-moon':'fas fa-sun';});
|
| 127 |
+
try{localStorage.setItem('tiq_theme',dark?'light':'dark');}catch(e){}
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
/* LANG */
|
| 131 |
+
const T={
|
| 132 |
+
en:{title:'Annotated History',player:'Video player',dl:'Download','select-vid':'Select a video below to play it',tipAnalyze:'Analyze',tipDash:'Dashboard',tipLogs:'Logs',tipHist:'History',reanalyze:'Re-analyze',download:'Download',delete:'Delete',empty:'No annotated videos yet.',goAnalyze:'Go to Analyze'},
|
| 133 |
+
fr:{title:'Historique annoté',player:'Lecteur vidéo',dl:'Télécharger','select-vid':'Sélectionnez une vidéo ci-dessous',tipAnalyze:'Analyser',tipDash:'Tableau de bord',tipLogs:'Logs',tipHist:'Historique',reanalyze:'Ré-analyser',download:'Télécharger',delete:'Supprimer',empty:'Aucune vidéo annotée.',goAnalyze:'Aller sur Analyser'},
|
| 134 |
+
wo:{title:'Màgg yi',player:'Video bi',dl:'Télécharger','select-vid':'Tànn video ci suuf',tipAnalyze:'Jàng',tipDash:'Dashboard',tipLogs:'Logs',tipHist:'Màgg',reanalyze:'Wërtëk',download:'Télécharger',delete:'Bañ',empty:'Amul video.',goAnalyze:'Dem Analyser'}
|
| 135 |
+
};
|
| 136 |
+
let curLang='en';
|
| 137 |
+
function setLang(l,btn){
|
| 138 |
+
curLang=l;
|
| 139 |
+
document.querySelectorAll('.lang-btn').forEach(b=>b.classList.remove('active'));btn.classList.add('active');
|
| 140 |
+
const t=T[l];
|
| 141 |
+
Object.entries({title:'t-title',player:'t-player',dl:'t-dl','select-vid':'t-select-vid',tipAnalyze:'tip-analyze',tipDash:'tip-dash',tipLogs:'tip-logs',tipHist:'tip-hist'}).forEach(([k,id])=>{const el=document.getElementById(id);if(el&&t[k])el.textContent=t[k];});
|
| 142 |
+
renderGrid();
|
| 143 |
+
try{localStorage.setItem('tiq_lang',l);}catch(e){}
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
/* HISTORY DATA */
|
| 147 |
+
function getHistory(){try{return JSON.parse(localStorage.getItem('tiq_history')||'[]');}catch(e){return[];}}
|
| 148 |
+
function saveHistory(h){try{localStorage.setItem('tiq_history',JSON.stringify(h));}catch(e){}}
|
| 149 |
+
|
| 150 |
+
/* FORMAT DATE */
|
| 151 |
+
function fmtDate(iso){
|
| 152 |
+
try{const d=new Date(iso);return d.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'})+' '+d.toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit'});}catch(e){return 'Unknown date';}
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
let activeUrl=null,activeIdx=null;
|
| 156 |
+
|
| 157 |
+
function dlVideo(url,name){
|
| 158 |
+
const a=document.createElement('a');a.href=url;
|
| 159 |
+
a.download=name||'trackiq_video.mp4';a.click();
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
function playVideo(url,name,idx){
|
| 163 |
+
activeUrl=url;activeIdx=idx;
|
| 164 |
+
const op=document.getElementById('overlayPlayer');
|
| 165 |
+
op.src=url;op.play().catch(()=>{});
|
| 166 |
+
document.getElementById('overlayName').textContent=name;
|
| 167 |
+
document.getElementById('vidOverlay').style.display='flex';
|
| 168 |
+
}
|
| 169 |
+
function closeOverlay(){
|
| 170 |
+
const op=document.getElementById('overlayPlayer');op.pause();op.src='';
|
| 171 |
+
document.getElementById('vidOverlay').style.display='none';
|
| 172 |
+
activeUrl=null;activeIdx=null;
|
| 173 |
+
}
|
| 174 |
+
function dlOverlay(){if(activeUrl){const a=document.createElement('a');a.href=activeUrl;a.download='trackiq.mp4';a.click();}}
|
| 175 |
+
|
| 176 |
+
function dlActive(){
|
| 177 |
+
if(!activeUrl)return;
|
| 178 |
+
const a=document.createElement('a');a.href=activeUrl;a.download='trackiq_annotated.mp4';a.click();
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
function deleteVideo(idx){
|
| 182 |
+
const h=getHistory();h.splice(idx,1);saveHistory(h);
|
| 183 |
+
if(activeIdx===idx){
|
| 184 |
+
closeModal();
|
| 185 |
+
activeUrl=null;activeIdx=null;
|
| 186 |
+
}
|
| 187 |
+
renderGrid();
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
function renderGrid(){
|
| 191 |
+
const hist=getHistory();
|
| 192 |
+
const t=T[curLang];
|
| 193 |
+
const grid=document.getElementById('videoGrid');grid.innerHTML='';
|
| 194 |
+
|
| 195 |
+
if(!hist.length){
|
| 196 |
+
const empty=document.createElement('div');empty.className='empty-hist';
|
| 197 |
+
empty.innerHTML=`<i class="fas fa-film"></i><p>${t.empty}</p><a href="/home">${t.goAnalyze} <i class="fas fa-arrow-right"></i></a>`;
|
| 198 |
+
grid.appendChild(empty);return;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
hist.forEach((item,i)=>{
|
| 202 |
+
const card=document.createElement('div');card.className='vid-card'+(activeIdx===i?' selected':'');
|
| 203 |
+
const isBlob=item.url&&item.url.startsWith('blob:');
|
| 204 |
+
const isBackend=item.url&&(item.url.startsWith('/api/')||item.url.startsWith('http'));
|
| 205 |
+
const canPlay=isBlob||isBackend;
|
| 206 |
+
card.innerHTML=`
|
| 207 |
+
<div class="vid-thumb">
|
| 208 |
+
${isBlob
|
| 209 |
+
? `<video src="${item.url}" style="width:100%;height:100%;object-fit:cover;" muted preload="metadata" onloadeddata="this.currentTime=1"></video>`
|
| 210 |
+
: `<div class="vid-thumb-ph" style="background:#0a0e15;display:flex;align-items:center;justify-content:center;height:100%;">
|
| 211 |
+
<i class="fas fa-film" style="font-size:32px;color:rgba(255,255,255,0.2);"></i>
|
| 212 |
+
</div>`
|
| 213 |
+
}
|
| 214 |
+
<div class="play-overlay"><i class="fas fa-play"></i></div>
|
| 215 |
+
</div>
|
| 216 |
+
<div class="vid-info">
|
| 217 |
+
<div class="vid-name" title="${item.name}">${item.name||'Unnamed'}</div>
|
| 218 |
+
<div class="vid-meta">
|
| 219 |
+
<span><i class="fas fa-calendar" style="font-size:9px;"></i> ${fmtDate(item.date)}</span>
|
| 220 |
+
<span class="badge bg">${isBlob?'Local':'Server'}</span>
|
| 221 |
+
</div>
|
| 222 |
+
<div class="vid-actions">
|
| 223 |
+
<button class="btn-vi" onclick="event.stopPropagation();playVideo('${item.url}','${item.name}',${i})">
|
| 224 |
+
<i class="fas fa-play"></i> Play
|
| 225 |
+
</button>
|
| 226 |
+
<button class="btn-vi" onclick="event.stopPropagation();dlVideo('${item.url}','${item.name}')">
|
| 227 |
+
<i class="fas fa-download"></i>
|
| 228 |
+
</button>
|
| 229 |
+
<button class="btn-vi danger" onclick="event.stopPropagation();deleteVideo(${i})">
|
| 230 |
+
<i class="fas fa-trash"></i>
|
| 231 |
+
</button>
|
| 232 |
+
</div>
|
| 233 |
+
</div>
|
| 234 |
+
`;
|
| 235 |
+
card.onclick=(ev)=>{if(!ev.target.closest('.btn-vi'))playVideo(item.url,item.name,i);};
|
| 236 |
+
grid.appendChild(card);
|
| 237 |
+
});
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
/* HUD ticker on player */
|
| 241 |
+
setInterval(()=>{
|
| 242 |
+
if(!activeUrl)return;
|
| 243 |
+
const f=570+Math.floor(Math.random()*80),s=295+Math.floor(Math.random()*40);
|
| 244 |
+
const hf=document.getElementById('oHudF');if(hf)hf.textContent=`F: = ${f} ▶ ${s}`;
|
| 245 |
+
const car=2+Math.floor(Math.random()*6),van=Math.floor(Math.random()*3),bike=Math.floor(Math.random()*6),p=Math.floor(Math.random()*7);
|
| 246 |
+
const hc=document.getElementById('oHudC');if(hc)hc.innerHTML=`Car = ${car}<br>Van = ${van}<br>bike = ${bike}<br>person = ${p}`;
|
| 247 |
+
},2200);
|
| 248 |
+
|
| 249 |
+
document.addEventListener('keydown',e=>{if(e.key==='Escape')closeOverlay();});
|
| 250 |
+
/* INIT */
|
| 251 |
+
(function(){
|
| 252 |
+
try{
|
| 253 |
+
const th=localStorage.getItem('tiq_theme');if(th){document.documentElement.setAttribute('data-theme',th);if(th==='dark'){['themeIcon','themeIcon2'].forEach(id=>{const el=document.getElementById(id);if(el)el.className='fas fa-sun';});}}
|
| 254 |
+
const lg=localStorage.getItem('tiq_lang');if(lg&&lg!=='en'){const btn=document.querySelector(`.lang-btn[onclick*="'${lg}'"]`);if(btn)setLang(lg,btn);}
|
| 255 |
+
}catch(e){}
|
| 256 |
+
renderGrid();
|
| 257 |
+
})();
|
| 258 |
+
</script>
|
| 259 |
+
<script src="/static/trackiq-api.js"></script>
|
| 260 |
+
|
| 261 |
+
<!-- ── Modal lecteur vidéo ── -->
|
| 262 |
+
<div id="vidOverlay" style="display:none;position:fixed;inset:0;z-index:1000;
|
| 263 |
+
background:rgba(0,0,0,0.88);backdrop-filter:blur(8px);
|
| 264 |
+
align-items:center;justify-content:center;flex-direction:column;gap:12px;">
|
| 265 |
+
|
| 266 |
+
<!-- Header -->
|
| 267 |
+
<div style="display:flex;align-items:center;justify-content:space-between;
|
| 268 |
+
width:min(90vw,860px);padding:0 4px;">
|
| 269 |
+
<span id="overlayName" style="color:#fff;font-size:13px;font-weight:500;
|
| 270 |
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:70%;"></span>
|
| 271 |
+
<div style="display:flex;gap:8px;">
|
| 272 |
+
<button onclick="dlOverlay()" style="background:rgba(255,255,255,0.12);color:#fff;
|
| 273 |
+
border:none;border-radius:7px;padding:6px 13px;font-size:12px;cursor:pointer;
|
| 274 |
+
font-family:'DM Sans',sans-serif;display:flex;align-items:center;gap:5px;">
|
| 275 |
+
<i class="fas fa-download"></i> Download
|
| 276 |
+
</button>
|
| 277 |
+
<button onclick="closeOverlay()" style="background:rgba(220,38,38,0.7);color:#fff;
|
| 278 |
+
border:none;border-radius:7px;padding:6px 13px;font-size:12px;cursor:pointer;
|
| 279 |
+
font-family:'DM Sans',sans-serif;display:flex;align-items:center;gap:5px;">
|
| 280 |
+
<i class="fas fa-times"></i> Close
|
| 281 |
+
</button>
|
| 282 |
+
</div>
|
| 283 |
+
</div>
|
| 284 |
+
|
| 285 |
+
<!-- Vidéo -->
|
| 286 |
+
<video id="overlayPlayer" controls autoplay
|
| 287 |
+
style="width:min(90vw,860px);max-height:75vh;border-radius:10px;
|
| 288 |
+
background:#000;object-fit:contain;box-shadow:0 20px 60px rgba(0,0,0,0.6);">
|
| 289 |
+
</video>
|
| 290 |
+
|
| 291 |
+
<!-- Hint ESC -->
|
| 292 |
+
<span style="color:rgba(255,255,255,0.3);font-size:10px;">Press ESC to close</span>
|
| 293 |
+
</div>
|
| 294 |
+
</body>
|
| 295 |
+
</html>
|
TRAFFIC_ROAD_APP/templates/home.html
ADDED
|
@@ -0,0 +1,684 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" data-theme="light">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>TrackIQ — Analyze</title>
|
| 7 |
+
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
|
| 8 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
| 9 |
+
<style>
|
| 10 |
+
:root{--bg:#f0f4f8;--sur:#fff;--sur2:#f6f8fb;--bd:rgba(0,0,0,0.07);--sh:0 2px 16px rgba(0,0,0,0.09);--shlg:0 8px 36px rgba(0,0,0,0.13);--t:#0f1923;--t2:#5a6a7a;--t3:#9aaab8;--ac:#880000;--ac2:#660000;--acl:rgba(136,0,0,0.07);--gr:#16a34a;--sw:60px;--r:14px;--rs:9px;}
|
| 11 |
+
[data-theme="dark"]{--bg:#0c1117;--sur:#151c25;--sur2:#1c2531;--bd:rgba(255,255,255,0.07);--sh:0 2px 16px rgba(0,0,0,0.45);--t:#e8edf2;--t2:#7a8fa0;--t3:#3a4a5a;}
|
| 12 |
+
*{box-sizing:border-box;margin:0;padding:0;}::-webkit-scrollbar{width:0;}
|
| 13 |
+
body{font-family:'DM Sans',sans-serif;background:var(--bg);color:var(--t);font-size:13px;display:flex;min-height:100vh;}
|
| 14 |
+
|
| 15 |
+
/* Sidebar */
|
| 16 |
+
.sb{width:var(--sw);background:var(--sur);border-right:1px solid var(--bd);display:flex;flex-direction:column;align-items:center;padding:14px 0;gap:4px;position:fixed;top:0;left:0;bottom:0;z-index:200;}
|
| 17 |
+
.sbl{width:34px;height:34px;background:var(--ac);border-radius:8px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px;margin-bottom:10px;text-decoration:none;}
|
| 18 |
+
.sbsep{width:26px;height:1px;background:var(--bd);margin:3px 0;}
|
| 19 |
+
.sbb{width:38px;height:38px;border-radius:9px;display:flex;align-items:center;justify-content:center;color:var(--t3);font-size:14px;cursor:pointer;transition:all 0.2s;text-decoration:none;border:none;background:none;position:relative;}
|
| 20 |
+
.sbb:hover{background:var(--sur2);color:var(--t);}.sbb.on{background:var(--acl);color:var(--ac);}
|
| 21 |
+
.sbb .tip{position:absolute;left:44px;top:50%;transform:translateY(-50%);background:var(--t);color:var(--sur);font-size:10px;padding:3px 7px;border-radius:4px;white-space:nowrap;opacity:0;pointer-events:none;z-index:300;}.sbb:hover .tip{opacity:1;}
|
| 22 |
+
.sbsp{flex:1;}
|
| 23 |
+
|
| 24 |
+
/* Layout */
|
| 25 |
+
.pw{margin-left:var(--sw);flex:1;display:flex;flex-direction:column;}
|
| 26 |
+
.topb{height:50px;background:var(--sur);border-bottom:1px solid var(--bd);display:flex;align-items:center;justify-content:space-between;padding:0 20px;position:sticky;top:0;z-index:100;}
|
| 27 |
+
.topl{display:flex;align-items:center;gap:9px;}
|
| 28 |
+
.bn{font-size:14px;font-weight:600;}.bn b{color:var(--ac);}
|
| 29 |
+
.topr{display:flex;align-items:center;gap:6px;}
|
| 30 |
+
.lg{display:flex;background:var(--sur2);border-radius:5px;padding:2px;border:1px solid var(--bd);}
|
| 31 |
+
.lb{font-size:10px;font-weight:500;padding:2px 8px;border:none;background:none;cursor:pointer;border-radius:3px;color:var(--t2);font-family:'DM Sans',sans-serif;transition:all 0.2s;}.lb.on{background:var(--ac);color:#fff;}
|
| 32 |
+
.ib{width:28px;height:28px;border-radius:5px;border:1px solid var(--bd);background:var(--sur);cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--t2);font-size:11px;}
|
| 33 |
+
.ib:hover{background:var(--sur2);}
|
| 34 |
+
|
| 35 |
+
main{flex:1;padding:14px 16px;display:grid;grid-template-columns:340px 1fr;gap:12px;max-width:1400px;width:100%;margin:0 auto;}
|
| 36 |
+
|
| 37 |
+
/* Card */
|
| 38 |
+
.card{background:var(--sur);border-radius:var(--r);border:1px solid var(--bd);box-shadow:var(--sh);padding:14px 16px;}
|
| 39 |
+
.ct{font-size:12px;font-weight:500;margin-bottom:11px;display:flex;align-items:center;gap:6px;}
|
| 40 |
+
.ct i{color:var(--ac);}
|
| 41 |
+
|
| 42 |
+
/* Source tabs */
|
| 43 |
+
.stabs{display:flex;gap:3px;background:var(--sur2);border-radius:7px;padding:3px;border:1px solid var(--bd);margin-bottom:11px;}
|
| 44 |
+
.stab{flex:1;padding:6px 4px;border-radius:5px;font-size:11px;cursor:pointer;color:var(--t2);border:none;background:none;font-family:'DM Sans',sans-serif;transition:all 0.2s;display:flex;align-items:center;justify-content:center;gap:4px;}
|
| 45 |
+
.stab.on{background:var(--sur);color:var(--t);box-shadow:0 1px 4px rgba(0,0,0,0.1);font-weight:500;}
|
| 46 |
+
|
| 47 |
+
/* Upload zone */
|
| 48 |
+
.uzone{border:2px dashed var(--bd);border-radius:var(--rs);min-height:110px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;cursor:pointer;transition:all 0.2s;padding:14px;position:relative;overflow:hidden;}
|
| 49 |
+
.uzone:hover,.uzone.drag{border-color:var(--ac);background:var(--acl);}
|
| 50 |
+
.uzone.has{border-style:solid;border-color:var(--ac);}
|
| 51 |
+
.uzone i{font-size:22px;color:var(--t3);}
|
| 52 |
+
.uzone.has i{color:var(--ac);}
|
| 53 |
+
.uzone p{font-size:11px;color:var(--t2);text-align:center;}
|
| 54 |
+
.uzone input{position:absolute;inset:0;opacity:0;cursor:pointer;}
|
| 55 |
+
.fname{font-size:11px;font-weight:500;color:var(--ac);display:none;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
| 56 |
+
.prog{display:none;background:var(--sur2);border-radius:3px;height:4px;margin-top:7px;overflow:hidden;}
|
| 57 |
+
.progb{height:100%;background:var(--ac);width:0%;transition:width 0.3s;}
|
| 58 |
+
.progt{font-size:10px;color:var(--t2);margin-top:4px;display:none;}
|
| 59 |
+
|
| 60 |
+
/* URL */
|
| 61 |
+
.urow{display:flex;gap:6px;}
|
| 62 |
+
.uin{flex:1;padding:7px 9px;border:1px solid var(--bd);border-radius:7px;font-size:12px;background:var(--sur2);color:var(--t);font-family:'DM Sans',sans-serif;outline:none;}
|
| 63 |
+
.uin:focus{border-color:var(--ac);}
|
| 64 |
+
.uin::placeholder{color:var(--t3);}
|
| 65 |
+
.bsm{padding:7px 12px;background:var(--ac);color:#fff;border:none;border-radius:7px;font-size:12px;cursor:pointer;font-family:'DM Sans',sans-serif;}
|
| 66 |
+
|
| 67 |
+
/* Webcam */
|
| 68 |
+
.wcbox{background:#000;border-radius:8px;overflow:hidden;aspect-ratio:16/9;position:relative;margin-bottom:10px;}
|
| 69 |
+
.wcbox video,.wcbox img{width:100%;height:100%;object-fit:cover;display:block;}
|
| 70 |
+
.wcph{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;color:rgba(255,255,255,0.3);font-size:11px;}
|
| 71 |
+
.wcph i{font-size:22px;}
|
| 72 |
+
.recbdg{position:absolute;top:7px;left:8px;display:none;align-items:center;gap:4px;background:rgba(220,38,38,0.85);color:#fff;font-size:10px;padding:2px 8px;border-radius:10px;}
|
| 73 |
+
@keyframes blink{0%,100%{opacity:1}50%{opacity:0.3}}
|
| 74 |
+
.recbdg i{font-size:8px;animation:blink 1s infinite;}
|
| 75 |
+
|
| 76 |
+
/* Classes */
|
| 77 |
+
.clshd{display:flex;align-items:center;justify-content:space-between;margin-bottom:7px;}
|
| 78 |
+
.clshd span{font-size:11px;color:var(--t2);}
|
| 79 |
+
.clshd button{font-size:11px;color:var(--ac);background:none;border:none;cursor:pointer;font-family:'DM Sans',sans-serif;}
|
| 80 |
+
.clsg{display:grid;grid-template-columns:1fr 1fr;gap:5px;}
|
| 81 |
+
.clsi{display:flex;align-items:center;gap:5px;padding:5px 8px;border-radius:6px;border:1px solid var(--bd);cursor:pointer;transition:all 0.2s;font-size:11px;background:var(--sur2);}
|
| 82 |
+
.clsi:hover{border-color:var(--ac);}.clsi.on{border-color:var(--ac);background:var(--acl);}
|
| 83 |
+
.cldot{width:8px;height:8px;border-radius:3px;flex-shrink:0;}
|
| 84 |
+
|
| 85 |
+
/* Buttons */
|
| 86 |
+
.runbtn{width:100%;padding:10px;background:var(--ac);color:#fff;border:none;border-radius:9px;font-size:13px;font-weight:500;cursor:pointer;font-family:'DM Sans',sans-serif;display:flex;align-items:center;justify-content:center;gap:7px;transition:all 0.2s;margin-top:4px;}
|
| 87 |
+
.runbtn:hover{background:var(--ac2);}.runbtn:disabled{opacity:0.4;cursor:not-allowed;}
|
| 88 |
+
.rstbtn{width:100%;padding:8px;background:var(--sur2);color:var(--t2);border:1px solid var(--bd);border-radius:9px;font-size:12px;cursor:pointer;font-family:'DM Sans',sans-serif;display:flex;align-items:center;justify-content:center;gap:6px;transition:all 0.2s;}
|
| 89 |
+
.rstbtn:hover{border-color:var(--ac);color:var(--ac);}
|
| 90 |
+
|
| 91 |
+
/* Right col */
|
| 92 |
+
.rc{display:flex;flex-direction:column;gap:11px;}
|
| 93 |
+
|
| 94 |
+
/* Big panel */
|
| 95 |
+
.bp{background:var(--sur);border-radius:var(--r);border:1px solid var(--bd);box-shadow:var(--shlg);overflow:hidden;}
|
| 96 |
+
.bph{padding:10px 14px;border-bottom:1px solid var(--bd);display:flex;align-items:center;justify-content:space-between;}
|
| 97 |
+
.bpht{font-size:12px;font-weight:500;display:flex;align-items:center;gap:6px;}
|
| 98 |
+
.bpht i{color:var(--ac);}
|
| 99 |
+
.bpb{position:relative;background:#080e15;height:420px;}
|
| 100 |
+
|
| 101 |
+
/* Video dans le panel */
|
| 102 |
+
.bpb video,.bpb img#wcYoloImg{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;display:none;}
|
| 103 |
+
|
| 104 |
+
/* HUD top-left */
|
| 105 |
+
.hud{position:absolute;top:10px;left:11px;color:#fff;font-family:'DM Mono',monospace;font-size:11px;line-height:1.85;text-shadow:1px 1px 4px rgba(0,0,0,0.98);pointer-events:none;display:none;z-index:4;}
|
| 106 |
+
.hudf{font-size:9px;color:rgba(255,255,255,0.4);margin-bottom:2px;}
|
| 107 |
+
|
| 108 |
+
/* Count overlay top-right */
|
| 109 |
+
.cov{position:absolute;top:10px;right:11px;display:none;flex-direction:column;gap:4px;pointer-events:none;z-index:4;}
|
| 110 |
+
.cbdg{display:flex;flex-direction:column;background:rgba(0,0,0,0.6);backdrop-filter:blur(8px);border-left:3px solid transparent;border-radius:5px;padding:4px 9px;min-width:115px;}
|
| 111 |
+
.cbdg .cn{font-size:13px;font-weight:600;line-height:1.2;}
|
| 112 |
+
.cbdg .cu{font-size:9px;color:#ccc;line-height:1.2;}
|
| 113 |
+
|
| 114 |
+
/* No-object banner */
|
| 115 |
+
.noobj{display:none;position:absolute;bottom:14px;left:50%;transform:translateX(-50%);z-index:5;
|
| 116 |
+
background:rgba(220,38,38,0.9);color:#fff;font-size:12px;font-weight:500;
|
| 117 |
+
padding:6px 16px;border-radius:20px;align-items:center;gap:7px;white-space:nowrap;}
|
| 118 |
+
|
| 119 |
+
/* Empty state */
|
| 120 |
+
.es{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:9px;color:rgba(255,255,255,0.18);}
|
| 121 |
+
.es i{font-size:38px;}.es p{font-size:12px;}
|
| 122 |
+
|
| 123 |
+
/* Stats row */
|
| 124 |
+
.sr{display:grid;grid-template-columns:repeat(4,1fr);gap:9px;}
|
| 125 |
+
.sc{background:var(--sur);border-radius:var(--rs);border:1px solid var(--bd);box-shadow:var(--sh);padding:10px 12px;}
|
| 126 |
+
.sc .sl{font-size:10px;color:var(--t2);margin-bottom:3px;}
|
| 127 |
+
.sc .sv{font-size:18px;font-weight:300;letter-spacing:-0.5px;}
|
| 128 |
+
.sc .su{font-size:10px;color:var(--t2);}
|
| 129 |
+
|
| 130 |
+
/* Download bar */
|
| 131 |
+
.dlb{display:flex;align-items:center;justify-content:space-between;background:var(--sur);border-radius:var(--rs);border:1px solid var(--bd);box-shadow:var(--sh);padding:9px 13px;}
|
| 132 |
+
.dlb span{font-size:11px;color:var(--t2);}
|
| 133 |
+
.btndl{display:flex;align-items:center;gap:5px;background:var(--ac);color:#fff;border:none;border-radius:7px;padding:7px 13px;font-size:12px;cursor:pointer;font-family:'DM Sans',sans-serif;}
|
| 134 |
+
.btndl:hover{background:var(--ac2);}.btndl:disabled{opacity:0.4;cursor:not-allowed;background:var(--t3);}
|
| 135 |
+
</style>
|
| 136 |
+
</head>
|
| 137 |
+
<body>
|
| 138 |
+
|
| 139 |
+
<nav class="sb">
|
| 140 |
+
<a href="/" class="sbl"><i class="fas fa-satellite-dish"></i></a>
|
| 141 |
+
<div class="sbsep"></div>
|
| 142 |
+
<a href="/home" class="sbb on"><i class="fas fa-play-circle"></i><span class="tip">Analyze</span></a>
|
| 143 |
+
<a href="/dashboard" class="sbb"><i class="fas fa-chart-line"></i><span class="tip">Dashboard</span></a>
|
| 144 |
+
<a href="/logs" class="sbb"><i class="fas fa-list-ul"></i><span class="tip">Logs</span></a>
|
| 145 |
+
<a href="/history" class="sbb"><i class="fas fa-film"></i><span class="tip">History</span></a>
|
| 146 |
+
<div class="sbsp"></div>
|
| 147 |
+
<button class="sbb" onclick="toggleTheme()"><i class="fas fa-moon" id="ti"></i><span class="tip">Theme</span></button>
|
| 148 |
+
</nav>
|
| 149 |
+
|
| 150 |
+
<div class="pw">
|
| 151 |
+
<header class="topb">
|
| 152 |
+
<div class="topl">
|
| 153 |
+
<span class="bn">Track<b>IQ</b></span>
|
| 154 |
+
<span style="color:var(--bd);font-size:16px;">|</span>
|
| 155 |
+
<span style="font-size:12px;font-weight:500;color:var(--t2);">Video Analysis</span>
|
| 156 |
+
</div>
|
| 157 |
+
<div class="topr">
|
| 158 |
+
<div class="lg">
|
| 159 |
+
<button class="lb on" onclick="setLang('en',this)">EN</button>
|
| 160 |
+
<button class="lb" onclick="setLang('fr',this)">FR</button>
|
| 161 |
+
<button class="lb" onclick="setLang('wo',this)">WO</button>
|
| 162 |
+
</div>
|
| 163 |
+
<button class="ib" onclick="toggleTheme()"><i class="fas fa-moon" id="ti2"></i></button>
|
| 164 |
+
</div>
|
| 165 |
+
</header>
|
| 166 |
+
|
| 167 |
+
<main>
|
| 168 |
+
<!-- Colonne gauche -->
|
| 169 |
+
<div style="display:flex;flex-direction:column;gap:11px;">
|
| 170 |
+
|
| 171 |
+
<!-- Input source -->
|
| 172 |
+
<div class="card">
|
| 173 |
+
<div class="ct"><i class="fas fa-video"></i> Input source</div>
|
| 174 |
+
<div class="stabs">
|
| 175 |
+
<button class="stab on" onclick="switchSrc('upload',this)"><i class="fas fa-upload"></i> Video</button>
|
| 176 |
+
<button class="stab" onclick="switchSrc('url',this)"><i class="fas fa-link"></i> URL</button>
|
| 177 |
+
<button class="stab" onclick="switchSrc('webcam',this)"><i class="fas fa-camera"></i> Webcam</button>
|
| 178 |
+
</div>
|
| 179 |
+
|
| 180 |
+
<!-- Upload -->
|
| 181 |
+
<div id="src-upload">
|
| 182 |
+
<label class="uzone" id="uzone">
|
| 183 |
+
<i class="fas fa-cloud-upload-alt"></i>
|
| 184 |
+
<p>Drop video here or click to browse</p>
|
| 185 |
+
<p style="font-size:10px;color:var(--t3);">MP4, AVI, MOV — max 500 MB</p>
|
| 186 |
+
<span class="fname" id="fname"></span>
|
| 187 |
+
<input type="file" id="fileInput" accept="video/*" onchange="onFile(event)">
|
| 188 |
+
</label>
|
| 189 |
+
<div class="prog" id="progWrap"><div class="progb" id="progBar"></div></div>
|
| 190 |
+
<div class="progt" id="progTxt"></div>
|
| 191 |
+
</div>
|
| 192 |
+
|
| 193 |
+
<!-- URL -->
|
| 194 |
+
<div id="src-url" style="display:none;">
|
| 195 |
+
<div class="urow">
|
| 196 |
+
<input type="text" class="uin" id="urlIn" placeholder="https://… or rtsp://…">
|
| 197 |
+
<button class="bsm" onclick="loadUrl()">Load</button>
|
| 198 |
+
</div>
|
| 199 |
+
</div>
|
| 200 |
+
|
| 201 |
+
<!-- Webcam -->
|
| 202 |
+
<div id="src-webcam" style="display:none;">
|
| 203 |
+
<!-- Mini preview : flux YOLO annoté -->
|
| 204 |
+
<div class="wcbox" id="wcbox">
|
| 205 |
+
<img id="wcMiniYolo" alt="YOLO stream" style="display:none;">
|
| 206 |
+
<div class="wcph" id="wcPh"><i class="fas fa-camera-slash"></i><span>Click Start</span></div>
|
| 207 |
+
<div class="recbdg" id="recBadge"><i class="fas fa-circle"></i> LIVE</div>
|
| 208 |
+
</div>
|
| 209 |
+
<!-- Bouton stop webcam seulement (plus de bouton Enregistrer) -->
|
| 210 |
+
<button class="runbtn" id="wcBtn" onclick="startWc()">
|
| 211 |
+
<i class="fas fa-circle" style="color:#f87171;font-size:8px;"></i> Start webcam
|
| 212 |
+
</button>
|
| 213 |
+
</div>
|
| 214 |
+
</div>
|
| 215 |
+
|
| 216 |
+
<!-- Detection classes -->
|
| 217 |
+
<div class="card">
|
| 218 |
+
<div class="ct"><i class="fas fa-tags"></i> Detection classes</div>
|
| 219 |
+
<div class="clshd">
|
| 220 |
+
<span id="clsCnt">0 selected</span>
|
| 221 |
+
<button onclick="toggleAll()">Select all</button>
|
| 222 |
+
</div>
|
| 223 |
+
<div class="clsg" id="clsGrid"></div>
|
| 224 |
+
</div>
|
| 225 |
+
|
| 226 |
+
<button class="runbtn" id="runBtn" onclick="runAnalysis()" disabled>
|
| 227 |
+
<i class="fas fa-play"></i> Run Analysis
|
| 228 |
+
</button>
|
| 229 |
+
<button class="rstbtn" onclick="resetAll()">
|
| 230 |
+
<i class="fas fa-rotate-right"></i> Reset
|
| 231 |
+
</button>
|
| 232 |
+
</div>
|
| 233 |
+
|
| 234 |
+
<!-- Colonne droite -->
|
| 235 |
+
<div class="rc">
|
| 236 |
+
|
| 237 |
+
<!-- Big result panel -->
|
| 238 |
+
<div class="bp">
|
| 239 |
+
<div class="bph">
|
| 240 |
+
<div class="bpht"><i class="fas fa-film"></i> Result</div>
|
| 241 |
+
<span style="font-size:11px;color:var(--t2);" id="srcLabel"></span>
|
| 242 |
+
</div>
|
| 243 |
+
<div class="bpb" id="bigBody">
|
| 244 |
+
<!-- Vidéo annotée (upload/url) -->
|
| 245 |
+
<video id="resultVid" controls></video>
|
| 246 |
+
<!-- Flux YOLO webcam (grand) -->
|
| 247 |
+
<img id="wcYoloImg" alt="YOLO stream">
|
| 248 |
+
<!-- HUD top-left -->
|
| 249 |
+
<div class="hud" id="hudEl">
|
| 250 |
+
<div class="hudf" id="hudFr">—</div>
|
| 251 |
+
<div id="hudCo">—</div>
|
| 252 |
+
</div>
|
| 253 |
+
<!-- Compteurs top-right -->
|
| 254 |
+
<div class="cov" id="cOverlay"></div>
|
| 255 |
+
<!-- Banner no-object -->
|
| 256 |
+
<div class="noobj" id="noObjBanner">
|
| 257 |
+
<i class="fas fa-eye-slash"></i> No object detected
|
| 258 |
+
</div>
|
| 259 |
+
<!-- Canvas caché pour enregistrement webcam -->
|
| 260 |
+
<canvas id="wcCanvas" style="display:none;"></canvas>
|
| 261 |
+
<!-- Empty state -->
|
| 262 |
+
<div class="es" id="emptyState">
|
| 263 |
+
<i class="fas fa-film"></i>
|
| 264 |
+
<p>Result will appear here</p>
|
| 265 |
+
</div>
|
| 266 |
+
</div>
|
| 267 |
+
</div>
|
| 268 |
+
|
| 269 |
+
<!-- Stats -->
|
| 270 |
+
<div class="sr">
|
| 271 |
+
<div class="sc"><div class="sl">Frames</div><div class="sv" id="stF">—</div></div>
|
| 272 |
+
<div class="sc"><div class="sl">Objects</div><div class="sv" id="stO">—</div></div>
|
| 273 |
+
<div class="sc"><div class="sl">FPS</div><div class="sv" id="stFps">—</div></div>
|
| 274 |
+
<div class="sc"><div class="sl">Duration</div><div class="sv" id="stD">—</div></div>
|
| 275 |
+
</div>
|
| 276 |
+
|
| 277 |
+
<!-- Download -->
|
| 278 |
+
<div class="dlb">
|
| 279 |
+
<span id="dlName">No result yet</span>
|
| 280 |
+
<button class="btndl" id="dlBtn" disabled onclick="doDownload()">
|
| 281 |
+
<i class="fas fa-download"></i> Download
|
| 282 |
+
</button>
|
| 283 |
+
</div>
|
| 284 |
+
|
| 285 |
+
</div>
|
| 286 |
+
</main>
|
| 287 |
+
</div>
|
| 288 |
+
|
| 289 |
+
<script>
|
| 290 |
+
/* ── Theme ── */
|
| 291 |
+
function toggleTheme(){
|
| 292 |
+
const d=document.documentElement,dk=d.getAttribute('data-theme')==='dark';
|
| 293 |
+
d.setAttribute('data-theme',dk?'light':'dark');
|
| 294 |
+
['ti','ti2'].forEach(x=>{const e=document.getElementById(x);if(e)e.className=dk?'fas fa-moon':'fas fa-sun';});
|
| 295 |
+
try{localStorage.setItem('tiq_theme',dk?'light':'dark');}catch(e){}
|
| 296 |
+
}
|
| 297 |
+
function setLang(l,btn){
|
| 298 |
+
document.querySelectorAll('.lb').forEach(b=>b.classList.remove('on'));btn.classList.add('on');
|
| 299 |
+
try{localStorage.setItem('tiq_lang',l);}catch(e){}
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
/* ── Classes ── */
|
| 303 |
+
const CLASSES=[
|
| 304 |
+
{name:'Vehicle',color:'#3b82f6'},{name:'Motorcycle',color:'#10b981'},
|
| 305 |
+
{name:'Truck',color:'#f59e0b'},{name:'Bus',color:'#8b5cf6'},
|
| 306 |
+
{name:'Human',color:'#ec4899'},{name:'Bicycle',color:'#06b6d4'},
|
| 307 |
+
{name:'Traffic light',color:'#ef4444'},{name:'Road sign',color:'#f97316'},
|
| 308 |
+
];
|
| 309 |
+
let sel=new Set(['Human']);
|
| 310 |
+
|
| 311 |
+
function buildCls(){
|
| 312 |
+
const g=document.getElementById('clsGrid'); g.innerHTML='';
|
| 313 |
+
CLASSES.forEach(c=>{
|
| 314 |
+
const d=document.createElement('div');
|
| 315 |
+
d.className='clsi'+(sel.has(c.name)?' on':'');
|
| 316 |
+
d.innerHTML=`<span class="cldot" style="background:${c.color}"></span>${c.name}`;
|
| 317 |
+
d.onclick=()=>{sel.has(c.name)?sel.delete(c.name):sel.add(c.name);d.classList.toggle('on');upCls();};
|
| 318 |
+
g.appendChild(d);
|
| 319 |
+
});
|
| 320 |
+
upCls();
|
| 321 |
+
}
|
| 322 |
+
function toggleAll(){sel.size===CLASSES.length?sel.clear():CLASSES.forEach(c=>sel.add(c.name));buildCls();}
|
| 323 |
+
function upCls(){document.getElementById('clsCnt').textContent=sel.size+' selected';}
|
| 324 |
+
buildCls();
|
| 325 |
+
|
| 326 |
+
/* ── State ── */
|
| 327 |
+
let curSrc='upload', vidFile=null, vidUrl=null, wcStream=null;
|
| 328 |
+
let _jobId=null, _sse=null, _wcPollIv=null, _annotatedUrl=null;
|
| 329 |
+
|
| 330 |
+
/* ── Source switch ── */
|
| 331 |
+
function switchSrc(s,btn){
|
| 332 |
+
curSrc=s;
|
| 333 |
+
document.querySelectorAll('.stab').forEach(b=>b.classList.remove('on'));btn.classList.add('on');
|
| 334 |
+
['upload','url','webcam'].forEach(id=>{
|
| 335 |
+
const el=document.getElementById('src-'+id);if(el)el.style.display='none';
|
| 336 |
+
});
|
| 337 |
+
document.getElementById('src-'+s).style.display='block';
|
| 338 |
+
checkReady();
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
/* ── File upload ── */
|
| 342 |
+
function onFile(e){
|
| 343 |
+
const f=e.target.files[0];if(!f)return;
|
| 344 |
+
vidFile=f;
|
| 345 |
+
const fn=document.getElementById('fname');fn.textContent=f.name;fn.style.display='block';
|
| 346 |
+
document.getElementById('uzone').classList.add('has');
|
| 347 |
+
checkReady();
|
| 348 |
+
}
|
| 349 |
+
function loadUrl(){
|
| 350 |
+
const u=document.getElementById('urlIn').value.trim();if(!u)return;
|
| 351 |
+
vidUrl=u;checkReady();
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
/* ── Run Analysis ── */
|
| 355 |
+
window.runAnalysis=async function(){
|
| 356 |
+
const btn=document.getElementById('runBtn');
|
| 357 |
+
btn.disabled=true;
|
| 358 |
+
btn.innerHTML='<i class="fas fa-spinner fa-spin"></i> Processing…';
|
| 359 |
+
document.getElementById('progWrap').style.display='block';
|
| 360 |
+
document.getElementById('progTxt').style.display='block';
|
| 361 |
+
document.getElementById('progTxt').textContent='Uploading…';
|
| 362 |
+
document.getElementById('progBar').style.width='5%';
|
| 363 |
+
document.getElementById('emptyState').style.display='none';
|
| 364 |
+
|
| 365 |
+
/* Upload si pas encore fait */
|
| 366 |
+
if(!_jobId){
|
| 367 |
+
if(!vidFile&&!vidUrl){
|
| 368 |
+
btn.disabled=false;btn.innerHTML='<i class="fas fa-play"></i> Run Analysis';return;
|
| 369 |
+
}
|
| 370 |
+
const fd=new FormData();
|
| 371 |
+
if(vidFile) fd.append('video',vidFile);
|
| 372 |
+
try{
|
| 373 |
+
const r=await fetch('/api/upload',{method:'POST',body:fd});
|
| 374 |
+
const d=await r.json();
|
| 375 |
+
_jobId=d.job_id;
|
| 376 |
+
}catch(err){
|
| 377 |
+
console.error('[TrackIQ] Upload:',err);
|
| 378 |
+
btn.disabled=false;btn.innerHTML='<i class="fas fa-play"></i> Run Analysis';return;
|
| 379 |
+
}
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
document.getElementById('progTxt').textContent='Starting detection…';
|
| 383 |
+
document.getElementById('progBar').style.width='10%';
|
| 384 |
+
|
| 385 |
+
try{
|
| 386 |
+
await fetch('/api/run',{method:'POST',headers:{'Content-Type':'application/json'},
|
| 387 |
+
body:JSON.stringify({job_id:_jobId,classes:[...sel],model:'yolo11n'})});
|
| 388 |
+
}catch(err){console.error('[TrackIQ] Run:',err);return;}
|
| 389 |
+
|
| 390 |
+
/* SSE */
|
| 391 |
+
if(_sse)_sse.close();
|
| 392 |
+
_sse=new EventSource(`/api/stream/${_jobId}`);
|
| 393 |
+
_sse.onmessage=(e)=>{
|
| 394 |
+
let d;try{d=JSON.parse(e.data);}catch{return;}
|
| 395 |
+
|
| 396 |
+
if(d.event==='done'){
|
| 397 |
+
_sse.close();
|
| 398 |
+
_showBanner(false);
|
| 399 |
+
document.getElementById('progBar').style.width='100%';
|
| 400 |
+
document.getElementById('progTxt').textContent='Done!';
|
| 401 |
+
btn.disabled=false;btn.innerHTML='<i class="fas fa-play"></i> Run Analysis';
|
| 402 |
+
|
| 403 |
+
/* ✅ Affiche la vidéo annotée */
|
| 404 |
+
const rv=document.getElementById('resultVid');
|
| 405 |
+
rv.src=`/api/video/${_jobId}`;
|
| 406 |
+
rv.style.display='block';
|
| 407 |
+
rv.load();
|
| 408 |
+
document.getElementById('srcLabel').textContent=vidFile?vidFile.name:'video';
|
| 409 |
+
document.getElementById('hudEl').style.display='block';
|
| 410 |
+
document.getElementById('cOverlay').style.display='flex';
|
| 411 |
+
_annotatedUrl=`/api/video/${_jobId}`;
|
| 412 |
+
|
| 413 |
+
/* Stats */
|
| 414 |
+
const s=d.stats||{};
|
| 415 |
+
_set('stF',s.total_frames??'—');_set('stO',s.total_unique??'—');
|
| 416 |
+
_set('stFps',s.fps?s.fps.toFixed(1):'—');_set('stD',s.duration_s?s.duration_s+'s':'—');
|
| 417 |
+
_showCounts(s.unique_counts||{});
|
| 418 |
+
_set('dlName',vidFile?vidFile.name:'annotated.mp4');
|
| 419 |
+
document.getElementById('dlBtn').removeAttribute('disabled');
|
| 420 |
+
|
| 421 |
+
/* Sauvegarde localStorage */
|
| 422 |
+
try{
|
| 423 |
+
localStorage.setItem('tiq_job_id',_jobId);
|
| 424 |
+
localStorage.setItem('tiq_video_url',`/api/video/${_jobId}`);
|
| 425 |
+
localStorage.setItem('tiq_video_name',s.video_name||'');
|
| 426 |
+
localStorage.setItem('tiq_stats',JSON.stringify(s));
|
| 427 |
+
}catch(ex){}
|
| 428 |
+
return;
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
/* Progression */
|
| 432 |
+
if(d.pct!=null){
|
| 433 |
+
document.getElementById('progBar').style.width=d.pct+'%';
|
| 434 |
+
document.getElementById('progTxt').textContent=`Processing… ${d.pct.toFixed(0)}%`;
|
| 435 |
+
}
|
| 436 |
+
_showBanner(d.no_objects===true);
|
| 437 |
+
if(d.hud){
|
| 438 |
+
_set('hudFr',d.hud.frame_str);
|
| 439 |
+
const hco=document.getElementById('hudCo');
|
| 440 |
+
if(hco) hco.innerHTML=Object.entries(d.hud.counts||{})
|
| 441 |
+
.map(([k,v])=>`${k} = ${v}`).join('<br>')||'No object';
|
| 442 |
+
}
|
| 443 |
+
if(d.unique_counts) _showCounts(d.unique_counts);
|
| 444 |
+
};
|
| 445 |
+
_sse.onerror=()=>_sse.close();
|
| 446 |
+
};
|
| 447 |
+
|
| 448 |
+
/* ── Webcam ── */
|
| 449 |
+
let _wcFrameIv=null, _wcHiddenVideo=null, _wcCanvas2=null;
|
| 450 |
+
|
| 451 |
+
async function startWc(){
|
| 452 |
+
try{
|
| 453 |
+
wcStream=await navigator.mediaDevices.getUserMedia({video:{width:640,height:480},audio:false});
|
| 454 |
+
}catch(e){alert('Webcam unavailable: '+e.message);return;}
|
| 455 |
+
|
| 456 |
+
document.getElementById('wcPh').style.display='none';
|
| 457 |
+
document.getElementById('recBadge').style.display='flex';
|
| 458 |
+
document.getElementById('runBtn').style.display='none';
|
| 459 |
+
|
| 460 |
+
const btn=document.getElementById('wcBtn');
|
| 461 |
+
btn.innerHTML='<i class="fas fa-stop" style="color:#f87171;font-size:9px;"></i> Stop webcam';
|
| 462 |
+
btn.onclick=stopWc;
|
| 463 |
+
|
| 464 |
+
/* Lance YOLO backend */
|
| 465 |
+
await fetch('/api/webcam/start',{method:'POST',headers:{'Content-Type':'application/json'},
|
| 466 |
+
body:JSON.stringify({classes:[...sel],model:'yolo11n'})}).catch(()=>{});
|
| 467 |
+
|
| 468 |
+
/* Setup hidden video + canvas pour capturer frames */
|
| 469 |
+
_wcHiddenVideo=document.createElement('video');
|
| 470 |
+
_wcHiddenVideo.srcObject=wcStream;
|
| 471 |
+
_wcHiddenVideo.setAttribute('playsinline','');
|
| 472 |
+
_wcHiddenVideo.muted=true;
|
| 473 |
+
_wcHiddenVideo.play();
|
| 474 |
+
_wcCanvas2=document.createElement('canvas');
|
| 475 |
+
_wcCanvas2.width=640;_wcCanvas2.height=480;
|
| 476 |
+
const ctx2=_wcCanvas2.getContext('2d');
|
| 477 |
+
|
| 478 |
+
/* Envoie une frame toutes les 200ms → reçoit JPEG annoté → l'affiche */
|
| 479 |
+
const miniImg=document.getElementById('wcMiniYolo');
|
| 480 |
+
const bigImg=document.getElementById('wcYoloImg');
|
| 481 |
+
miniImg.style.display='block';
|
| 482 |
+
bigImg.style.display='block';
|
| 483 |
+
|
| 484 |
+
_wcFrameIv=setInterval(async()=>{
|
| 485 |
+
if(!wcStream||!_wcHiddenVideo)return;
|
| 486 |
+
ctx2.drawImage(_wcHiddenVideo,0,0,640,480);
|
| 487 |
+
_wcCanvas2.toBlob(async(blob)=>{
|
| 488 |
+
if(!blob)return;
|
| 489 |
+
try{
|
| 490 |
+
const resp=await fetch('/api/webcam/frame',{method:'POST',
|
| 491 |
+
headers:{'Content-Type':'image/jpeg'},body:blob});
|
| 492 |
+
if(!resp.ok)return;
|
| 493 |
+
const url=URL.createObjectURL(await resp.blob());
|
| 494 |
+
miniImg.src=url;bigImg.src=url;
|
| 495 |
+
}catch(e){}
|
| 496 |
+
},'image/jpeg',0.75);
|
| 497 |
+
},200);
|
| 498 |
+
|
| 499 |
+
/* Grand panel droite */
|
| 500 |
+
document.getElementById('emptyState').style.display='none';
|
| 501 |
+
document.getElementById('resultVid').style.display='none';
|
| 502 |
+
document.getElementById('hudEl').style.display='block';
|
| 503 |
+
document.getElementById('cOverlay').style.display='flex';
|
| 504 |
+
document.getElementById('srcLabel').textContent='Webcam — LIVE';
|
| 505 |
+
|
| 506 |
+
/* Démarre l'enregistrement automatiquement */
|
| 507 |
+
setTimeout(_startRec, 800);
|
| 508 |
+
/* Polling stats toutes les 800ms */
|
| 509 |
+
clearInterval(_wcPollIv);
|
| 510 |
+
_wcPollIv=setInterval(_pollWc,800);
|
| 511 |
+
checkReady();
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
/* ── Recording webcam → grand Result ── */
|
| 515 |
+
let _recIv=null, _recChunks=[], _mediaRec=null;
|
| 516 |
+
|
| 517 |
+
function _startRec(){
|
| 518 |
+
const canvas=document.getElementById('wcCanvas');
|
| 519 |
+
const img=document.getElementById('wcYoloImg');
|
| 520 |
+
if(!canvas||!img)return;
|
| 521 |
+
canvas.width=640;canvas.height=480;
|
| 522 |
+
const ctx=canvas.getContext('2d');
|
| 523 |
+
_recIv=setInterval(()=>{
|
| 524 |
+
if(img.naturalWidth>0){canvas.width=img.naturalWidth;canvas.height=img.naturalHeight;ctx.drawImage(img,0,0);}
|
| 525 |
+
},40);
|
| 526 |
+
const mime=MediaRecorder.isTypeSupported('video/webm;codecs=vp9')?'video/webm;codecs=vp9':'video/webm';
|
| 527 |
+
_mediaRec=new MediaRecorder(canvas.captureStream(25),{mimeType:mime,videoBitsPerSecond:2000000});
|
| 528 |
+
_recChunks=[];
|
| 529 |
+
_mediaRec.ondataavailable=e=>{if(e.data.size>0)_recChunks.push(e.data);};
|
| 530 |
+
_mediaRec.onstop=_saveRec;
|
| 531 |
+
_mediaRec.start(500);
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
function _saveRec(){
|
| 535 |
+
clearInterval(_recIv);_recIv=null;
|
| 536 |
+
if(!_recChunks.length)return;
|
| 537 |
+
const blob=new Blob(_recChunks,{type:'video/webm'});
|
| 538 |
+
const url=URL.createObjectURL(blob);
|
| 539 |
+
_annotatedUrl=url;
|
| 540 |
+
/* Affiche dans le grand Result */
|
| 541 |
+
const rv=document.getElementById('resultVid');
|
| 542 |
+
rv.src=url;rv.style.display='block';rv.controls=true;
|
| 543 |
+
rv.onloadeddata=()=>rv.play().catch(()=>{});rv.load();
|
| 544 |
+
document.getElementById('srcLabel').textContent='Webcam — recorded';
|
| 545 |
+
document.getElementById('emptyState').style.display='none';
|
| 546 |
+
document.getElementById('hudEl').style.display='block';
|
| 547 |
+
/* Active download */
|
| 548 |
+
_set('dlName','webcam_trackiq.webm');
|
| 549 |
+
document.getElementById('dlBtn').removeAttribute('disabled');
|
| 550 |
+
document.getElementById('dlBtn').onclick=()=>{
|
| 551 |
+
const a=document.createElement('a');a.href=url;a.download='webcam_trackiq.webm';a.click();
|
| 552 |
+
};
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
function stopWc(){
|
| 556 |
+
/* 1. Arrête enregistrement → déclenche _saveRec → vidéo dans Result */
|
| 557 |
+
if(_mediaRec&&_mediaRec.state==='recording') _mediaRec.stop();
|
| 558 |
+
clearInterval(_wcFrameIv);_wcFrameIv=null;
|
| 559 |
+
if(_wcHiddenVideo){_wcHiddenVideo.srcObject=null;_wcHiddenVideo=null;}
|
| 560 |
+
_wcCanvas2=null;
|
| 561 |
+
if(wcStream){wcStream.getTracks().forEach(t=>t.stop());wcStream=null;}
|
| 562 |
+
fetch('/api/webcam/stop',{method:'POST'}).catch(()=>{});
|
| 563 |
+
clearInterval(_wcPollIv);_wcPollIv=null;
|
| 564 |
+
|
| 565 |
+
/* 2. Cache flux live */
|
| 566 |
+
const mi=document.getElementById('wcMiniYolo');mi.src='';mi.style.display='none';
|
| 567 |
+
const bi=document.getElementById('wcYoloImg');bi.src='';bi.style.display='none';
|
| 568 |
+
|
| 569 |
+
document.getElementById('wcPh').style.display='flex';
|
| 570 |
+
document.getElementById('recBadge').style.display='none';
|
| 571 |
+
document.getElementById('runBtn').style.display='flex';
|
| 572 |
+
document.getElementById('cOverlay').style.display='none';
|
| 573 |
+
document.getElementById('noObjBanner').style.display='none';
|
| 574 |
+
|
| 575 |
+
const btn=document.getElementById('wcBtn');
|
| 576 |
+
btn.innerHTML='<i class="fas fa-circle" style="color:#f87171;font-size:8px;"></i> Start webcam';
|
| 577 |
+
btn.onclick=startWc;
|
| 578 |
+
checkReady();
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
async function _pollWc(){
|
| 582 |
+
try{
|
| 583 |
+
const r=await fetch('/api/webcam/stats');
|
| 584 |
+
if(!r.ok)return;
|
| 585 |
+
const d=await r.json();
|
| 586 |
+
const fc=d.frame_counts||{};
|
| 587 |
+
const uc=d.unique_counts||{};
|
| 588 |
+
_set('hudFr',`F:${d.frame||0} ▶ LIVE`);
|
| 589 |
+
const hco=document.getElementById('hudCo');
|
| 590 |
+
if(hco) hco.innerHTML=Object.entries(fc).map(([k,v])=>`${k} = ${v}`).join('<br>')||
|
| 591 |
+
'<span style="color:#ef4444">No object</span>';
|
| 592 |
+
_showCountsWc(fc,uc);
|
| 593 |
+
_showBanner(!Object.keys(fc).length);
|
| 594 |
+
}catch(e){}
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
/* ── Affichage compteurs ── */
|
| 598 |
+
function _showCounts(uc){
|
| 599 |
+
const ov=document.getElementById('cOverlay');
|
| 600 |
+
ov.innerHTML='';
|
| 601 |
+
if(!Object.keys(uc).length){
|
| 602 |
+
_showBanner(true);return;
|
| 603 |
+
}
|
| 604 |
+
Object.entries(uc).forEach(([cls,cnt])=>{
|
| 605 |
+
const cl=CLASSES.find(c=>c.name===cls);
|
| 606 |
+
const color=cl?cl.color:'#a78bfa';
|
| 607 |
+
const div=document.createElement('div');div.className='cbdg';
|
| 608 |
+
div.style.borderLeftColor=color;
|
| 609 |
+
div.innerHTML=`<span class="cn" style="color:${color}">${cls} = ${cnt}</span>`;
|
| 610 |
+
ov.appendChild(div);
|
| 611 |
+
});
|
| 612 |
+
_showBanner(false);
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
function _showCountsWc(fc,uc){
|
| 616 |
+
const ov=document.getElementById('cOverlay');
|
| 617 |
+
ov.innerHTML='';
|
| 618 |
+
const all=new Set([...Object.keys(fc),...Object.keys(uc)]);
|
| 619 |
+
if(!all.size){_showBanner(true);return;}
|
| 620 |
+
all.forEach(cls=>{
|
| 621 |
+
const cl=CLASSES.find(c=>c.name===cls);
|
| 622 |
+
const color=cl?cl.color:'#a78bfa';
|
| 623 |
+
const div=document.createElement('div');div.className='cbdg';
|
| 624 |
+
div.style.borderLeftColor=color;
|
| 625 |
+
div.innerHTML=`<span class="cn" style="color:${color}">${cls} = ${fc[cls]||0}</span>
|
| 626 |
+
<span class="cu">Total unique : ${uc[cls]||0}</span>`;
|
| 627 |
+
ov.appendChild(div);
|
| 628 |
+
});
|
| 629 |
+
_showBanner(false);
|
| 630 |
+
}
|
| 631 |
+
|
| 632 |
+
function _showBanner(show){
|
| 633 |
+
const b=document.getElementById('noObjBanner');
|
| 634 |
+
b.style.display=show?'flex':'none';
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
/* ── Helpers ── */
|
| 638 |
+
function _set(id,val){const el=document.getElementById(id);if(el)el.textContent=val;}
|
| 639 |
+
function checkReady(){
|
| 640 |
+
const ok=(curSrc==='upload'&&vidFile)||(curSrc==='url'&&vidUrl)||(curSrc==='webcam'&&wcStream);
|
| 641 |
+
document.getElementById('runBtn').disabled=!ok;
|
| 642 |
+
}
|
| 643 |
+
function doDownload(){
|
| 644 |
+
const url=_annotatedUrl;if(!url)return;
|
| 645 |
+
const a=document.createElement('a');a.href=url;a.download='trackiq_annotated.mp4';a.click();
|
| 646 |
+
}
|
| 647 |
+
function resetAll(){
|
| 648 |
+
if(wcStream)stopWc();
|
| 649 |
+
if(_sse)_sse.close();
|
| 650 |
+
_jobId=null;_annotatedUrl=null;vidFile=null;vidUrl=null;
|
| 651 |
+
const rv=document.getElementById('resultVid');rv.src='';rv.style.display='none';
|
| 652 |
+
document.getElementById('fname').style.display='none';
|
| 653 |
+
document.getElementById('fname').textContent='';
|
| 654 |
+
document.getElementById('uzone').classList.remove('has');
|
| 655 |
+
document.getElementById('urlIn').value='';
|
| 656 |
+
document.getElementById('progWrap').style.display='none';
|
| 657 |
+
document.getElementById('progBar').style.width='0%';
|
| 658 |
+
document.getElementById('progTxt').style.display='none';
|
| 659 |
+
document.getElementById('emptyState').style.display='flex';
|
| 660 |
+
document.getElementById('cOverlay').style.display='none';
|
| 661 |
+
document.getElementById('hudEl').style.display='none';
|
| 662 |
+
document.getElementById('noObjBanner').style.display='none';
|
| 663 |
+
document.getElementById('dlBtn').disabled=true;
|
| 664 |
+
document.getElementById('dlName').textContent='No result yet';
|
| 665 |
+
document.getElementById('srcLabel').textContent='';
|
| 666 |
+
['stF','stO','stFps','stD'].forEach(id=>_set(id,'—'));
|
| 667 |
+
const fi=document.getElementById('fileInput');if(fi)fi.value='';
|
| 668 |
+
checkReady();
|
| 669 |
+
}
|
| 670 |
+
|
| 671 |
+
/* ── Init ── */
|
| 672 |
+
(function(){
|
| 673 |
+
try{
|
| 674 |
+
const th=localStorage.getItem('tiq_theme');
|
| 675 |
+
if(th){document.documentElement.setAttribute('data-theme',th);
|
| 676 |
+
if(th==='dark'){['ti','ti2'].forEach(x=>{const e=document.getElementById(x);if(e)e.className='fas fa-sun';});}}
|
| 677 |
+
const lg=localStorage.getItem('tiq_lang');
|
| 678 |
+
if(lg&&lg!=='en'){const btn=document.querySelector('.lb[onclick*="\''+lg+'\'"]');if(btn)setLang(lg,btn);}
|
| 679 |
+
}catch(e){}
|
| 680 |
+
})();
|
| 681 |
+
</script>
|
| 682 |
+
<script src="/static/trackiq-api.js"></script>
|
| 683 |
+
</body>
|
| 684 |
+
</html>
|
TRAFFIC_ROAD_APP/templates/index.html
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" data-theme="light">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>TrackIQ — Urban Traffic Detection</title>
|
| 7 |
+
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,300&family=DM+Mono:wght@400&display=swap" rel="stylesheet">
|
| 8 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
| 9 |
+
<style>
|
| 10 |
+
:root {
|
| 11 |
+
--ac: #880000;
|
| 12 |
+
--ac2: #660000;
|
| 13 |
+
--bg: #f9fafb;
|
| 14 |
+
--s: #fff;
|
| 15 |
+
--bd: rgba(0,0,0,0.07);
|
| 16 |
+
--t: #111827;
|
| 17 |
+
--t2: #6b7280;
|
| 18 |
+
--t3: #d1d5db;
|
| 19 |
+
--sh: 0 4px 24px rgba(0,0,0,0.09);
|
| 20 |
+
--shlg: 0 20px 25px -5px rgba(0,0,0,0.10), 0 10px 10px -5px rgba(0,0,0,0.04);
|
| 21 |
+
--r: 16px;
|
| 22 |
+
}
|
| 23 |
+
[data-theme="dark"] {
|
| 24 |
+
--bg: #0c1117;
|
| 25 |
+
--s: #151c25;
|
| 26 |
+
--t: #e8edf2;
|
| 27 |
+
--t2: #7a8fa0;
|
| 28 |
+
--t3: #3a4a5a;
|
| 29 |
+
--bd: rgba(255,255,255,0.07);
|
| 30 |
+
--sh: 0 4px 24px rgba(0,0,0,0.5);
|
| 31 |
+
--shlg: 0 20px 40px rgba(0,0,0,0.5);
|
| 32 |
+
}
|
| 33 |
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
| 34 |
+
html, body { min-height: 100%; }
|
| 35 |
+
body {
|
| 36 |
+
font-family: "DM Sans", sans-serif;
|
| 37 |
+
background: var(--bg);
|
| 38 |
+
color: var(--t);
|
| 39 |
+
transition: background 0.3s, color 0.3s;
|
| 40 |
+
}
|
| 41 |
+
::-webkit-scrollbar { width: 6px; }
|
| 42 |
+
::-webkit-scrollbar-track { background: transparent; }
|
| 43 |
+
::-webkit-scrollbar-thumb { background: var(--t3); border-radius: 3px; }
|
| 44 |
+
|
| 45 |
+
/* ── FLOATING NAV ── */
|
| 46 |
+
.floatnav {
|
| 47 |
+
position: absolute;
|
| 48 |
+
top: 0; left: 0; right: 0;
|
| 49 |
+
z-index: 50;
|
| 50 |
+
pointer-events: none;
|
| 51 |
+
}
|
| 52 |
+
.floatnav-inner {
|
| 53 |
+
max-width: 1280px;
|
| 54 |
+
margin: 0 auto;
|
| 55 |
+
padding: 16px 24px;
|
| 56 |
+
display: flex;
|
| 57 |
+
justify-content: space-between;
|
| 58 |
+
align-items: flex-start;
|
| 59 |
+
}
|
| 60 |
+
.nav-brand {
|
| 61 |
+
pointer-events: auto;
|
| 62 |
+
background: rgba(255,255,255,0.92);
|
| 63 |
+
backdrop-filter: blur(12px);
|
| 64 |
+
border-radius: 10px;
|
| 65 |
+
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
|
| 66 |
+
padding: 8px 16px;
|
| 67 |
+
display: flex;
|
| 68 |
+
align-items: center;
|
| 69 |
+
gap: 8px;
|
| 70 |
+
text-decoration: none;
|
| 71 |
+
transition: all 0.2s;
|
| 72 |
+
}
|
| 73 |
+
.nav-brand:hover { background: #fff; box-shadow: 0 8px 24px rgba(0,0,0,0.16); }
|
| 74 |
+
.nav-brand-icon {
|
| 75 |
+
width: 28px; height: 28px;
|
| 76 |
+
background: var(--ac);
|
| 77 |
+
border-radius: 7px;
|
| 78 |
+
display: flex; align-items: center; justify-content: center;
|
| 79 |
+
color: #fff; font-size: 12px;
|
| 80 |
+
}
|
| 81 |
+
.nav-brand-name {
|
| 82 |
+
font-size: 14px; font-weight: 600;
|
| 83 |
+
color: #111827; letter-spacing: -0.3px;
|
| 84 |
+
}
|
| 85 |
+
.nav-brand-name b { color: var(--ac); }
|
| 86 |
+
.nav-right {
|
| 87 |
+
pointer-events: auto;
|
| 88 |
+
display: flex; gap: 8px; align-items: center;
|
| 89 |
+
}
|
| 90 |
+
.nav-lang {
|
| 91 |
+
background: #fff;
|
| 92 |
+
border-radius: 10px;
|
| 93 |
+
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
|
| 94 |
+
padding: 4px;
|
| 95 |
+
display: flex; gap: 4px;
|
| 96 |
+
}
|
| 97 |
+
.lb {
|
| 98 |
+
font-size: 12px; font-weight: 500;
|
| 99 |
+
padding: 4px 10px;
|
| 100 |
+
border: none; background: none;
|
| 101 |
+
cursor: pointer; border-radius: 6px;
|
| 102 |
+
color: #6b7280;
|
| 103 |
+
font-family: "DM Sans", sans-serif;
|
| 104 |
+
transition: all 0.2s;
|
| 105 |
+
}
|
| 106 |
+
.lb.on { background: var(--ac); color: #fff; }
|
| 107 |
+
.lb:not(.on):hover { background: #f3f4f6; color: #111827; }
|
| 108 |
+
.nav-theme {
|
| 109 |
+
background: #fff;
|
| 110 |
+
border-radius: 10px;
|
| 111 |
+
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
|
| 112 |
+
width: 38px; height: 38px;
|
| 113 |
+
border: none; cursor: pointer;
|
| 114 |
+
display: flex; align-items: center; justify-content: center;
|
| 115 |
+
color: #6b7280; font-size: 13px;
|
| 116 |
+
transition: all 0.2s;
|
| 117 |
+
}
|
| 118 |
+
.nav-theme:hover { color: var(--ac); }
|
| 119 |
+
|
| 120 |
+
/* ── HERO ── */
|
| 121 |
+
.hero {
|
| 122 |
+
background-color: var(--ac);
|
| 123 |
+
background-image: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23991111' fill-opacity='0.4'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
| 124 |
+
color: #fff;
|
| 125 |
+
padding-top: 90px;
|
| 126 |
+
padding-bottom: 90px;
|
| 127 |
+
padding-left: 24px;
|
| 128 |
+
padding-right: 24px;
|
| 129 |
+
text-align: center;
|
| 130 |
+
position: relative;
|
| 131 |
+
overflow: hidden;
|
| 132 |
+
}
|
| 133 |
+
.hero-inner {
|
| 134 |
+
max-width: 860px;
|
| 135 |
+
margin: 0 auto;
|
| 136 |
+
position: relative;
|
| 137 |
+
z-index: 10;
|
| 138 |
+
animation: fadeUp 0.6s ease both;
|
| 139 |
+
}
|
| 140 |
+
@keyframes fadeUp {
|
| 141 |
+
from { opacity: 0; transform: translateY(24px); }
|
| 142 |
+
to { opacity: 1; transform: translateY(0); }
|
| 143 |
+
}
|
| 144 |
+
.hero-logo-wrap {
|
| 145 |
+
display: flex;
|
| 146 |
+
justify-content: center;
|
| 147 |
+
margin-bottom: 16px;
|
| 148 |
+
}
|
| 149 |
+
.hero-logo-box {
|
| 150 |
+
background: #fff;
|
| 151 |
+
padding: 14px;
|
| 152 |
+
border-radius: 20px;
|
| 153 |
+
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
|
| 154 |
+
display: flex; align-items: center; justify-content: center;
|
| 155 |
+
width: 80px; height: 80px;
|
| 156 |
+
}
|
| 157 |
+
.hero-logo-box i {
|
| 158 |
+
font-size: 36px;
|
| 159 |
+
color: var(--ac);
|
| 160 |
+
}
|
| 161 |
+
.hero h1 {
|
| 162 |
+
font-size: clamp(32px, 5vw, 60px);
|
| 163 |
+
font-weight: 800;
|
| 164 |
+
letter-spacing: -2px;
|
| 165 |
+
line-height: 1.1;
|
| 166 |
+
margin-bottom: 14px;
|
| 167 |
+
}
|
| 168 |
+
.hero h1 .dim { opacity: 0.75; font-weight: 300; font-style: italic; }
|
| 169 |
+
.hero-sub {
|
| 170 |
+
font-size: clamp(14px, 1.8vw, 18px);
|
| 171 |
+
color: rgba(255,255,255,0.88);
|
| 172 |
+
font-weight: 400;
|
| 173 |
+
line-height: 1.65;
|
| 174 |
+
margin-bottom: 24px;
|
| 175 |
+
max-width: 560px;
|
| 176 |
+
margin-left: auto;
|
| 177 |
+
margin-right: auto;
|
| 178 |
+
}
|
| 179 |
+
.hero-cta {
|
| 180 |
+
display: inline-flex; align-items: center; gap: 10px;
|
| 181 |
+
background: #fff;
|
| 182 |
+
color: var(--ac);
|
| 183 |
+
font-weight: 700; font-size: 16px;
|
| 184 |
+
padding: 14px 32px;
|
| 185 |
+
border-radius: 50px;
|
| 186 |
+
text-decoration: none;
|
| 187 |
+
box-shadow: 0 8px 24px rgba(0,0,0,0.18);
|
| 188 |
+
transition: all 0.3s;
|
| 189 |
+
}
|
| 190 |
+
.hero-cta:hover {
|
| 191 |
+
background: #f3f4f6;
|
| 192 |
+
transform: scale(1.05);
|
| 193 |
+
box-shadow: 0 16px 40px rgba(0,0,0,0.22);
|
| 194 |
+
}
|
| 195 |
+
/* Wave bottom */
|
| 196 |
+
.hero-wave {
|
| 197 |
+
position: absolute;
|
| 198 |
+
bottom: 0; left: 0; right: 0;
|
| 199 |
+
pointer-events: none;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
/* ── CARDS GRID ── */
|
| 203 |
+
.cards-section {
|
| 204 |
+
max-width: 1280px;
|
| 205 |
+
margin: 0 auto;
|
| 206 |
+
padding: 0 24px 64px;
|
| 207 |
+
margin-top: -64px;
|
| 208 |
+
position: relative;
|
| 209 |
+
z-index: 20;
|
| 210 |
+
}
|
| 211 |
+
.cards-grid {
|
| 212 |
+
display: grid;
|
| 213 |
+
grid-template-columns: repeat(1, 1fr);
|
| 214 |
+
gap: 24px;
|
| 215 |
+
}
|
| 216 |
+
@media (min-width: 640px) { .cards-grid { grid-template-columns: repeat(2, 1fr); } }
|
| 217 |
+
@media (min-width: 1024px) { .cards-grid { grid-template-columns: repeat(4, 1fr); } }
|
| 218 |
+
|
| 219 |
+
.card {
|
| 220 |
+
background: var(--s);
|
| 221 |
+
border-radius: 20px;
|
| 222 |
+
padding: 32px 24px;
|
| 223 |
+
box-shadow: var(--shlg);
|
| 224 |
+
border: 1px solid rgba(0,0,0,0.05);
|
| 225 |
+
text-decoration: none;
|
| 226 |
+
display: block;
|
| 227 |
+
transition: all 0.3s ease;
|
| 228 |
+
animation: fadeUp 0.6s ease both;
|
| 229 |
+
}
|
| 230 |
+
.card:nth-child(1) { animation-delay: 0.05s; }
|
| 231 |
+
.card:nth-child(2) { animation-delay: 0.12s; }
|
| 232 |
+
.card:nth-child(3) { animation-delay: 0.18s; }
|
| 233 |
+
.card:nth-child(4) { animation-delay: 0.24s; }
|
| 234 |
+
.card:hover {
|
| 235 |
+
transform: translateY(-8px);
|
| 236 |
+
box-shadow: 0 20px 25px -5px rgba(0,0,0,0.13), 0 10px 10px -5px rgba(0,0,0,0.05);
|
| 237 |
+
border-color: var(--ac);
|
| 238 |
+
}
|
| 239 |
+
.card-icon-wrap {
|
| 240 |
+
width: 80px; height: 80px;
|
| 241 |
+
background: #fff1f1;
|
| 242 |
+
color: var(--ac);
|
| 243 |
+
border-radius: 50%;
|
| 244 |
+
display: flex; align-items: center; justify-content: center;
|
| 245 |
+
font-size: 28px;
|
| 246 |
+
margin: 0 auto 24px;
|
| 247 |
+
transition: all 0.3s;
|
| 248 |
+
box-shadow: 0 2px 8px rgba(136,0,0,0.08);
|
| 249 |
+
}
|
| 250 |
+
[data-theme="dark"] .card-icon-wrap {
|
| 251 |
+
background: rgba(136,0,0,0.18);
|
| 252 |
+
}
|
| 253 |
+
.card:hover .card-icon-wrap {
|
| 254 |
+
background: var(--ac);
|
| 255 |
+
color: #fff;
|
| 256 |
+
}
|
| 257 |
+
.card-title {
|
| 258 |
+
text-align: center;
|
| 259 |
+
font-size: 18px; font-weight: 700;
|
| 260 |
+
color: var(--t);
|
| 261 |
+
margin-bottom: 10px;
|
| 262 |
+
transition: color 0.2s;
|
| 263 |
+
}
|
| 264 |
+
.card:hover .card-title { color: var(--ac); }
|
| 265 |
+
.card-desc {
|
| 266 |
+
text-align: center;
|
| 267 |
+
font-size: 13px;
|
| 268 |
+
color: var(--t2);
|
| 269 |
+
line-height: 1.65;
|
| 270 |
+
}
|
| 271 |
+
.card-launch {
|
| 272 |
+
margin-top: 24px;
|
| 273 |
+
display: flex; justify-content: center;
|
| 274 |
+
}
|
| 275 |
+
.card-launch span {
|
| 276 |
+
font-size: 13px; font-weight: 600;
|
| 277 |
+
color: var(--ac);
|
| 278 |
+
display: flex; align-items: center; gap: 6px;
|
| 279 |
+
opacity: 0;
|
| 280 |
+
transition: opacity 0.2s;
|
| 281 |
+
}
|
| 282 |
+
.card:hover .card-launch span { opacity: 1; }
|
| 283 |
+
|
| 284 |
+
/* ── FOOTER ── */
|
| 285 |
+
footer {
|
| 286 |
+
background: var(--s);
|
| 287 |
+
border-top: 1px solid var(--bd);
|
| 288 |
+
padding: 28px 24px;
|
| 289 |
+
text-align: center;
|
| 290 |
+
transition: background 0.3s;
|
| 291 |
+
}
|
| 292 |
+
footer p {
|
| 293 |
+
font-size: 13px;
|
| 294 |
+
color: var(--t2);
|
| 295 |
+
line-height: 1.7;
|
| 296 |
+
}
|
| 297 |
+
footer p span {
|
| 298 |
+
font-size: 11px;
|
| 299 |
+
opacity: 0.6;
|
| 300 |
+
display: block;
|
| 301 |
+
margin-top: 4px;
|
| 302 |
+
}
|
| 303 |
+
</style>
|
| 304 |
+
</head>
|
| 305 |
+
<body>
|
| 306 |
+
|
| 307 |
+
<!-- FLOATING NAV -->
|
| 308 |
+
<div class="floatnav">
|
| 309 |
+
<div class="floatnav-inner">
|
| 310 |
+
<a href="#" class="nav-brand">
|
| 311 |
+
<div class="nav-brand-icon"><i class="fas fa-satellite-dish"></i></div>
|
| 312 |
+
<span class="nav-brand-name">Track<b>IQ</b></span>
|
| 313 |
+
</a>
|
| 314 |
+
<div class="nav-right">
|
| 315 |
+
<div class="nav-lang">
|
| 316 |
+
<button class="lb on" onclick="setLang('en',this)">EN</button>
|
| 317 |
+
<button class="lb" onclick="setLang('fr',this)">FR</button>
|
| 318 |
+
<button class="lb" onclick="setLang('wo',this)">WO</button>
|
| 319 |
+
</div>
|
| 320 |
+
<button class="nav-theme" onclick="toggleTheme()"><i class="fas fa-moon" id="ti"></i></button>
|
| 321 |
+
</div>
|
| 322 |
+
</div>
|
| 323 |
+
</div>
|
| 324 |
+
|
| 325 |
+
<!-- HERO -->
|
| 326 |
+
<div class="hero">
|
| 327 |
+
<div class="hero-inner">
|
| 328 |
+
<div class="hero-logo-wrap">
|
| 329 |
+
<div class="hero-logo-box">
|
| 330 |
+
<i class="fas fa-satellite-dish"></i>
|
| 331 |
+
</div>
|
| 332 |
+
</div>
|
| 333 |
+
<h1 id="t-h1">
|
| 334 |
+
TrackIQ Urban Traffic<br>
|
| 335 |
+
<span class="dim">Surveillance & Detection</span>
|
| 336 |
+
</h1>
|
| 337 |
+
<p class="hero-sub" id="t-sub">
|
| 338 |
+
Based on Computer Vision — real-time vehicle detection and classification, speed estimation, congestion analysis and traffic sign identification.
|
| 339 |
+
</p>
|
| 340 |
+
<a href="/home" class="hero-cta" id="t-launch">
|
| 341 |
+
<i class="fas fa-play"></i>
|
| 342 |
+
<span>Launch</span>
|
| 343 |
+
</a>
|
| 344 |
+
</div>
|
| 345 |
+
<div class="hero-wave">
|
| 346 |
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 120" style="display:block;width:100%;">
|
| 347 |
+
<path fill="var(--bg)" fill-opacity="1"
|
| 348 |
+
d="M0,64L80,69.3C160,75,320,85,480,80C640,75,800,53,960,48C1120,43,1280,53,1360,58.7L1440,64L1440,120L1360,120C1280,120,1120,120,960,120C800,120,640,120,480,120C320,120,160,120,80,120L0,120Z"/>
|
| 349 |
+
</svg>
|
| 350 |
+
</div>
|
| 351 |
+
</div>
|
| 352 |
+
|
| 353 |
+
<!-- CARDS -->
|
| 354 |
+
<div class="cards-section">
|
| 355 |
+
<div class="cards-grid">
|
| 356 |
+
|
| 357 |
+
<!-- Card 1 -->
|
| 358 |
+
<a href="/home" class="card">
|
| 359 |
+
<div class="card-icon-wrap"><i class="fas fa-car-side"></i></div>
|
| 360 |
+
<div class="card-title" id="t-c1t">Vehicle Detection</div>
|
| 361 |
+
<p class="card-desc" id="t-c1d">Real-time detection and classification of vehicles: cars, trucks, motorcycles, buses and more.</p>
|
| 362 |
+
<div class="card-launch"><span id="t-launch2">Launch <i class="fas fa-arrow-right"></i></span></div>
|
| 363 |
+
</a>
|
| 364 |
+
|
| 365 |
+
<!-- Card 2 -->
|
| 366 |
+
<a href="/home" class="card">
|
| 367 |
+
<div class="card-icon-wrap"><i class="fas fa-tachometer-alt"></i></div>
|
| 368 |
+
<div class="card-title" id="t-c2t">Speed Estimation</div>
|
| 369 |
+
<p class="card-desc" id="t-c2d">Automatic estimation of vehicle speed from video stream without additional sensors.</p>
|
| 370 |
+
<div class="card-launch"><span id="t-launch3">Launch <i class="fas fa-arrow-right"></i></span></div>
|
| 371 |
+
</a>
|
| 372 |
+
|
| 373 |
+
<!-- Card 3 -->
|
| 374 |
+
<a href="/home" class="card">
|
| 375 |
+
<div class="card-icon-wrap"><i class="fas fa-traffic-light"></i></div>
|
| 376 |
+
<div class="card-title" id="t-c3t">Signs & Lights</div>
|
| 377 |
+
<p class="card-desc" id="t-c3d">Identification of traffic signs and lights, red light detection, stop signs and more.</p>
|
| 378 |
+
<div class="card-launch"><span id="t-launch4">Launch <i class="fas fa-arrow-right"></i></span></div>
|
| 379 |
+
</a>
|
| 380 |
+
|
| 381 |
+
<!-- Card 4 -->
|
| 382 |
+
<a href="/home" class="card">
|
| 383 |
+
<div class="card-icon-wrap"><i class="fas fa-chart-bar"></i></div>
|
| 384 |
+
<div class="card-title" id="t-c4t">Flow & Congestion</div>
|
| 385 |
+
<p class="card-desc" id="t-c4d">Congestion analysis, flow measurement and annotated video export for reporting.</p>
|
| 386 |
+
<div class="card-launch"><span id="t-launch5">Launch <i class="fas fa-arrow-right"></i></span></div>
|
| 387 |
+
</a>
|
| 388 |
+
|
| 389 |
+
</div>
|
| 390 |
+
</div>
|
| 391 |
+
|
| 392 |
+
<!-- FOOTER -->
|
| 393 |
+
<footer>
|
| 394 |
+
<p>
|
| 395 |
+
© 2026 <strong>TrackIQ</strong> — Computer Vision Project. All rights reserved.
|
| 396 |
+
<span id="t-foot">Academic Project — Version 1.0</span>
|
| 397 |
+
</p>
|
| 398 |
+
</footer>
|
| 399 |
+
|
| 400 |
+
<script>
|
| 401 |
+
const T = {
|
| 402 |
+
en: {
|
| 403 |
+
h1: 'TrackIQ Urban Traffic<br><span class="dim">Surveillance & Detection</span>',
|
| 404 |
+
sub: "Based on Computer Vision — real-time vehicle detection and classification, speed estimation, congestion analysis and traffic sign identification.",
|
| 405 |
+
launch: '<i class="fas fa-play"></i><span>Launch</span>',
|
| 406 |
+
c1t: "Vehicle Detection", c1d: "Real-time detection and classification of vehicles: cars, trucks, motorcycles, buses and more.",
|
| 407 |
+
c2t: "Speed Estimation", c2d: "Automatic estimation of vehicle speed from video stream without additional sensors.",
|
| 408 |
+
c3t: "Signs & Lights", c3d: "Identification of traffic signs and lights, red light detection, stop signs and more.",
|
| 409 |
+
c4t: "Flow & Congestion", c4d: "Congestion analysis, flow measurement and annotated video export for reporting.",
|
| 410 |
+
launchbtn: 'Launch <i class="fas fa-arrow-right"></i>',
|
| 411 |
+
foot: "Academic Project — Version 1.0"
|
| 412 |
+
},
|
| 413 |
+
fr: {
|
| 414 |
+
h1: 'TrackIQ Trafic Urbain<br><span class="dim">Surveillance & Détection</span>',
|
| 415 |
+
sub: "Basé sur Computer Vision — détection et classification en temps réel des véhicules, estimation de vitesse, analyse de congestion et identification des feux et panneaux.",
|
| 416 |
+
launch: '<i class="fas fa-play"></i><span>Lancer</span>',
|
| 417 |
+
c1t: "Détection Véhicules", c1d: "Détection et classification en temps réel des véhicules : voitures, camions, motos, bus et plus.",
|
| 418 |
+
c2t: "Estimation Vitesse", c2d: "Estimation automatique de la vitesse des véhicules depuis un flux vidéo, sans capteur additionnel.",
|
| 419 |
+
c3t: "Feux & Panneaux", c3d: "Identification des panneaux et feux de signalisation, détection des feux rouges, stops et plus.",
|
| 420 |
+
c4t: "Flux & Congestion",c4d: "Analyse de congestion, mesure de flux et export vidéo annoté pour rapport.",
|
| 421 |
+
launchbtn: 'Lancer <i class="fas fa-arrow-right"></i>',
|
| 422 |
+
foot: "Projet Académique — Version 1.0"
|
| 423 |
+
},
|
| 424 |
+
wo: {
|
| 425 |
+
h1: 'TrackIQ Trafik Kanam<br><span class="dim">Xam ak Réy</span>',
|
| 426 |
+
sub: "Moom Computer Vision la tax — léegi-léegi di réy wërsëg yi, ak xam njariñ yoom yi, ci kanam.",
|
| 427 |
+
launch: '<i class="fas fa-play"></i><span>Tambali</span>',
|
| 428 |
+
c1t: "Réy Wërsëg", c1d: "Réy ak xam njariñ wërsëg yi ci kanam: yëgël, camion, moto, bus ak yeneen.",
|
| 429 |
+
c2t: "Yóbbu Yoom", c2d: "Xam yóbbu yoom wërsëg yi ci video, benn jëriñ mujj dafa soxor.",
|
| 430 |
+
c3t: "Siñali", c3d: "Xam siñali yi ak dëkkandiku, réy feux rouge, stop ak yeneen.",
|
| 431 |
+
c4t: "Rafet & Xonq", c4d: "Xam xonq, jëf rafet ci kanam ak dëkkandiku video bu annotéwoon.",
|
| 432 |
+
launchbtn: 'Tambali <i class="fas fa-arrow-right"></i>',
|
| 433 |
+
foot: "Liggéey Académique — Version 1.0"
|
| 434 |
+
}
|
| 435 |
+
};
|
| 436 |
+
|
| 437 |
+
function setLang(l, btn) {
|
| 438 |
+
document.querySelectorAll(".lb").forEach(b => b.classList.remove("on"));
|
| 439 |
+
btn.classList.add("on");
|
| 440 |
+
const t = T[l];
|
| 441 |
+
if (!t) return;
|
| 442 |
+
document.getElementById("t-h1").innerHTML = t.h1;
|
| 443 |
+
document.getElementById("t-sub").textContent = t.sub;
|
| 444 |
+
document.getElementById("t-launch").innerHTML = t.launch;
|
| 445 |
+
["c1t","c2t","c3t","c4t"].forEach(k => {
|
| 446 |
+
const el = document.getElementById("t-"+k);
|
| 447 |
+
if (el) el.innerHTML = t[k];
|
| 448 |
+
});
|
| 449 |
+
["c1d","c2d","c3d","c4d"].forEach(k => {
|
| 450 |
+
const el = document.getElementById("t-"+k);
|
| 451 |
+
if (el) el.textContent = t[k];
|
| 452 |
+
});
|
| 453 |
+
["t-launch2","t-launch3","t-launch4","t-launch5"].forEach(id => {
|
| 454 |
+
const el = document.getElementById(id);
|
| 455 |
+
if (el) el.innerHTML = t.launchbtn;
|
| 456 |
+
});
|
| 457 |
+
document.getElementById("t-foot").textContent = t.foot;
|
| 458 |
+
try { localStorage.setItem("tiq_lang", l); } catch(e) {}
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
function toggleTheme() {
|
| 462 |
+
const d = document.documentElement;
|
| 463 |
+
const dk = d.getAttribute("data-theme") === "dark";
|
| 464 |
+
d.setAttribute("data-theme", dk ? "light" : "dark");
|
| 465 |
+
document.getElementById("ti").className = dk ? "fas fa-moon" : "fas fa-sun";
|
| 466 |
+
try { localStorage.setItem("tiq_theme", dk ? "light" : "dark"); } catch(e) {}
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
(function() {
|
| 470 |
+
try {
|
| 471 |
+
const th = localStorage.getItem("tiq_theme");
|
| 472 |
+
if (th) {
|
| 473 |
+
document.documentElement.setAttribute("data-theme", th);
|
| 474 |
+
if (th === "dark") document.getElementById("ti").className = "fas fa-sun";
|
| 475 |
+
}
|
| 476 |
+
const lg = localStorage.getItem("tiq_lang");
|
| 477 |
+
if (lg && lg !== "en") {
|
| 478 |
+
const btn = document.querySelector('.lb[onclick*="\'' + lg + '\'"]');
|
| 479 |
+
if (btn) setLang(lg, btn);
|
| 480 |
+
}
|
| 481 |
+
} catch(e) {}
|
| 482 |
+
})();
|
| 483 |
+
</script>
|
| 484 |
+
<script src="/static/trackiq-api.js"></script>
|
| 485 |
+
</body>
|
| 486 |
+
</html>
|
TRAFFIC_ROAD_APP/templates/logs.html
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" data-theme="light">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>TrackIQ Logs</title>
|
| 7 |
+
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
|
| 8 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
| 9 |
+
<style>
|
| 10 |
+
:root{--bg:#f0f4f8;--surface:#fff;--surface2:#f6f8fb;--border:rgba(0,0,0,0.07);--shadow:0 2px 16px rgba(0,0,0,0.09);--shadow-lg:0 8px 36px rgba(0,0,0,0.13);--text:#0f1923;--text2:#5a6a7a;--text3:#9aaab8;--accent:#880000;--accent2:#660000;--accent-light:rgba(136,0,0,0.07);--sidebar-w:60px;--radius:14px;--radius-sm:9px;}
|
| 11 |
+
[data-theme="dark"]{--bg:#0c1117;--surface:#151c25;--surface2:#1c2531;--border:rgba(255,255,255,0.07);--shadow:0 2px 16px rgba(0,0,0,0.45);--shadow-lg:0 8px 36px rgba(0,0,0,0.55);--text:#e8edf2;--text2:#7a8fa0;--text3:#3a4a5a;}
|
| 12 |
+
*{box-sizing:border-box;margin:0;padding:0}::-webkit-scrollbar{width:0;height:0}
|
| 13 |
+
body{font-family:'DM Sans',sans-serif;background:var(--bg);color:var(--text);font-size:13px;display:flex;min-height:100vh}
|
| 14 |
+
.sidebar{width:var(--sidebar-w);background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;align-items:center;padding:16px 0;gap:4px;position:fixed;top:0;left:0;bottom:0;z-index:200}
|
| 15 |
+
.sb-logo{width:34px;height:34px;background:var(--accent);border-radius:8px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px;margin-bottom:10px;text-decoration:none}
|
| 16 |
+
.sb-sep{width:28px;height:1px;background:var(--border);margin:4px 0}
|
| 17 |
+
.sb-btn{width:38px;height:38px;border-radius:9px;display:flex;align-items:center;justify-content:center;color:var(--text3);font-size:15px;cursor:pointer;transition:all .2s;text-decoration:none;border:none;background:none;position:relative}
|
| 18 |
+
.sb-btn:hover,.sb-btn.active{background:var(--accent-light);color:var(--accent)}
|
| 19 |
+
.sb-btn .tip{position:absolute;left:46px;top:50%;transform:translateY(-50%);background:var(--text);color:var(--surface);font-size:11px;padding:3px 8px;border-radius:5px;white-space:nowrap;opacity:0;pointer-events:none}.sb-btn:hover .tip{opacity:1}
|
| 20 |
+
.sb-spacer{flex:1}.page-wrap{margin-left:var(--sidebar-w);flex:1;display:flex;flex-direction:column}
|
| 21 |
+
.topbar{height:52px;background:var(--surface);border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;padding:0 22px}
|
| 22 |
+
.topbar-l,.topbar-r{display:flex;align-items:center;gap:10px}
|
| 23 |
+
.brand-name{font-size:15px;font-weight:600}.brand-name b{color:var(--accent)}.sep{color:var(--border);font-size:18px;margin:0 2px}
|
| 24 |
+
.lang-group{display:flex;background:var(--surface2);border-radius:6px;padding:2px;border:1px solid var(--border)}
|
| 25 |
+
.lang-btn{font-size:11px;font-weight:500;padding:3px 8px;border:none;background:none;cursor:pointer;border-radius:4px;color:var(--text2);font-family:'DM Sans',sans-serif}.lang-btn.active{background:var(--accent);color:white}
|
| 26 |
+
.icon-btn{width:30px;height:30px;border-radius:6px;border:1px solid var(--border);background:var(--surface);cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--text2);font-size:12px}
|
| 27 |
+
main{flex:1;padding:16px 18px;max-width:1440px;width:100%;margin:0 auto;display:flex;flex-direction:column;gap:12px}
|
| 28 |
+
.sum-row{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}
|
| 29 |
+
.sc,.card,.tcard{background:var(--surface);border-radius:var(--radius);border:1px solid var(--border);box-shadow:var(--shadow)}
|
| 30 |
+
.sc{padding:13px 15px}.sc-l{font-size:11px;color:var(--text2);margin-bottom:5px}.sc-v{font-size:22px;font-weight:300}.sc-s{font-size:10px;color:var(--text3);margin-top:3px}
|
| 31 |
+
.card{padding:14px 16px}.filter-row{display:flex;align-items:flex-end;gap:9px;flex-wrap:wrap}.fg{display:flex;flex-direction:column;gap:4px}.fg label{font-size:11px;color:var(--text2)}
|
| 32 |
+
.fin{padding:8px 10px;border:1px solid var(--border);border-radius:7px;font-size:12px;background:var(--surface2);color:var(--text);font-family:'DM Sans',sans-serif;outline:none;min-width:160px}.fin:focus{border-color:var(--accent)}
|
| 33 |
+
.f-sp{flex:1}
|
| 34 |
+
.btn{display:flex;align-items:center;gap:5px;border:none;border-radius:7px;padding:8px 13px;font-size:12px;cursor:pointer;font-family:'DM Sans',sans-serif}
|
| 35 |
+
.btn-exp{background:var(--accent);color:white}.btn-exp:hover{background:var(--accent2)}
|
| 36 |
+
.save-opt{display:flex;align-items:center;gap:10px;width:100%;padding:9px 14px;border:none;background:none;cursor:pointer;font-size:12px;color:var(--text);font-family:'DM Sans',sans-serif;text-align:left}.save-opt:hover{background:var(--surface2)}
|
| 37 |
+
.btn-clr{background:var(--surface2);color:var(--text2);border:1px solid var(--border)}
|
| 38 |
+
.tcard{overflow:hidden}.twrap{overflow:auto}
|
| 39 |
+
table{width:100%;border-collapse:collapse}
|
| 40 |
+
thead th{padding:10px 12px;text-align:left;font-size:11px;font-weight:500;color:var(--text2);background:var(--surface2);border-bottom:1px solid var(--border);white-space:nowrap}
|
| 41 |
+
tbody tr{border-bottom:1px solid var(--border)} tbody tr:last-child{border-bottom:none}
|
| 42 |
+
tbody td{padding:10px 12px;font-size:12px;vertical-align:middle}
|
| 43 |
+
.mono{font-family:'DM Mono',monospace}
|
| 44 |
+
.pill{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--border);background:var(--surface2);border-radius:999px;padding:3px 8px}
|
| 45 |
+
.dot{width:8px;height:8px;border-radius:50%}
|
| 46 |
+
.empty{padding:38px;text-align:center;color:var(--text3)}
|
| 47 |
+
@media(max-width:1100px){.sum-row{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
| 48 |
+
@media(max-width:760px){.sum-row{grid-template-columns:1fr}}
|
| 49 |
+
</style>
|
| 50 |
+
</head>
|
| 51 |
+
<body>
|
| 52 |
+
<nav class="sidebar">
|
| 53 |
+
<a href="/" class="sb-logo"><i class="fas fa-satellite-dish"></i></a>
|
| 54 |
+
<div class="sb-sep"></div>
|
| 55 |
+
<a href="/home" class="sb-btn"><i class="fas fa-play-circle"></i><span class="tip">Analyze</span></a>
|
| 56 |
+
<a href="/dashboard" class="sb-btn"><i class="fas fa-chart-line"></i><span class="tip">Dashboard</span></a>
|
| 57 |
+
<a href="/logs" class="sb-btn active"><i class="fas fa-list-ul"></i><span class="tip">Logs</span></a>
|
| 58 |
+
<a href="/history" class="sb-btn"><i class="fas fa-film"></i><span class="tip">History</span></a>
|
| 59 |
+
<div class="sb-spacer"></div>
|
| 60 |
+
<button class="sb-btn" onclick="toggleTheme()"><i class="fas fa-moon" id="themeIcon"></i><span class="tip">Theme</span></button>
|
| 61 |
+
</nav>
|
| 62 |
+
|
| 63 |
+
<div class="page-wrap">
|
| 64 |
+
<header class="topbar">
|
| 65 |
+
<div class="topbar-l">
|
| 66 |
+
<span class="brand-name">Track<b>IQ</b></span>
|
| 67 |
+
<span class="sep">|</span>
|
| 68 |
+
<span style="font-size:13px;font-weight:500;" id="t-title">Detection Logs</span>
|
| 69 |
+
</div>
|
| 70 |
+
<div class="topbar-r">
|
| 71 |
+
<div class="lang-group">
|
| 72 |
+
<button class="lang-btn active" onclick="setLang('en',this)">EN</button>
|
| 73 |
+
<button class="lang-btn" onclick="setLang('fr',this)">FR</button>
|
| 74 |
+
<button class="lang-btn" onclick="setLang('wo',this)">WO</button>
|
| 75 |
+
</div>
|
| 76 |
+
<button class="icon-btn" onclick="toggleTheme()"><i class="fas fa-moon" id="themeIcon2"></i></button>
|
| 77 |
+
</div>
|
| 78 |
+
</header>
|
| 79 |
+
|
| 80 |
+
<main>
|
| 81 |
+
<div class="sum-row">
|
| 82 |
+
<div class="sc"><div class="sc-l" id="t-s1">Detections</div><div class="sc-v" id="sumDetections">0</div><div class="sc-s" id="sumDetectionsSub">Real rows from generated videos</div></div>
|
| 83 |
+
<div class="sc"><div class="sc-l" id="t-s2">Scenes</div><div class="sc-v" id="sumScenes">0</div><div class="sc-s" id="sumScenesSub">Processed analyses</div></div>
|
| 84 |
+
<div class="sc"><div class="sc-l" id="t-s3">Classes</div><div class="sc-v" id="sumClasses">0</div><div class="sc-s" id="sumClassesSub">Distinct detected labels</div></div>
|
| 85 |
+
<div class="sc"><div class="sc-l" id="t-s4">Latest video</div><div class="sc-v" id="sumLatest">-</div><div class="sc-s" id="sumLatestSub">Most recent analyzed clip</div></div>
|
| 86 |
+
</div>
|
| 87 |
+
|
| 88 |
+
<div class="card">
|
| 89 |
+
<div class="filter-row">
|
| 90 |
+
<div class="fg"><label id="t-search">Search video or scene</label><input id="searchIn" class="fin" oninput="applyFilters()"></div>
|
| 91 |
+
<div class="fg"><label id="t-scene">Scene</label><select id="sceneF" class="fin" onchange="applyFilters()"><option value="">All</option></select></div>
|
| 92 |
+
<div class="fg"><label id="t-type">Class</label><select id="classF" class="fin" onchange="applyFilters()"><option value="">All</option></select></div>
|
| 93 |
+
<div class="fg"><label id="t-direction">Direction</label><select id="dirF" class="fin" onchange="applyFilters()"><option value="">All</option><option>up</option><option>down</option><option>left</option><option>right</option><option>unknown</option></select></div>
|
| 94 |
+
<div class="f-sp"></div>
|
| 95 |
+
<button class="btn btn-clr" onclick="clearFilters()" ondblclick="clearAllLogs()" title="Click=clear filters | Double-click=delete all logs"><i class="fas fa-rotate-left"></i> <span id="t-clear">Clear</span></button>
|
| 96 |
+
<div style="position:relative">
|
| 97 |
+
<button class="btn btn-exp" onclick="toggleSaveMenu(event)"><i class="fas fa-cloud-arrow-up"></i> <span id="t-export">Save Logs</span> <i class="fas fa-chevron-down" style="font-size:9px;margin-left:2px"></i></button>
|
| 98 |
+
<div id="saveMenu" style="display:none;position:absolute;right:0;top:calc(100% + 6px);background:var(--surface);border:1px solid var(--border);border-radius:10px;box-shadow:var(--shadow-lg);min-width:240px;z-index:999;overflow:hidden">
|
| 99 |
+
<div style="padding:10px 14px 6px;font-size:11px;color:var(--text2);font-weight:500;border-bottom:1px solid var(--border)">
|
| 100 |
+
<div style="display:flex;align-items:center;gap:6px;margin-bottom:6px">
|
| 101 |
+
<label style="font-size:11px;color:var(--text2)">Group name:</label>
|
| 102 |
+
<input id="groupNameIn" value="trackiq" style="flex:1;padding:4px 7px;border:1px solid var(--border);border-radius:5px;font-size:11px;background:var(--surface2);color:var(--text);outline:none" placeholder="e.g. session1">
|
| 103 |
+
</div>
|
| 104 |
+
<span style="color:var(--text3);font-size:10px">File: <span id="csvPreview">trackiq_all.csv</span></span>
|
| 105 |
+
</div>
|
| 106 |
+
<button class="save-opt" onclick="saveLocal()"><i class="fas fa-download" style="color:#3b82f6"></i> Download locally</button>
|
| 107 |
+
<button class="save-opt" onclick="showGithubPanel()"><i class="fab fa-github" style="color:#e2e8f0"></i> Push to GitHub</button>
|
| 108 |
+
<button class="save-opt" onclick="saveDrive()"><i class="fab fa-google-drive" style="color:#34a853"></i> Save to Google Drive</button>
|
| 109 |
+
</div>
|
| 110 |
+
</div>
|
| 111 |
+
<!-- GitHub panel -->
|
| 112 |
+
<div id="githubPanel" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:1000;display:none;align-items:center;justify-content:center">
|
| 113 |
+
<div style="background:var(--surface);border-radius:14px;padding:22px 24px;width:420px;box-shadow:var(--shadow-lg)">
|
| 114 |
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
|
| 115 |
+
<span style="font-weight:600;font-size:14px"><i class="fab fa-github"></i> Push to GitHub</span>
|
| 116 |
+
<button onclick="closeGithubPanel()" style="background:none;border:none;color:var(--text2);cursor:pointer;font-size:16px"><i class="fas fa-times"></i></button>
|
| 117 |
+
</div>
|
| 118 |
+
<div style="display:flex;flex-direction:column;gap:9px">
|
| 119 |
+
<div class="fg"><label>Personal Access Token (PAT)</label><input id="ghToken" class="fin" type="password" placeholder="ghp_xxxxxxxxxxxx"></div>
|
| 120 |
+
<div class="fg"><label>Repository (owner/repo)</label><input id="ghRepo" class="fin" placeholder="username/trackiq-logs"></div>
|
| 121 |
+
<div class="fg"><label>Branch</label><input id="ghBranch" class="fin" value="main" placeholder="main"></div>
|
| 122 |
+
<div class="fg"><label>Path in repo</label><input id="ghPath" class="fin" placeholder="logs/" value="logs/"></div>
|
| 123 |
+
</div>
|
| 124 |
+
<div id="ghStatus" style="margin-top:10px;font-size:12px;color:var(--text2);min-height:18px"></div>
|
| 125 |
+
<div style="display:flex;gap:8px;margin-top:14px;justify-content:flex-end">
|
| 126 |
+
<button class="btn btn-clr" onclick="closeGithubPanel()">Cancel</button>
|
| 127 |
+
<button class="btn btn-exp" onclick="pushToGithub()"><i class="fab fa-github"></i> Push CSV</button>
|
| 128 |
+
</div>
|
| 129 |
+
</div>
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
</div>
|
| 133 |
+
|
| 134 |
+
<div class="tcard">
|
| 135 |
+
<div class="twrap">
|
| 136 |
+
<table>
|
| 137 |
+
<thead>
|
| 138 |
+
<tr>
|
| 139 |
+
<th id="th-video">Video</th>
|
| 140 |
+
<th id="th-scene">Scene</th>
|
| 141 |
+
<th id="th-time">Time</th>
|
| 142 |
+
<th id="th-frame">Frame</th>
|
| 143 |
+
<th id="th-track">Track</th>
|
| 144 |
+
<th id="th-class">Class</th>
|
| 145 |
+
<th id="th-confidence">Confidence</th>
|
| 146 |
+
<th id="th-direction">Direction</th>
|
| 147 |
+
<th id="th-bbox">Bounding Box</th>
|
| 148 |
+
<th id="th-position">Center</th>
|
| 149 |
+
</tr>
|
| 150 |
+
</thead>
|
| 151 |
+
<tbody id="tbody">
|
| 152 |
+
<tr><td colspan="9" class="empty">Loading real logs...</td></tr>
|
| 153 |
+
</tbody>
|
| 154 |
+
</table>
|
| 155 |
+
</div>
|
| 156 |
+
</div>
|
| 157 |
+
</main>
|
| 158 |
+
</div>
|
| 159 |
+
|
| 160 |
+
<script>
|
| 161 |
+
let allRows = [];
|
| 162 |
+
let filteredRows = [];
|
| 163 |
+
|
| 164 |
+
function toggleTheme(){
|
| 165 |
+
const d=document.documentElement;
|
| 166 |
+
const dark=d.getAttribute('data-theme')==='dark';
|
| 167 |
+
d.setAttribute('data-theme',dark?'light':'dark');
|
| 168 |
+
['themeIcon','themeIcon2'].forEach(id=>{const el=document.getElementById(id);if(el)el.className=dark?'fas fa-moon':'fas fa-sun';});
|
| 169 |
+
try{localStorage.setItem('tiq_theme',dark?'light':'dark');}catch(e){}
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
const TL = {
|
| 173 |
+
en:{title:'Detection Logs',s1:'Detections',s2:'Scenes',s3:'Classes',s4:'Latest video',search:'Search video or scene',scene:'Scene',type:'Class',direction:'Direction',clear:'Clear',export:'Save Logs',video:'Video',time:'Time',frame:'Frame',track:'Track',class:'Class',confidence:'Confidence',bbox:'Bounding Box',position:'Center'},
|
| 174 |
+
fr:{title:'Logs de détection',s1:'Détections',s2:'Scènes',s3:'Classes',s4:'Dernière vidéo',search:'Recherche vidéo ou scène',scene:'Scène',type:'Classe',direction:'Direction',clear:'Effacer',export:'Sauvegarder',video:'Vidéo',time:'Temps',frame:'Frame',track:'Track',class:'Classe',confidence:'Confiance',bbox:'Boîte englobante',position:'Centre'},
|
| 175 |
+
wo:{title:'Logs yi',s1:'Réy',s2:'Scène yi',s3:'Classe yi',s4:'Video mujj',search:'Seet video walla scène',scene:'Scène',type:'Classe',direction:'Direction',clear:'Baň',export:'Sauvegarder',video:'Video',time:'Waxtu',frame:'Frame',track:'Track',class:'Classe',confidence:'Confiance',bbox:'Bounding Box',position:'Centre'}
|
| 176 |
+
};
|
| 177 |
+
|
| 178 |
+
function setLang(l, btn){
|
| 179 |
+
document.querySelectorAll('.lang-btn').forEach(b=>b.classList.remove('active'));
|
| 180 |
+
btn.classList.add('active');
|
| 181 |
+
const t = TL[l];
|
| 182 |
+
const ids = {title:'t-title',s1:'t-s1',s2:'t-s2',s3:'t-s3',s4:'t-s4',search:'t-search',scene:'t-scene',type:'t-type',direction:'t-direction',clear:'t-clear',export:'t-export',video:'th-video',time:'th-time',frame:'th-frame',track:'th-track',class:'th-class',confidence:'th-confidence',bbox:'th-bbox',position:'th-position'};
|
| 183 |
+
Object.entries(ids).forEach(([k,id])=>{const el=document.getElementById(id);if(el)el.textContent=t[k];});
|
| 184 |
+
document.getElementById('th-scene').textContent = t.scene;
|
| 185 |
+
document.getElementById('th-direction').textContent = t.direction;
|
| 186 |
+
try{localStorage.setItem('tiq_lang',l);}catch(e){}
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
async function loadLogs(){
|
| 190 |
+
try{
|
| 191 |
+
const [rowsRes, scenesRes] = await Promise.all([fetch('/api/logs/rows'), fetch('/api/logs')]);
|
| 192 |
+
allRows = await rowsRes.json();
|
| 193 |
+
const scenes = await scenesRes.json();
|
| 194 |
+
fillFilters(scenes);
|
| 195 |
+
fillSummary(scenes, allRows);
|
| 196 |
+
applyFilters();
|
| 197 |
+
}catch(err){
|
| 198 |
+
const tb=document.getElementById('tbody');
|
| 199 |
+
tb.innerHTML = '<tr><td colspan="9" class="empty">Unable to load real logs.</td></tr>';
|
| 200 |
+
console.warn('[TrackIQ] real logs:', err);
|
| 201 |
+
}
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
function fillFilters(scenes){
|
| 205 |
+
const sceneF = document.getElementById('sceneF');
|
| 206 |
+
const classF = document.getElementById('classF');
|
| 207 |
+
sceneF.innerHTML = '<option value="">All</option>' + scenes.map(s=>`<option value="${s.scene_id}">${s.video_name || s.scene_id}</option>`).join('');
|
| 208 |
+
const classes = [...new Set(allRows.map(r=>r.class_name).filter(Boolean))].sort();
|
| 209 |
+
classF.innerHTML = '<option value="">All</option>' + classes.map(c=>`<option value="${c}">${c}</option>`).join('');
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
function fillSummary(scenes, rows){
|
| 213 |
+
document.getElementById('sumDetections').textContent = rows.length.toLocaleString();
|
| 214 |
+
document.getElementById('sumScenes').textContent = scenes.length.toLocaleString();
|
| 215 |
+
document.getElementById('sumClasses').textContent = new Set(rows.map(r=>r.class_name).filter(Boolean)).size.toLocaleString();
|
| 216 |
+
document.getElementById('sumLatest').textContent = scenes[0]?.video_name || '-';
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
function applyFilters(){
|
| 220 |
+
const search = document.getElementById('searchIn').value.toLowerCase();
|
| 221 |
+
const scene = document.getElementById('sceneF').value;
|
| 222 |
+
const cls = document.getElementById('classF').value;
|
| 223 |
+
const dir = document.getElementById('dirF').value;
|
| 224 |
+
filteredRows = allRows.filter(r =>
|
| 225 |
+
(!search || (r.video_name || '').toLowerCase().includes(search) || (r.scene_id || '').toLowerCase().includes(search)) &&
|
| 226 |
+
(!scene || r.scene_id === scene) &&
|
| 227 |
+
(!cls || r.class_name === cls) &&
|
| 228 |
+
(!dir || r.direction === dir)
|
| 229 |
+
);
|
| 230 |
+
renderRows();
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
async function clearAllLogs(){
|
| 234 |
+
if(!confirm('Delete all logs? This cannot be undone.')) return;
|
| 235 |
+
try{
|
| 236 |
+
const scenes = await fetch('/api/logs').then(r=>r.json());
|
| 237 |
+
await Promise.all(scenes.map(s=>fetch(`/api/logs/${s.scene_id}`,{method:'DELETE'})));
|
| 238 |
+
location.reload();
|
| 239 |
+
}catch(e){ alert('Error: '+e.message); }
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
function clearFilters(){
|
| 243 |
+
['searchIn','sceneF','classF','dirF'].forEach(id=>{document.getElementById(id).value='';});
|
| 244 |
+
applyFilters();
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
function renderRows(){
|
| 248 |
+
const tb = document.getElementById('tbody');
|
| 249 |
+
if(!filteredRows.length){
|
| 250 |
+
tb.innerHTML = '<tr><td colspan="10" class="empty">No real detections found for this filter.</td></tr>';
|
| 251 |
+
return;
|
| 252 |
+
}
|
| 253 |
+
const colors = {Vehicle:'#3b82f6',Motorcycle:'#10b981',Truck:'#f59e0b',Bus:'#8b5cf6',Human:'#ec4899',Bicycle:'#06b6d4'};
|
| 254 |
+
tb.innerHTML = '';
|
| 255 |
+
filteredRows.forEach(r=>{
|
| 256 |
+
const tr = document.createElement('tr');
|
| 257 |
+
tr.innerHTML = `
|
| 258 |
+
<td title="${r.video_name || ''}">${(r.video_name || '-').slice(0,28)}</td>
|
| 259 |
+
<td class="mono">${r.scene_id}</td>
|
| 260 |
+
<td class="mono">${Number(r.timestamp_s || 0).toFixed(2)}s</td>
|
| 261 |
+
<td class="mono">${r.frame_id}</td>
|
| 262 |
+
<td class="mono">#${r.track_id}</td>
|
| 263 |
+
<td><span class="pill"><span class="dot" style="background:${colors[r.class_name] || '#888'}"></span>${r.class_name || '-'}</span></td>
|
| 264 |
+
<td class="mono">${Math.round((r.confidence || 0) * 100)}%</td>
|
| 265 |
+
<td>${r.direction || 'unknown'}</td>
|
| 266 |
+
<td class="mono" style="font-size:11px">(${r.x1},${r.y1})→(${r.x2},${r.y2})</td>
|
| 267 |
+
<td class="mono">(${r.cx}, ${r.cy})</td>
|
| 268 |
+
`;
|
| 269 |
+
tb.appendChild(tr);
|
| 270 |
+
});
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
/* ── Helpers CSV ─────────────────────────────────────────────── */
|
| 274 |
+
function getGroupName(){ return (document.getElementById('groupNameIn')?.value||'trackiq').trim().replace(/\s+/g,'_')||'trackiq'; }
|
| 275 |
+
function getCsvFilename(){
|
| 276 |
+
const g=getGroupName();
|
| 277 |
+
const scenes=[...new Set(filteredRows.map(r=>r.scene_id))];
|
| 278 |
+
return `${g}_${scenes.length===1?scenes[0]:'all'}.csv`;
|
| 279 |
+
}
|
| 280 |
+
function buildCsvContent(){
|
| 281 |
+
const hdr = 'group,scene_id,video_name,timestamp_s,frame_id,track_id,class_name,confidence,x1,y1,x2,y2,cx,cy,direction\n';
|
| 282 |
+
const g = getGroupName();
|
| 283 |
+
const rows = filteredRows.map(r => [g,r.scene_id,`"${(r.video_name||'').replace(/"/g,'""')}"`,r.timestamp_s,r.frame_id,r.track_id,r.class_name,r.confidence,r.x1,r.y1,r.x2,r.y2,r.cx,r.cy,r.direction].join(',')).join('\n');
|
| 284 |
+
return hdr + rows;
|
| 285 |
+
}
|
| 286 |
+
function updateCsvPreview(){
|
| 287 |
+
const el = document.getElementById('csvPreview');
|
| 288 |
+
if(el) el.textContent = getCsvFilename();
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
/* ── Save menu ───────────────────────────────────���───────────── */
|
| 292 |
+
function toggleSaveMenu(e){
|
| 293 |
+
e.stopPropagation();
|
| 294 |
+
const m = document.getElementById('saveMenu');
|
| 295 |
+
m.style.display = m.style.display==='none'?'block':'none';
|
| 296 |
+
updateCsvPreview();
|
| 297 |
+
}
|
| 298 |
+
document.addEventListener('click', ()=>{ const m=document.getElementById('saveMenu'); if(m) m.style.display='none'; });
|
| 299 |
+
|
| 300 |
+
/* ── 1. Download local ───────────────────────────────────────── */
|
| 301 |
+
function saveLocal(){
|
| 302 |
+
const a = document.createElement('a');
|
| 303 |
+
a.href = URL.createObjectURL(new Blob([buildCsvContent()], {type:'text/csv'}));
|
| 304 |
+
a.download = getCsvFilename();
|
| 305 |
+
a.click();
|
| 306 |
+
document.getElementById('saveMenu').style.display='none';
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
/* ── 2. GitHub push ──────────────────────────────────────────── */
|
| 310 |
+
function showGithubPanel(){
|
| 311 |
+
document.getElementById('saveMenu').style.display='none';
|
| 312 |
+
const p = document.getElementById('githubPanel');
|
| 313 |
+
p.style.display='flex';
|
| 314 |
+
updateCsvPreview();
|
| 315 |
+
}
|
| 316 |
+
function closeGithubPanel(){ document.getElementById('githubPanel').style.display='none'; }
|
| 317 |
+
|
| 318 |
+
async function pushToGithub(){
|
| 319 |
+
const token = document.getElementById('ghToken').value.trim();
|
| 320 |
+
const repo = document.getElementById('ghRepo').value.trim();
|
| 321 |
+
const branch = document.getElementById('ghBranch').value.trim() || 'main';
|
| 322 |
+
const path = (document.getElementById('ghPath').value.trim().replace(/\/+$/,'')||'logs') + '/' + getCsvFilename();
|
| 323 |
+
const status = document.getElementById('ghStatus');
|
| 324 |
+
|
| 325 |
+
if(!token||!repo){ status.textContent='⚠ Token and repo are required.'; return; }
|
| 326 |
+
status.textContent='Pushing…';
|
| 327 |
+
|
| 328 |
+
const content = btoa(unescape(encodeURIComponent(buildCsvContent())));
|
| 329 |
+
// Check if file exists (to get SHA for update)
|
| 330 |
+
let sha = null;
|
| 331 |
+
try{
|
| 332 |
+
const check = await fetch(`https://api.github.com/repos/${repo}/contents/${path}`,{headers:{Authorization:`token ${token}`}});
|
| 333 |
+
if(check.ok){ const d=await check.json(); sha=d.sha; }
|
| 334 |
+
}catch(e){}
|
| 335 |
+
|
| 336 |
+
try{
|
| 337 |
+
const body = {message:`TrackIQ logs export — ${getCsvFilename()}`, content, branch};
|
| 338 |
+
if(sha) body.sha = sha;
|
| 339 |
+
const res = await fetch(`https://api.github.com/repos/${repo}/contents/${path}`,{
|
| 340 |
+
method:'PUT', headers:{Authorization:`token ${token}`,'Content-Type':'application/json'},
|
| 341 |
+
body: JSON.stringify(body)
|
| 342 |
+
});
|
| 343 |
+
if(res.ok){ status.innerHTML='<span style="color:#10b981">✓ Pushed successfully!</span>'; }
|
| 344 |
+
else{ const e=await res.json(); status.innerHTML=`<span style="color:#ef4444">Error: ${e.message}</span>`; }
|
| 345 |
+
}catch(err){ status.innerHTML=`<span style="color:#ef4444">Network error: ${err.message}</span>`; }
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
/* ── 3. Google Drive ─────────────────────────────────────────── */
|
| 349 |
+
async function saveDrive(){
|
| 350 |
+
document.getElementById('saveMenu').style.display='none';
|
| 351 |
+
// Uses Google Drive File Picker API via OAuth implicit flow
|
| 352 |
+
const CLIENT_ID = ''; // User must fill their own GCP OAuth client ID
|
| 353 |
+
if(!CLIENT_ID){
|
| 354 |
+
const ok = confirm('To save to Google Drive you need a Google OAuth Client ID.\n\nWould you like instructions on how to set it up?\n\n(For now the file will be downloaded locally as a fallback)');
|
| 355 |
+
if(ok){ window.open('https://developers.google.com/drive/api/guides/about-auth','_blank'); }
|
| 356 |
+
saveLocal();
|
| 357 |
+
return;
|
| 358 |
+
}
|
| 359 |
+
// Full Drive upload with OAuth token
|
| 360 |
+
const scope = 'https://www.googleapis.com/auth/drive.file';
|
| 361 |
+
const redirect = location.origin + location.pathname;
|
| 362 |
+
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${CLIENT_ID}&redirect_uri=${encodeURIComponent(redirect)}&response_type=token&scope=${encodeURIComponent(scope)}`;
|
| 363 |
+
const popup = window.open(authUrl,'_blank','width=500,height=600');
|
| 364 |
+
// Listen for token in URL hash after redirect
|
| 365 |
+
const interval = setInterval(()=>{
|
| 366 |
+
try{
|
| 367 |
+
if(popup.closed){ clearInterval(interval); return; }
|
| 368 |
+
const hash = popup.location.hash;
|
| 369 |
+
if(hash && hash.includes('access_token')){
|
| 370 |
+
clearInterval(interval); popup.close();
|
| 371 |
+
const token = new URLSearchParams(hash.replace('#','')).get('access_token');
|
| 372 |
+
uploadToDrive(token);
|
| 373 |
+
}
|
| 374 |
+
}catch(e){}
|
| 375 |
+
},500);
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
async function uploadToDrive(token){
|
| 379 |
+
const csv = buildCsvContent();
|
| 380 |
+
const fname = getCsvFilename();
|
| 381 |
+
const meta = JSON.stringify({name:fname,mimeType:'text/csv'});
|
| 382 |
+
const form = new FormData();
|
| 383 |
+
form.append('metadata', new Blob([meta],{type:'application/json'}));
|
| 384 |
+
form.append('file', new Blob([csv], {type:'text/csv'}));
|
| 385 |
+
try{
|
| 386 |
+
const res = await fetch('https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart',{
|
| 387 |
+
method:'POST', headers:{Authorization:`Bearer ${token}`}, body:form
|
| 388 |
+
});
|
| 389 |
+
if(res.ok){ alert(`✓ "${fname}" saved to Google Drive!`); }
|
| 390 |
+
else{ const e=await res.json(); alert('Drive error: '+e.error?.message); }
|
| 391 |
+
}catch(err){ alert('Drive upload failed: '+err.message); }
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
function exportFiltered(){ saveLocal(); } // legacy alias
|
| 395 |
+
|
| 396 |
+
(function(){
|
| 397 |
+
try{
|
| 398 |
+
const th=localStorage.getItem('tiq_theme');
|
| 399 |
+
if(th){
|
| 400 |
+
document.documentElement.setAttribute('data-theme',th);
|
| 401 |
+
if(th==='dark'){['themeIcon','themeIcon2'].forEach(id=>{const el=document.getElementById(id);if(el)el.className='fas fa-sun';});}
|
| 402 |
+
}
|
| 403 |
+
const lg=localStorage.getItem('tiq_lang');
|
| 404 |
+
if(lg&&lg!=='en'){
|
| 405 |
+
const btn=document.querySelector(`.lang-btn[onclick*="'${lg}'"]`);
|
| 406 |
+
if(btn)setLang(lg,btn);
|
| 407 |
+
}
|
| 408 |
+
}catch(e){}
|
| 409 |
+
loadLogs();
|
| 410 |
+
// Update CSV filename preview when group name changes
|
| 411 |
+
document.addEventListener('input', e=>{ if(e.target.id==='groupNameIn') updateCsvPreview(); });
|
| 412 |
+
})();
|
| 413 |
+
</script>
|
| 414 |
+
</body>
|
| 415 |
+
</html>
|
app.py
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
app.py — TrackIQ Backend (v3 — optimisé CPU)
|
| 3 |
+
"""
|
| 4 |
+
import cv2, csv, json, threading, queue, uuid, mimetypes, subprocess
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from collections import defaultdict
|
| 8 |
+
from typing import Dict
|
| 9 |
+
|
| 10 |
+
from flask import (Flask, request, jsonify, Response,
|
| 11 |
+
send_file, send_from_directory, abort, stream_with_context)
|
| 12 |
+
from flask_cors import CORS
|
| 13 |
+
from ultralytics import YOLO
|
| 14 |
+
|
| 15 |
+
app = Flask(__name__, static_folder="static", template_folder="templates")
|
| 16 |
+
CORS(app)
|
| 17 |
+
|
| 18 |
+
BASE = Path(__file__).parent
|
| 19 |
+
UPLOAD_DIR = BASE / "uploads"; UPLOAD_DIR.mkdir(exist_ok=True)
|
| 20 |
+
OUTPUT_DIR = BASE / "outputs"; OUTPUT_DIR.mkdir(exist_ok=True)
|
| 21 |
+
LOG_DIR = BASE / "logs"; LOG_DIR.mkdir(exist_ok=True)
|
| 22 |
+
MODELS_DIR = BASE / "models"; MODELS_DIR.mkdir(exist_ok=True)
|
| 23 |
+
|
| 24 |
+
COCO_MAP = {
|
| 25 |
+
"Vehicle":2,"Motorcycle":3,"Truck":7,"Bus":5,
|
| 26 |
+
"Human":0,"Bicycle":1,"Traffic light":9,"Road sign":11,
|
| 27 |
+
}
|
| 28 |
+
DEFAULT_CLASSES = ["Vehicle","Motorcycle","Truck","Bus","Human","Bicycle","Traffic light","Road sign"]
|
| 29 |
+
CLASS_COLORS = {
|
| 30 |
+
"Vehicle":(255,120,80),"Motorcycle":(80,200,80),"Truck":(0,180,255),
|
| 31 |
+
"Bus":(220,80,255),"Human":(236,72,153),"Bicycle":(6,182,212),
|
| 32 |
+
"Traffic light":(239,68,68),"Road sign":(249,115,22),
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
CONF = 0.55
|
| 36 |
+
IOU = 0.45
|
| 37 |
+
SKIP = 2
|
| 38 |
+
INFER_SZ = 480
|
| 39 |
+
|
| 40 |
+
_jobs: Dict[str,dict] = {}
|
| 41 |
+
_sse_queues: Dict[str,queue.Queue] = {}
|
| 42 |
+
_webcam_active = False
|
| 43 |
+
_webcam_queue: queue.Queue = queue.Queue(maxsize=4)
|
| 44 |
+
_webcam_stats: dict = {}
|
| 45 |
+
_webcam_model_key = "yolo11n"
|
| 46 |
+
_webcam_classes = DEFAULT_CLASSES
|
| 47 |
+
_webcam_frame_id = 0
|
| 48 |
+
_webcam_unique = defaultdict(set)
|
| 49 |
+
_webcam_hist_xy = defaultdict(list)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def _get_model(key="yolo11n"):
|
| 53 |
+
p = MODELS_DIR / f"{key}.pt"
|
| 54 |
+
if not p.exists():
|
| 55 |
+
print(f"[TrackIQ] Downloading {key}.pt …")
|
| 56 |
+
m = YOLO(f"{key}.pt")
|
| 57 |
+
import shutil
|
| 58 |
+
dl = Path(f"{key}.pt")
|
| 59 |
+
if dl.exists(): shutil.move(str(dl), str(p))
|
| 60 |
+
return m
|
| 61 |
+
return YOLO(str(p))
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def _make_video_writer(jid, fps, size):
|
| 65 |
+
"""Essaie avc1 (H.264 natif) d'abord, sinon mp4v avec conversion ffmpeg après."""
|
| 66 |
+
out_path = OUTPUT_DIR / f"{jid}_tracked.mp4"
|
| 67 |
+
# Tente H.264 directement (macOS/Linux avec codec installé)
|
| 68 |
+
for fourcc_str in ["avc1", "H264", "mp4v"]:
|
| 69 |
+
writer = cv2.VideoWriter(
|
| 70 |
+
str(out_path), cv2.VideoWriter_fourcc(*fourcc_str),
|
| 71 |
+
max(fps / SKIP, 1), size
|
| 72 |
+
)
|
| 73 |
+
if writer.isOpened():
|
| 74 |
+
print(f"[TrackIQ] VideoWriter ouvert avec codec: {fourcc_str}")
|
| 75 |
+
return writer, out_path
|
| 76 |
+
raise RuntimeError("Unable to initialize video writer with any codec")
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _convert_to_h264(src_path):
|
| 80 |
+
"""Convertit mp4v → H.264 avec ffmpeg pour compatibilité navigateur."""
|
| 81 |
+
final = src_path.parent / (src_path.stem + "_h264.mp4")
|
| 82 |
+
# Essaie plusieurs encodeurs dans l'ordre
|
| 83 |
+
encoders = [
|
| 84 |
+
["-c:v", "libx264", "-preset", "fast", "-crf", "23", "-movflags", "+faststart"],
|
| 85 |
+
["-c:v", "h264", "-movflags", "+faststart"],
|
| 86 |
+
["-c:v", "mpeg4", "-q:v", "5"],
|
| 87 |
+
]
|
| 88 |
+
for enc_args in encoders:
|
| 89 |
+
try:
|
| 90 |
+
subprocess.run(
|
| 91 |
+
["ffmpeg", "-y", "-i", str(src_path)] + enc_args + [str(final)],
|
| 92 |
+
check=True, capture_output=True, timeout=600
|
| 93 |
+
)
|
| 94 |
+
if final.exists() and final.stat().st_size > 1000:
|
| 95 |
+
src_path.unlink(missing_ok=True)
|
| 96 |
+
final.rename(src_path)
|
| 97 |
+
print(f"[TrackIQ] Converted to H.264: {src_path.name}")
|
| 98 |
+
return True
|
| 99 |
+
except FileNotFoundError:
|
| 100 |
+
print("[TrackIQ] ffmpeg not found — keeping original codec")
|
| 101 |
+
return False
|
| 102 |
+
except Exception as e:
|
| 103 |
+
print(f"[TrackIQ] ffmpeg encoder failed ({enc_args[1]}): {e}")
|
| 104 |
+
continue
|
| 105 |
+
print("[TrackIQ] All ffmpeg encoders failed — keeping mp4v")
|
| 106 |
+
return False
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
# ── Pages ─────────────────────────────────────────────────────────────────────
|
| 110 |
+
@app.route("/")
|
| 111 |
+
def page_index(): return send_from_directory("templates","index.html")
|
| 112 |
+
@app.route("/home")
|
| 113 |
+
def page_home(): return send_from_directory("templates","home.html")
|
| 114 |
+
@app.route("/dashboard")
|
| 115 |
+
def page_dashboard(): return send_from_directory("templates","dashboard.html")
|
| 116 |
+
@app.route("/logs")
|
| 117 |
+
def page_logs(): return send_from_directory("templates","logs.html")
|
| 118 |
+
@app.route("/history")
|
| 119 |
+
def page_history(): return send_from_directory("templates","history.html")
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
# ── Upload ────────────────────────────────────────────────────────────────────
|
| 123 |
+
@app.route("/api/upload", methods=["POST"])
|
| 124 |
+
def api_upload():
|
| 125 |
+
if "video" not in request.files:
|
| 126 |
+
return jsonify({"error":"No file"}),400
|
| 127 |
+
f = request.files["video"]
|
| 128 |
+
jid = uuid.uuid4().hex[:10]
|
| 129 |
+
dest = UPLOAD_DIR / f"{jid}_{f.filename}"
|
| 130 |
+
f.save(str(dest))
|
| 131 |
+
_jobs[jid] = {"status":"uploaded","path":str(dest),"name":f.filename,"stats":{}}
|
| 132 |
+
return jsonify({"job_id":jid,"filename":f.filename})
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
# ── Run ───────────────────────────────────────────────────────────────────────
|
| 136 |
+
@app.route("/api/run", methods=["POST"])
|
| 137 |
+
def api_run():
|
| 138 |
+
data = request.json or {}
|
| 139 |
+
jid = data.get("job_id")
|
| 140 |
+
sel_names = data.get("classes") or DEFAULT_CLASSES
|
| 141 |
+
model_key = data.get("model","yolo11n")
|
| 142 |
+
if not jid or jid not in _jobs:
|
| 143 |
+
return jsonify({"error":"Unknown job_id"}),404
|
| 144 |
+
_jobs[jid]["status"] = "running"
|
| 145 |
+
_sse_queues[jid] = queue.Queue(maxsize=300)
|
| 146 |
+
threading.Thread(target=_worker, args=(jid,model_key,sel_names), daemon=True).start()
|
| 147 |
+
return jsonify({"job_id":jid,"status":"started"})
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
# ── Worker ────────────────────────────────────────────────────────────────────
|
| 151 |
+
def _worker(jid, model_key, sel_names):
|
| 152 |
+
job = _jobs[jid]
|
| 153 |
+
q = _sse_queues[jid]
|
| 154 |
+
vpath = job["path"]
|
| 155 |
+
class_ids = [COCO_MAP[n] for n in sel_names if n in COCO_MAP]
|
| 156 |
+
|
| 157 |
+
cap = cv2.VideoCapture(vpath)
|
| 158 |
+
fps = cap.get(cv2.CAP_PROP_FPS) or 25
|
| 159 |
+
W = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 160 |
+
H = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 161 |
+
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 162 |
+
|
| 163 |
+
writer, out_path = _make_video_writer(jid, fps, (W, H))
|
| 164 |
+
log_csv = LOG_DIR / f"{jid}_detections.csv"
|
| 165 |
+
log_json = LOG_DIR / f"{jid}_stats.json"
|
| 166 |
+
_f = open(str(log_csv),"w",newline="")
|
| 167 |
+
schema = ["scene_id","frame_id","timestamp_s","track_id","class_name",
|
| 168 |
+
"confidence","x1","y1","x2","y2","cx","cy","direction"]
|
| 169 |
+
_w = csv.DictWriter(_f, fieldnames=schema); _w.writeheader()
|
| 170 |
+
|
| 171 |
+
unique = defaultdict(set)
|
| 172 |
+
hist_xy = defaultdict(list)
|
| 173 |
+
timeline= []
|
| 174 |
+
model = _get_model(model_key)
|
| 175 |
+
frame_id= 0
|
| 176 |
+
last_frame_counts = {}
|
| 177 |
+
peak_frame_counts = defaultdict(int)
|
| 178 |
+
|
| 179 |
+
while True:
|
| 180 |
+
ret, frame = cap.read()
|
| 181 |
+
if not ret: break
|
| 182 |
+
ts = frame_id / fps
|
| 183 |
+
|
| 184 |
+
if frame_id % SKIP != 0:
|
| 185 |
+
frame_id += 1
|
| 186 |
+
continue
|
| 187 |
+
|
| 188 |
+
results = model.track(
|
| 189 |
+
frame, classes=class_ids, conf=CONF, iou=IOU,
|
| 190 |
+
imgsz=INFER_SZ, tracker="bytetrack.yaml",
|
| 191 |
+
persist=True, verbose=False,
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
detections = []; frame_cnts = defaultdict(int)
|
| 195 |
+
if results[0].boxes is not None and results[0].boxes.id is not None:
|
| 196 |
+
for box,tid,cls_id,conf in zip(
|
| 197 |
+
results[0].boxes.xyxy.cpu().numpy(),
|
| 198 |
+
results[0].boxes.id.cpu().numpy().astype(int),
|
| 199 |
+
results[0].boxes.cls.cpu().numpy().astype(int),
|
| 200 |
+
results[0].boxes.conf.cpu().numpy(),
|
| 201 |
+
):
|
| 202 |
+
cname = next((n for n,c in COCO_MAP.items() if c==int(cls_id)),"unknown")
|
| 203 |
+
if cname not in sel_names: continue
|
| 204 |
+
x1,y1,x2,y2 = map(int,box)
|
| 205 |
+
cx,cy = (x1+x2)//2,(y1+y2)//2
|
| 206 |
+
h = hist_xy[int(tid)]; h.append((cx,cy))
|
| 207 |
+
if len(h)>=4:
|
| 208 |
+
dx=h[-1][0]-h[-4][0]; dy=h[-1][1]-h[-4][1]
|
| 209 |
+
direction=("right" if dx>0 else "left") if abs(dx)>abs(dy) else ("down" if dy>0 else "up")
|
| 210 |
+
else: direction="unknown"
|
| 211 |
+
unique[cname].add(int(tid)); frame_cnts[cname]+=1
|
| 212 |
+
det=dict(scene_id=jid,frame_id=frame_id,timestamp_s=round(ts,3),
|
| 213 |
+
track_id=int(tid),class_name=cname,confidence=round(float(conf),3),
|
| 214 |
+
x1=x1,y1=y1,x2=x2,y2=y2,cx=cx,cy=cy,direction=direction)
|
| 215 |
+
_w.writerow(det); detections.append(det)
|
| 216 |
+
job.setdefault("_all_dets",[]).append(det)
|
| 217 |
+
|
| 218 |
+
last_frame_counts = dict(frame_cnts)
|
| 219 |
+
for cname, count in frame_cnts.items():
|
| 220 |
+
peak_frame_counts[cname] = max(peak_frame_counts[cname], count)
|
| 221 |
+
|
| 222 |
+
timeline.append({"frame":frame_id,"ts":round(ts,2),**dict(frame_cnts)})
|
| 223 |
+
writer.write(_draw(frame.copy(), detections, frame_cnts))
|
| 224 |
+
|
| 225 |
+
uc = {k:len(v) for k,v in unique.items()}
|
| 226 |
+
payload = {
|
| 227 |
+
"frame":frame_id,"total":total,"ts":round(ts,2),
|
| 228 |
+
"pct":round(frame_id/max(total,1)*100,1),
|
| 229 |
+
"detections":len(detections),"unique_counts":uc,
|
| 230 |
+
"total_unique":sum(uc.values()),"no_objects":len(detections)==0,
|
| 231 |
+
"frame_counts":dict(frame_cnts),
|
| 232 |
+
"hud":{"frame_str":f"F:{frame_id} ▶{int(fps)}fps","counts":dict(frame_cnts)},
|
| 233 |
+
}
|
| 234 |
+
if not q.full(): q.put(payload)
|
| 235 |
+
frame_id+=1
|
| 236 |
+
|
| 237 |
+
cap.release(); writer.release(); _f.close()
|
| 238 |
+
|
| 239 |
+
# ✅ Conversion H.264 pour le navigateur (mp4v seul ne fonctionne pas dans Chrome)
|
| 240 |
+
_convert_to_h264(out_path)
|
| 241 |
+
|
| 242 |
+
all_confs = [d["confidence"] for d in job.get("_all_dets", [])] if job.get("_all_dets") else []
|
| 243 |
+
avg_conf = round(sum(all_confs)/len(all_confs),3) if all_confs else None
|
| 244 |
+
stats={
|
| 245 |
+
"scene_id":jid,"video_name":job.get("name",""),
|
| 246 |
+
"generated_at":datetime.now().isoformat(),
|
| 247 |
+
"fps":fps,"total_frames":frame_id,"duration_s":round(frame_id/fps,1),
|
| 248 |
+
"unique_counts":{k:len(v) for k,v in unique.items()},
|
| 249 |
+
"total_unique":sum(len(v) for v in unique.values()),
|
| 250 |
+
"last_frame_counts":last_frame_counts,
|
| 251 |
+
"peak_counts":dict(peak_frame_counts),
|
| 252 |
+
"avg_conf":avg_conf,
|
| 253 |
+
"timeline":timeline,
|
| 254 |
+
}
|
| 255 |
+
with open(str(log_json),"w") as f: json.dump(stats,f,indent=2)
|
| 256 |
+
job["status"]="done"; job["stats"]=stats; job["out_mp4"]=str(out_path)
|
| 257 |
+
q.put({"event":"done","stats":stats})
|
| 258 |
+
print(f"[TrackIQ] {jid} done — {stats['total_unique']} objets")
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def _draw(frame, detections, frame_counts):
|
| 262 |
+
for det in detections:
|
| 263 |
+
cls=det["class_name"]; color=CLASS_COLORS.get(cls,(200,200,200))
|
| 264 |
+
x1,y1,x2,y2=det["x1"],det["y1"],det["x2"],det["y2"]
|
| 265 |
+
cv2.rectangle(frame,(x1,y1),(x2,y2),color,2)
|
| 266 |
+
label=f"id={det['track_id']} {cls} {int(det['confidence']*100)}%"
|
| 267 |
+
(lw,lh),_=cv2.getTextSize(label,cv2.FONT_HERSHEY_SIMPLEX,0.48,1)
|
| 268 |
+
cv2.rectangle(frame,(x1,y1-lh-6),(x1+lw+4,y1),color,-1)
|
| 269 |
+
cv2.putText(frame,label,(x1+2,y1-3),cv2.FONT_HERSHEY_SIMPLEX,0.48,(255,255,255),1,cv2.LINE_AA)
|
| 270 |
+
y_pos=26
|
| 271 |
+
for cls,count in sorted(frame_counts.items()):
|
| 272 |
+
txt=f"{cls}: {count}"
|
| 273 |
+
cv2.putText(frame,txt,(10,y_pos),cv2.FONT_HERSHEY_SIMPLEX,0.58,(0,0,0),3,cv2.LINE_AA)
|
| 274 |
+
cv2.putText(frame,txt,(10,y_pos),cv2.FONT_HERSHEY_SIMPLEX,0.58,(255,255,255),2,cv2.LINE_AA)
|
| 275 |
+
y_pos+=26
|
| 276 |
+
if not detections:
|
| 277 |
+
h=frame.shape[0]
|
| 278 |
+
cv2.putText(frame,"No target objects detected",(10,h-16),
|
| 279 |
+
cv2.FONT_HERSHEY_SIMPLEX,0.6,(80,80,255),2,cv2.LINE_AA)
|
| 280 |
+
return frame
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
# ── SSE ───────────────────────────────────────────────────────────────────────
|
| 284 |
+
@app.route("/api/stream/<jid>")
|
| 285 |
+
def api_stream(jid):
|
| 286 |
+
@stream_with_context
|
| 287 |
+
def generate():
|
| 288 |
+
q=_sse_queues.get(jid)
|
| 289 |
+
if not q: yield 'data:{"error":"no job"}\n\n'; return
|
| 290 |
+
while True:
|
| 291 |
+
try:
|
| 292 |
+
p=q.get(timeout=30)
|
| 293 |
+
yield f"data:{json.dumps(p)}\n\n"
|
| 294 |
+
if p.get("event")=="done": break
|
| 295 |
+
except: break
|
| 296 |
+
return Response(generate(), mimetype="text/event-stream",
|
| 297 |
+
headers={"Cache-Control":"no-cache","X-Accel-Buffering":"no"})
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
# ── Video (range requests) ───────────────────────────────────────────────────
|
| 301 |
+
@app.route("/api/video/<jid>")
|
| 302 |
+
def api_video(jid):
|
| 303 |
+
job=_jobs.get(jid)
|
| 304 |
+
if not job: abort(404)
|
| 305 |
+
out=job.get("out_mp4") or str(OUTPUT_DIR/f"{jid}_tracked.mp4")
|
| 306 |
+
p=Path(out)
|
| 307 |
+
if not p.exists(): abort(404)
|
| 308 |
+
mime_type = mimetypes.guess_type(str(p))[0] or "video/mp4"
|
| 309 |
+
file_size=p.stat().st_size
|
| 310 |
+
range_hdr=request.headers.get("Range")
|
| 311 |
+
if range_hdr:
|
| 312 |
+
import re
|
| 313 |
+
byte1,byte2=0,file_size-1
|
| 314 |
+
m=re.search(r"bytes=(\d+)-(\d*)",range_hdr)
|
| 315 |
+
if m:
|
| 316 |
+
byte1=int(m.group(1))
|
| 317 |
+
if m.group(2): byte2=int(m.group(2))
|
| 318 |
+
length=byte2-byte1+1
|
| 319 |
+
def chunk():
|
| 320 |
+
with open(str(p),"rb") as f:
|
| 321 |
+
f.seek(byte1); remaining=length
|
| 322 |
+
while remaining>0:
|
| 323 |
+
c=f.read(min(65536,remaining))
|
| 324 |
+
if not c: break
|
| 325 |
+
remaining-=len(c); yield c
|
| 326 |
+
resp=Response(chunk(),status=206,mimetype=mime_type,direct_passthrough=True)
|
| 327 |
+
resp.headers["Content-Range"]=f"bytes {byte1}-{byte2}/{file_size}"
|
| 328 |
+
resp.headers["Accept-Ranges"]="bytes"
|
| 329 |
+
resp.headers["Content-Length"]=str(length)
|
| 330 |
+
resp.headers["Access-Control-Allow-Origin"]="*"
|
| 331 |
+
return resp
|
| 332 |
+
resp = send_file(str(p),mimetype=mime_type,conditional=True)
|
| 333 |
+
resp.headers["Accept-Ranges"] = "bytes"
|
| 334 |
+
resp.headers["Access-Control-Allow-Origin"] = "*"
|
| 335 |
+
return resp
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
# ── Status ────────────────────────────────────────────────────────────────────
|
| 339 |
+
@app.route("/api/status/<jid>")
|
| 340 |
+
def api_status(jid):
|
| 341 |
+
job=_jobs.get(jid)
|
| 342 |
+
if not job: return jsonify({"error":"not found"}),404
|
| 343 |
+
return jsonify({"status":job["status"],"stats":job.get("stats",{}),
|
| 344 |
+
"video_url":f"/api/video/{jid}" if job.get("out_mp4") else None})
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
# ── Logs ───────────────────────────��──────────────────────────────────────────
|
| 348 |
+
@app.route("/api/logs")
|
| 349 |
+
def api_logs_list():
|
| 350 |
+
result=[]
|
| 351 |
+
for jf in sorted(LOG_DIR.glob("*_stats.json"),reverse=True):
|
| 352 |
+
try:
|
| 353 |
+
with open(str(jf)) as f: data=json.load(f)
|
| 354 |
+
result.append({"scene_id":data.get("scene_id",jf.stem),
|
| 355 |
+
"video_name":data.get("video_name",""),
|
| 356 |
+
"generated_at":data.get("generated_at",""),
|
| 357 |
+
"fps":data.get("fps",0),"total_frames":data.get("total_frames",0),
|
| 358 |
+
"duration_s":data.get("duration_s",0),
|
| 359 |
+
"unique_counts":data.get("unique_counts",{}),"total_unique":data.get("total_unique",0)})
|
| 360 |
+
except: pass
|
| 361 |
+
return jsonify(result)
|
| 362 |
+
|
| 363 |
+
@app.route("/api/logs/rows")
|
| 364 |
+
def api_logs_rows():
|
| 365 |
+
rows = []
|
| 366 |
+
for csv_path in sorted(LOG_DIR.glob("*_detections.csv"), reverse=True):
|
| 367 |
+
try:
|
| 368 |
+
scene_id = csv_path.stem.replace("_detections", "")
|
| 369 |
+
stats_path = LOG_DIR / f"{scene_id}_stats.json"
|
| 370 |
+
stats = {}
|
| 371 |
+
if stats_path.exists():
|
| 372 |
+
with open(str(stats_path), encoding="utf-8") as sf:
|
| 373 |
+
stats = json.load(sf)
|
| 374 |
+
with open(str(csv_path), newline="", encoding="utf-8") as cf:
|
| 375 |
+
reader = csv.DictReader(cf)
|
| 376 |
+
for row in reader:
|
| 377 |
+
rows.append({
|
| 378 |
+
"scene_id": row.get("scene_id", scene_id),
|
| 379 |
+
"video_name": stats.get("video_name", ""),
|
| 380 |
+
"generated_at": stats.get("generated_at", ""),
|
| 381 |
+
"frame_id": int(float(row.get("frame_id") or 0)),
|
| 382 |
+
"timestamp_s": float(row.get("timestamp_s") or 0),
|
| 383 |
+
"track_id": int(float(row.get("track_id") or 0)),
|
| 384 |
+
"class_name": row.get("class_name", ""),
|
| 385 |
+
"confidence": float(row.get("confidence") or 0),
|
| 386 |
+
"x1": int(float(row.get("x1") or 0)),
|
| 387 |
+
"y1": int(float(row.get("y1") or 0)),
|
| 388 |
+
"x2": int(float(row.get("x2") or 0)),
|
| 389 |
+
"y2": int(float(row.get("y2") or 0)),
|
| 390 |
+
"cx": int(float(row.get("cx") or 0)),
|
| 391 |
+
"cy": int(float(row.get("cy") or 0)),
|
| 392 |
+
"direction": row.get("direction", "unknown"),
|
| 393 |
+
})
|
| 394 |
+
except Exception:
|
| 395 |
+
pass
|
| 396 |
+
return jsonify(rows)
|
| 397 |
+
|
| 398 |
+
@app.route("/api/logs/<scene_id>/csv")
|
| 399 |
+
def api_logs_csv(scene_id):
|
| 400 |
+
p=LOG_DIR/f"{scene_id}_detections.csv"
|
| 401 |
+
if not p.exists(): abort(404)
|
| 402 |
+
return send_file(str(p),as_attachment=True,download_name=f"{scene_id}_detections.csv",mimetype="text/csv")
|
| 403 |
+
|
| 404 |
+
@app.route("/api/logs/export", methods=["POST"])
|
| 405 |
+
def api_logs_export():
|
| 406 |
+
"""Génère un CSV groupé: group_scene_id.csv avec toutes les bounding boxes"""
|
| 407 |
+
import io
|
| 408 |
+
from flask import make_response
|
| 409 |
+
data = request.json or {}
|
| 410 |
+
scene_ids = data.get("scene_ids", [])
|
| 411 |
+
group = (data.get("group", "trackiq") or "trackiq").strip().replace(" ","_")
|
| 412 |
+
|
| 413 |
+
if not scene_ids:
|
| 414 |
+
scene_ids = [p.stem.replace("_detections","") for p in sorted(LOG_DIR.glob("*_detections.csv"))]
|
| 415 |
+
|
| 416 |
+
buf = io.StringIO()
|
| 417 |
+
fieldnames = ["group","scene_id","video_name","generated_at","frame_id","timestamp_s",
|
| 418 |
+
"track_id","class_name","confidence","x1","y1","x2","y2","cx","cy","direction"]
|
| 419 |
+
writer = csv.DictWriter(buf, fieldnames=fieldnames)
|
| 420 |
+
writer.writeheader()
|
| 421 |
+
|
| 422 |
+
for sid in scene_ids:
|
| 423 |
+
csv_path = LOG_DIR / f"{sid}_detections.csv"
|
| 424 |
+
stats_path = LOG_DIR / f"{sid}_stats.json"
|
| 425 |
+
if not csv_path.exists(): continue
|
| 426 |
+
stats = {}
|
| 427 |
+
if stats_path.exists():
|
| 428 |
+
with open(str(stats_path), encoding="utf-8") as sf:
|
| 429 |
+
stats = json.load(sf)
|
| 430 |
+
with open(str(csv_path), newline="", encoding="utf-8") as cf:
|
| 431 |
+
reader = csv.DictReader(cf)
|
| 432 |
+
for row in reader:
|
| 433 |
+
writer.writerow({
|
| 434 |
+
"group": group, "scene_id": row.get("scene_id", sid),
|
| 435 |
+
"video_name": stats.get("video_name",""), "generated_at": stats.get("generated_at",""),
|
| 436 |
+
"frame_id": row.get("frame_id",""), "timestamp_s": row.get("timestamp_s",""),
|
| 437 |
+
"track_id": row.get("track_id",""), "class_name": row.get("class_name",""),
|
| 438 |
+
"confidence": row.get("confidence",""),
|
| 439 |
+
"x1": row.get("x1",""), "y1": row.get("y1",""),
|
| 440 |
+
"x2": row.get("x2",""), "y2": row.get("y2",""),
|
| 441 |
+
"cx": row.get("cx",""), "cy": row.get("cy",""),
|
| 442 |
+
"direction": row.get("direction",""),
|
| 443 |
+
})
|
| 444 |
+
|
| 445 |
+
csv_bytes = buf.getvalue().encode("utf-8")
|
| 446 |
+
suffix = scene_ids[0] if len(scene_ids)==1 else "all"
|
| 447 |
+
filename = f"{group}_{suffix}.csv"
|
| 448 |
+
resp = make_response(csv_bytes)
|
| 449 |
+
resp.headers["Content-Type"] = "text/csv; charset=utf-8"
|
| 450 |
+
resp.headers["Content-Disposition"] = f"attachment; filename={filename}"
|
| 451 |
+
return resp
|
| 452 |
+
|
| 453 |
+
@app.route("/api/logs/<scene_id>", methods=["DELETE"])
|
| 454 |
+
def api_logs_delete(scene_id):
|
| 455 |
+
for ext in ("_stats.json","_detections.csv"):
|
| 456 |
+
p=LOG_DIR/f"{scene_id}{ext}"
|
| 457 |
+
if p.exists(): p.unlink()
|
| 458 |
+
for out in OUTPUT_DIR.glob(f"{scene_id}_tracked.*"):
|
| 459 |
+
if out.exists(): out.unlink()
|
| 460 |
+
return jsonify({"deleted":scene_id})
|
| 461 |
+
|
| 462 |
+
|
| 463 |
+
# ── Dashboard ─────────────────────────────────────────────────────────────────
|
| 464 |
+
@app.route("/api/dashboard/stats")
|
| 465 |
+
def api_dashboard_stats():
|
| 466 |
+
global_uc=defaultdict(int); total_veh=0; scenes=[]
|
| 467 |
+
for jf in sorted(LOG_DIR.glob("*_stats.json"),reverse=True)[:20]:
|
| 468 |
+
try:
|
| 469 |
+
with open(str(jf)) as f: data=json.load(f)
|
| 470 |
+
uc=data.get("unique_counts",{}); sv=sum(uc.values()); total_veh+=sv
|
| 471 |
+
for cls,cnt in uc.items(): global_uc[cls]+=cnt
|
| 472 |
+
scenes.append({"scene_id":data.get("scene_id",""),"video_name":data.get("video_name",""),
|
| 473 |
+
"total":sv,"counts":uc,"duration_s":data.get("duration_s",0),
|
| 474 |
+
"avg_conf":data.get("avg_conf"),
|
| 475 |
+
"timeline":data.get("timeline",[])})
|
| 476 |
+
except: pass
|
| 477 |
+
return jsonify({"total_vehicles_today":total_veh,
|
| 478 |
+
"global_unique_counts":dict(global_uc),"scenes":scenes[:10]})
|
| 479 |
+
|
| 480 |
+
|
| 481 |
+
# ── History ───────────────────────────────────────────────────────────────────
|
| 482 |
+
@app.route("/api/history")
|
| 483 |
+
def api_history():
|
| 484 |
+
return jsonify([{"job_id":jid,"name":job.get("name","video"),
|
| 485 |
+
"url":f"/api/video/{jid}","date":job.get("stats",{}).get("generated_at",""),
|
| 486 |
+
"counts":job.get("stats",{}).get("unique_counts",{})}
|
| 487 |
+
for jid,job in reversed(list(_jobs.items())) if job.get("status")=="done"])
|
| 488 |
+
|
| 489 |
+
|
| 490 |
+
# ── Webcam ────────────────────────────────────────────────────────────────────
|
| 491 |
+
@app.route("/api/webcam/start", methods=["POST"])
|
| 492 |
+
def api_webcam_start():
|
| 493 |
+
global _webcam_active, _webcam_model_key, _webcam_classes
|
| 494 |
+
d=request.json or {}
|
| 495 |
+
_webcam_active=True
|
| 496 |
+
_webcam_model_key = d.get("model","yolo11n")
|
| 497 |
+
_webcam_classes = d.get("classes") or DEFAULT_CLASSES
|
| 498 |
+
# Load model eagerly so first frame is fast
|
| 499 |
+
threading.Thread(target=lambda: _get_model(_webcam_model_key), daemon=True).start()
|
| 500 |
+
return jsonify({"status":"started"})
|
| 501 |
+
|
| 502 |
+
@app.route("/api/webcam/frame", methods=["POST"])
|
| 503 |
+
def api_webcam_frame():
|
| 504 |
+
"""Receive a JPEG frame from the browser, run YOLO, return annotated JPEG."""
|
| 505 |
+
global _webcam_stats, _webcam_frame_id, _webcam_unique, _webcam_hist_xy
|
| 506 |
+
if not _webcam_active:
|
| 507 |
+
return jsonify({"error":"not active"}), 400
|
| 508 |
+
data = request.get_data()
|
| 509 |
+
if not data:
|
| 510 |
+
return jsonify({"error":"no frame"}), 400
|
| 511 |
+
import numpy as np
|
| 512 |
+
arr = np.frombuffer(data, dtype=np.uint8)
|
| 513 |
+
frame = cv2.imdecode(arr, cv2.IMREAD_COLOR)
|
| 514 |
+
if frame is None:
|
| 515 |
+
return jsonify({"error":"decode failed"}), 400
|
| 516 |
+
|
| 517 |
+
class_ids = [COCO_MAP[n] for n in _webcam_classes if n in COCO_MAP]
|
| 518 |
+
model = _get_model(_webcam_model_key)
|
| 519 |
+
fid = _webcam_frame_id
|
| 520 |
+
_webcam_frame_id += 1
|
| 521 |
+
|
| 522 |
+
res = model.track(frame, classes=class_ids, conf=CONF, iou=IOU,
|
| 523 |
+
imgsz=INFER_SZ, tracker="bytetrack.yaml",
|
| 524 |
+
persist=True, verbose=False)
|
| 525 |
+
|
| 526 |
+
dets = []; frame_counts = defaultdict(int)
|
| 527 |
+
if res[0].boxes is not None and res[0].boxes.id is not None:
|
| 528 |
+
for box, tid, cls_id, conf in zip(
|
| 529 |
+
res[0].boxes.xyxy.cpu().numpy(),
|
| 530 |
+
res[0].boxes.id.cpu().numpy().astype(int),
|
| 531 |
+
res[0].boxes.cls.cpu().numpy().astype(int),
|
| 532 |
+
res[0].boxes.conf.cpu().numpy()
|
| 533 |
+
):
|
| 534 |
+
cname = next((n for n,c in COCO_MAP.items() if c==int(cls_id)), "unknown")
|
| 535 |
+
if cname not in _webcam_classes: continue
|
| 536 |
+
x1,y1,x2,y2 = map(int, box)
|
| 537 |
+
cx,cy = (x1+x2)//2,(y1+y2)//2
|
| 538 |
+
h = _webcam_hist_xy[int(tid)]; h.append((cx,cy))
|
| 539 |
+
if len(h)>=4:
|
| 540 |
+
dx=h[-1][0]-h[-4][0]; dy=h[-1][1]-h[-4][1]
|
| 541 |
+
direction=("right" if dx>0 else "left") if abs(dx)>abs(dy) else ("down" if dy>0 else "up")
|
| 542 |
+
else: direction="unknown"
|
| 543 |
+
_webcam_unique[cname].add(int(tid)); frame_counts[cname]+=1
|
| 544 |
+
det = dict(frame_id=fid,track_id=int(tid),class_name=cname,
|
| 545 |
+
confidence=round(float(conf),3),x1=x1,y1=y1,x2=x2,y2=y2,
|
| 546 |
+
cx=cx,cy=cy,direction=direction)
|
| 547 |
+
dets.append(det)
|
| 548 |
+
|
| 549 |
+
ann = _draw(frame.copy(), dets, frame_counts)
|
| 550 |
+
_, buf = cv2.imencode(".jpg", ann, [cv2.IMWRITE_JPEG_QUALITY, 72])
|
| 551 |
+
if not _webcam_queue.full():
|
| 552 |
+
_webcam_queue.put(buf.tobytes())
|
| 553 |
+
|
| 554 |
+
_webcam_stats = {
|
| 555 |
+
"unique_counts": {k:len(v) for k,v in _webcam_unique.items()},
|
| 556 |
+
"frame_counts": dict(frame_counts),
|
| 557 |
+
"frame": fid, "detections": len(dets)
|
| 558 |
+
}
|
| 559 |
+
return Response(buf.tobytes(), mimetype="image/jpeg")
|
| 560 |
+
|
| 561 |
+
@app.route("/api/webcam/stop", methods=["POST"])
|
| 562 |
+
def api_webcam_stop():
|
| 563 |
+
global _webcam_active, _webcam_frame_id, _webcam_unique, _webcam_hist_xy
|
| 564 |
+
_webcam_active = False
|
| 565 |
+
_webcam_frame_id = 0
|
| 566 |
+
_webcam_unique = defaultdict(set)
|
| 567 |
+
_webcam_hist_xy = defaultdict(list)
|
| 568 |
+
return jsonify({"status":"stopped"})
|
| 569 |
+
|
| 570 |
+
@app.route("/api/webcam/stats")
|
| 571 |
+
def api_webcam_stats():
|
| 572 |
+
return jsonify(_webcam_stats)
|
| 573 |
+
|
| 574 |
+
@app.route("/api/webcam/stream")
|
| 575 |
+
def api_webcam_stream():
|
| 576 |
+
def gen():
|
| 577 |
+
while _webcam_active:
|
| 578 |
+
try:
|
| 579 |
+
fb=_webcam_queue.get(timeout=2)
|
| 580 |
+
yield b"--frame\r\nContent-Type: image/jpeg\r\n\r\n"+fb+b"\r\n"
|
| 581 |
+
except: pass
|
| 582 |
+
return Response(gen(),mimetype="multipart/x-mixed-replace; boundary=frame")
|
| 583 |
+
|
| 584 |
+
# ── Static ────────────────────────────────────────────────────────────────────
|
| 585 |
+
@app.route("/outputs/<path:fname>")
|
| 586 |
+
def serve_output(fname): return send_from_directory(str(OUTPUT_DIR),fname)
|
| 587 |
+
@app.route("/uploads/<path:fname>")
|
| 588 |
+
def serve_upload(fname): return send_from_directory(str(UPLOAD_DIR),fname)
|
| 589 |
+
|
| 590 |
+
|
| 591 |
+
if __name__=="__main__":
|
| 592 |
+
print("="*50)
|
| 593 |
+
print(" TrackIQ — http://localhost:5000")
|
| 594 |
+
print(f" Skip={SKIP} | imgsz={INFER_SZ} | CPU optimisé")
|
| 595 |
+
print("="*50)
|
| 596 |
+
app.run(host="0.0.0.0",port=5000,debug=False,threaded=True)
|
models/.gitkeep
ADDED
|
File without changes
|
models/yolo11n.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:49da7a898d364366a0925c144b6701980a3bf0525c5b904020a0352228cb2e45
|
| 3 |
+
size 22523050
|
requirements.txt
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
absl-py==2.4.0
|
| 2 |
+
alabaster==1.0.0
|
| 3 |
+
altair==5.5.0
|
| 4 |
+
annotated-doc==0.0.4
|
| 5 |
+
annotated-types==0.7.0
|
| 6 |
+
anyio==4.13.0
|
| 7 |
+
asttokens==3.0.0
|
| 8 |
+
astunparse==1.6.3
|
| 9 |
+
attrs==25.4.0
|
| 10 |
+
babel==2.17.0
|
| 11 |
+
beautifulsoup4==4.14.2
|
| 12 |
+
bleach==6.3.0
|
| 13 |
+
blinker==1.9.0
|
| 14 |
+
cachetools==6.2.2
|
| 15 |
+
certifi==2025.11.12
|
| 16 |
+
cffi==2.0.0
|
| 17 |
+
charset-normalizer==3.4.4
|
| 18 |
+
clarabel==0.11.1
|
| 19 |
+
click==8.3.1
|
| 20 |
+
colorama==0.4.6
|
| 21 |
+
comm==0.2.3
|
| 22 |
+
contourpy==1.3.3
|
| 23 |
+
cvxpy==1.7.5
|
| 24 |
+
cycler==0.12.1
|
| 25 |
+
debugpy==1.8.17
|
| 26 |
+
decorator==5.2.1
|
| 27 |
+
defusedxml==0.7.1
|
| 28 |
+
dill==0.4.1
|
| 29 |
+
docutils==0.22.4
|
| 30 |
+
et_xmlfile==2.0.0
|
| 31 |
+
executing==2.2.1
|
| 32 |
+
fastjsonschema==2.21.2
|
| 33 |
+
fica==0.4.1
|
| 34 |
+
filelock==3.24.3
|
| 35 |
+
Flask==3.1.3
|
| 36 |
+
flask-cors==6.0.2
|
| 37 |
+
flatbuffers==25.12.19
|
| 38 |
+
fonttools==4.60.1
|
| 39 |
+
fsspec==2026.2.0
|
| 40 |
+
gast==0.7.0
|
| 41 |
+
geopandas==1.1.2
|
| 42 |
+
git-filter-repo==2.47.0
|
| 43 |
+
gitdb==4.0.12
|
| 44 |
+
GitPython==3.1.45
|
| 45 |
+
google-pasta==0.2.0
|
| 46 |
+
graphviz==0.21
|
| 47 |
+
greenlet==3.3.1
|
| 48 |
+
grpcio==1.80.0
|
| 49 |
+
h11==0.16.0
|
| 50 |
+
h5py==3.14.0
|
| 51 |
+
hf-xet==1.4.3
|
| 52 |
+
httpcore==1.0.9
|
| 53 |
+
httpx==0.28.1
|
| 54 |
+
huggingface_hub==1.10.2
|
| 55 |
+
idna==3.11
|
| 56 |
+
imagesize==1.4.1
|
| 57 |
+
ipykernel==7.1.0
|
| 58 |
+
ipylab==1.1.0
|
| 59 |
+
ipython==9.6.0
|
| 60 |
+
ipython_pygments_lexers==1.1.1
|
| 61 |
+
ipywidgets==8.1.8
|
| 62 |
+
itsdangerous==2.2.0
|
| 63 |
+
jedi==0.19.2
|
| 64 |
+
Jinja2==3.1.6
|
| 65 |
+
joblib==1.5.3
|
| 66 |
+
jsonschema==4.25.1
|
| 67 |
+
jsonschema-specifications==2025.9.1
|
| 68 |
+
jupyter_client==8.6.3
|
| 69 |
+
jupyter_core==5.9.1
|
| 70 |
+
jupyterlab_pygments==0.3.0
|
| 71 |
+
jupyterlab_widgets==3.0.16
|
| 72 |
+
jupytext==1.19.1
|
| 73 |
+
keras==3.14.0
|
| 74 |
+
kiwisolver==1.4.9
|
| 75 |
+
lap==0.5.13
|
| 76 |
+
lapx==0.9.4
|
| 77 |
+
libclang==18.1.1
|
| 78 |
+
lxml==6.0.2
|
| 79 |
+
markdown-it-py==4.0.0
|
| 80 |
+
MarkupSafe==3.0.3
|
| 81 |
+
matplotlib==3.10.7
|
| 82 |
+
matplotlib-inline==0.2.1
|
| 83 |
+
mdit-py-plugins==0.5.0
|
| 84 |
+
mdurl==0.1.2
|
| 85 |
+
mistune==3.2.0
|
| 86 |
+
ml_dtypes==0.5.4
|
| 87 |
+
mpmath==1.3.0
|
| 88 |
+
namex==0.1.0
|
| 89 |
+
narwhals==2.11.0
|
| 90 |
+
nbclient==0.10.4
|
| 91 |
+
nbconvert==7.17.0
|
| 92 |
+
nbformat==5.10.4
|
| 93 |
+
nest-asyncio==1.6.0
|
| 94 |
+
networkx==3.6.1
|
| 95 |
+
numpy==2.3.4
|
| 96 |
+
opencv-python==4.13.0.92
|
| 97 |
+
opencv-python-headless==4.13.0.92
|
| 98 |
+
openpyxl==3.1.5
|
| 99 |
+
opt_einsum==3.4.0
|
| 100 |
+
optree==0.19.0
|
| 101 |
+
osqp==1.0.5
|
| 102 |
+
otter-grader==6.1.6
|
| 103 |
+
outcome==1.3.0.post0
|
| 104 |
+
packaging==25.0
|
| 105 |
+
pandas==2.3.3
|
| 106 |
+
pandocfilters==1.5.1
|
| 107 |
+
parso==0.8.5
|
| 108 |
+
pillow==12.0.0
|
| 109 |
+
platformdirs==4.5.0
|
| 110 |
+
playwright==1.58.0
|
| 111 |
+
plotly==6.4.0
|
| 112 |
+
polars==1.40.0
|
| 113 |
+
polars-runtime-32==1.40.0
|
| 114 |
+
prompt_toolkit==3.0.52
|
| 115 |
+
protobuf==6.33.1
|
| 116 |
+
psutil==7.1.2
|
| 117 |
+
pure_eval==0.2.3
|
| 118 |
+
pyarrow==21.0.0
|
| 119 |
+
pycparser==2.23
|
| 120 |
+
pydantic==2.12.5
|
| 121 |
+
pydantic_core==2.41.5
|
| 122 |
+
pydeck==0.9.1
|
| 123 |
+
pyee==13.0.0
|
| 124 |
+
Pygments==2.19.2
|
| 125 |
+
pyogrio==0.12.1
|
| 126 |
+
pyparsing==3.2.5
|
| 127 |
+
pyproj==3.7.2
|
| 128 |
+
PySocks==1.7.1
|
| 129 |
+
python-dateutil==2.9.0.post0
|
| 130 |
+
python-dotenv==1.2.1
|
| 131 |
+
python-on-whales==0.80.0
|
| 132 |
+
pytz==2025.2
|
| 133 |
+
PyYAML==6.0.3
|
| 134 |
+
pyzmq==27.1.0
|
| 135 |
+
referencing==0.37.0
|
| 136 |
+
requests==2.32.5
|
| 137 |
+
rich==14.3.3
|
| 138 |
+
roman-numerals==4.1.0
|
| 139 |
+
rpds-py==0.28.0
|
| 140 |
+
scikit-learn==1.8.0
|
| 141 |
+
scipy==1.16.3
|
| 142 |
+
scs==3.2.11
|
| 143 |
+
seaborn==0.13.2
|
| 144 |
+
selenium==4.38.0
|
| 145 |
+
setuptools==80.9.0
|
| 146 |
+
shapely==2.1.2
|
| 147 |
+
shellingham==1.5.4
|
| 148 |
+
six==1.17.0
|
| 149 |
+
smmap==5.0.2
|
| 150 |
+
sniffio==1.3.1
|
| 151 |
+
snowballstemmer==3.0.1
|
| 152 |
+
sortedcontainers==2.4.0
|
| 153 |
+
soupsieve==2.8
|
| 154 |
+
Sphinx==9.1.0
|
| 155 |
+
sphinxcontrib-applehelp==2.0.0
|
| 156 |
+
sphinxcontrib-devhelp==2.0.0
|
| 157 |
+
sphinxcontrib-htmlhelp==2.1.0
|
| 158 |
+
sphinxcontrib-jsmath==1.0.1
|
| 159 |
+
sphinxcontrib-qthelp==2.0.0
|
| 160 |
+
sphinxcontrib-serializinghtml==2.0.0
|
| 161 |
+
stack-data==0.6.3
|
| 162 |
+
streamlit==1.51.0
|
| 163 |
+
sympy==1.14.0
|
| 164 |
+
tenacity==9.1.2
|
| 165 |
+
tensorflow==2.21.0
|
| 166 |
+
termcolor==3.3.0
|
| 167 |
+
threadpoolctl==3.6.0
|
| 168 |
+
tinycss2==1.4.0
|
| 169 |
+
toml==0.10.2
|
| 170 |
+
torch==2.11.0
|
| 171 |
+
torchvision==0.26.0
|
| 172 |
+
torchviz==0.0.3
|
| 173 |
+
tornado==6.5.2
|
| 174 |
+
tqdm==4.67.3
|
| 175 |
+
traitlets==5.14.3
|
| 176 |
+
trio==0.32.0
|
| 177 |
+
trio-websocket==0.12.2
|
| 178 |
+
typer==0.24.1
|
| 179 |
+
typing-inspection==0.4.2
|
| 180 |
+
typing_extensions==4.15.0
|
| 181 |
+
tzdata==2025.2
|
| 182 |
+
ultralytics==8.4.39
|
| 183 |
+
ultralytics-thop==2.0.18
|
| 184 |
+
urllib3==2.5.0
|
| 185 |
+
watchdog==6.0.0
|
| 186 |
+
wcwidth==0.2.14
|
| 187 |
+
webdriver-manager==4.0.2
|
| 188 |
+
webencodings==0.5.1
|
| 189 |
+
websocket-client==1.9.0
|
| 190 |
+
Werkzeug==3.1.8
|
| 191 |
+
wheel==0.46.3
|
| 192 |
+
widgetsnbextension==4.0.15
|
| 193 |
+
wrapt==1.17.3
|
| 194 |
+
wsproto==1.3.2
|
requirements_hf.txt
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
absl-py==2.4.0
|
| 2 |
+
alabaster==1.0.0
|
| 3 |
+
altair==5.5.0
|
| 4 |
+
annotated-doc==0.0.4
|
| 5 |
+
annotated-types==0.7.0
|
| 6 |
+
anyio==4.13.0
|
| 7 |
+
asttokens==3.0.0
|
| 8 |
+
astunparse==1.6.3
|
| 9 |
+
attrs==25.4.0
|
| 10 |
+
babel==2.17.0
|
| 11 |
+
beautifulsoup4==4.14.2
|
| 12 |
+
bleach==6.3.0
|
| 13 |
+
blinker==1.9.0
|
| 14 |
+
cachetools==6.2.2
|
| 15 |
+
certifi==2025.11.12
|
| 16 |
+
cffi==2.0.0
|
| 17 |
+
charset-normalizer==3.4.4
|
| 18 |
+
clarabel==0.11.1
|
| 19 |
+
click==8.3.1
|
| 20 |
+
colorama==0.4.6
|
| 21 |
+
comm==0.2.3
|
| 22 |
+
contourpy==1.3.3
|
| 23 |
+
cvxpy==1.7.5
|
| 24 |
+
cycler==0.12.1
|
| 25 |
+
debugpy==1.8.17
|
| 26 |
+
decorator==5.2.1
|
| 27 |
+
defusedxml==0.7.1
|
| 28 |
+
dill==0.4.1
|
| 29 |
+
docutils==0.22.4
|
| 30 |
+
et_xmlfile==2.0.0
|
| 31 |
+
executing==2.2.1
|
| 32 |
+
fastjsonschema==2.21.2
|
| 33 |
+
fica==0.4.1
|
| 34 |
+
filelock==3.24.3
|
| 35 |
+
Flask==3.1.3
|
| 36 |
+
flask-cors==6.0.2
|
| 37 |
+
flatbuffers==25.12.19
|
| 38 |
+
fonttools==4.60.1
|
| 39 |
+
fsspec==2026.2.0
|
| 40 |
+
gast==0.7.0
|
| 41 |
+
geopandas==1.1.2
|
| 42 |
+
git-filter-repo==2.47.0
|
| 43 |
+
gitdb==4.0.12
|
| 44 |
+
GitPython==3.1.45
|
| 45 |
+
google-pasta==0.2.0
|
| 46 |
+
graphviz==0.21
|
| 47 |
+
greenlet==3.3.1
|
| 48 |
+
grpcio==1.80.0
|
| 49 |
+
h11==0.16.0
|
| 50 |
+
h5py==3.14.0
|
| 51 |
+
hf-xet==1.4.3
|
| 52 |
+
httpcore==1.0.9
|
| 53 |
+
httpx==0.28.1
|
| 54 |
+
huggingface_hub==1.10.2
|
| 55 |
+
idna==3.11
|
| 56 |
+
imagesize==1.4.1
|
| 57 |
+
ipykernel==7.1.0
|
| 58 |
+
ipylab==1.1.0
|
| 59 |
+
ipython==9.6.0
|
| 60 |
+
ipython_pygments_lexers==1.1.1
|
| 61 |
+
ipywidgets==8.1.8
|
| 62 |
+
itsdangerous==2.2.0
|
| 63 |
+
jedi==0.19.2
|
| 64 |
+
Jinja2==3.1.6
|
| 65 |
+
joblib==1.5.3
|
| 66 |
+
jsonschema==4.25.1
|
| 67 |
+
jsonschema-specifications==2025.9.1
|
| 68 |
+
jupyter_client==8.6.3
|
| 69 |
+
jupyter_core==5.9.1
|
| 70 |
+
jupyterlab_pygments==0.3.0
|
| 71 |
+
jupyterlab_widgets==3.0.16
|
| 72 |
+
jupytext==1.19.1
|
| 73 |
+
keras==3.14.0
|
| 74 |
+
kiwisolver==1.4.9
|
| 75 |
+
lap==0.5.13
|
| 76 |
+
lapx==0.9.4
|
| 77 |
+
libclang==18.1.1
|
| 78 |
+
lxml==6.0.2
|
| 79 |
+
markdown-it-py==4.0.0
|
| 80 |
+
MarkupSafe==3.0.3
|
| 81 |
+
matplotlib==3.10.7
|
| 82 |
+
matplotlib-inline==0.2.1
|
| 83 |
+
mdit-py-plugins==0.5.0
|
| 84 |
+
mdurl==0.1.2
|
| 85 |
+
mistune==3.2.0
|
| 86 |
+
ml_dtypes==0.5.4
|
| 87 |
+
mpmath==1.3.0
|
| 88 |
+
namex==0.1.0
|
| 89 |
+
narwhals==2.11.0
|
| 90 |
+
nbclient==0.10.4
|
| 91 |
+
nbconvert==7.17.0
|
| 92 |
+
nbformat==5.10.4
|
| 93 |
+
nest-asyncio==1.6.0
|
| 94 |
+
networkx==3.6.1
|
| 95 |
+
numpy==2.3.4
|
| 96 |
+
opencv-python==4.13.0.92
|
| 97 |
+
opencv-python-headless==4.13.0.92
|
| 98 |
+
openpyxl==3.1.5
|
| 99 |
+
opt_einsum==3.4.0
|
| 100 |
+
optree==0.19.0
|
| 101 |
+
osqp==1.0.5
|
| 102 |
+
otter-grader==6.1.6
|
| 103 |
+
outcome==1.3.0.post0
|
| 104 |
+
packaging==25.0
|
| 105 |
+
pandas==2.3.3
|
| 106 |
+
pandocfilters==1.5.1
|
| 107 |
+
parso==0.8.5
|
| 108 |
+
pillow==12.0.0
|
| 109 |
+
platformdirs==4.5.0
|
| 110 |
+
playwright==1.58.0
|
| 111 |
+
plotly==6.4.0
|
| 112 |
+
polars==1.40.0
|
| 113 |
+
polars-runtime-32==1.40.0
|
| 114 |
+
prompt_toolkit==3.0.52
|
| 115 |
+
protobuf==6.33.1
|
| 116 |
+
psutil==7.1.2
|
| 117 |
+
pure_eval==0.2.3
|
| 118 |
+
pyarrow==21.0.0
|
| 119 |
+
pycparser==2.23
|
| 120 |
+
pydantic==2.12.5
|
| 121 |
+
pydantic_core==2.41.5
|
| 122 |
+
pydeck==0.9.1
|
| 123 |
+
pyee==13.0.0
|
| 124 |
+
Pygments==2.19.2
|
| 125 |
+
pyogrio==0.12.1
|
| 126 |
+
pyparsing==3.2.5
|
| 127 |
+
pyproj==3.7.2
|
| 128 |
+
PySocks==1.7.1
|
| 129 |
+
python-dateutil==2.9.0.post0
|
| 130 |
+
python-dotenv==1.2.1
|
| 131 |
+
python-on-whales==0.80.0
|
| 132 |
+
pytz==2025.2
|
| 133 |
+
PyYAML==6.0.3
|
| 134 |
+
pyzmq==27.1.0
|
| 135 |
+
referencing==0.37.0
|
| 136 |
+
requests==2.32.5
|
| 137 |
+
rich==14.3.3
|
| 138 |
+
roman-numerals==4.1.0
|
| 139 |
+
rpds-py==0.28.0
|
| 140 |
+
scikit-learn==1.8.0
|
| 141 |
+
scipy==1.16.3
|
| 142 |
+
scs==3.2.11
|
| 143 |
+
seaborn==0.13.2
|
| 144 |
+
selenium==4.38.0
|
| 145 |
+
setuptools==80.9.0
|
| 146 |
+
shapely==2.1.2
|
| 147 |
+
shellingham==1.5.4
|
| 148 |
+
six==1.17.0
|
| 149 |
+
smmap==5.0.2
|
| 150 |
+
sniffio==1.3.1
|
| 151 |
+
snowballstemmer==3.0.1
|
| 152 |
+
sortedcontainers==2.4.0
|
| 153 |
+
soupsieve==2.8
|
| 154 |
+
Sphinx==9.1.0
|
| 155 |
+
sphinxcontrib-applehelp==2.0.0
|
| 156 |
+
sphinxcontrib-devhelp==2.0.0
|
| 157 |
+
sphinxcontrib-htmlhelp==2.1.0
|
| 158 |
+
sphinxcontrib-jsmath==1.0.1
|
| 159 |
+
sphinxcontrib-qthelp==2.0.0
|
| 160 |
+
sphinxcontrib-serializinghtml==2.0.0
|
| 161 |
+
stack-data==0.6.3
|
| 162 |
+
streamlit==1.51.0
|
| 163 |
+
sympy==1.14.0
|
| 164 |
+
tenacity==9.1.2
|
| 165 |
+
tensorflow==2.21.0
|
| 166 |
+
termcolor==3.3.0
|
| 167 |
+
threadpoolctl==3.6.0
|
| 168 |
+
tinycss2==1.4.0
|
| 169 |
+
toml==0.10.2
|
| 170 |
+
torch==2.11.0
|
| 171 |
+
torchvision==0.26.0
|
| 172 |
+
torchviz==0.0.3
|
| 173 |
+
tornado==6.5.2
|
| 174 |
+
tqdm==4.67.3
|
| 175 |
+
traitlets==5.14.3
|
| 176 |
+
trio==0.32.0
|
| 177 |
+
trio-websocket==0.12.2
|
| 178 |
+
typer==0.24.1
|
| 179 |
+
typing-inspection==0.4.2
|
| 180 |
+
typing_extensions==4.15.0
|
| 181 |
+
tzdata==2025.2
|
| 182 |
+
ultralytics==8.4.39
|
| 183 |
+
ultralytics-thop==2.0.18
|
| 184 |
+
urllib3==2.5.0
|
| 185 |
+
watchdog==6.0.0
|
| 186 |
+
wcwidth==0.2.14
|
| 187 |
+
webdriver-manager==4.0.2
|
| 188 |
+
webencodings==0.5.1
|
| 189 |
+
websocket-client==1.9.0
|
| 190 |
+
Werkzeug==3.1.8
|
| 191 |
+
wheel==0.46.3
|
| 192 |
+
widgetsnbextension==4.0.15
|
| 193 |
+
wrapt==1.17.3
|
| 194 |
+
wsproto==1.3.2
|
static/trackiq-api.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* trackiq-api.js v5
|
| 3 |
+
* home.html est autonome — ce fichier gère dashboard, logs, history
|
| 4 |
+
*/
|
| 5 |
+
const PAGE=(()=>{
|
| 6 |
+
const p=location.pathname;
|
| 7 |
+
if(p==='/'||p.includes('index')) return 'index';
|
| 8 |
+
if(p.includes('home')) return 'home';
|
| 9 |
+
if(p.includes('dashboard')) return 'dashboard';
|
| 10 |
+
if(p.includes('logs')) return 'logs';
|
| 11 |
+
if(p.includes('history')) return 'history';
|
| 12 |
+
return 'unknown';
|
| 13 |
+
})();
|
| 14 |
+
|
| 15 |
+
/* ══ DASHBOARD ══ */
|
| 16 |
+
if(PAGE==='dashboard'){
|
| 17 |
+
window.addEventListener('load',()=>{
|
| 18 |
+
loadDashboard();
|
| 19 |
+
setInterval(loadDashboard, 5000); // toutes les 5s, pas 6s
|
| 20 |
+
});
|
| 21 |
+
|
| 22 |
+
async function loadDashboard(){
|
| 23 |
+
try{
|
| 24 |
+
// ── Charge stats backend ──
|
| 25 |
+
const res = await fetch('/api/dashboard/stats');
|
| 26 |
+
const data = await res.json();
|
| 27 |
+
const uc = data.global_unique_counts || {};
|
| 28 |
+
const scenes = data.scenes || [];
|
| 29 |
+
const total = Object.values(uc).reduce((a,b)=>a+b,0);
|
| 30 |
+
|
| 31 |
+
// ── Metric card 1 : total objets ──
|
| 32 |
+
const m1 = document.getElementById('mv1');
|
| 33 |
+
if(m1) m1.textContent = total || '—';
|
| 34 |
+
|
| 35 |
+
// ── Metric card 2 : nb scènes ──
|
| 36 |
+
const m2 = document.getElementById('mv2');
|
| 37 |
+
if(m2) m2.textContent = scenes.length || '—';
|
| 38 |
+
|
| 39 |
+
// ── Metric card 3 : FPS de la dernière analyse ──
|
| 40 |
+
const lastScene = scenes[0];
|
| 41 |
+
const m3 = document.getElementById('mv3');
|
| 42 |
+
if(m3) m3.textContent = lastScene?.fps ? lastScene.fps.toFixed(1) : '—';
|
| 43 |
+
|
| 44 |
+
// ── Metric card 4 : durée dernière analyse ──
|
| 45 |
+
const m4 = document.getElementById('mv4');
|
| 46 |
+
if(m4) m4.textContent = lastScene?.duration_s ? lastScene.duration_s+'s' : '—';
|
| 47 |
+
|
| 48 |
+
// ── Scene summary (se met à jour maintenant !) ──
|
| 49 |
+
if(lastScene){
|
| 50 |
+
const ls = document.getElementById('liveSource');
|
| 51 |
+
const lsm = document.getElementById('liveSourceMeta');
|
| 52 |
+
if(ls) ls.textContent = lastScene.video_name || lastScene.scene_id || 'Unknown';
|
| 53 |
+
if(lsm) lsm.textContent = `${lastScene.duration_s||0}s analyzed`;
|
| 54 |
+
|
| 55 |
+
const lo = document.getElementById('liveObjects');
|
| 56 |
+
if(lo) lo.textContent = lastScene.total || 0;
|
| 57 |
+
|
| 58 |
+
const lf = document.getElementById('liveFps');
|
| 59 |
+
if(lf) lf.textContent = lastScene.fps ? lastScene.fps.toFixed(1) : '—';
|
| 60 |
+
|
| 61 |
+
const ld = document.getElementById('liveDuration');
|
| 62 |
+
if(ld) ld.textContent = lastScene.duration_s ? lastScene.duration_s+'s' : '—';
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
// ── Pie chart Object Type Distribution ──
|
| 66 |
+
if(Object.keys(uc).length && typeof updatePie === 'function'){
|
| 67 |
+
updatePie(uc);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// ── Live stats (depuis webcam stats si dispo, sinon dernière analyse) ──
|
| 71 |
+
try{
|
| 72 |
+
const wcRes = await fetch('/api/webcam/stats');
|
| 73 |
+
if(wcRes.ok){
|
| 74 |
+
const wc = await wcRes.json();
|
| 75 |
+
const det = document.getElementById('sbdet');
|
| 76 |
+
const acc = document.getElementById('sbacc');
|
| 77 |
+
if(det) det.textContent = wc.detections ?? 0;
|
| 78 |
+
if(acc) acc.textContent = wc.frame ? Math.round(wc.frame) : '—';
|
| 79 |
+
// Montre le panneau live seulement si webcam active
|
| 80 |
+
if(wc.frame > 0){
|
| 81 |
+
const note = document.getElementById('livePanelNote');
|
| 82 |
+
if(note) note.style.display='none';
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
}catch(e){}
|
| 86 |
+
|
| 87 |
+
// ── Charts flow (vraies données timeline) ──
|
| 88 |
+
if(window.fch && lastScene?.timeline?.length > 1){
|
| 89 |
+
const tl = lastScene.timeline;
|
| 90 |
+
const step = Math.max(1,Math.floor(tl.length/20));
|
| 91 |
+
const lbl=[],objDat=[];
|
| 92 |
+
for(let i=0;i<tl.length;i+=step){
|
| 93 |
+
const pt=tl[i];
|
| 94 |
+
lbl.push(pt.ts.toFixed(0)+'s');
|
| 95 |
+
const cnt=Object.entries(pt)
|
| 96 |
+
.filter(([k])=>!['frame','ts'].includes(k))
|
| 97 |
+
.reduce((a,[,v])=>a+(v||0),0);
|
| 98 |
+
objDat.push(cnt);
|
| 99 |
+
}
|
| 100 |
+
window.fch.data.labels = lbl;
|
| 101 |
+
window.fch.data.datasets[0].data = objDat;
|
| 102 |
+
// FPS dataset : valeur constante (fps de la scène)
|
| 103 |
+
window.fch.data.datasets[1].data = lbl.map(()=>lastScene.fps||0);
|
| 104 |
+
window.fch.update('none');
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
// ── Chart classification (vraies classes) ──
|
| 108 |
+
if(window.tch && Object.keys(uc).length){
|
| 109 |
+
const COLORS=['#3b82f6','#10b981','#f59e0b','#ef4444','#8b5cf6','#ec4899','#06b6d4','#f97316'];
|
| 110 |
+
window.tch.data.labels = Object.keys(uc);
|
| 111 |
+
window.tch.data.datasets[0].data = Object.values(uc);
|
| 112 |
+
window.tch.data.datasets[0].backgroundColor = Object.keys(uc).map((_,i)=>COLORS[i%COLORS.length]);
|
| 113 |
+
window.tch.update('none');
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
// ── Heatmap depuis vraies positions (déjà gérée par loadHeatmap) ──
|
| 117 |
+
|
| 118 |
+
}catch(err){ console.warn('[TrackIQ] Dashboard fetch:',err); }
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
/* ══ LOGS ══ */
|
| 123 |
+
if(PAGE==='logs'){
|
| 124 |
+
let _rows=[];
|
| 125 |
+
window.addEventListener('load',async()=>{
|
| 126 |
+
try{
|
| 127 |
+
const res=await fetch('/api/logs');
|
| 128 |
+
const scenes=await res.json();
|
| 129 |
+
if(!scenes.length)return;
|
| 130 |
+
_rows=[];
|
| 131 |
+
scenes.forEach(s=>{
|
| 132 |
+
Object.entries(s.unique_counts||{}).forEach(([cls,cnt])=>{
|
| 133 |
+
_rows.push({date:s.generated_at?new Date(s.generated_at).toLocaleString():'—',
|
| 134 |
+
scene:s.scene_id,video:s.video_name||'—',cls,cnt,total:s.total_unique||0,sid:s.scene_id});
|
| 135 |
+
});
|
| 136 |
+
});
|
| 137 |
+
const scvs=document.querySelectorAll('.sc-v');
|
| 138 |
+
if(scvs[0]) scvs[0].textContent=scenes.reduce((a,s)=>a+(s.total_unique||0),0);
|
| 139 |
+
if(scvs[1]) scvs[1].textContent=scenes.length;
|
| 140 |
+
renderTable(_rows);
|
| 141 |
+
hookFilters(scenes);
|
| 142 |
+
const expBtn=document.querySelector('.btn-exp');
|
| 143 |
+
if(expBtn) expBtn.onclick=()=>scenes.forEach((s,i)=>setTimeout(()=>window.open(`/api/logs/${s.scene_id}/csv`),i*300));
|
| 144 |
+
}catch(err){console.warn('[TrackIQ] Logs:',err);}
|
| 145 |
+
});
|
| 146 |
+
|
| 147 |
+
function renderTable(rows){
|
| 148 |
+
const tbody=document.getElementById('tbody');if(!tbody)return;
|
| 149 |
+
if(!rows.length){tbody.innerHTML=`<tr><td colspan="7" style="text-align:center;padding:30px;color:var(--text3);">No data yet — run an analysis first</td></tr>`;return;}
|
| 150 |
+
const DOT={'Vehicle':'#3b82f6','Motorcycle':'#10b981','Truck':'#f59e0b','Bus':'#8b5cf6','Human':'#ec4899','Bicycle':'#06b6d4'};
|
| 151 |
+
const EMO={'Vehicle':'🚗','Motorcycle':'🏍','Truck':'🚚','Bus':'🚌','Human':'🚶','Bicycle':'🚲'};
|
| 152 |
+
tbody.innerHTML='';
|
| 153 |
+
rows.forEach(r=>{
|
| 154 |
+
const tr=document.createElement('tr');
|
| 155 |
+
tr.innerHTML=`<td><div class="thumb">${EMO[r.cls]||'📦'}</div></td>
|
| 156 |
+
<td style="font-size:11px;color:var(--text2);">${r.date}</td>
|
| 157 |
+
<td style="font-size:11px;color:var(--text2);">${r.scene}</td>
|
| 158 |
+
<td style="font-size:11px;" title="${r.video}">${r.video.length>18?r.video.slice(0,18)+'…':r.video}</td>
|
| 159 |
+
<td><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${DOT[r.cls]||'#888'};margin-right:4px;"></span>${r.cls}</td>
|
| 160 |
+
<td><strong>${r.cnt}</strong></td>
|
| 161 |
+
<td><a href="/api/logs/${r.sid}/csv" style="color:var(--accent);font-size:11px;"><i class="fas fa-download"></i> CSV</a></td>`;
|
| 162 |
+
tbody.appendChild(tr);
|
| 163 |
+
});
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
function hookFilters(scenes){
|
| 167 |
+
const sIn=document.getElementById('sIn');
|
| 168 |
+
const camF=document.getElementById('camF');
|
| 169 |
+
const typeF=document.getElementById('typeF');
|
| 170 |
+
if(typeF){
|
| 171 |
+
const cls=[...new Set(_rows.map(r=>r.cls))];
|
| 172 |
+
typeF.innerHTML='<option value="">All</option>'+cls.map(c=>`<option>${c}</option>`).join('');
|
| 173 |
+
}
|
| 174 |
+
if(camF) camF.innerHTML='<option value="">All</option>'+
|
| 175 |
+
scenes.map(s=>`<option value="${s.scene_id}">${s.video_name||s.scene_id}</option>`).join('');
|
| 176 |
+
const apply=()=>{
|
| 177 |
+
const s=(sIn?.value||'').toLowerCase(),sc=camF?.value||'',tp=typeF?.value||'';
|
| 178 |
+
renderTable(_rows.filter(r=>(!s||r.video.toLowerCase().includes(s)||r.scene.toLowerCase().includes(s))&&(!sc||r.scene===sc)&&(!tp||r.cls===tp)));
|
| 179 |
+
};
|
| 180 |
+
if(sIn) sIn.oninput=apply;
|
| 181 |
+
if(camF) camF.onchange=apply;
|
| 182 |
+
if(typeF) typeF.onchange=apply;
|
| 183 |
+
}
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
/* ══ HISTORY ══ */
|
| 187 |
+
if(PAGE==='history'){
|
| 188 |
+
async function mergeHistory(){
|
| 189 |
+
try{
|
| 190 |
+
const res=await fetch('/api/history');
|
| 191 |
+
const items=await res.json();
|
| 192 |
+
if(!items.length)return;
|
| 193 |
+
let hist=[];try{hist=JSON.parse(localStorage.getItem('tiq_history')||'[]');}catch(e){}
|
| 194 |
+
items.forEach(item=>{if(!hist.find(h=>h.url===item.url))hist.unshift({name:item.name,url:item.url,date:item.date});});
|
| 195 |
+
localStorage.setItem('tiq_history',JSON.stringify(hist.slice(0,50)));
|
| 196 |
+
if(window.renderGrid)window.renderGrid();
|
| 197 |
+
}catch(err){console.warn('[TrackIQ] History:',err);}
|
| 198 |
+
}
|
| 199 |
+
setTimeout(mergeHistory,300);
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
console.log(`[TrackIQ v5] page=${PAGE}`);
|
templates/dashboard.html
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" data-theme="light">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>TrackIQ Dashboard</title>
|
| 7 |
+
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
|
| 8 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
| 9 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
| 10 |
+
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.2.0/dist/chartjs-plugin-datalabels.min.js"></script>
|
| 11 |
+
<style>
|
| 12 |
+
:root{--bg:#0c1117;--s:#151c25;--s2:#1c2531;--bd:rgba(255,255,255,0.07);--sh:0 2px 16px rgba(0,0,0,0.45);--shlg:0 8px 36px rgba(0,0,0,0.55);--t:#e8edf2;--t2:#7a8fa0;--t3:#3a4a5a;--ac:#880000;--ac2:#660000;--acl:rgba(136,0,0,0.12);--gr:#16a34a;--grb:#052e16;--bl:#3b82f6;--blb:#0f2040;--yw:#f59e0b;--ywb:#1a0f00;--re:#ef4444;--reb:#2d0000;--sw:60px;--r:14px;--rs:9px;}
|
| 13 |
+
[data-theme="light"]{--bg:#f0f4f8;--s:#fff;--s2:#f6f8fb;--bd:rgba(0,0,0,0.07);--sh:0 2px 16px rgba(0,0,0,0.09);--shlg:0 8px 36px rgba(0,0,0,0.13);--t:#0f1923;--t2:#5a6a7a;--t3:#9aaab8;--grb:#dcfce7;--blb:#dbeafe;--ywb:#fef9c3;--reb:#fee2e2;}
|
| 14 |
+
*{box-sizing:border-box;margin:0;padding:0}::-webkit-scrollbar{width:0;height:0}
|
| 15 |
+
body{font-family:'DM Sans',sans-serif;background:var(--bg);color:var(--t);font-size:13px;display:flex;height:100vh;overflow:hidden;transition:background .3s,color .3s}
|
| 16 |
+
.sb{width:var(--sw);background:var(--s);border-right:1px solid var(--bd);display:flex;flex-direction:column;align-items:center;padding:14px 0;gap:4px;flex-shrink:0}
|
| 17 |
+
.sbl{width:34px;height:34px;background:var(--ac);border-radius:8px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px;margin-bottom:10px;text-decoration:none}
|
| 18 |
+
.sbsep{width:26px;height:1px;background:var(--bd);margin:3px 0}
|
| 19 |
+
.sbb{width:38px;height:38px;border-radius:9px;display:flex;align-items:center;justify-content:center;color:var(--t3);font-size:14px;cursor:pointer;transition:all .2s;text-decoration:none;border:none;background:none;position:relative}
|
| 20 |
+
.sbb:hover{background:var(--s2);color:var(--t)}.sbb.on{background:var(--acl);color:var(--ac)}
|
| 21 |
+
.sbb .tip{position:absolute;left:44px;top:50%;transform:translateY(-50%);background:var(--t);color:var(--bg);font-size:10px;padding:3px 7px;border-radius:4px;white-space:nowrap;opacity:0;pointer-events:none;z-index:300}.sbb:hover .tip{opacity:1}
|
| 22 |
+
.sbsp{flex:1}.pw{flex:1;display:flex;flex-direction:column;overflow:hidden}
|
| 23 |
+
.topb{height:50px;background:var(--s);border-bottom:1px solid var(--bd);display:flex;align-items:center;justify-content:space-between;padding:0 18px;flex-shrink:0}
|
| 24 |
+
.topl,.topr{display:flex;align-items:center;gap:9px}.bn{font-size:14px;font-weight:600}.bn b{color:var(--ac)}
|
| 25 |
+
.pill{display:flex;align-items:center;gap:4px;font-size:10px;color:var(--gr);background:var(--grb);padding:2px 8px;border-radius:20px}
|
| 26 |
+
.ld{width:6px;height:6px;border-radius:50%;background:var(--gr);animation:blink 2s infinite}
|
| 27 |
+
@keyframes blink{0%,100%{opacity:1}50%{opacity:.3}}
|
| 28 |
+
.clk{font-size:10px;font-family:'DM Mono',monospace;color:var(--t2)}
|
| 29 |
+
.lg{display:flex;background:var(--s2);border-radius:5px;padding:2px;border:1px solid var(--bd)}
|
| 30 |
+
.lb{font-size:10px;font-weight:500;padding:2px 8px;border:none;background:none;cursor:pointer;border-radius:3px;color:var(--t2);font-family:'DM Sans',sans-serif;transition:all .2s}.lb.on{background:var(--ac);color:#fff}
|
| 31 |
+
.ib{width:28px;height:28px;border-radius:5px;border:1px solid var(--bd);background:var(--s);cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--t2);font-size:11px;position:relative;transition:all .2s}.ib:hover{background:var(--s2)}
|
| 32 |
+
.nd{position:absolute;top:3px;right:3px;width:6px;height:6px;border-radius:50%;background:var(--ac)}
|
| 33 |
+
main{flex:1;padding:12px 14px;display:grid;grid-template-columns:minmax(0,1.4fr) 360px;gap:12px;overflow:auto;min-height:0}
|
| 34 |
+
.left-col{display:grid;grid-template-rows:auto minmax(190px,1fr) minmax(190px,1fr);gap:10px;min-height:0}
|
| 35 |
+
.right-col{display:grid;grid-template-rows:minmax(300px,1.15fr) auto auto;gap:10px;min-height:0}
|
| 36 |
+
.mr{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
|
| 37 |
+
.mc,.cd{background:var(--s);border:1px solid var(--bd);box-shadow:var(--sh);overflow:hidden}
|
| 38 |
+
.mc{border-radius:var(--rs);padding:11px 13px;transition:transform .2s}.mc:hover{transform:translateY(-2px)}
|
| 39 |
+
.cd{border-radius:var(--r);padding:12px 14px;display:flex;flex-direction:column}.cd:hover{box-shadow:var(--shlg)}
|
| 40 |
+
.mct{display:flex;align-items:center;justify-content:space-between;margin-bottom:5px}
|
| 41 |
+
.mcl{font-size:10px;color:var(--t2)}.bx{font-size:9px;font-weight:500;padding:1px 5px;border-radius:4px}
|
| 42 |
+
.mv{font-size:20px;font-weight:300;letter-spacing:-.5px;line-height:1}.mu{font-size:10px;color:var(--t2)}.ms{font-size:9px;color:var(--t3);margin-top:2px}
|
| 43 |
+
.ct{font-size:11px;font-weight:500;margin-bottom:8px;display:flex;align-items:center;gap:5px;flex-shrink:0}.ct i{color:var(--ac);font-size:10px}
|
| 44 |
+
.chart-wrap{flex:1;min-height:0}
|
| 45 |
+
.stack{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
| 46 |
+
.hmcd{padding:0}
|
| 47 |
+
.hmhd{padding:10px 12px 6px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0}
|
| 48 |
+
.hmhd .ct{margin-bottom:0}
|
| 49 |
+
.hmbd{flex:1;position:relative;min-height:220px}
|
| 50 |
+
#hmc2{position:absolute;inset:0;width:100%!important;height:100%!important;display:block}
|
| 51 |
+
.stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:9px}
|
| 52 |
+
.statbox{background:var(--s2);border:1px solid var(--bd);border-radius:8px;padding:10px 11px}
|
| 53 |
+
.statbox .k{font-size:9px;color:var(--t2);text-transform:uppercase;letter-spacing:.04em}
|
| 54 |
+
.statbox .v{font-size:18px;font-weight:300;margin-top:4px}
|
| 55 |
+
.subline{font-size:10px;color:var(--t2)}
|
| 56 |
+
.listbox{display:flex;flex-direction:column;gap:6px}
|
| 57 |
+
.vr{display:flex;align-items:center;justify-content:space-between;background:var(--s2);border:1px solid var(--bd);border-radius:8px;padding:8px 10px}
|
| 58 |
+
.vrl{display:flex;align-items:center;gap:7px;font-size:11px}.vic{width:22px;height:22px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px}.vp{font-size:13px;font-weight:500}
|
| 59 |
+
.al{display:flex;align-items:flex-start;gap:8px;padding:7px 0;border-bottom:1px solid var(--bd)}.al:last-child{border-bottom:none}
|
| 60 |
+
.ai{width:22px;height:22px;border-radius:5px;display:flex;align-items:center;justify-content:center;font-size:9px;flex-shrink:0;margin-top:1px}
|
| 61 |
+
.at2{font-size:10px;font-weight:500;margin-bottom:1px}.as2{font-size:9px;color:var(--t2)}
|
| 62 |
+
.pill-row{display:flex;gap:6px;flex-wrap:wrap;margin-top:8px}.mini-pill{font-size:9px;padding:3px 7px;border-radius:999px;background:var(--s2);border:1px solid var(--bd);color:var(--t2)}
|
| 63 |
+
.empty-note{font-size:10px;color:var(--t3);padding-top:4px}
|
| 64 |
+
@media(max-width:1180px){main{grid-template-columns:1fr}.right-col{grid-template-rows:minmax(260px,auto) auto auto}.stack{grid-template-columns:1fr}.mr{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
| 65 |
+
</style>
|
| 66 |
+
</head>
|
| 67 |
+
<body>
|
| 68 |
+
<nav class="sb">
|
| 69 |
+
<a href="/" class="sbl"><i class="fas fa-satellite-dish"></i></a>
|
| 70 |
+
<div class="sbsep"></div>
|
| 71 |
+
<a href="/home" class="sbb"><i class="fas fa-play-circle"></i><span class="tip">Analyze</span></a>
|
| 72 |
+
<a href="/dashboard" class="sbb on"><i class="fas fa-chart-line"></i><span class="tip">Dashboard</span></a>
|
| 73 |
+
<a href="/logs" class="sbb"><i class="fas fa-list-ul"></i><span class="tip">Logs</span></a>
|
| 74 |
+
<a href="/history" class="sbb"><i class="fas fa-film"></i><span class="tip">History</span></a>
|
| 75 |
+
<div class="sbsp"></div>
|
| 76 |
+
<button class="sbb" onclick="toggleTheme()"><i class="fas fa-moon" id="ti"></i><span class="tip">Theme</span></button>
|
| 77 |
+
</nav>
|
| 78 |
+
|
| 79 |
+
<div class="pw">
|
| 80 |
+
<header class="topb">
|
| 81 |
+
<div class="topl">
|
| 82 |
+
<span class="bn">Track<b>IQ</b></span>
|
| 83 |
+
<span style="color:var(--bd);font-size:16px;margin:0 1px;">|</span>
|
| 84 |
+
<span style="font-size:12px;font-weight:500;" id="t-dash">Dashboard</span>
|
| 85 |
+
<div class="pill"><span class="ld"></span> <span id="liveBadge">Live</span></div>
|
| 86 |
+
</div>
|
| 87 |
+
<div class="topr">
|
| 88 |
+
<span class="clk" id="clk">--:--:--</span>
|
| 89 |
+
<div class="lg">
|
| 90 |
+
<button class="lb on" onclick="setLang('en',this)">EN</button>
|
| 91 |
+
<button class="lb" onclick="setLang('fr',this)">FR</button>
|
| 92 |
+
<button class="lb" onclick="setLang('wo',this)">WO</button>
|
| 93 |
+
</div>
|
| 94 |
+
<button class="ib" onclick="toggleNotifs()"><i class="fas fa-bell"></i><span class="nd" id="nd"></span></button>
|
| 95 |
+
<button class="ib" onclick="toggleTheme()"><i class="fas fa-moon" id="ti2"></i></button>
|
| 96 |
+
</div>
|
| 97 |
+
</header>
|
| 98 |
+
|
| 99 |
+
<div id="notifP" style="display:none;position:fixed;top:57px;right:65px;z-index:500;width:255px;background:var(--s);border:1px solid var(--bd);border-radius:var(--r);box-shadow:var(--shlg);padding:12px 14px;">
|
| 100 |
+
<div style="font-size:11px;font-weight:500;margin-bottom:8px;display:flex;justify-content:space-between;"><span id="t-notif">Notifications</span><button onclick="clearN()" style="font-size:10px;color:var(--ac);background:none;border:none;cursor:pointer;">Clear all</button></div>
|
| 101 |
+
<div id="nl">
|
| 102 |
+
<div style="padding:6px 0;border-bottom:1px solid var(--bd);"><div style="font-size:10px;font-weight:500;color:var(--re);" id="t-a1">Congestion Zone A</div><div style="font-size:9px;color:var(--t2);">4 min ago</div></div>
|
| 103 |
+
<div style="padding:6px 0;"><div style="font-size:10px;font-weight:500;" id="t-source-loaded">Source ready</div><div style="font-size:9px;color:var(--t2);">Just now</div></div>
|
| 104 |
+
</div>
|
| 105 |
+
</div>
|
| 106 |
+
|
| 107 |
+
<main>
|
| 108 |
+
<section class="left-col">
|
| 109 |
+
<div class="mr">
|
| 110 |
+
<div class="mc"><div class="mct"><span class="mcl" id="t-m1">Objects Today</span><span class="bx" style="background:var(--grb);color:var(--gr);" id="m1Trend">+0%</span></div><div><span class="mv" id="mvToday">0</span> <span class="mu" id="t-veh">obj.</span></div><div class="ms" id="m1Sub">Latest analyzed scenes</div></div>
|
| 111 |
+
<div class="mc"><div class="mct"><span class="mcl" id="t-m2">Current Flow</span><span class="bx" style="background:var(--grb);color:var(--gr);" id="flowBadge">Smooth</span></div><div><span class="mv" id="mvFlow">0</span> <span class="mu">obj/min</span></div><div class="ms" id="m2Sub">Based on recent activity</div></div>
|
| 112 |
+
<div class="mc"><div class="mct"><span class="mcl" id="t-m3">Avg FPS</span><span class="bx" style="background:var(--blb);color:var(--bl);" id="fpsBadge">Stable</span></div><div><span class="mv" id="mvFps">0</span> <span class="mu">fps</span></div><div class="ms" id="m3Sub">Last completed analysis</div></div>
|
| 113 |
+
<div class="mc"><div class="mct"><span class="mcl" id="t-m4">Latest clip</span><span class="bx" style="background:var(--ywb);color:var(--yw);" id="accBadge">Current</span></div><div><span class="mv" id="mvAcc">0</span> <span class="mu">s</span></div><div class="ms" id="m4Sub">Duration of latest processed video</div></div>
|
| 114 |
+
</div>
|
| 115 |
+
|
| 116 |
+
<div class="stack">
|
| 117 |
+
<div class="cd">
|
| 118 |
+
<div class="ct"><i class="fas fa-chart-area"></i><span id="t-c1">Flow & speed</span></div>
|
| 119 |
+
<div class="chart-wrap"><canvas id="fc"></canvas></div>
|
| 120 |
+
</div>
|
| 121 |
+
<div class="cd">
|
| 122 |
+
<div class="ct"><i class="fas fa-chart-bar"></i><span id="t-c2">Classification</span></div>
|
| 123 |
+
<div class="chart-wrap"><canvas id="tc"></canvas></div>
|
| 124 |
+
</div>
|
| 125 |
+
</div>
|
| 126 |
+
|
| 127 |
+
<div class="cd">
|
| 128 |
+
<div class="ct"><i class="fas fa-layer-group"></i><span id="t-scenes">Scene summary</span></div>
|
| 129 |
+
<div class="stats-grid">
|
| 130 |
+
<div class="statbox"><div class="k" id="t-live-source">Source</div><div class="v" id="liveSource">No source</div><div class="subline" id="liveSourceMeta">Ready after analysis</div></div>
|
| 131 |
+
<div class="statbox"><div class="k" id="t-live-objects">Objects</div><div class="v" id="liveObjects" id="liveObjects">0</div><div class="subline" id="liveObjectsMeta">Peak visible count</div></div>
|
| 132 |
+
<div class="statbox"><div class="k" id="t-live-fps">FPS</div><div class="v" id="liveFps" id="liveFps">0</div><div class="subline" id="liveFpsMeta">Captured stream rate</div></div>
|
| 133 |
+
<div class="statbox"><div class="k" id="t-live-duration">Duration</div><div class="v" id="liveDuration" id="liveDuration">0s</div><div class="subline" id="liveDurationMeta">Latest processed clip</div></div>
|
| 134 |
+
</div>
|
| 135 |
+
<div class="pill-row" id="liveClassPills"></div>
|
| 136 |
+
<div class="empty-note" id="liveNote">Current summary follows the latest processed analysis instead of the old floating video preview.</div>
|
| 137 |
+
<div id="sceneSummaryGrid" style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:10px;"></div>
|
| 138 |
+
</div>
|
| 139 |
+
</section>
|
| 140 |
+
|
| 141 |
+
<aside class="right-col">
|
| 142 |
+
<div class="cd hmcd">
|
| 143 |
+
<div class="hmhd"><div class="ct"><i class="fas fa-map-marker-alt"></i><span id="t-hm">Position heatmap</span></div><span style="font-size:9px;color:var(--t3);" id="t-hms">Object density</span></div>
|
| 144 |
+
<div class="hmbd"><canvas id="hmc2"></canvas></div>
|
| 145 |
+
</div>
|
| 146 |
+
|
| 147 |
+
<div class="stack">
|
| 148 |
+
<div class="cd" style="padding:11px 12px;">
|
| 149 |
+
<div class="ct">
|
| 150 |
+
<i class="fas fa-chart-pie"></i>
|
| 151 |
+
<span>Object Type Distribution</span>
|
| 152 |
+
</div>
|
| 153 |
+
<!-- Pie chart + légende -->
|
| 154 |
+
<div style="position:relative;display:flex;align-items:center;gap:10px;min-height:140px;">
|
| 155 |
+
<div style="width:145px;height:145px;flex-shrink:0;">
|
| 156 |
+
<canvas id="pieChart"></canvas>
|
| 157 |
+
|
| 158 |
+
</div>
|
| 159 |
+
<!-- Légende dynamique -->
|
| 160 |
+
<div id="pieLegend" style="flex:1;display:flex;flex-direction:column;gap:5px;">
|
| 161 |
+
<div style="font-size:10px;color:var(--t3);text-align:center;">No data yet</div>
|
| 162 |
+
</div>
|
| 163 |
+
</div>
|
| 164 |
+
</div>
|
| 165 |
+
<div class="cd" style="padding:11px 12px;">
|
| 166 |
+
<div class="ct"><i class="fas fa-wave-square"></i><span id="t-live-panel">Live stats</span></div>
|
| 167 |
+
<div class="stats-grid">
|
| 168 |
+
<div class="statbox"><div class="k" id="t-det">Detected</div><div class="v" id="sbdet">0</div></div>
|
| 169 |
+
<div class="statbox"><div class="k" id="t-acc">Latest FPS</div><div class="v" id="sbacc">0</div></div>
|
| 170 |
+
</div>
|
| 171 |
+
<div class="empty-note" id="livePanelNote">Heatmap and key stats stay visible at all times.</div>
|
| 172 |
+
</div>
|
| 173 |
+
</div>
|
| 174 |
+
</aside>
|
| 175 |
+
</main>
|
| 176 |
+
</div>
|
| 177 |
+
|
| 178 |
+
<script>
|
| 179 |
+
setInterval(()=>{document.getElementById('clk').textContent=new Date().toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit',second:'2-digit'});},1000);
|
| 180 |
+
|
| 181 |
+
function toggleTheme(){
|
| 182 |
+
const d=document.documentElement,dk=d.getAttribute('data-theme')==='dark';
|
| 183 |
+
d.setAttribute('data-theme',dk?'light':'dark');
|
| 184 |
+
['ti','ti2'].forEach(x=>{const e=document.getElementById(x);if(e)e.className=dk?'fas fa-moon':'fas fa-sun';});
|
| 185 |
+
rebuildCharts();
|
| 186 |
+
setTimeout(drawHm,50);
|
| 187 |
+
rebuildPie();
|
| 188 |
+
try{localStorage.setItem('tiq_theme',dk?'light':'dark');}catch(e){}
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
const TL={
|
| 192 |
+
en:{dash:'Dashboard',m1:'Objects Today',veh:'obj.',m2:'Current Flow',m3:'Avg FPS',m4:'Latest clip',c1:'Flow from video',c2:'Classification',hm:'Position heatmap',hms:'Detected positions',bk:'Breakdown',ca:'Cars',mo:'Moto',tr:'Trucks',al:'Alerts',notif:'Notifications',scenes:'Scene summary',livesource:'Source',liveobjects:'Objects',livefps:'FPS',liveduration:'Duration',livepanel:'Live stats',det:'Detected',acc:'Latest FPS'},
|
| 193 |
+
fr:{dash:'Tableau de bord',m1:'Objets du jour',veh:'obj.',m2:'Flux actuel',m3:'FPS moy.',m4:'Dernier clip',c1:'Flux vidéo',c2:'Classification',hm:'Carte de densité',hms:'Positions détectées',bk:'Répartition',ca:'Voitures',mo:'Motos',tr:'Camions',al:'Alertes',notif:'Notifications',scenes:'Résumé scène',livesource:'Source',liveobjects:'Objets',livefps:'FPS',liveduration:'Durée',livepanel:'Stats live',det:'Détectés',acc:'FPS récent'},
|
| 194 |
+
wo:{dash:'Dashboard',m1:'Objets tey',veh:'obj.',m2:'Yóbbu',m3:'FPS moy.',m4:'Clip mujj',c1:'Flux video',c2:'Wërsëg',hm:'Kàrt',hms:'Posision yi',bk:'Wërsëg',ca:'Cars',mo:'Moto',tr:'Trucks',al:'Yëgël',notif:'Yëgël yi',scenes:'Melo scène',livesource:'Source',liveobjects:'Objets',livefps:'FPS',liveduration:'Yoon',livepanel:'Stats live',det:'Yi réy',acc:'FPS mujj'}
|
| 195 |
+
};
|
| 196 |
+
|
| 197 |
+
function setLang(l,btn){
|
| 198 |
+
document.querySelectorAll('.lb').forEach(b=>b.classList.remove('on'));
|
| 199 |
+
btn.classList.add('on');
|
| 200 |
+
const t=TL[l];
|
| 201 |
+
const mp={dash:'t-dash',m1:'t-m1',veh:'t-veh',m2:'t-m2',m3:'t-m3',m4:'t-m4',c1:'t-c1',c2:'t-c2',hm:'t-hm',hms:'t-hms',bk:'t-bk',ca:'t-ca',mo:'t-mo',tr:'t-tr',al:'t-al',notif:'t-notif',scenes:'t-scenes',livesource:'t-live-source',liveobjects:'t-live-objects',livefps:'t-live-fps',liveduration:'t-live-duration',livepanel:'t-live-panel',det:'t-det',acc:'t-acc'};
|
| 202 |
+
Object.entries(mp).forEach(([k,id])=>{const el=document.getElementById(id);if(el&&t[k])el.textContent=t[k];});
|
| 203 |
+
try{localStorage.setItem('tiq_lang',l);}catch(e){}
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
let no=false;
|
| 207 |
+
function toggleNotifs(){no=!no;document.getElementById('notifP').style.display=no?'block':'none';if(no)document.getElementById('nd').style.display='none';}
|
| 208 |
+
function clearN(){document.getElementById('nl').innerHTML='<div style="font-size:10px;color:var(--t3);text-align:center;padding:10px;">No notifications</div>';no=false;document.getElementById('notifP').style.display='none';}
|
| 209 |
+
document.addEventListener('click',e=>{if(no&&!e.target.closest('#notifP')&&!e.target.closest('.ib'))toggleNotifs();});
|
| 210 |
+
|
| 211 |
+
window.fch=null;
|
| 212 |
+
window.tch=null;
|
| 213 |
+
window._tiqHeatPoints=[];
|
| 214 |
+
|
| 215 |
+
function gc(){
|
| 216 |
+
const dk=document.documentElement.getAttribute('data-theme')==='dark';
|
| 217 |
+
return{grid:dk?'rgba(255,255,255,0.05)':'rgba(0,0,0,0.06)',tick:dk?'#4a5a6a':'#94a3b8'};
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
/* ── Pie chart — Object Type Distribution ── */
|
| 221 |
+
window.pch = null;
|
| 222 |
+
|
| 223 |
+
function rebuildPie(labels=[], data=[], colors=[]){
|
| 224 |
+
// Enregistre datalabels seulement si dispo
|
| 225 |
+
if(window.ChartDataLabels) Chart.register(ChartDataLabels);
|
| 226 |
+
if(window.pch) window.pch.destroy();
|
| 227 |
+
const canvas = document.getElementById('pieChart');
|
| 228 |
+
if(!canvas) return;
|
| 229 |
+
const dk = document.documentElement.getAttribute('data-theme')==='dark';
|
| 230 |
+
|
| 231 |
+
const defaultColors = ['#3b82f6','#10b981','#f59e0b','#8b5cf6','#ec4899','#06b6d4','#ef4444','#f97316'];
|
| 232 |
+
|
| 233 |
+
window.pch = new Chart(canvas, {
|
| 234 |
+
type: 'pie',
|
| 235 |
+
data: {
|
| 236 |
+
labels: labels.length ? labels : ['No data'],
|
| 237 |
+
datasets: [{
|
| 238 |
+
data: data.length ? data : [1],
|
| 239 |
+
backgroundColor: colors.length ? colors : [dk?'#1c2531':'#e2e8f0'],
|
| 240 |
+
borderColor: dk ? '#151c25' : '#ffffff',
|
| 241 |
+
borderWidth: 2,
|
| 242 |
+
hoverOffset: 6,
|
| 243 |
+
}]
|
| 244 |
+
},
|
| 245 |
+
options: {
|
| 246 |
+
responsive: true,
|
| 247 |
+
maintainAspectRatio: false,
|
| 248 |
+
plugins: {
|
| 249 |
+
legend: { display: false },
|
| 250 |
+
tooltip: {
|
| 251 |
+
callbacks: {
|
| 252 |
+
label: ctx => {
|
| 253 |
+
const tot = ctx.dataset.data.reduce((a,b)=>a+b,0);
|
| 254 |
+
return ` ${ctx.label}: ${ctx.raw} (${Math.round(ctx.raw/tot*100)}%)`;
|
| 255 |
+
}
|
| 256 |
+
}
|
| 257 |
+
},
|
| 258 |
+
/* Affiche les % à l'intérieur de chaque part */
|
| 259 |
+
datalabels: {
|
| 260 |
+
color: '#fff',
|
| 261 |
+
font: { size: 11, weight: 'bold' },
|
| 262 |
+
formatter: (value, ctx) => {
|
| 263 |
+
const tot = ctx.dataset.data.reduce((a,b)=>a+b,0);
|
| 264 |
+
const pct = Math.round(value/tot*100);
|
| 265 |
+
return pct >= 5 ? pct+'%' : ''; // cache si trop petit
|
| 266 |
+
}
|
| 267 |
+
}
|
| 268 |
+
}
|
| 269 |
+
}
|
| 270 |
+
});
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
function updatePie(uc){
|
| 274 |
+
const COLORS = {
|
| 275 |
+
'Vehicle':'#3b82f6','Motorcycle':'#10b981','Truck':'#f59e0b',
|
| 276 |
+
'Bus':'#8b5cf6','Human':'#ec4899','Bicycle':'#06b6d4',
|
| 277 |
+
'Traffic light':'#ef4444','Road sign':'#f97316'
|
| 278 |
+
};
|
| 279 |
+
const EMO = {
|
| 280 |
+
'Vehicle':'🚗','Motorcycle':'🏍','Truck':'🚚','Bus':'🚌',
|
| 281 |
+
'Human':'🚶','Bicycle':'🚲','Traffic light':'🚦','Road sign':'🛑'
|
| 282 |
+
};
|
| 283 |
+
|
| 284 |
+
const labels = Object.keys(uc);
|
| 285 |
+
const data = Object.values(uc);
|
| 286 |
+
const colors = labels.map(l => COLORS[l] || '#888');
|
| 287 |
+
const total = data.reduce((a,b)=>a+b,0);
|
| 288 |
+
|
| 289 |
+
// Recrée le chart
|
| 290 |
+
rebuildPie(labels, data, colors);
|
| 291 |
+
|
| 292 |
+
// Met à jour le label central
|
| 293 |
+
const ptn = document.getElementById('pieTotalNum');
|
| 294 |
+
if(ptn) ptn.textContent = total;
|
| 295 |
+
|
| 296 |
+
// Légende dynamique
|
| 297 |
+
const leg = document.getElementById('pieLegend');
|
| 298 |
+
if(!leg) return;
|
| 299 |
+
leg.innerHTML = '';
|
| 300 |
+
labels.forEach((lbl, i) => {
|
| 301 |
+
const pct = Math.round(data[i]/Math.max(total,1)*100);
|
| 302 |
+
const row = document.createElement('div');
|
| 303 |
+
row.style.cssText = 'display:flex;align-items:center;justify-content:space-between;gap:6px;';
|
| 304 |
+
row.innerHTML = `
|
| 305 |
+
<div style="display:flex;align-items:center;gap:5px;font-size:11px;overflow:hidden;">
|
| 306 |
+
<span style="width:9px;height:9px;border-radius:2px;background:${colors[i]};flex-shrink:0;display:inline-block;"></span>
|
| 307 |
+
<span style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${EMO[lbl]||'📦'} ${lbl}</span>
|
| 308 |
+
</div>
|
| 309 |
+
<span style="font-size:10px;color:var(--t2);flex-shrink:0;">${data[i]}</span>
|
| 310 |
+
`;
|
| 311 |
+
leg.appendChild(row);
|
| 312 |
+
});
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
function rebuildCharts(){
|
| 316 |
+
if(window.fch)window.fch.destroy();
|
| 317 |
+
if(window.tch)window.tch.destroy();
|
| 318 |
+
const {grid,tick}=gc();
|
| 319 |
+
/* Flow chart : 2 datasets (Objects + FPS) — compatible avec trackiq-api.js */
|
| 320 |
+
window.fch=new Chart(document.getElementById('fc'),{
|
| 321 |
+
data:{labels:['—'],datasets:[
|
| 322 |
+
{type:'line',label:'Objects',data:[0],borderColor:'#3b82f6',backgroundColor:'rgba(59,130,246,0.08)',borderWidth:1.5,tension:0.45,pointRadius:0,fill:true,yAxisID:'y'},
|
| 323 |
+
{type:'line',label:'FPS',data:[0],borderColor:'#10b981',backgroundColor:'rgba(16,185,129,0.05)',borderWidth:1.5,tension:0.45,pointRadius:0,yAxisID:'y2'}
|
| 324 |
+
]},
|
| 325 |
+
options:{responsive:true,maintainAspectRatio:false,interaction:{mode:'index',intersect:false},plugins:{legend:{labels:{boxWidth:8,font:{size:9},color:tick}}},scales:{x:{ticks:{font:{size:9},color:tick,maxTicksLimit:8},grid:{color:grid},border:{display:false}},y:{position:'left',beginAtZero:true,ticks:{font:{size:9},color:tick},grid:{color:grid},border:{display:false}},y2:{position:'right',beginAtZero:true,ticks:{font:{size:9},color:tick},grid:{drawOnChartArea:false},border:{display:false}}}}
|
| 326 |
+
});
|
| 327 |
+
/* Classification chart : NON stacké, 1 dataset — compatible avec trackiq-api.js */
|
| 328 |
+
window.tch=new Chart(document.getElementById('tc'),{
|
| 329 |
+
type:'bar',
|
| 330 |
+
data:{labels:['—'],datasets:[
|
| 331 |
+
{label:'Count',data:[0],backgroundColor:'#3b82f6',borderRadius:3,borderSkipped:false}
|
| 332 |
+
]},
|
| 333 |
+
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false}},scales:{x:{ticks:{font:{size:9},color:tick},grid:{color:grid},border:{display:false}},y:{beginAtZero:true,ticks:{font:{size:9},color:tick},grid:{color:grid},border:{display:false}}}}
|
| 334 |
+
});
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
function drawHm(){
|
| 338 |
+
const c=document.getElementById('hmc2'),b=c.parentElement;
|
| 339 |
+
const w=b.offsetWidth,h=b.offsetHeight;
|
| 340 |
+
if(!w||!h)return;
|
| 341 |
+
c.width=w;c.height=h;
|
| 342 |
+
const ctx=c.getContext('2d');
|
| 343 |
+
const dk=document.documentElement.getAttribute('data-theme')==='dark';
|
| 344 |
+
ctx.fillStyle=dk?'#0c1219':'#e8eef5';
|
| 345 |
+
ctx.fillRect(0,0,w,h);
|
| 346 |
+
ctx.strokeStyle=dk?'rgba(255,255,255,0.07)':'rgba(0,0,0,0.07)';
|
| 347 |
+
ctx.lineWidth=1;
|
| 348 |
+
[0.25,0.5,0.75].forEach(r=>{ctx.beginPath();ctx.moveTo(0,h*r);ctx.lineTo(w,h*r);ctx.stroke();ctx.beginPath();ctx.moveTo(w*r,0);ctx.lineTo(w*r,h);ctx.stroke();});
|
| 349 |
+
const pts=Array.isArray(window._tiqHeatPoints)?window._tiqHeatPoints:[];
|
| 350 |
+
if(pts.length){
|
| 351 |
+
let mxR=0,mxH=0;
|
| 352 |
+
pts.forEach(p=>{if(p.r>mxR)mxR=p.r;if(p.h>mxH)mxH=p.h;});
|
| 353 |
+
mxR=Math.max(mxR,0.01);mxH=Math.max(mxH,0.01);
|
| 354 |
+
pts.forEach(p=>{
|
| 355 |
+
const px=(p.x/mxR)*w,py=(p.y/mxH)*h;
|
| 356 |
+
const rad=Math.max(10,Math.min(w,h)*0.015);
|
| 357 |
+
const grd=ctx.createRadialGradient(px,py,0,px,py,rad);
|
| 358 |
+
grd.addColorStop(0,'rgba(220,38,38,0.5)');
|
| 359 |
+
grd.addColorStop(0.4,'rgba(180,0,0,0.18)');
|
| 360 |
+
grd.addColorStop(1,'rgba(0,0,0,0)');
|
| 361 |
+
ctx.fillStyle=grd;ctx.fillRect(0,0,w,h);
|
| 362 |
+
});
|
| 363 |
+
ctx.font="9px 'DM Mono',monospace";
|
| 364 |
+
ctx.fillStyle=dk?'rgba(255,255,255,0.3)':'rgba(0,0,0,0.25)';
|
| 365 |
+
ctx.textAlign='left';ctx.fillText(pts.length+' points',8,13);
|
| 366 |
+
}else{
|
| 367 |
+
ctx.fillStyle=dk?'rgba(255,255,255,0.18)':'rgba(0,0,0,0.2)';
|
| 368 |
+
ctx.font="12px 'DM Sans',sans-serif";ctx.textAlign='center';
|
| 369 |
+
ctx.fillText('Run an analysis to build the heatmap',w/2,h/2);
|
| 370 |
+
ctx.textAlign='left';
|
| 371 |
+
}
|
| 372 |
+
ctx.font="10px 'DM Mono',monospace";
|
| 373 |
+
ctx.fillStyle=dk?'rgba(255,255,255,0.24)':'rgba(0,0,0,0.2)';
|
| 374 |
+
ctx.fillText('N',w/2-3,12);ctx.fillText('S',w/2-3,h-5);ctx.fillText('W',5,h/2+3);ctx.fillText('E',w-10,h/2+3);
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
async function loadHeatmap(){
|
| 378 |
+
try{
|
| 379 |
+
const r=await fetch('/api/logs/rows');
|
| 380 |
+
if(!r.ok)return;
|
| 381 |
+
const rows=await r.json();
|
| 382 |
+
if(!rows.length)return;
|
| 383 |
+
const pts=[];let mxX=0,mxY=0;
|
| 384 |
+
rows.slice(0,4000).forEach(r=>{
|
| 385 |
+
if(r.cx>0&&r.cy>0){pts.push({x:r.cx,y:r.cy,r:0,h:0});if(r.cx>mxX)mxX=r.cx;if(r.cy>mxY)mxY=r.cy;}
|
| 386 |
+
});
|
| 387 |
+
if(!pts.length)return;
|
| 388 |
+
pts.forEach(p=>{p.r=mxX;p.h=mxY;});
|
| 389 |
+
window._tiqHeatPoints=pts;
|
| 390 |
+
drawHm();
|
| 391 |
+
}catch(e){}
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
setTimeout(rebuildCharts,120);
|
| 395 |
+
setTimeout(rebuildPie,150);
|
| 396 |
+
setTimeout(drawHm,180);
|
| 397 |
+
setTimeout(loadHeatmap,600);
|
| 398 |
+
setInterval(loadHeatmap,20000);
|
| 399 |
+
window.addEventListener('resize',()=>setTimeout(drawHm,50));
|
| 400 |
+
|
| 401 |
+
(function(){
|
| 402 |
+
try{
|
| 403 |
+
const th=localStorage.getItem('tiq_theme');
|
| 404 |
+
if(th){
|
| 405 |
+
document.documentElement.setAttribute('data-theme',th);
|
| 406 |
+
if(th==='dark'){['ti','ti2'].forEach(x=>{const e=document.getElementById(x);if(e)e.className='fas fa-sun';});}
|
| 407 |
+
}
|
| 408 |
+
const lg=localStorage.getItem('tiq_lang');
|
| 409 |
+
if(lg&&lg!=='en'){
|
| 410 |
+
const btn=document.querySelector('.lb[onclick*="\''+lg+'\'"]');
|
| 411 |
+
if(btn)setLang(lg,btn);
|
| 412 |
+
}
|
| 413 |
+
}catch(e){}
|
| 414 |
+
})();
|
| 415 |
+
</script>
|
| 416 |
+
<script src="/static/trackiq-api.js"></script>
|
| 417 |
+
</body>
|
| 418 |
+
</html>
|
templates/history.html
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" data-theme="light">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>TrackIQ — History</title>
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
|
| 9 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
| 10 |
+
<style>
|
| 11 |
+
:root{--bg:#f0f4f8;--surface:#fff;--surface2:#f6f8fb;--border:rgba(0,0,0,0.07);--shadow:0 2px 16px rgba(0,0,0,0.09);--shadow-lg:0 8px 36px rgba(0,0,0,0.13);--text:#0f1923;--text2:#5a6a7a;--text3:#9aaab8;--accent:#880000;--accent2:#660000;--accent-light:rgba(136,0,0,0.07);--green:#16a34a;--green-bg:#dcfce7;--sidebar-w:60px;--radius:14px;--radius-sm:9px;}
|
| 12 |
+
[data-theme="dark"]{--bg:#0c1117;--surface:#151c25;--surface2:#1c2531;--border:rgba(255,255,255,0.07);--shadow:0 2px 16px rgba(0,0,0,0.45);--text:#e8edf2;--text2:#7a8fa0;--text3:#3a4a5a;--green-bg:#052e16;}
|
| 13 |
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
|
| 14 |
+
::-webkit-scrollbar{width:0;}
|
| 15 |
+
body{font-family:'DM Sans',sans-serif;background:var(--bg);color:var(--text);font-size:13px;display:flex;min-height:100vh;transition:background 0.3s,color 0.3s;}
|
| 16 |
+
|
| 17 |
+
.sidebar{width:var(--sidebar-w);background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;align-items:center;padding:16px 0;gap:4px;position:fixed;top:0;left:0;bottom:0;z-index:200;box-shadow:2px 0 12px rgba(0,0,0,0.04);}
|
| 18 |
+
.sb-logo{width:34px;height:34px;background:var(--accent);border-radius:8px;display:flex;align-items:center;justify-content:center;color:white;font-size:14px;margin-bottom:10px;text-decoration:none;}
|
| 19 |
+
.sb-sep{width:28px;height:1px;background:var(--border);margin:4px 0;}
|
| 20 |
+
.sb-btn{width:38px;height:38px;border-radius:9px;display:flex;align-items:center;justify-content:center;color:var(--text3);font-size:15px;cursor:pointer;transition:all 0.2s;text-decoration:none;border:none;background:none;position:relative;}
|
| 21 |
+
.sb-btn:hover{background:var(--surface2);color:var(--text);}
|
| 22 |
+
.sb-btn.active{background:var(--accent-light);color:var(--accent);}
|
| 23 |
+
.sb-btn .tip{position:absolute;left:46px;top:50%;transform:translateY(-50%);background:var(--text);color:var(--surface);font-size:11px;padding:3px 8px;border-radius:5px;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity 0.15s;z-index:300;}
|
| 24 |
+
.sb-btn:hover .tip{opacity:1;}
|
| 25 |
+
.sb-spacer{flex:1;}
|
| 26 |
+
|
| 27 |
+
.page-wrap{margin-left:var(--sidebar-w);flex:1;display:flex;flex-direction:column;}
|
| 28 |
+
.topbar{height:52px;background:var(--surface);border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;padding:0 22px;position:sticky;top:0;z-index:100;}
|
| 29 |
+
.topbar-l{display:flex;align-items:center;gap:10px;}
|
| 30 |
+
.brand-name{font-size:15px;font-weight:600;} .brand-name b{color:var(--accent);}
|
| 31 |
+
.sep{color:var(--border);font-size:18px;margin:0 2px;}
|
| 32 |
+
.topbar-r{display:flex;align-items:center;gap:7px;}
|
| 33 |
+
.lang-group{display:flex;background:var(--surface2);border-radius:6px;padding:2px;border:1px solid var(--border);}
|
| 34 |
+
.lang-btn{font-size:11px;font-weight:500;padding:3px 8px;border:none;background:none;cursor:pointer;border-radius:4px;color:var(--text2);font-family:'DM Sans',sans-serif;transition:all 0.2s;}
|
| 35 |
+
.lang-btn.active{background:var(--accent);color:white;}
|
| 36 |
+
.icon-btn{width:30px;height:30px;border-radius:6px;border:1px solid var(--border);background:var(--surface);cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--text2);font-size:12px;}
|
| 37 |
+
.icon-btn:hover{background:var(--surface2);}
|
| 38 |
+
|
| 39 |
+
main{flex:1;padding:18px 20px;max-width:1380px;width:100%;margin:0 auto;display:flex;flex-direction:column;gap:16px;}
|
| 40 |
+
|
| 41 |
+
/* PLAYER CARD — full width */
|
| 42 |
+
.player-card{grid-column:1/3;background:var(--surface);border-radius:var(--radius);border:1px solid var(--border);box-shadow:var(--shadow-lg);overflow:hidden;}
|
| 43 |
+
.player-head{padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;}
|
| 44 |
+
.player-title{font-size:13px;font-weight:500;display:flex;align-items:center;gap:7px;}
|
| 45 |
+
.player-title i{color:var(--accent);}
|
| 46 |
+
.player-body{position:relative;background:#000;}
|
| 47 |
+
.player-body video{width:100%;display:block;max-height:420px;object-fit:contain;}
|
| 48 |
+
.player-hud{position:absolute;top:11px;left:13px;color:white;font-family:'DM Mono',monospace;font-size:12px;line-height:1.8;text-shadow:1px 1px 4px rgba(0,0,0,0.98);pointer-events:none;}
|
| 49 |
+
.player-empty{padding:80px 20px;text-align:center;color:var(--text3);}
|
| 50 |
+
.player-empty i{font-size:44px;display:block;margin-bottom:12px;}
|
| 51 |
+
.player-empty p{font-size:13px;}
|
| 52 |
+
|
| 53 |
+
/* VIDEO GRID */
|
| 54 |
+
.vid-card{background:var(--surface);border-radius:var(--radius);border:1px solid var(--border);box-shadow:var(--shadow);overflow:hidden;cursor:pointer;transition:all 0.2s;}
|
| 55 |
+
.vid-card:hover{box-shadow:var(--shadow-lg);transform:translateY(-3px);}
|
| 56 |
+
.vid-card.selected{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent);}
|
| 57 |
+
.vid-thumb{position:relative;background:#0a0a0a;aspect-ratio:16/9;display:flex;align-items:center;justify-content:center;overflow:hidden;}
|
| 58 |
+
.vid-thumb video{width:100%;height:100%;object-fit:cover;display:block;}
|
| 59 |
+
.vid-thumb-ph{color:rgba(255,255,255,0.3);font-size:32px;}
|
| 60 |
+
.play-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.3);opacity:0;transition:opacity 0.2s;}
|
| 61 |
+
.play-overlay i{font-size:32px;color:white;}
|
| 62 |
+
.vid-card:hover .play-overlay{opacity:1;}
|
| 63 |
+
.vid-info{padding:11px 13px;}
|
| 64 |
+
.vid-name{font-size:12px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:3px;}
|
| 65 |
+
.vid-meta{font-size:10px;color:var(--text2);display:flex;align-items:center;justify-content:space-between;}
|
| 66 |
+
.vid-meta span{display:flex;align-items:center;gap:4px;}
|
| 67 |
+
.vid-actions{display:flex;gap:6px;margin-top:8px;}
|
| 68 |
+
.btn-vi{flex:1;padding:5px 8px;border-radius:6px;border:1px solid var(--border);background:var(--surface2);color:var(--text2);font-size:11px;cursor:pointer;font-family:'DM Sans',sans-serif;display:flex;align-items:center;justify-content:center;gap:4px;transition:all 0.2s;}
|
| 69 |
+
.btn-vi:hover{background:var(--accent);color:white;border-color:var(--accent);}
|
| 70 |
+
.btn-vi.danger:hover{background:#dc2626;border-color:#dc2626;}
|
| 71 |
+
|
| 72 |
+
/* EMPTY HISTORY */
|
| 73 |
+
.empty-hist{text-align:center;padding:60px 20px;color:var(--text3);}
|
| 74 |
+
.empty-hist i{font-size:48px;display:block;margin-bottom:14px;}
|
| 75 |
+
.empty-hist p{font-size:13px;margin-bottom:16px;}
|
| 76 |
+
.empty-hist a{display:inline-flex;align-items:center;gap:6px;background:var(--accent);color:white;padding:9px 18px;border-radius:9px;text-decoration:none;font-size:12px;}
|
| 77 |
+
|
| 78 |
+
/* BADGE */
|
| 79 |
+
.badge{font-size:10px;font-weight:500;padding:1px 6px;border-radius:4px;}
|
| 80 |
+
.bg{background:var(--green-bg);color:var(--green);}
|
| 81 |
+
</style>
|
| 82 |
+
</head>
|
| 83 |
+
<body>
|
| 84 |
+
|
| 85 |
+
<nav class="sidebar">
|
| 86 |
+
<a href="/" class="sb-logo"><i class="fas fa-satellite-dish"></i></a>
|
| 87 |
+
<div class="sb-sep"></div>
|
| 88 |
+
<a href="/home" class="sb-btn"><i class="fas fa-play-circle"></i><span class="tip" id="tip-analyze">Analyze</span></a>
|
| 89 |
+
<a href="/dashboard" class="sb-btn"><i class="fas fa-chart-line"></i><span class="tip" id="tip-dash">Dashboard</span></a>
|
| 90 |
+
<a href="/logs" class="sb-btn"><i class="fas fa-list-ul"></i><span class="tip" id="tip-logs">Logs</span></a>
|
| 91 |
+
<a href="/history" class="sb-btn active"><i class="fas fa-film"></i><span class="tip" id="tip-hist">History</span></a>
|
| 92 |
+
<div class="sb-spacer"></div>
|
| 93 |
+
<button class="sb-btn" onclick="toggleTheme()"><i class="fas fa-moon" id="themeIcon"></i><span class="tip">Theme</span></button>
|
| 94 |
+
</nav>
|
| 95 |
+
|
| 96 |
+
<div class="page-wrap">
|
| 97 |
+
<header class="topbar">
|
| 98 |
+
<div class="topbar-l">
|
| 99 |
+
<span class="brand-name">Track<b>IQ</b></span>
|
| 100 |
+
<span class="sep">|</span>
|
| 101 |
+
<span style="font-size:13px;font-weight:500;" id="t-title">Annotated History</span>
|
| 102 |
+
</div>
|
| 103 |
+
<div class="topbar-r">
|
| 104 |
+
<div class="lang-group">
|
| 105 |
+
<button class="lang-btn active" onclick="setLang('en',this)">EN</button>
|
| 106 |
+
<button class="lang-btn" onclick="setLang('fr',this)">FR</button>
|
| 107 |
+
<button class="lang-btn" onclick="setLang('wo',this)">WO</button>
|
| 108 |
+
</div>
|
| 109 |
+
<button class="icon-btn" onclick="toggleTheme()"><i class="fas fa-moon" id="themeIcon2"></i></button>
|
| 110 |
+
</div>
|
| 111 |
+
</header>
|
| 112 |
+
|
| 113 |
+
<main id="mainGrid">
|
| 114 |
+
|
| 115 |
+
<!-- Videos grid -->
|
| 116 |
+
<div id="videoGrid" style="display:grid;grid-template-columns:1fr 1fr;gap:16px;"></div>
|
| 117 |
+
|
| 118 |
+
</main>
|
| 119 |
+
</div>
|
| 120 |
+
|
| 121 |
+
<script>
|
| 122 |
+
/* THEME */
|
| 123 |
+
function toggleTheme(){
|
| 124 |
+
const d=document.documentElement,dark=d.getAttribute('data-theme')==='dark';
|
| 125 |
+
d.setAttribute('data-theme',dark?'light':'dark');
|
| 126 |
+
['themeIcon','themeIcon2'].forEach(id=>{const el=document.getElementById(id);if(el)el.className=dark?'fas fa-moon':'fas fa-sun';});
|
| 127 |
+
try{localStorage.setItem('tiq_theme',dark?'light':'dark');}catch(e){}
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
/* LANG */
|
| 131 |
+
const T={
|
| 132 |
+
en:{title:'Annotated History',player:'Video player',dl:'Download','select-vid':'Select a video below to play it',tipAnalyze:'Analyze',tipDash:'Dashboard',tipLogs:'Logs',tipHist:'History',reanalyze:'Re-analyze',download:'Download',delete:'Delete',empty:'No annotated videos yet.',goAnalyze:'Go to Analyze'},
|
| 133 |
+
fr:{title:'Historique annoté',player:'Lecteur vidéo',dl:'Télécharger','select-vid':'Sélectionnez une vidéo ci-dessous',tipAnalyze:'Analyser',tipDash:'Tableau de bord',tipLogs:'Logs',tipHist:'Historique',reanalyze:'Ré-analyser',download:'Télécharger',delete:'Supprimer',empty:'Aucune vidéo annotée.',goAnalyze:'Aller sur Analyser'},
|
| 134 |
+
wo:{title:'Màgg yi',player:'Video bi',dl:'Télécharger','select-vid':'Tànn video ci suuf',tipAnalyze:'Jàng',tipDash:'Dashboard',tipLogs:'Logs',tipHist:'Màgg',reanalyze:'Wërtëk',download:'Télécharger',delete:'Bañ',empty:'Amul video.',goAnalyze:'Dem Analyser'}
|
| 135 |
+
};
|
| 136 |
+
let curLang='en';
|
| 137 |
+
function setLang(l,btn){
|
| 138 |
+
curLang=l;
|
| 139 |
+
document.querySelectorAll('.lang-btn').forEach(b=>b.classList.remove('active'));btn.classList.add('active');
|
| 140 |
+
const t=T[l];
|
| 141 |
+
Object.entries({title:'t-title',player:'t-player',dl:'t-dl','select-vid':'t-select-vid',tipAnalyze:'tip-analyze',tipDash:'tip-dash',tipLogs:'tip-logs',tipHist:'tip-hist'}).forEach(([k,id])=>{const el=document.getElementById(id);if(el&&t[k])el.textContent=t[k];});
|
| 142 |
+
renderGrid();
|
| 143 |
+
try{localStorage.setItem('tiq_lang',l);}catch(e){}
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
/* HISTORY DATA */
|
| 147 |
+
function getHistory(){try{return JSON.parse(localStorage.getItem('tiq_history')||'[]');}catch(e){return[];}}
|
| 148 |
+
function saveHistory(h){try{localStorage.setItem('tiq_history',JSON.stringify(h));}catch(e){}}
|
| 149 |
+
|
| 150 |
+
/* FORMAT DATE */
|
| 151 |
+
function fmtDate(iso){
|
| 152 |
+
try{const d=new Date(iso);return d.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'})+' '+d.toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit'});}catch(e){return 'Unknown date';}
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
let activeUrl=null,activeIdx=null;
|
| 156 |
+
|
| 157 |
+
function dlVideo(url,name){
|
| 158 |
+
const a=document.createElement('a');a.href=url;
|
| 159 |
+
a.download=name||'trackiq_video.mp4';a.click();
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
function playVideo(url,name,idx){
|
| 163 |
+
activeUrl=url;activeIdx=idx;
|
| 164 |
+
const op=document.getElementById('overlayPlayer');
|
| 165 |
+
op.src=url;op.play().catch(()=>{});
|
| 166 |
+
document.getElementById('overlayName').textContent=name;
|
| 167 |
+
document.getElementById('vidOverlay').style.display='flex';
|
| 168 |
+
}
|
| 169 |
+
function closeOverlay(){
|
| 170 |
+
const op=document.getElementById('overlayPlayer');op.pause();op.src='';
|
| 171 |
+
document.getElementById('vidOverlay').style.display='none';
|
| 172 |
+
activeUrl=null;activeIdx=null;
|
| 173 |
+
}
|
| 174 |
+
function dlOverlay(){if(activeUrl){const a=document.createElement('a');a.href=activeUrl;a.download='trackiq.mp4';a.click();}}
|
| 175 |
+
|
| 176 |
+
function dlActive(){
|
| 177 |
+
if(!activeUrl)return;
|
| 178 |
+
const a=document.createElement('a');a.href=activeUrl;a.download='trackiq_annotated.mp4';a.click();
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
function deleteVideo(idx){
|
| 182 |
+
const h=getHistory();h.splice(idx,1);saveHistory(h);
|
| 183 |
+
if(activeIdx===idx){
|
| 184 |
+
closeModal();
|
| 185 |
+
activeUrl=null;activeIdx=null;
|
| 186 |
+
}
|
| 187 |
+
renderGrid();
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
function renderGrid(){
|
| 191 |
+
const hist=getHistory();
|
| 192 |
+
const t=T[curLang];
|
| 193 |
+
const grid=document.getElementById('videoGrid');grid.innerHTML='';
|
| 194 |
+
|
| 195 |
+
if(!hist.length){
|
| 196 |
+
const empty=document.createElement('div');empty.className='empty-hist';
|
| 197 |
+
empty.innerHTML=`<i class="fas fa-film"></i><p>${t.empty}</p><a href="/home">${t.goAnalyze} <i class="fas fa-arrow-right"></i></a>`;
|
| 198 |
+
grid.appendChild(empty);return;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
hist.forEach((item,i)=>{
|
| 202 |
+
const card=document.createElement('div');card.className='vid-card'+(activeIdx===i?' selected':'');
|
| 203 |
+
const isBlob=item.url&&item.url.startsWith('blob:');
|
| 204 |
+
const isBackend=item.url&&(item.url.startsWith('/api/')||item.url.startsWith('http'));
|
| 205 |
+
const canPlay=isBlob||isBackend;
|
| 206 |
+
card.innerHTML=`
|
| 207 |
+
<div class="vid-thumb">
|
| 208 |
+
${isBlob
|
| 209 |
+
? `<video src="${item.url}" style="width:100%;height:100%;object-fit:cover;" muted preload="metadata" onloadeddata="this.currentTime=1"></video>`
|
| 210 |
+
: `<div class="vid-thumb-ph" style="background:#0a0e15;display:flex;align-items:center;justify-content:center;height:100%;">
|
| 211 |
+
<i class="fas fa-film" style="font-size:32px;color:rgba(255,255,255,0.2);"></i>
|
| 212 |
+
</div>`
|
| 213 |
+
}
|
| 214 |
+
<div class="play-overlay"><i class="fas fa-play"></i></div>
|
| 215 |
+
</div>
|
| 216 |
+
<div class="vid-info">
|
| 217 |
+
<div class="vid-name" title="${item.name}">${item.name||'Unnamed'}</div>
|
| 218 |
+
<div class="vid-meta">
|
| 219 |
+
<span><i class="fas fa-calendar" style="font-size:9px;"></i> ${fmtDate(item.date)}</span>
|
| 220 |
+
<span class="badge bg">${isBlob?'Local':'Server'}</span>
|
| 221 |
+
</div>
|
| 222 |
+
<div class="vid-actions">
|
| 223 |
+
<button class="btn-vi" onclick="event.stopPropagation();playVideo('${item.url}','${item.name}',${i})">
|
| 224 |
+
<i class="fas fa-play"></i> Play
|
| 225 |
+
</button>
|
| 226 |
+
<button class="btn-vi" onclick="event.stopPropagation();dlVideo('${item.url}','${item.name}')">
|
| 227 |
+
<i class="fas fa-download"></i>
|
| 228 |
+
</button>
|
| 229 |
+
<button class="btn-vi danger" onclick="event.stopPropagation();deleteVideo(${i})">
|
| 230 |
+
<i class="fas fa-trash"></i>
|
| 231 |
+
</button>
|
| 232 |
+
</div>
|
| 233 |
+
</div>
|
| 234 |
+
`;
|
| 235 |
+
card.onclick=(ev)=>{if(!ev.target.closest('.btn-vi'))playVideo(item.url,item.name,i);};
|
| 236 |
+
grid.appendChild(card);
|
| 237 |
+
});
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
/* HUD ticker on player */
|
| 241 |
+
setInterval(()=>{
|
| 242 |
+
if(!activeUrl)return;
|
| 243 |
+
const f=570+Math.floor(Math.random()*80),s=295+Math.floor(Math.random()*40);
|
| 244 |
+
const hf=document.getElementById('oHudF');if(hf)hf.textContent=`F: = ${f} ▶ ${s}`;
|
| 245 |
+
const car=2+Math.floor(Math.random()*6),van=Math.floor(Math.random()*3),bike=Math.floor(Math.random()*6),p=Math.floor(Math.random()*7);
|
| 246 |
+
const hc=document.getElementById('oHudC');if(hc)hc.innerHTML=`Car = ${car}<br>Van = ${van}<br>bike = ${bike}<br>person = ${p}`;
|
| 247 |
+
},2200);
|
| 248 |
+
|
| 249 |
+
document.addEventListener('keydown',e=>{if(e.key==='Escape')closeOverlay();});
|
| 250 |
+
/* INIT */
|
| 251 |
+
(function(){
|
| 252 |
+
try{
|
| 253 |
+
const th=localStorage.getItem('tiq_theme');if(th){document.documentElement.setAttribute('data-theme',th);if(th==='dark'){['themeIcon','themeIcon2'].forEach(id=>{const el=document.getElementById(id);if(el)el.className='fas fa-sun';});}}
|
| 254 |
+
const lg=localStorage.getItem('tiq_lang');if(lg&&lg!=='en'){const btn=document.querySelector(`.lang-btn[onclick*="'${lg}'"]`);if(btn)setLang(lg,btn);}
|
| 255 |
+
}catch(e){}
|
| 256 |
+
renderGrid();
|
| 257 |
+
})();
|
| 258 |
+
</script>
|
| 259 |
+
<script src="/static/trackiq-api.js"></script>
|
| 260 |
+
|
| 261 |
+
<!-- ── Modal lecteur vidéo ── -->
|
| 262 |
+
<div id="vidOverlay" style="display:none;position:fixed;inset:0;z-index:1000;
|
| 263 |
+
background:rgba(0,0,0,0.88);backdrop-filter:blur(8px);
|
| 264 |
+
align-items:center;justify-content:center;flex-direction:column;gap:12px;">
|
| 265 |
+
|
| 266 |
+
<!-- Header -->
|
| 267 |
+
<div style="display:flex;align-items:center;justify-content:space-between;
|
| 268 |
+
width:min(90vw,860px);padding:0 4px;">
|
| 269 |
+
<span id="overlayName" style="color:#fff;font-size:13px;font-weight:500;
|
| 270 |
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:70%;"></span>
|
| 271 |
+
<div style="display:flex;gap:8px;">
|
| 272 |
+
<button onclick="dlOverlay()" style="background:rgba(255,255,255,0.12);color:#fff;
|
| 273 |
+
border:none;border-radius:7px;padding:6px 13px;font-size:12px;cursor:pointer;
|
| 274 |
+
font-family:'DM Sans',sans-serif;display:flex;align-items:center;gap:5px;">
|
| 275 |
+
<i class="fas fa-download"></i> Download
|
| 276 |
+
</button>
|
| 277 |
+
<button onclick="closeOverlay()" style="background:rgba(220,38,38,0.7);color:#fff;
|
| 278 |
+
border:none;border-radius:7px;padding:6px 13px;font-size:12px;cursor:pointer;
|
| 279 |
+
font-family:'DM Sans',sans-serif;display:flex;align-items:center;gap:5px;">
|
| 280 |
+
<i class="fas fa-times"></i> Close
|
| 281 |
+
</button>
|
| 282 |
+
</div>
|
| 283 |
+
</div>
|
| 284 |
+
|
| 285 |
+
<!-- Vidéo -->
|
| 286 |
+
<video id="overlayPlayer" controls autoplay
|
| 287 |
+
style="width:min(90vw,860px);max-height:75vh;border-radius:10px;
|
| 288 |
+
background:#000;object-fit:contain;box-shadow:0 20px 60px rgba(0,0,0,0.6);">
|
| 289 |
+
</video>
|
| 290 |
+
|
| 291 |
+
<!-- Hint ESC -->
|
| 292 |
+
<span style="color:rgba(255,255,255,0.3);font-size:10px;">Press ESC to close</span>
|
| 293 |
+
</div>
|
| 294 |
+
</body>
|
| 295 |
+
</html>
|
templates/home.html
ADDED
|
@@ -0,0 +1,684 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" data-theme="light">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>TrackIQ — Analyze</title>
|
| 7 |
+
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
|
| 8 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
| 9 |
+
<style>
|
| 10 |
+
:root{--bg:#f0f4f8;--sur:#fff;--sur2:#f6f8fb;--bd:rgba(0,0,0,0.07);--sh:0 2px 16px rgba(0,0,0,0.09);--shlg:0 8px 36px rgba(0,0,0,0.13);--t:#0f1923;--t2:#5a6a7a;--t3:#9aaab8;--ac:#880000;--ac2:#660000;--acl:rgba(136,0,0,0.07);--gr:#16a34a;--sw:60px;--r:14px;--rs:9px;}
|
| 11 |
+
[data-theme="dark"]{--bg:#0c1117;--sur:#151c25;--sur2:#1c2531;--bd:rgba(255,255,255,0.07);--sh:0 2px 16px rgba(0,0,0,0.45);--t:#e8edf2;--t2:#7a8fa0;--t3:#3a4a5a;}
|
| 12 |
+
*{box-sizing:border-box;margin:0;padding:0;}::-webkit-scrollbar{width:0;}
|
| 13 |
+
body{font-family:'DM Sans',sans-serif;background:var(--bg);color:var(--t);font-size:13px;display:flex;min-height:100vh;}
|
| 14 |
+
|
| 15 |
+
/* Sidebar */
|
| 16 |
+
.sb{width:var(--sw);background:var(--sur);border-right:1px solid var(--bd);display:flex;flex-direction:column;align-items:center;padding:14px 0;gap:4px;position:fixed;top:0;left:0;bottom:0;z-index:200;}
|
| 17 |
+
.sbl{width:34px;height:34px;background:var(--ac);border-radius:8px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px;margin-bottom:10px;text-decoration:none;}
|
| 18 |
+
.sbsep{width:26px;height:1px;background:var(--bd);margin:3px 0;}
|
| 19 |
+
.sbb{width:38px;height:38px;border-radius:9px;display:flex;align-items:center;justify-content:center;color:var(--t3);font-size:14px;cursor:pointer;transition:all 0.2s;text-decoration:none;border:none;background:none;position:relative;}
|
| 20 |
+
.sbb:hover{background:var(--sur2);color:var(--t);}.sbb.on{background:var(--acl);color:var(--ac);}
|
| 21 |
+
.sbb .tip{position:absolute;left:44px;top:50%;transform:translateY(-50%);background:var(--t);color:var(--sur);font-size:10px;padding:3px 7px;border-radius:4px;white-space:nowrap;opacity:0;pointer-events:none;z-index:300;}.sbb:hover .tip{opacity:1;}
|
| 22 |
+
.sbsp{flex:1;}
|
| 23 |
+
|
| 24 |
+
/* Layout */
|
| 25 |
+
.pw{margin-left:var(--sw);flex:1;display:flex;flex-direction:column;}
|
| 26 |
+
.topb{height:50px;background:var(--sur);border-bottom:1px solid var(--bd);display:flex;align-items:center;justify-content:space-between;padding:0 20px;position:sticky;top:0;z-index:100;}
|
| 27 |
+
.topl{display:flex;align-items:center;gap:9px;}
|
| 28 |
+
.bn{font-size:14px;font-weight:600;}.bn b{color:var(--ac);}
|
| 29 |
+
.topr{display:flex;align-items:center;gap:6px;}
|
| 30 |
+
.lg{display:flex;background:var(--sur2);border-radius:5px;padding:2px;border:1px solid var(--bd);}
|
| 31 |
+
.lb{font-size:10px;font-weight:500;padding:2px 8px;border:none;background:none;cursor:pointer;border-radius:3px;color:var(--t2);font-family:'DM Sans',sans-serif;transition:all 0.2s;}.lb.on{background:var(--ac);color:#fff;}
|
| 32 |
+
.ib{width:28px;height:28px;border-radius:5px;border:1px solid var(--bd);background:var(--sur);cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--t2);font-size:11px;}
|
| 33 |
+
.ib:hover{background:var(--sur2);}
|
| 34 |
+
|
| 35 |
+
main{flex:1;padding:14px 16px;display:grid;grid-template-columns:340px 1fr;gap:12px;max-width:1400px;width:100%;margin:0 auto;}
|
| 36 |
+
|
| 37 |
+
/* Card */
|
| 38 |
+
.card{background:var(--sur);border-radius:var(--r);border:1px solid var(--bd);box-shadow:var(--sh);padding:14px 16px;}
|
| 39 |
+
.ct{font-size:12px;font-weight:500;margin-bottom:11px;display:flex;align-items:center;gap:6px;}
|
| 40 |
+
.ct i{color:var(--ac);}
|
| 41 |
+
|
| 42 |
+
/* Source tabs */
|
| 43 |
+
.stabs{display:flex;gap:3px;background:var(--sur2);border-radius:7px;padding:3px;border:1px solid var(--bd);margin-bottom:11px;}
|
| 44 |
+
.stab{flex:1;padding:6px 4px;border-radius:5px;font-size:11px;cursor:pointer;color:var(--t2);border:none;background:none;font-family:'DM Sans',sans-serif;transition:all 0.2s;display:flex;align-items:center;justify-content:center;gap:4px;}
|
| 45 |
+
.stab.on{background:var(--sur);color:var(--t);box-shadow:0 1px 4px rgba(0,0,0,0.1);font-weight:500;}
|
| 46 |
+
|
| 47 |
+
/* Upload zone */
|
| 48 |
+
.uzone{border:2px dashed var(--bd);border-radius:var(--rs);min-height:110px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;cursor:pointer;transition:all 0.2s;padding:14px;position:relative;overflow:hidden;}
|
| 49 |
+
.uzone:hover,.uzone.drag{border-color:var(--ac);background:var(--acl);}
|
| 50 |
+
.uzone.has{border-style:solid;border-color:var(--ac);}
|
| 51 |
+
.uzone i{font-size:22px;color:var(--t3);}
|
| 52 |
+
.uzone.has i{color:var(--ac);}
|
| 53 |
+
.uzone p{font-size:11px;color:var(--t2);text-align:center;}
|
| 54 |
+
.uzone input{position:absolute;inset:0;opacity:0;cursor:pointer;}
|
| 55 |
+
.fname{font-size:11px;font-weight:500;color:var(--ac);display:none;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
| 56 |
+
.prog{display:none;background:var(--sur2);border-radius:3px;height:4px;margin-top:7px;overflow:hidden;}
|
| 57 |
+
.progb{height:100%;background:var(--ac);width:0%;transition:width 0.3s;}
|
| 58 |
+
.progt{font-size:10px;color:var(--t2);margin-top:4px;display:none;}
|
| 59 |
+
|
| 60 |
+
/* URL */
|
| 61 |
+
.urow{display:flex;gap:6px;}
|
| 62 |
+
.uin{flex:1;padding:7px 9px;border:1px solid var(--bd);border-radius:7px;font-size:12px;background:var(--sur2);color:var(--t);font-family:'DM Sans',sans-serif;outline:none;}
|
| 63 |
+
.uin:focus{border-color:var(--ac);}
|
| 64 |
+
.uin::placeholder{color:var(--t3);}
|
| 65 |
+
.bsm{padding:7px 12px;background:var(--ac);color:#fff;border:none;border-radius:7px;font-size:12px;cursor:pointer;font-family:'DM Sans',sans-serif;}
|
| 66 |
+
|
| 67 |
+
/* Webcam */
|
| 68 |
+
.wcbox{background:#000;border-radius:8px;overflow:hidden;aspect-ratio:16/9;position:relative;margin-bottom:10px;}
|
| 69 |
+
.wcbox video,.wcbox img{width:100%;height:100%;object-fit:cover;display:block;}
|
| 70 |
+
.wcph{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;color:rgba(255,255,255,0.3);font-size:11px;}
|
| 71 |
+
.wcph i{font-size:22px;}
|
| 72 |
+
.recbdg{position:absolute;top:7px;left:8px;display:none;align-items:center;gap:4px;background:rgba(220,38,38,0.85);color:#fff;font-size:10px;padding:2px 8px;border-radius:10px;}
|
| 73 |
+
@keyframes blink{0%,100%{opacity:1}50%{opacity:0.3}}
|
| 74 |
+
.recbdg i{font-size:8px;animation:blink 1s infinite;}
|
| 75 |
+
|
| 76 |
+
/* Classes */
|
| 77 |
+
.clshd{display:flex;align-items:center;justify-content:space-between;margin-bottom:7px;}
|
| 78 |
+
.clshd span{font-size:11px;color:var(--t2);}
|
| 79 |
+
.clshd button{font-size:11px;color:var(--ac);background:none;border:none;cursor:pointer;font-family:'DM Sans',sans-serif;}
|
| 80 |
+
.clsg{display:grid;grid-template-columns:1fr 1fr;gap:5px;}
|
| 81 |
+
.clsi{display:flex;align-items:center;gap:5px;padding:5px 8px;border-radius:6px;border:1px solid var(--bd);cursor:pointer;transition:all 0.2s;font-size:11px;background:var(--sur2);}
|
| 82 |
+
.clsi:hover{border-color:var(--ac);}.clsi.on{border-color:var(--ac);background:var(--acl);}
|
| 83 |
+
.cldot{width:8px;height:8px;border-radius:3px;flex-shrink:0;}
|
| 84 |
+
|
| 85 |
+
/* Buttons */
|
| 86 |
+
.runbtn{width:100%;padding:10px;background:var(--ac);color:#fff;border:none;border-radius:9px;font-size:13px;font-weight:500;cursor:pointer;font-family:'DM Sans',sans-serif;display:flex;align-items:center;justify-content:center;gap:7px;transition:all 0.2s;margin-top:4px;}
|
| 87 |
+
.runbtn:hover{background:var(--ac2);}.runbtn:disabled{opacity:0.4;cursor:not-allowed;}
|
| 88 |
+
.rstbtn{width:100%;padding:8px;background:var(--sur2);color:var(--t2);border:1px solid var(--bd);border-radius:9px;font-size:12px;cursor:pointer;font-family:'DM Sans',sans-serif;display:flex;align-items:center;justify-content:center;gap:6px;transition:all 0.2s;}
|
| 89 |
+
.rstbtn:hover{border-color:var(--ac);color:var(--ac);}
|
| 90 |
+
|
| 91 |
+
/* Right col */
|
| 92 |
+
.rc{display:flex;flex-direction:column;gap:11px;}
|
| 93 |
+
|
| 94 |
+
/* Big panel */
|
| 95 |
+
.bp{background:var(--sur);border-radius:var(--r);border:1px solid var(--bd);box-shadow:var(--shlg);overflow:hidden;}
|
| 96 |
+
.bph{padding:10px 14px;border-bottom:1px solid var(--bd);display:flex;align-items:center;justify-content:space-between;}
|
| 97 |
+
.bpht{font-size:12px;font-weight:500;display:flex;align-items:center;gap:6px;}
|
| 98 |
+
.bpht i{color:var(--ac);}
|
| 99 |
+
.bpb{position:relative;background:#080e15;height:420px;}
|
| 100 |
+
|
| 101 |
+
/* Video dans le panel */
|
| 102 |
+
.bpb video,.bpb img#wcYoloImg{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;display:none;}
|
| 103 |
+
|
| 104 |
+
/* HUD top-left */
|
| 105 |
+
.hud{position:absolute;top:10px;left:11px;color:#fff;font-family:'DM Mono',monospace;font-size:11px;line-height:1.85;text-shadow:1px 1px 4px rgba(0,0,0,0.98);pointer-events:none;display:none;z-index:4;}
|
| 106 |
+
.hudf{font-size:9px;color:rgba(255,255,255,0.4);margin-bottom:2px;}
|
| 107 |
+
|
| 108 |
+
/* Count overlay top-right */
|
| 109 |
+
.cov{position:absolute;top:10px;right:11px;display:none;flex-direction:column;gap:4px;pointer-events:none;z-index:4;}
|
| 110 |
+
.cbdg{display:flex;flex-direction:column;background:rgba(0,0,0,0.6);backdrop-filter:blur(8px);border-left:3px solid transparent;border-radius:5px;padding:4px 9px;min-width:115px;}
|
| 111 |
+
.cbdg .cn{font-size:13px;font-weight:600;line-height:1.2;}
|
| 112 |
+
.cbdg .cu{font-size:9px;color:#ccc;line-height:1.2;}
|
| 113 |
+
|
| 114 |
+
/* No-object banner */
|
| 115 |
+
.noobj{display:none;position:absolute;bottom:14px;left:50%;transform:translateX(-50%);z-index:5;
|
| 116 |
+
background:rgba(220,38,38,0.9);color:#fff;font-size:12px;font-weight:500;
|
| 117 |
+
padding:6px 16px;border-radius:20px;align-items:center;gap:7px;white-space:nowrap;}
|
| 118 |
+
|
| 119 |
+
/* Empty state */
|
| 120 |
+
.es{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:9px;color:rgba(255,255,255,0.18);}
|
| 121 |
+
.es i{font-size:38px;}.es p{font-size:12px;}
|
| 122 |
+
|
| 123 |
+
/* Stats row */
|
| 124 |
+
.sr{display:grid;grid-template-columns:repeat(4,1fr);gap:9px;}
|
| 125 |
+
.sc{background:var(--sur);border-radius:var(--rs);border:1px solid var(--bd);box-shadow:var(--sh);padding:10px 12px;}
|
| 126 |
+
.sc .sl{font-size:10px;color:var(--t2);margin-bottom:3px;}
|
| 127 |
+
.sc .sv{font-size:18px;font-weight:300;letter-spacing:-0.5px;}
|
| 128 |
+
.sc .su{font-size:10px;color:var(--t2);}
|
| 129 |
+
|
| 130 |
+
/* Download bar */
|
| 131 |
+
.dlb{display:flex;align-items:center;justify-content:space-between;background:var(--sur);border-radius:var(--rs);border:1px solid var(--bd);box-shadow:var(--sh);padding:9px 13px;}
|
| 132 |
+
.dlb span{font-size:11px;color:var(--t2);}
|
| 133 |
+
.btndl{display:flex;align-items:center;gap:5px;background:var(--ac);color:#fff;border:none;border-radius:7px;padding:7px 13px;font-size:12px;cursor:pointer;font-family:'DM Sans',sans-serif;}
|
| 134 |
+
.btndl:hover{background:var(--ac2);}.btndl:disabled{opacity:0.4;cursor:not-allowed;background:var(--t3);}
|
| 135 |
+
</style>
|
| 136 |
+
</head>
|
| 137 |
+
<body>
|
| 138 |
+
|
| 139 |
+
<nav class="sb">
|
| 140 |
+
<a href="/" class="sbl"><i class="fas fa-satellite-dish"></i></a>
|
| 141 |
+
<div class="sbsep"></div>
|
| 142 |
+
<a href="/home" class="sbb on"><i class="fas fa-play-circle"></i><span class="tip">Analyze</span></a>
|
| 143 |
+
<a href="/dashboard" class="sbb"><i class="fas fa-chart-line"></i><span class="tip">Dashboard</span></a>
|
| 144 |
+
<a href="/logs" class="sbb"><i class="fas fa-list-ul"></i><span class="tip">Logs</span></a>
|
| 145 |
+
<a href="/history" class="sbb"><i class="fas fa-film"></i><span class="tip">History</span></a>
|
| 146 |
+
<div class="sbsp"></div>
|
| 147 |
+
<button class="sbb" onclick="toggleTheme()"><i class="fas fa-moon" id="ti"></i><span class="tip">Theme</span></button>
|
| 148 |
+
</nav>
|
| 149 |
+
|
| 150 |
+
<div class="pw">
|
| 151 |
+
<header class="topb">
|
| 152 |
+
<div class="topl">
|
| 153 |
+
<span class="bn">Track<b>IQ</b></span>
|
| 154 |
+
<span style="color:var(--bd);font-size:16px;">|</span>
|
| 155 |
+
<span style="font-size:12px;font-weight:500;color:var(--t2);">Video Analysis</span>
|
| 156 |
+
</div>
|
| 157 |
+
<div class="topr">
|
| 158 |
+
<div class="lg">
|
| 159 |
+
<button class="lb on" onclick="setLang('en',this)">EN</button>
|
| 160 |
+
<button class="lb" onclick="setLang('fr',this)">FR</button>
|
| 161 |
+
<button class="lb" onclick="setLang('wo',this)">WO</button>
|
| 162 |
+
</div>
|
| 163 |
+
<button class="ib" onclick="toggleTheme()"><i class="fas fa-moon" id="ti2"></i></button>
|
| 164 |
+
</div>
|
| 165 |
+
</header>
|
| 166 |
+
|
| 167 |
+
<main>
|
| 168 |
+
<!-- Colonne gauche -->
|
| 169 |
+
<div style="display:flex;flex-direction:column;gap:11px;">
|
| 170 |
+
|
| 171 |
+
<!-- Input source -->
|
| 172 |
+
<div class="card">
|
| 173 |
+
<div class="ct"><i class="fas fa-video"></i> Input source</div>
|
| 174 |
+
<div class="stabs">
|
| 175 |
+
<button class="stab on" onclick="switchSrc('upload',this)"><i class="fas fa-upload"></i> Video</button>
|
| 176 |
+
<button class="stab" onclick="switchSrc('url',this)"><i class="fas fa-link"></i> URL</button>
|
| 177 |
+
<button class="stab" onclick="switchSrc('webcam',this)"><i class="fas fa-camera"></i> Webcam</button>
|
| 178 |
+
</div>
|
| 179 |
+
|
| 180 |
+
<!-- Upload -->
|
| 181 |
+
<div id="src-upload">
|
| 182 |
+
<label class="uzone" id="uzone">
|
| 183 |
+
<i class="fas fa-cloud-upload-alt"></i>
|
| 184 |
+
<p>Drop video here or click to browse</p>
|
| 185 |
+
<p style="font-size:10px;color:var(--t3);">MP4, AVI, MOV — max 500 MB</p>
|
| 186 |
+
<span class="fname" id="fname"></span>
|
| 187 |
+
<input type="file" id="fileInput" accept="video/*" onchange="onFile(event)">
|
| 188 |
+
</label>
|
| 189 |
+
<div class="prog" id="progWrap"><div class="progb" id="progBar"></div></div>
|
| 190 |
+
<div class="progt" id="progTxt"></div>
|
| 191 |
+
</div>
|
| 192 |
+
|
| 193 |
+
<!-- URL -->
|
| 194 |
+
<div id="src-url" style="display:none;">
|
| 195 |
+
<div class="urow">
|
| 196 |
+
<input type="text" class="uin" id="urlIn" placeholder="https://… or rtsp://…">
|
| 197 |
+
<button class="bsm" onclick="loadUrl()">Load</button>
|
| 198 |
+
</div>
|
| 199 |
+
</div>
|
| 200 |
+
|
| 201 |
+
<!-- Webcam -->
|
| 202 |
+
<div id="src-webcam" style="display:none;">
|
| 203 |
+
<!-- Mini preview : flux YOLO annoté -->
|
| 204 |
+
<div class="wcbox" id="wcbox">
|
| 205 |
+
<img id="wcMiniYolo" alt="YOLO stream" style="display:none;">
|
| 206 |
+
<div class="wcph" id="wcPh"><i class="fas fa-camera-slash"></i><span>Click Start</span></div>
|
| 207 |
+
<div class="recbdg" id="recBadge"><i class="fas fa-circle"></i> LIVE</div>
|
| 208 |
+
</div>
|
| 209 |
+
<!-- Bouton stop webcam seulement (plus de bouton Enregistrer) -->
|
| 210 |
+
<button class="runbtn" id="wcBtn" onclick="startWc()">
|
| 211 |
+
<i class="fas fa-circle" style="color:#f87171;font-size:8px;"></i> Start webcam
|
| 212 |
+
</button>
|
| 213 |
+
</div>
|
| 214 |
+
</div>
|
| 215 |
+
|
| 216 |
+
<!-- Detection classes -->
|
| 217 |
+
<div class="card">
|
| 218 |
+
<div class="ct"><i class="fas fa-tags"></i> Detection classes</div>
|
| 219 |
+
<div class="clshd">
|
| 220 |
+
<span id="clsCnt">0 selected</span>
|
| 221 |
+
<button onclick="toggleAll()">Select all</button>
|
| 222 |
+
</div>
|
| 223 |
+
<div class="clsg" id="clsGrid"></div>
|
| 224 |
+
</div>
|
| 225 |
+
|
| 226 |
+
<button class="runbtn" id="runBtn" onclick="runAnalysis()" disabled>
|
| 227 |
+
<i class="fas fa-play"></i> Run Analysis
|
| 228 |
+
</button>
|
| 229 |
+
<button class="rstbtn" onclick="resetAll()">
|
| 230 |
+
<i class="fas fa-rotate-right"></i> Reset
|
| 231 |
+
</button>
|
| 232 |
+
</div>
|
| 233 |
+
|
| 234 |
+
<!-- Colonne droite -->
|
| 235 |
+
<div class="rc">
|
| 236 |
+
|
| 237 |
+
<!-- Big result panel -->
|
| 238 |
+
<div class="bp">
|
| 239 |
+
<div class="bph">
|
| 240 |
+
<div class="bpht"><i class="fas fa-film"></i> Result</div>
|
| 241 |
+
<span style="font-size:11px;color:var(--t2);" id="srcLabel"></span>
|
| 242 |
+
</div>
|
| 243 |
+
<div class="bpb" id="bigBody">
|
| 244 |
+
<!-- Vidéo annotée (upload/url) -->
|
| 245 |
+
<video id="resultVid" controls></video>
|
| 246 |
+
<!-- Flux YOLO webcam (grand) -->
|
| 247 |
+
<img id="wcYoloImg" alt="YOLO stream">
|
| 248 |
+
<!-- HUD top-left -->
|
| 249 |
+
<div class="hud" id="hudEl">
|
| 250 |
+
<div class="hudf" id="hudFr">—</div>
|
| 251 |
+
<div id="hudCo">—</div>
|
| 252 |
+
</div>
|
| 253 |
+
<!-- Compteurs top-right -->
|
| 254 |
+
<div class="cov" id="cOverlay"></div>
|
| 255 |
+
<!-- Banner no-object -->
|
| 256 |
+
<div class="noobj" id="noObjBanner">
|
| 257 |
+
<i class="fas fa-eye-slash"></i> No object detected
|
| 258 |
+
</div>
|
| 259 |
+
<!-- Canvas caché pour enregistrement webcam -->
|
| 260 |
+
<canvas id="wcCanvas" style="display:none;"></canvas>
|
| 261 |
+
<!-- Empty state -->
|
| 262 |
+
<div class="es" id="emptyState">
|
| 263 |
+
<i class="fas fa-film"></i>
|
| 264 |
+
<p>Result will appear here</p>
|
| 265 |
+
</div>
|
| 266 |
+
</div>
|
| 267 |
+
</div>
|
| 268 |
+
|
| 269 |
+
<!-- Stats -->
|
| 270 |
+
<div class="sr">
|
| 271 |
+
<div class="sc"><div class="sl">Frames</div><div class="sv" id="stF">—</div></div>
|
| 272 |
+
<div class="sc"><div class="sl">Objects</div><div class="sv" id="stO">—</div></div>
|
| 273 |
+
<div class="sc"><div class="sl">FPS</div><div class="sv" id="stFps">—</div></div>
|
| 274 |
+
<div class="sc"><div class="sl">Duration</div><div class="sv" id="stD">—</div></div>
|
| 275 |
+
</div>
|
| 276 |
+
|
| 277 |
+
<!-- Download -->
|
| 278 |
+
<div class="dlb">
|
| 279 |
+
<span id="dlName">No result yet</span>
|
| 280 |
+
<button class="btndl" id="dlBtn" disabled onclick="doDownload()">
|
| 281 |
+
<i class="fas fa-download"></i> Download
|
| 282 |
+
</button>
|
| 283 |
+
</div>
|
| 284 |
+
|
| 285 |
+
</div>
|
| 286 |
+
</main>
|
| 287 |
+
</div>
|
| 288 |
+
|
| 289 |
+
<script>
|
| 290 |
+
/* ── Theme ── */
|
| 291 |
+
function toggleTheme(){
|
| 292 |
+
const d=document.documentElement,dk=d.getAttribute('data-theme')==='dark';
|
| 293 |
+
d.setAttribute('data-theme',dk?'light':'dark');
|
| 294 |
+
['ti','ti2'].forEach(x=>{const e=document.getElementById(x);if(e)e.className=dk?'fas fa-moon':'fas fa-sun';});
|
| 295 |
+
try{localStorage.setItem('tiq_theme',dk?'light':'dark');}catch(e){}
|
| 296 |
+
}
|
| 297 |
+
function setLang(l,btn){
|
| 298 |
+
document.querySelectorAll('.lb').forEach(b=>b.classList.remove('on'));btn.classList.add('on');
|
| 299 |
+
try{localStorage.setItem('tiq_lang',l);}catch(e){}
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
/* ── Classes ── */
|
| 303 |
+
const CLASSES=[
|
| 304 |
+
{name:'Vehicle',color:'#3b82f6'},{name:'Motorcycle',color:'#10b981'},
|
| 305 |
+
{name:'Truck',color:'#f59e0b'},{name:'Bus',color:'#8b5cf6'},
|
| 306 |
+
{name:'Human',color:'#ec4899'},{name:'Bicycle',color:'#06b6d4'},
|
| 307 |
+
{name:'Traffic light',color:'#ef4444'},{name:'Road sign',color:'#f97316'},
|
| 308 |
+
];
|
| 309 |
+
let sel=new Set(['Human']);
|
| 310 |
+
|
| 311 |
+
function buildCls(){
|
| 312 |
+
const g=document.getElementById('clsGrid'); g.innerHTML='';
|
| 313 |
+
CLASSES.forEach(c=>{
|
| 314 |
+
const d=document.createElement('div');
|
| 315 |
+
d.className='clsi'+(sel.has(c.name)?' on':'');
|
| 316 |
+
d.innerHTML=`<span class="cldot" style="background:${c.color}"></span>${c.name}`;
|
| 317 |
+
d.onclick=()=>{sel.has(c.name)?sel.delete(c.name):sel.add(c.name);d.classList.toggle('on');upCls();};
|
| 318 |
+
g.appendChild(d);
|
| 319 |
+
});
|
| 320 |
+
upCls();
|
| 321 |
+
}
|
| 322 |
+
function toggleAll(){sel.size===CLASSES.length?sel.clear():CLASSES.forEach(c=>sel.add(c.name));buildCls();}
|
| 323 |
+
function upCls(){document.getElementById('clsCnt').textContent=sel.size+' selected';}
|
| 324 |
+
buildCls();
|
| 325 |
+
|
| 326 |
+
/* ── State ── */
|
| 327 |
+
let curSrc='upload', vidFile=null, vidUrl=null, wcStream=null;
|
| 328 |
+
let _jobId=null, _sse=null, _wcPollIv=null, _annotatedUrl=null;
|
| 329 |
+
|
| 330 |
+
/* ── Source switch ── */
|
| 331 |
+
function switchSrc(s,btn){
|
| 332 |
+
curSrc=s;
|
| 333 |
+
document.querySelectorAll('.stab').forEach(b=>b.classList.remove('on'));btn.classList.add('on');
|
| 334 |
+
['upload','url','webcam'].forEach(id=>{
|
| 335 |
+
const el=document.getElementById('src-'+id);if(el)el.style.display='none';
|
| 336 |
+
});
|
| 337 |
+
document.getElementById('src-'+s).style.display='block';
|
| 338 |
+
checkReady();
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
/* ── File upload ── */
|
| 342 |
+
function onFile(e){
|
| 343 |
+
const f=e.target.files[0];if(!f)return;
|
| 344 |
+
vidFile=f;
|
| 345 |
+
const fn=document.getElementById('fname');fn.textContent=f.name;fn.style.display='block';
|
| 346 |
+
document.getElementById('uzone').classList.add('has');
|
| 347 |
+
checkReady();
|
| 348 |
+
}
|
| 349 |
+
function loadUrl(){
|
| 350 |
+
const u=document.getElementById('urlIn').value.trim();if(!u)return;
|
| 351 |
+
vidUrl=u;checkReady();
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
/* ── Run Analysis ── */
|
| 355 |
+
window.runAnalysis=async function(){
|
| 356 |
+
const btn=document.getElementById('runBtn');
|
| 357 |
+
btn.disabled=true;
|
| 358 |
+
btn.innerHTML='<i class="fas fa-spinner fa-spin"></i> Processing…';
|
| 359 |
+
document.getElementById('progWrap').style.display='block';
|
| 360 |
+
document.getElementById('progTxt').style.display='block';
|
| 361 |
+
document.getElementById('progTxt').textContent='Uploading…';
|
| 362 |
+
document.getElementById('progBar').style.width='5%';
|
| 363 |
+
document.getElementById('emptyState').style.display='none';
|
| 364 |
+
|
| 365 |
+
/* Upload si pas encore fait */
|
| 366 |
+
if(!_jobId){
|
| 367 |
+
if(!vidFile&&!vidUrl){
|
| 368 |
+
btn.disabled=false;btn.innerHTML='<i class="fas fa-play"></i> Run Analysis';return;
|
| 369 |
+
}
|
| 370 |
+
const fd=new FormData();
|
| 371 |
+
if(vidFile) fd.append('video',vidFile);
|
| 372 |
+
try{
|
| 373 |
+
const r=await fetch('/api/upload',{method:'POST',body:fd});
|
| 374 |
+
const d=await r.json();
|
| 375 |
+
_jobId=d.job_id;
|
| 376 |
+
}catch(err){
|
| 377 |
+
console.error('[TrackIQ] Upload:',err);
|
| 378 |
+
btn.disabled=false;btn.innerHTML='<i class="fas fa-play"></i> Run Analysis';return;
|
| 379 |
+
}
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
document.getElementById('progTxt').textContent='Starting detection…';
|
| 383 |
+
document.getElementById('progBar').style.width='10%';
|
| 384 |
+
|
| 385 |
+
try{
|
| 386 |
+
await fetch('/api/run',{method:'POST',headers:{'Content-Type':'application/json'},
|
| 387 |
+
body:JSON.stringify({job_id:_jobId,classes:[...sel],model:'yolo11n'})});
|
| 388 |
+
}catch(err){console.error('[TrackIQ] Run:',err);return;}
|
| 389 |
+
|
| 390 |
+
/* SSE */
|
| 391 |
+
if(_sse)_sse.close();
|
| 392 |
+
_sse=new EventSource(`/api/stream/${_jobId}`);
|
| 393 |
+
_sse.onmessage=(e)=>{
|
| 394 |
+
let d;try{d=JSON.parse(e.data);}catch{return;}
|
| 395 |
+
|
| 396 |
+
if(d.event==='done'){
|
| 397 |
+
_sse.close();
|
| 398 |
+
_showBanner(false);
|
| 399 |
+
document.getElementById('progBar').style.width='100%';
|
| 400 |
+
document.getElementById('progTxt').textContent='Done!';
|
| 401 |
+
btn.disabled=false;btn.innerHTML='<i class="fas fa-play"></i> Run Analysis';
|
| 402 |
+
|
| 403 |
+
/* ✅ Affiche la vidéo annotée */
|
| 404 |
+
const rv=document.getElementById('resultVid');
|
| 405 |
+
rv.src=`/api/video/${_jobId}`;
|
| 406 |
+
rv.style.display='block';
|
| 407 |
+
rv.load();
|
| 408 |
+
document.getElementById('srcLabel').textContent=vidFile?vidFile.name:'video';
|
| 409 |
+
document.getElementById('hudEl').style.display='block';
|
| 410 |
+
document.getElementById('cOverlay').style.display='flex';
|
| 411 |
+
_annotatedUrl=`/api/video/${_jobId}`;
|
| 412 |
+
|
| 413 |
+
/* Stats */
|
| 414 |
+
const s=d.stats||{};
|
| 415 |
+
_set('stF',s.total_frames??'—');_set('stO',s.total_unique??'—');
|
| 416 |
+
_set('stFps',s.fps?s.fps.toFixed(1):'—');_set('stD',s.duration_s?s.duration_s+'s':'—');
|
| 417 |
+
_showCounts(s.unique_counts||{});
|
| 418 |
+
_set('dlName',vidFile?vidFile.name:'annotated.mp4');
|
| 419 |
+
document.getElementById('dlBtn').removeAttribute('disabled');
|
| 420 |
+
|
| 421 |
+
/* Sauvegarde localStorage */
|
| 422 |
+
try{
|
| 423 |
+
localStorage.setItem('tiq_job_id',_jobId);
|
| 424 |
+
localStorage.setItem('tiq_video_url',`/api/video/${_jobId}`);
|
| 425 |
+
localStorage.setItem('tiq_video_name',s.video_name||'');
|
| 426 |
+
localStorage.setItem('tiq_stats',JSON.stringify(s));
|
| 427 |
+
}catch(ex){}
|
| 428 |
+
return;
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
/* Progression */
|
| 432 |
+
if(d.pct!=null){
|
| 433 |
+
document.getElementById('progBar').style.width=d.pct+'%';
|
| 434 |
+
document.getElementById('progTxt').textContent=`Processing… ${d.pct.toFixed(0)}%`;
|
| 435 |
+
}
|
| 436 |
+
_showBanner(d.no_objects===true);
|
| 437 |
+
if(d.hud){
|
| 438 |
+
_set('hudFr',d.hud.frame_str);
|
| 439 |
+
const hco=document.getElementById('hudCo');
|
| 440 |
+
if(hco) hco.innerHTML=Object.entries(d.hud.counts||{})
|
| 441 |
+
.map(([k,v])=>`${k} = ${v}`).join('<br>')||'No object';
|
| 442 |
+
}
|
| 443 |
+
if(d.unique_counts) _showCounts(d.unique_counts);
|
| 444 |
+
};
|
| 445 |
+
_sse.onerror=()=>_sse.close();
|
| 446 |
+
};
|
| 447 |
+
|
| 448 |
+
/* ── Webcam ── */
|
| 449 |
+
let _wcFrameIv=null, _wcHiddenVideo=null, _wcCanvas2=null;
|
| 450 |
+
|
| 451 |
+
async function startWc(){
|
| 452 |
+
try{
|
| 453 |
+
wcStream=await navigator.mediaDevices.getUserMedia({video:{width:640,height:480},audio:false});
|
| 454 |
+
}catch(e){alert('Webcam unavailable: '+e.message);return;}
|
| 455 |
+
|
| 456 |
+
document.getElementById('wcPh').style.display='none';
|
| 457 |
+
document.getElementById('recBadge').style.display='flex';
|
| 458 |
+
document.getElementById('runBtn').style.display='none';
|
| 459 |
+
|
| 460 |
+
const btn=document.getElementById('wcBtn');
|
| 461 |
+
btn.innerHTML='<i class="fas fa-stop" style="color:#f87171;font-size:9px;"></i> Stop webcam';
|
| 462 |
+
btn.onclick=stopWc;
|
| 463 |
+
|
| 464 |
+
/* Lance YOLO backend */
|
| 465 |
+
await fetch('/api/webcam/start',{method:'POST',headers:{'Content-Type':'application/json'},
|
| 466 |
+
body:JSON.stringify({classes:[...sel],model:'yolo11n'})}).catch(()=>{});
|
| 467 |
+
|
| 468 |
+
/* Setup hidden video + canvas pour capturer frames */
|
| 469 |
+
_wcHiddenVideo=document.createElement('video');
|
| 470 |
+
_wcHiddenVideo.srcObject=wcStream;
|
| 471 |
+
_wcHiddenVideo.setAttribute('playsinline','');
|
| 472 |
+
_wcHiddenVideo.muted=true;
|
| 473 |
+
_wcHiddenVideo.play();
|
| 474 |
+
_wcCanvas2=document.createElement('canvas');
|
| 475 |
+
_wcCanvas2.width=640;_wcCanvas2.height=480;
|
| 476 |
+
const ctx2=_wcCanvas2.getContext('2d');
|
| 477 |
+
|
| 478 |
+
/* Envoie une frame toutes les 200ms → reçoit JPEG annoté → l'affiche */
|
| 479 |
+
const miniImg=document.getElementById('wcMiniYolo');
|
| 480 |
+
const bigImg=document.getElementById('wcYoloImg');
|
| 481 |
+
miniImg.style.display='block';
|
| 482 |
+
bigImg.style.display='block';
|
| 483 |
+
|
| 484 |
+
_wcFrameIv=setInterval(async()=>{
|
| 485 |
+
if(!wcStream||!_wcHiddenVideo)return;
|
| 486 |
+
ctx2.drawImage(_wcHiddenVideo,0,0,640,480);
|
| 487 |
+
_wcCanvas2.toBlob(async(blob)=>{
|
| 488 |
+
if(!blob)return;
|
| 489 |
+
try{
|
| 490 |
+
const resp=await fetch('/api/webcam/frame',{method:'POST',
|
| 491 |
+
headers:{'Content-Type':'image/jpeg'},body:blob});
|
| 492 |
+
if(!resp.ok)return;
|
| 493 |
+
const url=URL.createObjectURL(await resp.blob());
|
| 494 |
+
miniImg.src=url;bigImg.src=url;
|
| 495 |
+
}catch(e){}
|
| 496 |
+
},'image/jpeg',0.75);
|
| 497 |
+
},200);
|
| 498 |
+
|
| 499 |
+
/* Grand panel droite */
|
| 500 |
+
document.getElementById('emptyState').style.display='none';
|
| 501 |
+
document.getElementById('resultVid').style.display='none';
|
| 502 |
+
document.getElementById('hudEl').style.display='block';
|
| 503 |
+
document.getElementById('cOverlay').style.display='flex';
|
| 504 |
+
document.getElementById('srcLabel').textContent='Webcam — LIVE';
|
| 505 |
+
|
| 506 |
+
/* Démarre l'enregistrement automatiquement */
|
| 507 |
+
setTimeout(_startRec, 800);
|
| 508 |
+
/* Polling stats toutes les 800ms */
|
| 509 |
+
clearInterval(_wcPollIv);
|
| 510 |
+
_wcPollIv=setInterval(_pollWc,800);
|
| 511 |
+
checkReady();
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
/* ── Recording webcam → grand Result ── */
|
| 515 |
+
let _recIv=null, _recChunks=[], _mediaRec=null;
|
| 516 |
+
|
| 517 |
+
function _startRec(){
|
| 518 |
+
const canvas=document.getElementById('wcCanvas');
|
| 519 |
+
const img=document.getElementById('wcYoloImg');
|
| 520 |
+
if(!canvas||!img)return;
|
| 521 |
+
canvas.width=640;canvas.height=480;
|
| 522 |
+
const ctx=canvas.getContext('2d');
|
| 523 |
+
_recIv=setInterval(()=>{
|
| 524 |
+
if(img.naturalWidth>0){canvas.width=img.naturalWidth;canvas.height=img.naturalHeight;ctx.drawImage(img,0,0);}
|
| 525 |
+
},40);
|
| 526 |
+
const mime=MediaRecorder.isTypeSupported('video/webm;codecs=vp9')?'video/webm;codecs=vp9':'video/webm';
|
| 527 |
+
_mediaRec=new MediaRecorder(canvas.captureStream(25),{mimeType:mime,videoBitsPerSecond:2000000});
|
| 528 |
+
_recChunks=[];
|
| 529 |
+
_mediaRec.ondataavailable=e=>{if(e.data.size>0)_recChunks.push(e.data);};
|
| 530 |
+
_mediaRec.onstop=_saveRec;
|
| 531 |
+
_mediaRec.start(500);
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
function _saveRec(){
|
| 535 |
+
clearInterval(_recIv);_recIv=null;
|
| 536 |
+
if(!_recChunks.length)return;
|
| 537 |
+
const blob=new Blob(_recChunks,{type:'video/webm'});
|
| 538 |
+
const url=URL.createObjectURL(blob);
|
| 539 |
+
_annotatedUrl=url;
|
| 540 |
+
/* Affiche dans le grand Result */
|
| 541 |
+
const rv=document.getElementById('resultVid');
|
| 542 |
+
rv.src=url;rv.style.display='block';rv.controls=true;
|
| 543 |
+
rv.onloadeddata=()=>rv.play().catch(()=>{});rv.load();
|
| 544 |
+
document.getElementById('srcLabel').textContent='Webcam — recorded';
|
| 545 |
+
document.getElementById('emptyState').style.display='none';
|
| 546 |
+
document.getElementById('hudEl').style.display='block';
|
| 547 |
+
/* Active download */
|
| 548 |
+
_set('dlName','webcam_trackiq.webm');
|
| 549 |
+
document.getElementById('dlBtn').removeAttribute('disabled');
|
| 550 |
+
document.getElementById('dlBtn').onclick=()=>{
|
| 551 |
+
const a=document.createElement('a');a.href=url;a.download='webcam_trackiq.webm';a.click();
|
| 552 |
+
};
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
function stopWc(){
|
| 556 |
+
/* 1. Arrête enregistrement → déclenche _saveRec → vidéo dans Result */
|
| 557 |
+
if(_mediaRec&&_mediaRec.state==='recording') _mediaRec.stop();
|
| 558 |
+
clearInterval(_wcFrameIv);_wcFrameIv=null;
|
| 559 |
+
if(_wcHiddenVideo){_wcHiddenVideo.srcObject=null;_wcHiddenVideo=null;}
|
| 560 |
+
_wcCanvas2=null;
|
| 561 |
+
if(wcStream){wcStream.getTracks().forEach(t=>t.stop());wcStream=null;}
|
| 562 |
+
fetch('/api/webcam/stop',{method:'POST'}).catch(()=>{});
|
| 563 |
+
clearInterval(_wcPollIv);_wcPollIv=null;
|
| 564 |
+
|
| 565 |
+
/* 2. Cache flux live */
|
| 566 |
+
const mi=document.getElementById('wcMiniYolo');mi.src='';mi.style.display='none';
|
| 567 |
+
const bi=document.getElementById('wcYoloImg');bi.src='';bi.style.display='none';
|
| 568 |
+
|
| 569 |
+
document.getElementById('wcPh').style.display='flex';
|
| 570 |
+
document.getElementById('recBadge').style.display='none';
|
| 571 |
+
document.getElementById('runBtn').style.display='flex';
|
| 572 |
+
document.getElementById('cOverlay').style.display='none';
|
| 573 |
+
document.getElementById('noObjBanner').style.display='none';
|
| 574 |
+
|
| 575 |
+
const btn=document.getElementById('wcBtn');
|
| 576 |
+
btn.innerHTML='<i class="fas fa-circle" style="color:#f87171;font-size:8px;"></i> Start webcam';
|
| 577 |
+
btn.onclick=startWc;
|
| 578 |
+
checkReady();
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
async function _pollWc(){
|
| 582 |
+
try{
|
| 583 |
+
const r=await fetch('/api/webcam/stats');
|
| 584 |
+
if(!r.ok)return;
|
| 585 |
+
const d=await r.json();
|
| 586 |
+
const fc=d.frame_counts||{};
|
| 587 |
+
const uc=d.unique_counts||{};
|
| 588 |
+
_set('hudFr',`F:${d.frame||0} ▶ LIVE`);
|
| 589 |
+
const hco=document.getElementById('hudCo');
|
| 590 |
+
if(hco) hco.innerHTML=Object.entries(fc).map(([k,v])=>`${k} = ${v}`).join('<br>')||
|
| 591 |
+
'<span style="color:#ef4444">No object</span>';
|
| 592 |
+
_showCountsWc(fc,uc);
|
| 593 |
+
_showBanner(!Object.keys(fc).length);
|
| 594 |
+
}catch(e){}
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
/* ── Affichage compteurs ── */
|
| 598 |
+
function _showCounts(uc){
|
| 599 |
+
const ov=document.getElementById('cOverlay');
|
| 600 |
+
ov.innerHTML='';
|
| 601 |
+
if(!Object.keys(uc).length){
|
| 602 |
+
_showBanner(true);return;
|
| 603 |
+
}
|
| 604 |
+
Object.entries(uc).forEach(([cls,cnt])=>{
|
| 605 |
+
const cl=CLASSES.find(c=>c.name===cls);
|
| 606 |
+
const color=cl?cl.color:'#a78bfa';
|
| 607 |
+
const div=document.createElement('div');div.className='cbdg';
|
| 608 |
+
div.style.borderLeftColor=color;
|
| 609 |
+
div.innerHTML=`<span class="cn" style="color:${color}">${cls} = ${cnt}</span>`;
|
| 610 |
+
ov.appendChild(div);
|
| 611 |
+
});
|
| 612 |
+
_showBanner(false);
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
function _showCountsWc(fc,uc){
|
| 616 |
+
const ov=document.getElementById('cOverlay');
|
| 617 |
+
ov.innerHTML='';
|
| 618 |
+
const all=new Set([...Object.keys(fc),...Object.keys(uc)]);
|
| 619 |
+
if(!all.size){_showBanner(true);return;}
|
| 620 |
+
all.forEach(cls=>{
|
| 621 |
+
const cl=CLASSES.find(c=>c.name===cls);
|
| 622 |
+
const color=cl?cl.color:'#a78bfa';
|
| 623 |
+
const div=document.createElement('div');div.className='cbdg';
|
| 624 |
+
div.style.borderLeftColor=color;
|
| 625 |
+
div.innerHTML=`<span class="cn" style="color:${color}">${cls} = ${fc[cls]||0}</span>
|
| 626 |
+
<span class="cu">Total unique : ${uc[cls]||0}</span>`;
|
| 627 |
+
ov.appendChild(div);
|
| 628 |
+
});
|
| 629 |
+
_showBanner(false);
|
| 630 |
+
}
|
| 631 |
+
|
| 632 |
+
function _showBanner(show){
|
| 633 |
+
const b=document.getElementById('noObjBanner');
|
| 634 |
+
b.style.display=show?'flex':'none';
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
/* ── Helpers ── */
|
| 638 |
+
function _set(id,val){const el=document.getElementById(id);if(el)el.textContent=val;}
|
| 639 |
+
function checkReady(){
|
| 640 |
+
const ok=(curSrc==='upload'&&vidFile)||(curSrc==='url'&&vidUrl)||(curSrc==='webcam'&&wcStream);
|
| 641 |
+
document.getElementById('runBtn').disabled=!ok;
|
| 642 |
+
}
|
| 643 |
+
function doDownload(){
|
| 644 |
+
const url=_annotatedUrl;if(!url)return;
|
| 645 |
+
const a=document.createElement('a');a.href=url;a.download='trackiq_annotated.mp4';a.click();
|
| 646 |
+
}
|
| 647 |
+
function resetAll(){
|
| 648 |
+
if(wcStream)stopWc();
|
| 649 |
+
if(_sse)_sse.close();
|
| 650 |
+
_jobId=null;_annotatedUrl=null;vidFile=null;vidUrl=null;
|
| 651 |
+
const rv=document.getElementById('resultVid');rv.src='';rv.style.display='none';
|
| 652 |
+
document.getElementById('fname').style.display='none';
|
| 653 |
+
document.getElementById('fname').textContent='';
|
| 654 |
+
document.getElementById('uzone').classList.remove('has');
|
| 655 |
+
document.getElementById('urlIn').value='';
|
| 656 |
+
document.getElementById('progWrap').style.display='none';
|
| 657 |
+
document.getElementById('progBar').style.width='0%';
|
| 658 |
+
document.getElementById('progTxt').style.display='none';
|
| 659 |
+
document.getElementById('emptyState').style.display='flex';
|
| 660 |
+
document.getElementById('cOverlay').style.display='none';
|
| 661 |
+
document.getElementById('hudEl').style.display='none';
|
| 662 |
+
document.getElementById('noObjBanner').style.display='none';
|
| 663 |
+
document.getElementById('dlBtn').disabled=true;
|
| 664 |
+
document.getElementById('dlName').textContent='No result yet';
|
| 665 |
+
document.getElementById('srcLabel').textContent='';
|
| 666 |
+
['stF','stO','stFps','stD'].forEach(id=>_set(id,'—'));
|
| 667 |
+
const fi=document.getElementById('fileInput');if(fi)fi.value='';
|
| 668 |
+
checkReady();
|
| 669 |
+
}
|
| 670 |
+
|
| 671 |
+
/* ── Init ── */
|
| 672 |
+
(function(){
|
| 673 |
+
try{
|
| 674 |
+
const th=localStorage.getItem('tiq_theme');
|
| 675 |
+
if(th){document.documentElement.setAttribute('data-theme',th);
|
| 676 |
+
if(th==='dark'){['ti','ti2'].forEach(x=>{const e=document.getElementById(x);if(e)e.className='fas fa-sun';});}}
|
| 677 |
+
const lg=localStorage.getItem('tiq_lang');
|
| 678 |
+
if(lg&&lg!=='en'){const btn=document.querySelector('.lb[onclick*="\''+lg+'\'"]');if(btn)setLang(lg,btn);}
|
| 679 |
+
}catch(e){}
|
| 680 |
+
})();
|
| 681 |
+
</script>
|
| 682 |
+
<script src="/static/trackiq-api.js"></script>
|
| 683 |
+
</body>
|
| 684 |
+
</html>
|
templates/index.html
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" data-theme="light">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>TrackIQ — Urban Traffic Detection</title>
|
| 7 |
+
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,300&family=DM+Mono:wght@400&display=swap" rel="stylesheet">
|
| 8 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
| 9 |
+
<style>
|
| 10 |
+
:root {
|
| 11 |
+
--ac: #880000;
|
| 12 |
+
--ac2: #660000;
|
| 13 |
+
--bg: #f9fafb;
|
| 14 |
+
--s: #fff;
|
| 15 |
+
--bd: rgba(0,0,0,0.07);
|
| 16 |
+
--t: #111827;
|
| 17 |
+
--t2: #6b7280;
|
| 18 |
+
--t3: #d1d5db;
|
| 19 |
+
--sh: 0 4px 24px rgba(0,0,0,0.09);
|
| 20 |
+
--shlg: 0 20px 25px -5px rgba(0,0,0,0.10), 0 10px 10px -5px rgba(0,0,0,0.04);
|
| 21 |
+
--r: 16px;
|
| 22 |
+
}
|
| 23 |
+
[data-theme="dark"] {
|
| 24 |
+
--bg: #0c1117;
|
| 25 |
+
--s: #151c25;
|
| 26 |
+
--t: #e8edf2;
|
| 27 |
+
--t2: #7a8fa0;
|
| 28 |
+
--t3: #3a4a5a;
|
| 29 |
+
--bd: rgba(255,255,255,0.07);
|
| 30 |
+
--sh: 0 4px 24px rgba(0,0,0,0.5);
|
| 31 |
+
--shlg: 0 20px 40px rgba(0,0,0,0.5);
|
| 32 |
+
}
|
| 33 |
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
| 34 |
+
html, body { min-height: 100%; }
|
| 35 |
+
body {
|
| 36 |
+
font-family: "DM Sans", sans-serif;
|
| 37 |
+
background: var(--bg);
|
| 38 |
+
color: var(--t);
|
| 39 |
+
transition: background 0.3s, color 0.3s;
|
| 40 |
+
}
|
| 41 |
+
::-webkit-scrollbar { width: 6px; }
|
| 42 |
+
::-webkit-scrollbar-track { background: transparent; }
|
| 43 |
+
::-webkit-scrollbar-thumb { background: var(--t3); border-radius: 3px; }
|
| 44 |
+
|
| 45 |
+
/* ── FLOATING NAV ── */
|
| 46 |
+
.floatnav {
|
| 47 |
+
position: absolute;
|
| 48 |
+
top: 0; left: 0; right: 0;
|
| 49 |
+
z-index: 50;
|
| 50 |
+
pointer-events: none;
|
| 51 |
+
}
|
| 52 |
+
.floatnav-inner {
|
| 53 |
+
max-width: 1280px;
|
| 54 |
+
margin: 0 auto;
|
| 55 |
+
padding: 16px 24px;
|
| 56 |
+
display: flex;
|
| 57 |
+
justify-content: space-between;
|
| 58 |
+
align-items: flex-start;
|
| 59 |
+
}
|
| 60 |
+
.nav-brand {
|
| 61 |
+
pointer-events: auto;
|
| 62 |
+
background: rgba(255,255,255,0.92);
|
| 63 |
+
backdrop-filter: blur(12px);
|
| 64 |
+
border-radius: 10px;
|
| 65 |
+
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
|
| 66 |
+
padding: 8px 16px;
|
| 67 |
+
display: flex;
|
| 68 |
+
align-items: center;
|
| 69 |
+
gap: 8px;
|
| 70 |
+
text-decoration: none;
|
| 71 |
+
transition: all 0.2s;
|
| 72 |
+
}
|
| 73 |
+
.nav-brand:hover { background: #fff; box-shadow: 0 8px 24px rgba(0,0,0,0.16); }
|
| 74 |
+
.nav-brand-icon {
|
| 75 |
+
width: 28px; height: 28px;
|
| 76 |
+
background: var(--ac);
|
| 77 |
+
border-radius: 7px;
|
| 78 |
+
display: flex; align-items: center; justify-content: center;
|
| 79 |
+
color: #fff; font-size: 12px;
|
| 80 |
+
}
|
| 81 |
+
.nav-brand-name {
|
| 82 |
+
font-size: 14px; font-weight: 600;
|
| 83 |
+
color: #111827; letter-spacing: -0.3px;
|
| 84 |
+
}
|
| 85 |
+
.nav-brand-name b { color: var(--ac); }
|
| 86 |
+
.nav-right {
|
| 87 |
+
pointer-events: auto;
|
| 88 |
+
display: flex; gap: 8px; align-items: center;
|
| 89 |
+
}
|
| 90 |
+
.nav-lang {
|
| 91 |
+
background: #fff;
|
| 92 |
+
border-radius: 10px;
|
| 93 |
+
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
|
| 94 |
+
padding: 4px;
|
| 95 |
+
display: flex; gap: 4px;
|
| 96 |
+
}
|
| 97 |
+
.lb {
|
| 98 |
+
font-size: 12px; font-weight: 500;
|
| 99 |
+
padding: 4px 10px;
|
| 100 |
+
border: none; background: none;
|
| 101 |
+
cursor: pointer; border-radius: 6px;
|
| 102 |
+
color: #6b7280;
|
| 103 |
+
font-family: "DM Sans", sans-serif;
|
| 104 |
+
transition: all 0.2s;
|
| 105 |
+
}
|
| 106 |
+
.lb.on { background: var(--ac); color: #fff; }
|
| 107 |
+
.lb:not(.on):hover { background: #f3f4f6; color: #111827; }
|
| 108 |
+
.nav-theme {
|
| 109 |
+
background: #fff;
|
| 110 |
+
border-radius: 10px;
|
| 111 |
+
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
|
| 112 |
+
width: 38px; height: 38px;
|
| 113 |
+
border: none; cursor: pointer;
|
| 114 |
+
display: flex; align-items: center; justify-content: center;
|
| 115 |
+
color: #6b7280; font-size: 13px;
|
| 116 |
+
transition: all 0.2s;
|
| 117 |
+
}
|
| 118 |
+
.nav-theme:hover { color: var(--ac); }
|
| 119 |
+
|
| 120 |
+
/* ── HERO ── */
|
| 121 |
+
.hero {
|
| 122 |
+
background-color: var(--ac);
|
| 123 |
+
background-image: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23991111' fill-opacity='0.4'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
| 124 |
+
color: #fff;
|
| 125 |
+
padding-top: 90px;
|
| 126 |
+
padding-bottom: 90px;
|
| 127 |
+
padding-left: 24px;
|
| 128 |
+
padding-right: 24px;
|
| 129 |
+
text-align: center;
|
| 130 |
+
position: relative;
|
| 131 |
+
overflow: hidden;
|
| 132 |
+
}
|
| 133 |
+
.hero-inner {
|
| 134 |
+
max-width: 860px;
|
| 135 |
+
margin: 0 auto;
|
| 136 |
+
position: relative;
|
| 137 |
+
z-index: 10;
|
| 138 |
+
animation: fadeUp 0.6s ease both;
|
| 139 |
+
}
|
| 140 |
+
@keyframes fadeUp {
|
| 141 |
+
from { opacity: 0; transform: translateY(24px); }
|
| 142 |
+
to { opacity: 1; transform: translateY(0); }
|
| 143 |
+
}
|
| 144 |
+
.hero-logo-wrap {
|
| 145 |
+
display: flex;
|
| 146 |
+
justify-content: center;
|
| 147 |
+
margin-bottom: 16px;
|
| 148 |
+
}
|
| 149 |
+
.hero-logo-box {
|
| 150 |
+
background: #fff;
|
| 151 |
+
padding: 14px;
|
| 152 |
+
border-radius: 20px;
|
| 153 |
+
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
|
| 154 |
+
display: flex; align-items: center; justify-content: center;
|
| 155 |
+
width: 80px; height: 80px;
|
| 156 |
+
}
|
| 157 |
+
.hero-logo-box i {
|
| 158 |
+
font-size: 36px;
|
| 159 |
+
color: var(--ac);
|
| 160 |
+
}
|
| 161 |
+
.hero h1 {
|
| 162 |
+
font-size: clamp(32px, 5vw, 60px);
|
| 163 |
+
font-weight: 800;
|
| 164 |
+
letter-spacing: -2px;
|
| 165 |
+
line-height: 1.1;
|
| 166 |
+
margin-bottom: 14px;
|
| 167 |
+
}
|
| 168 |
+
.hero h1 .dim { opacity: 0.75; font-weight: 300; font-style: italic; }
|
| 169 |
+
.hero-sub {
|
| 170 |
+
font-size: clamp(14px, 1.8vw, 18px);
|
| 171 |
+
color: rgba(255,255,255,0.88);
|
| 172 |
+
font-weight: 400;
|
| 173 |
+
line-height: 1.65;
|
| 174 |
+
margin-bottom: 24px;
|
| 175 |
+
max-width: 560px;
|
| 176 |
+
margin-left: auto;
|
| 177 |
+
margin-right: auto;
|
| 178 |
+
}
|
| 179 |
+
.hero-cta {
|
| 180 |
+
display: inline-flex; align-items: center; gap: 10px;
|
| 181 |
+
background: #fff;
|
| 182 |
+
color: var(--ac);
|
| 183 |
+
font-weight: 700; font-size: 16px;
|
| 184 |
+
padding: 14px 32px;
|
| 185 |
+
border-radius: 50px;
|
| 186 |
+
text-decoration: none;
|
| 187 |
+
box-shadow: 0 8px 24px rgba(0,0,0,0.18);
|
| 188 |
+
transition: all 0.3s;
|
| 189 |
+
}
|
| 190 |
+
.hero-cta:hover {
|
| 191 |
+
background: #f3f4f6;
|
| 192 |
+
transform: scale(1.05);
|
| 193 |
+
box-shadow: 0 16px 40px rgba(0,0,0,0.22);
|
| 194 |
+
}
|
| 195 |
+
/* Wave bottom */
|
| 196 |
+
.hero-wave {
|
| 197 |
+
position: absolute;
|
| 198 |
+
bottom: 0; left: 0; right: 0;
|
| 199 |
+
pointer-events: none;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
/* ── CARDS GRID ── */
|
| 203 |
+
.cards-section {
|
| 204 |
+
max-width: 1280px;
|
| 205 |
+
margin: 0 auto;
|
| 206 |
+
padding: 0 24px 64px;
|
| 207 |
+
margin-top: -64px;
|
| 208 |
+
position: relative;
|
| 209 |
+
z-index: 20;
|
| 210 |
+
}
|
| 211 |
+
.cards-grid {
|
| 212 |
+
display: grid;
|
| 213 |
+
grid-template-columns: repeat(1, 1fr);
|
| 214 |
+
gap: 24px;
|
| 215 |
+
}
|
| 216 |
+
@media (min-width: 640px) { .cards-grid { grid-template-columns: repeat(2, 1fr); } }
|
| 217 |
+
@media (min-width: 1024px) { .cards-grid { grid-template-columns: repeat(4, 1fr); } }
|
| 218 |
+
|
| 219 |
+
.card {
|
| 220 |
+
background: var(--s);
|
| 221 |
+
border-radius: 20px;
|
| 222 |
+
padding: 32px 24px;
|
| 223 |
+
box-shadow: var(--shlg);
|
| 224 |
+
border: 1px solid rgba(0,0,0,0.05);
|
| 225 |
+
text-decoration: none;
|
| 226 |
+
display: block;
|
| 227 |
+
transition: all 0.3s ease;
|
| 228 |
+
animation: fadeUp 0.6s ease both;
|
| 229 |
+
}
|
| 230 |
+
.card:nth-child(1) { animation-delay: 0.05s; }
|
| 231 |
+
.card:nth-child(2) { animation-delay: 0.12s; }
|
| 232 |
+
.card:nth-child(3) { animation-delay: 0.18s; }
|
| 233 |
+
.card:nth-child(4) { animation-delay: 0.24s; }
|
| 234 |
+
.card:hover {
|
| 235 |
+
transform: translateY(-8px);
|
| 236 |
+
box-shadow: 0 20px 25px -5px rgba(0,0,0,0.13), 0 10px 10px -5px rgba(0,0,0,0.05);
|
| 237 |
+
border-color: var(--ac);
|
| 238 |
+
}
|
| 239 |
+
.card-icon-wrap {
|
| 240 |
+
width: 80px; height: 80px;
|
| 241 |
+
background: #fff1f1;
|
| 242 |
+
color: var(--ac);
|
| 243 |
+
border-radius: 50%;
|
| 244 |
+
display: flex; align-items: center; justify-content: center;
|
| 245 |
+
font-size: 28px;
|
| 246 |
+
margin: 0 auto 24px;
|
| 247 |
+
transition: all 0.3s;
|
| 248 |
+
box-shadow: 0 2px 8px rgba(136,0,0,0.08);
|
| 249 |
+
}
|
| 250 |
+
[data-theme="dark"] .card-icon-wrap {
|
| 251 |
+
background: rgba(136,0,0,0.18);
|
| 252 |
+
}
|
| 253 |
+
.card:hover .card-icon-wrap {
|
| 254 |
+
background: var(--ac);
|
| 255 |
+
color: #fff;
|
| 256 |
+
}
|
| 257 |
+
.card-title {
|
| 258 |
+
text-align: center;
|
| 259 |
+
font-size: 18px; font-weight: 700;
|
| 260 |
+
color: var(--t);
|
| 261 |
+
margin-bottom: 10px;
|
| 262 |
+
transition: color 0.2s;
|
| 263 |
+
}
|
| 264 |
+
.card:hover .card-title { color: var(--ac); }
|
| 265 |
+
.card-desc {
|
| 266 |
+
text-align: center;
|
| 267 |
+
font-size: 13px;
|
| 268 |
+
color: var(--t2);
|
| 269 |
+
line-height: 1.65;
|
| 270 |
+
}
|
| 271 |
+
.card-launch {
|
| 272 |
+
margin-top: 24px;
|
| 273 |
+
display: flex; justify-content: center;
|
| 274 |
+
}
|
| 275 |
+
.card-launch span {
|
| 276 |
+
font-size: 13px; font-weight: 600;
|
| 277 |
+
color: var(--ac);
|
| 278 |
+
display: flex; align-items: center; gap: 6px;
|
| 279 |
+
opacity: 0;
|
| 280 |
+
transition: opacity 0.2s;
|
| 281 |
+
}
|
| 282 |
+
.card:hover .card-launch span { opacity: 1; }
|
| 283 |
+
|
| 284 |
+
/* ── FOOTER ── */
|
| 285 |
+
footer {
|
| 286 |
+
background: var(--s);
|
| 287 |
+
border-top: 1px solid var(--bd);
|
| 288 |
+
padding: 28px 24px;
|
| 289 |
+
text-align: center;
|
| 290 |
+
transition: background 0.3s;
|
| 291 |
+
}
|
| 292 |
+
footer p {
|
| 293 |
+
font-size: 13px;
|
| 294 |
+
color: var(--t2);
|
| 295 |
+
line-height: 1.7;
|
| 296 |
+
}
|
| 297 |
+
footer p span {
|
| 298 |
+
font-size: 11px;
|
| 299 |
+
opacity: 0.6;
|
| 300 |
+
display: block;
|
| 301 |
+
margin-top: 4px;
|
| 302 |
+
}
|
| 303 |
+
</style>
|
| 304 |
+
</head>
|
| 305 |
+
<body>
|
| 306 |
+
|
| 307 |
+
<!-- FLOATING NAV -->
|
| 308 |
+
<div class="floatnav">
|
| 309 |
+
<div class="floatnav-inner">
|
| 310 |
+
<a href="#" class="nav-brand">
|
| 311 |
+
<div class="nav-brand-icon"><i class="fas fa-satellite-dish"></i></div>
|
| 312 |
+
<span class="nav-brand-name">Track<b>IQ</b></span>
|
| 313 |
+
</a>
|
| 314 |
+
<div class="nav-right">
|
| 315 |
+
<div class="nav-lang">
|
| 316 |
+
<button class="lb on" onclick="setLang('en',this)">EN</button>
|
| 317 |
+
<button class="lb" onclick="setLang('fr',this)">FR</button>
|
| 318 |
+
<button class="lb" onclick="setLang('wo',this)">WO</button>
|
| 319 |
+
</div>
|
| 320 |
+
<button class="nav-theme" onclick="toggleTheme()"><i class="fas fa-moon" id="ti"></i></button>
|
| 321 |
+
</div>
|
| 322 |
+
</div>
|
| 323 |
+
</div>
|
| 324 |
+
|
| 325 |
+
<!-- HERO -->
|
| 326 |
+
<div class="hero">
|
| 327 |
+
<div class="hero-inner">
|
| 328 |
+
<div class="hero-logo-wrap">
|
| 329 |
+
<div class="hero-logo-box">
|
| 330 |
+
<i class="fas fa-satellite-dish"></i>
|
| 331 |
+
</div>
|
| 332 |
+
</div>
|
| 333 |
+
<h1 id="t-h1">
|
| 334 |
+
TrackIQ Urban Traffic<br>
|
| 335 |
+
<span class="dim">Surveillance & Detection</span>
|
| 336 |
+
</h1>
|
| 337 |
+
<p class="hero-sub" id="t-sub">
|
| 338 |
+
Based on Computer Vision — real-time vehicle detection and classification, speed estimation, congestion analysis and traffic sign identification.
|
| 339 |
+
</p>
|
| 340 |
+
<a href="/home" class="hero-cta" id="t-launch">
|
| 341 |
+
<i class="fas fa-play"></i>
|
| 342 |
+
<span>Launch</span>
|
| 343 |
+
</a>
|
| 344 |
+
</div>
|
| 345 |
+
<div class="hero-wave">
|
| 346 |
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 120" style="display:block;width:100%;">
|
| 347 |
+
<path fill="var(--bg)" fill-opacity="1"
|
| 348 |
+
d="M0,64L80,69.3C160,75,320,85,480,80C640,75,800,53,960,48C1120,43,1280,53,1360,58.7L1440,64L1440,120L1360,120C1280,120,1120,120,960,120C800,120,640,120,480,120C320,120,160,120,80,120L0,120Z"/>
|
| 349 |
+
</svg>
|
| 350 |
+
</div>
|
| 351 |
+
</div>
|
| 352 |
+
|
| 353 |
+
<!-- CARDS -->
|
| 354 |
+
<div class="cards-section">
|
| 355 |
+
<div class="cards-grid">
|
| 356 |
+
|
| 357 |
+
<!-- Card 1 -->
|
| 358 |
+
<a href="/home" class="card">
|
| 359 |
+
<div class="card-icon-wrap"><i class="fas fa-car-side"></i></div>
|
| 360 |
+
<div class="card-title" id="t-c1t">Vehicle Detection</div>
|
| 361 |
+
<p class="card-desc" id="t-c1d">Real-time detection and classification of vehicles: cars, trucks, motorcycles, buses and more.</p>
|
| 362 |
+
<div class="card-launch"><span id="t-launch2">Launch <i class="fas fa-arrow-right"></i></span></div>
|
| 363 |
+
</a>
|
| 364 |
+
|
| 365 |
+
<!-- Card 2 -->
|
| 366 |
+
<a href="/home" class="card">
|
| 367 |
+
<div class="card-icon-wrap"><i class="fas fa-tachometer-alt"></i></div>
|
| 368 |
+
<div class="card-title" id="t-c2t">Speed Estimation</div>
|
| 369 |
+
<p class="card-desc" id="t-c2d">Automatic estimation of vehicle speed from video stream without additional sensors.</p>
|
| 370 |
+
<div class="card-launch"><span id="t-launch3">Launch <i class="fas fa-arrow-right"></i></span></div>
|
| 371 |
+
</a>
|
| 372 |
+
|
| 373 |
+
<!-- Card 3 -->
|
| 374 |
+
<a href="/home" class="card">
|
| 375 |
+
<div class="card-icon-wrap"><i class="fas fa-traffic-light"></i></div>
|
| 376 |
+
<div class="card-title" id="t-c3t">Signs & Lights</div>
|
| 377 |
+
<p class="card-desc" id="t-c3d">Identification of traffic signs and lights, red light detection, stop signs and more.</p>
|
| 378 |
+
<div class="card-launch"><span id="t-launch4">Launch <i class="fas fa-arrow-right"></i></span></div>
|
| 379 |
+
</a>
|
| 380 |
+
|
| 381 |
+
<!-- Card 4 -->
|
| 382 |
+
<a href="/home" class="card">
|
| 383 |
+
<div class="card-icon-wrap"><i class="fas fa-chart-bar"></i></div>
|
| 384 |
+
<div class="card-title" id="t-c4t">Flow & Congestion</div>
|
| 385 |
+
<p class="card-desc" id="t-c4d">Congestion analysis, flow measurement and annotated video export for reporting.</p>
|
| 386 |
+
<div class="card-launch"><span id="t-launch5">Launch <i class="fas fa-arrow-right"></i></span></div>
|
| 387 |
+
</a>
|
| 388 |
+
|
| 389 |
+
</div>
|
| 390 |
+
</div>
|
| 391 |
+
|
| 392 |
+
<!-- FOOTER -->
|
| 393 |
+
<footer>
|
| 394 |
+
<p>
|
| 395 |
+
© 2026 <strong>TrackIQ</strong> — Computer Vision Project. All rights reserved.
|
| 396 |
+
<span id="t-foot">Academic Project — Version 1.0</span>
|
| 397 |
+
</p>
|
| 398 |
+
</footer>
|
| 399 |
+
|
| 400 |
+
<script>
|
| 401 |
+
const T = {
|
| 402 |
+
en: {
|
| 403 |
+
h1: 'TrackIQ Urban Traffic<br><span class="dim">Surveillance & Detection</span>',
|
| 404 |
+
sub: "Based on Computer Vision — real-time vehicle detection and classification, speed estimation, congestion analysis and traffic sign identification.",
|
| 405 |
+
launch: '<i class="fas fa-play"></i><span>Launch</span>',
|
| 406 |
+
c1t: "Vehicle Detection", c1d: "Real-time detection and classification of vehicles: cars, trucks, motorcycles, buses and more.",
|
| 407 |
+
c2t: "Speed Estimation", c2d: "Automatic estimation of vehicle speed from video stream without additional sensors.",
|
| 408 |
+
c3t: "Signs & Lights", c3d: "Identification of traffic signs and lights, red light detection, stop signs and more.",
|
| 409 |
+
c4t: "Flow & Congestion", c4d: "Congestion analysis, flow measurement and annotated video export for reporting.",
|
| 410 |
+
launchbtn: 'Launch <i class="fas fa-arrow-right"></i>',
|
| 411 |
+
foot: "Academic Project — Version 1.0"
|
| 412 |
+
},
|
| 413 |
+
fr: {
|
| 414 |
+
h1: 'TrackIQ Trafic Urbain<br><span class="dim">Surveillance & Détection</span>',
|
| 415 |
+
sub: "Basé sur Computer Vision — détection et classification en temps réel des véhicules, estimation de vitesse, analyse de congestion et identification des feux et panneaux.",
|
| 416 |
+
launch: '<i class="fas fa-play"></i><span>Lancer</span>',
|
| 417 |
+
c1t: "Détection Véhicules", c1d: "Détection et classification en temps réel des véhicules : voitures, camions, motos, bus et plus.",
|
| 418 |
+
c2t: "Estimation Vitesse", c2d: "Estimation automatique de la vitesse des véhicules depuis un flux vidéo, sans capteur additionnel.",
|
| 419 |
+
c3t: "Feux & Panneaux", c3d: "Identification des panneaux et feux de signalisation, détection des feux rouges, stops et plus.",
|
| 420 |
+
c4t: "Flux & Congestion",c4d: "Analyse de congestion, mesure de flux et export vidéo annoté pour rapport.",
|
| 421 |
+
launchbtn: 'Lancer <i class="fas fa-arrow-right"></i>',
|
| 422 |
+
foot: "Projet Académique — Version 1.0"
|
| 423 |
+
},
|
| 424 |
+
wo: {
|
| 425 |
+
h1: 'TrackIQ Trafik Kanam<br><span class="dim">Xam ak Réy</span>',
|
| 426 |
+
sub: "Moom Computer Vision la tax — léegi-léegi di réy wërsëg yi, ak xam njariñ yoom yi, ci kanam.",
|
| 427 |
+
launch: '<i class="fas fa-play"></i><span>Tambali</span>',
|
| 428 |
+
c1t: "Réy Wërsëg", c1d: "Réy ak xam njariñ wërsëg yi ci kanam: yëgël, camion, moto, bus ak yeneen.",
|
| 429 |
+
c2t: "Yóbbu Yoom", c2d: "Xam yóbbu yoom wërsëg yi ci video, benn jëriñ mujj dafa soxor.",
|
| 430 |
+
c3t: "Siñali", c3d: "Xam siñali yi ak dëkkandiku, réy feux rouge, stop ak yeneen.",
|
| 431 |
+
c4t: "Rafet & Xonq", c4d: "Xam xonq, jëf rafet ci kanam ak dëkkandiku video bu annotéwoon.",
|
| 432 |
+
launchbtn: 'Tambali <i class="fas fa-arrow-right"></i>',
|
| 433 |
+
foot: "Liggéey Académique — Version 1.0"
|
| 434 |
+
}
|
| 435 |
+
};
|
| 436 |
+
|
| 437 |
+
function setLang(l, btn) {
|
| 438 |
+
document.querySelectorAll(".lb").forEach(b => b.classList.remove("on"));
|
| 439 |
+
btn.classList.add("on");
|
| 440 |
+
const t = T[l];
|
| 441 |
+
if (!t) return;
|
| 442 |
+
document.getElementById("t-h1").innerHTML = t.h1;
|
| 443 |
+
document.getElementById("t-sub").textContent = t.sub;
|
| 444 |
+
document.getElementById("t-launch").innerHTML = t.launch;
|
| 445 |
+
["c1t","c2t","c3t","c4t"].forEach(k => {
|
| 446 |
+
const el = document.getElementById("t-"+k);
|
| 447 |
+
if (el) el.innerHTML = t[k];
|
| 448 |
+
});
|
| 449 |
+
["c1d","c2d","c3d","c4d"].forEach(k => {
|
| 450 |
+
const el = document.getElementById("t-"+k);
|
| 451 |
+
if (el) el.textContent = t[k];
|
| 452 |
+
});
|
| 453 |
+
["t-launch2","t-launch3","t-launch4","t-launch5"].forEach(id => {
|
| 454 |
+
const el = document.getElementById(id);
|
| 455 |
+
if (el) el.innerHTML = t.launchbtn;
|
| 456 |
+
});
|
| 457 |
+
document.getElementById("t-foot").textContent = t.foot;
|
| 458 |
+
try { localStorage.setItem("tiq_lang", l); } catch(e) {}
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
function toggleTheme() {
|
| 462 |
+
const d = document.documentElement;
|
| 463 |
+
const dk = d.getAttribute("data-theme") === "dark";
|
| 464 |
+
d.setAttribute("data-theme", dk ? "light" : "dark");
|
| 465 |
+
document.getElementById("ti").className = dk ? "fas fa-moon" : "fas fa-sun";
|
| 466 |
+
try { localStorage.setItem("tiq_theme", dk ? "light" : "dark"); } catch(e) {}
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
(function() {
|
| 470 |
+
try {
|
| 471 |
+
const th = localStorage.getItem("tiq_theme");
|
| 472 |
+
if (th) {
|
| 473 |
+
document.documentElement.setAttribute("data-theme", th);
|
| 474 |
+
if (th === "dark") document.getElementById("ti").className = "fas fa-sun";
|
| 475 |
+
}
|
| 476 |
+
const lg = localStorage.getItem("tiq_lang");
|
| 477 |
+
if (lg && lg !== "en") {
|
| 478 |
+
const btn = document.querySelector('.lb[onclick*="\'' + lg + '\'"]');
|
| 479 |
+
if (btn) setLang(lg, btn);
|
| 480 |
+
}
|
| 481 |
+
} catch(e) {}
|
| 482 |
+
})();
|
| 483 |
+
</script>
|
| 484 |
+
<script src="/static/trackiq-api.js"></script>
|
| 485 |
+
</body>
|
| 486 |
+
</html>
|
templates/logs.html
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" data-theme="light">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>TrackIQ Logs</title>
|
| 7 |
+
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
|
| 8 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
| 9 |
+
<style>
|
| 10 |
+
:root{--bg:#f0f4f8;--surface:#fff;--surface2:#f6f8fb;--border:rgba(0,0,0,0.07);--shadow:0 2px 16px rgba(0,0,0,0.09);--shadow-lg:0 8px 36px rgba(0,0,0,0.13);--text:#0f1923;--text2:#5a6a7a;--text3:#9aaab8;--accent:#880000;--accent2:#660000;--accent-light:rgba(136,0,0,0.07);--sidebar-w:60px;--radius:14px;--radius-sm:9px;}
|
| 11 |
+
[data-theme="dark"]{--bg:#0c1117;--surface:#151c25;--surface2:#1c2531;--border:rgba(255,255,255,0.07);--shadow:0 2px 16px rgba(0,0,0,0.45);--shadow-lg:0 8px 36px rgba(0,0,0,0.55);--text:#e8edf2;--text2:#7a8fa0;--text3:#3a4a5a;}
|
| 12 |
+
*{box-sizing:border-box;margin:0;padding:0}::-webkit-scrollbar{width:0;height:0}
|
| 13 |
+
body{font-family:'DM Sans',sans-serif;background:var(--bg);color:var(--text);font-size:13px;display:flex;min-height:100vh}
|
| 14 |
+
.sidebar{width:var(--sidebar-w);background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;align-items:center;padding:16px 0;gap:4px;position:fixed;top:0;left:0;bottom:0;z-index:200}
|
| 15 |
+
.sb-logo{width:34px;height:34px;background:var(--accent);border-radius:8px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px;margin-bottom:10px;text-decoration:none}
|
| 16 |
+
.sb-sep{width:28px;height:1px;background:var(--border);margin:4px 0}
|
| 17 |
+
.sb-btn{width:38px;height:38px;border-radius:9px;display:flex;align-items:center;justify-content:center;color:var(--text3);font-size:15px;cursor:pointer;transition:all .2s;text-decoration:none;border:none;background:none;position:relative}
|
| 18 |
+
.sb-btn:hover,.sb-btn.active{background:var(--accent-light);color:var(--accent)}
|
| 19 |
+
.sb-btn .tip{position:absolute;left:46px;top:50%;transform:translateY(-50%);background:var(--text);color:var(--surface);font-size:11px;padding:3px 8px;border-radius:5px;white-space:nowrap;opacity:0;pointer-events:none}.sb-btn:hover .tip{opacity:1}
|
| 20 |
+
.sb-spacer{flex:1}.page-wrap{margin-left:var(--sidebar-w);flex:1;display:flex;flex-direction:column}
|
| 21 |
+
.topbar{height:52px;background:var(--surface);border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;padding:0 22px}
|
| 22 |
+
.topbar-l,.topbar-r{display:flex;align-items:center;gap:10px}
|
| 23 |
+
.brand-name{font-size:15px;font-weight:600}.brand-name b{color:var(--accent)}.sep{color:var(--border);font-size:18px;margin:0 2px}
|
| 24 |
+
.lang-group{display:flex;background:var(--surface2);border-radius:6px;padding:2px;border:1px solid var(--border)}
|
| 25 |
+
.lang-btn{font-size:11px;font-weight:500;padding:3px 8px;border:none;background:none;cursor:pointer;border-radius:4px;color:var(--text2);font-family:'DM Sans',sans-serif}.lang-btn.active{background:var(--accent);color:white}
|
| 26 |
+
.icon-btn{width:30px;height:30px;border-radius:6px;border:1px solid var(--border);background:var(--surface);cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--text2);font-size:12px}
|
| 27 |
+
main{flex:1;padding:16px 18px;max-width:1440px;width:100%;margin:0 auto;display:flex;flex-direction:column;gap:12px}
|
| 28 |
+
.sum-row{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}
|
| 29 |
+
.sc,.card,.tcard{background:var(--surface);border-radius:var(--radius);border:1px solid var(--border);box-shadow:var(--shadow)}
|
| 30 |
+
.sc{padding:13px 15px}.sc-l{font-size:11px;color:var(--text2);margin-bottom:5px}.sc-v{font-size:22px;font-weight:300}.sc-s{font-size:10px;color:var(--text3);margin-top:3px}
|
| 31 |
+
.card{padding:14px 16px}.filter-row{display:flex;align-items:flex-end;gap:9px;flex-wrap:wrap}.fg{display:flex;flex-direction:column;gap:4px}.fg label{font-size:11px;color:var(--text2)}
|
| 32 |
+
.fin{padding:8px 10px;border:1px solid var(--border);border-radius:7px;font-size:12px;background:var(--surface2);color:var(--text);font-family:'DM Sans',sans-serif;outline:none;min-width:160px}.fin:focus{border-color:var(--accent)}
|
| 33 |
+
.f-sp{flex:1}
|
| 34 |
+
.btn{display:flex;align-items:center;gap:5px;border:none;border-radius:7px;padding:8px 13px;font-size:12px;cursor:pointer;font-family:'DM Sans',sans-serif}
|
| 35 |
+
.btn-exp{background:var(--accent);color:white}.btn-exp:hover{background:var(--accent2)}
|
| 36 |
+
.save-opt{display:flex;align-items:center;gap:10px;width:100%;padding:9px 14px;border:none;background:none;cursor:pointer;font-size:12px;color:var(--text);font-family:'DM Sans',sans-serif;text-align:left}.save-opt:hover{background:var(--surface2)}
|
| 37 |
+
.btn-clr{background:var(--surface2);color:var(--text2);border:1px solid var(--border)}
|
| 38 |
+
.tcard{overflow:hidden}.twrap{overflow:auto}
|
| 39 |
+
table{width:100%;border-collapse:collapse}
|
| 40 |
+
thead th{padding:10px 12px;text-align:left;font-size:11px;font-weight:500;color:var(--text2);background:var(--surface2);border-bottom:1px solid var(--border);white-space:nowrap}
|
| 41 |
+
tbody tr{border-bottom:1px solid var(--border)} tbody tr:last-child{border-bottom:none}
|
| 42 |
+
tbody td{padding:10px 12px;font-size:12px;vertical-align:middle}
|
| 43 |
+
.mono{font-family:'DM Mono',monospace}
|
| 44 |
+
.pill{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--border);background:var(--surface2);border-radius:999px;padding:3px 8px}
|
| 45 |
+
.dot{width:8px;height:8px;border-radius:50%}
|
| 46 |
+
.empty{padding:38px;text-align:center;color:var(--text3)}
|
| 47 |
+
@media(max-width:1100px){.sum-row{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
| 48 |
+
@media(max-width:760px){.sum-row{grid-template-columns:1fr}}
|
| 49 |
+
</style>
|
| 50 |
+
</head>
|
| 51 |
+
<body>
|
| 52 |
+
<nav class="sidebar">
|
| 53 |
+
<a href="/" class="sb-logo"><i class="fas fa-satellite-dish"></i></a>
|
| 54 |
+
<div class="sb-sep"></div>
|
| 55 |
+
<a href="/home" class="sb-btn"><i class="fas fa-play-circle"></i><span class="tip">Analyze</span></a>
|
| 56 |
+
<a href="/dashboard" class="sb-btn"><i class="fas fa-chart-line"></i><span class="tip">Dashboard</span></a>
|
| 57 |
+
<a href="/logs" class="sb-btn active"><i class="fas fa-list-ul"></i><span class="tip">Logs</span></a>
|
| 58 |
+
<a href="/history" class="sb-btn"><i class="fas fa-film"></i><span class="tip">History</span></a>
|
| 59 |
+
<div class="sb-spacer"></div>
|
| 60 |
+
<button class="sb-btn" onclick="toggleTheme()"><i class="fas fa-moon" id="themeIcon"></i><span class="tip">Theme</span></button>
|
| 61 |
+
</nav>
|
| 62 |
+
|
| 63 |
+
<div class="page-wrap">
|
| 64 |
+
<header class="topbar">
|
| 65 |
+
<div class="topbar-l">
|
| 66 |
+
<span class="brand-name">Track<b>IQ</b></span>
|
| 67 |
+
<span class="sep">|</span>
|
| 68 |
+
<span style="font-size:13px;font-weight:500;" id="t-title">Detection Logs</span>
|
| 69 |
+
</div>
|
| 70 |
+
<div class="topbar-r">
|
| 71 |
+
<div class="lang-group">
|
| 72 |
+
<button class="lang-btn active" onclick="setLang('en',this)">EN</button>
|
| 73 |
+
<button class="lang-btn" onclick="setLang('fr',this)">FR</button>
|
| 74 |
+
<button class="lang-btn" onclick="setLang('wo',this)">WO</button>
|
| 75 |
+
</div>
|
| 76 |
+
<button class="icon-btn" onclick="toggleTheme()"><i class="fas fa-moon" id="themeIcon2"></i></button>
|
| 77 |
+
</div>
|
| 78 |
+
</header>
|
| 79 |
+
|
| 80 |
+
<main>
|
| 81 |
+
<div class="sum-row">
|
| 82 |
+
<div class="sc"><div class="sc-l" id="t-s1">Detections</div><div class="sc-v" id="sumDetections">0</div><div class="sc-s" id="sumDetectionsSub">Real rows from generated videos</div></div>
|
| 83 |
+
<div class="sc"><div class="sc-l" id="t-s2">Scenes</div><div class="sc-v" id="sumScenes">0</div><div class="sc-s" id="sumScenesSub">Processed analyses</div></div>
|
| 84 |
+
<div class="sc"><div class="sc-l" id="t-s3">Classes</div><div class="sc-v" id="sumClasses">0</div><div class="sc-s" id="sumClassesSub">Distinct detected labels</div></div>
|
| 85 |
+
<div class="sc"><div class="sc-l" id="t-s4">Latest video</div><div class="sc-v" id="sumLatest">-</div><div class="sc-s" id="sumLatestSub">Most recent analyzed clip</div></div>
|
| 86 |
+
</div>
|
| 87 |
+
|
| 88 |
+
<div class="card">
|
| 89 |
+
<div class="filter-row">
|
| 90 |
+
<div class="fg"><label id="t-search">Search video or scene</label><input id="searchIn" class="fin" oninput="applyFilters()"></div>
|
| 91 |
+
<div class="fg"><label id="t-scene">Scene</label><select id="sceneF" class="fin" onchange="applyFilters()"><option value="">All</option></select></div>
|
| 92 |
+
<div class="fg"><label id="t-type">Class</label><select id="classF" class="fin" onchange="applyFilters()"><option value="">All</option></select></div>
|
| 93 |
+
<div class="fg"><label id="t-direction">Direction</label><select id="dirF" class="fin" onchange="applyFilters()"><option value="">All</option><option>up</option><option>down</option><option>left</option><option>right</option><option>unknown</option></select></div>
|
| 94 |
+
<div class="f-sp"></div>
|
| 95 |
+
<button class="btn btn-clr" onclick="clearFilters()" ondblclick="clearAllLogs()" title="Click=clear filters | Double-click=delete all logs"><i class="fas fa-rotate-left"></i> <span id="t-clear">Clear</span></button>
|
| 96 |
+
<div style="position:relative">
|
| 97 |
+
<button class="btn btn-exp" onclick="toggleSaveMenu(event)"><i class="fas fa-cloud-arrow-up"></i> <span id="t-export">Save Logs</span> <i class="fas fa-chevron-down" style="font-size:9px;margin-left:2px"></i></button>
|
| 98 |
+
<div id="saveMenu" style="display:none;position:absolute;right:0;top:calc(100% + 6px);background:var(--surface);border:1px solid var(--border);border-radius:10px;box-shadow:var(--shadow-lg);min-width:240px;z-index:999;overflow:hidden">
|
| 99 |
+
<div style="padding:10px 14px 6px;font-size:11px;color:var(--text2);font-weight:500;border-bottom:1px solid var(--border)">
|
| 100 |
+
<div style="display:flex;align-items:center;gap:6px;margin-bottom:6px">
|
| 101 |
+
<label style="font-size:11px;color:var(--text2)">Group name:</label>
|
| 102 |
+
<input id="groupNameIn" value="trackiq" style="flex:1;padding:4px 7px;border:1px solid var(--border);border-radius:5px;font-size:11px;background:var(--surface2);color:var(--text);outline:none" placeholder="e.g. session1">
|
| 103 |
+
</div>
|
| 104 |
+
<span style="color:var(--text3);font-size:10px">File: <span id="csvPreview">trackiq_all.csv</span></span>
|
| 105 |
+
</div>
|
| 106 |
+
<button class="save-opt" onclick="saveLocal()"><i class="fas fa-download" style="color:#3b82f6"></i> Download locally</button>
|
| 107 |
+
<button class="save-opt" onclick="showGithubPanel()"><i class="fab fa-github" style="color:#e2e8f0"></i> Push to GitHub</button>
|
| 108 |
+
<button class="save-opt" onclick="saveDrive()"><i class="fab fa-google-drive" style="color:#34a853"></i> Save to Google Drive</button>
|
| 109 |
+
</div>
|
| 110 |
+
</div>
|
| 111 |
+
<!-- GitHub panel -->
|
| 112 |
+
<div id="githubPanel" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:1000;display:none;align-items:center;justify-content:center">
|
| 113 |
+
<div style="background:var(--surface);border-radius:14px;padding:22px 24px;width:420px;box-shadow:var(--shadow-lg)">
|
| 114 |
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
|
| 115 |
+
<span style="font-weight:600;font-size:14px"><i class="fab fa-github"></i> Push to GitHub</span>
|
| 116 |
+
<button onclick="closeGithubPanel()" style="background:none;border:none;color:var(--text2);cursor:pointer;font-size:16px"><i class="fas fa-times"></i></button>
|
| 117 |
+
</div>
|
| 118 |
+
<div style="display:flex;flex-direction:column;gap:9px">
|
| 119 |
+
<div class="fg"><label>Personal Access Token (PAT)</label><input id="ghToken" class="fin" type="password" placeholder="ghp_xxxxxxxxxxxx"></div>
|
| 120 |
+
<div class="fg"><label>Repository (owner/repo)</label><input id="ghRepo" class="fin" placeholder="username/trackiq-logs"></div>
|
| 121 |
+
<div class="fg"><label>Branch</label><input id="ghBranch" class="fin" value="main" placeholder="main"></div>
|
| 122 |
+
<div class="fg"><label>Path in repo</label><input id="ghPath" class="fin" placeholder="logs/" value="logs/"></div>
|
| 123 |
+
</div>
|
| 124 |
+
<div id="ghStatus" style="margin-top:10px;font-size:12px;color:var(--text2);min-height:18px"></div>
|
| 125 |
+
<div style="display:flex;gap:8px;margin-top:14px;justify-content:flex-end">
|
| 126 |
+
<button class="btn btn-clr" onclick="closeGithubPanel()">Cancel</button>
|
| 127 |
+
<button class="btn btn-exp" onclick="pushToGithub()"><i class="fab fa-github"></i> Push CSV</button>
|
| 128 |
+
</div>
|
| 129 |
+
</div>
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
</div>
|
| 133 |
+
|
| 134 |
+
<div class="tcard">
|
| 135 |
+
<div class="twrap">
|
| 136 |
+
<table>
|
| 137 |
+
<thead>
|
| 138 |
+
<tr>
|
| 139 |
+
<th id="th-video">Video</th>
|
| 140 |
+
<th id="th-scene">Scene</th>
|
| 141 |
+
<th id="th-time">Time</th>
|
| 142 |
+
<th id="th-frame">Frame</th>
|
| 143 |
+
<th id="th-track">Track</th>
|
| 144 |
+
<th id="th-class">Class</th>
|
| 145 |
+
<th id="th-confidence">Confidence</th>
|
| 146 |
+
<th id="th-direction">Direction</th>
|
| 147 |
+
<th id="th-bbox">Bounding Box</th>
|
| 148 |
+
<th id="th-position">Center</th>
|
| 149 |
+
</tr>
|
| 150 |
+
</thead>
|
| 151 |
+
<tbody id="tbody">
|
| 152 |
+
<tr><td colspan="9" class="empty">Loading real logs...</td></tr>
|
| 153 |
+
</tbody>
|
| 154 |
+
</table>
|
| 155 |
+
</div>
|
| 156 |
+
</div>
|
| 157 |
+
</main>
|
| 158 |
+
</div>
|
| 159 |
+
|
| 160 |
+
<script>
|
| 161 |
+
let allRows = [];
|
| 162 |
+
let filteredRows = [];
|
| 163 |
+
|
| 164 |
+
function toggleTheme(){
|
| 165 |
+
const d=document.documentElement;
|
| 166 |
+
const dark=d.getAttribute('data-theme')==='dark';
|
| 167 |
+
d.setAttribute('data-theme',dark?'light':'dark');
|
| 168 |
+
['themeIcon','themeIcon2'].forEach(id=>{const el=document.getElementById(id);if(el)el.className=dark?'fas fa-moon':'fas fa-sun';});
|
| 169 |
+
try{localStorage.setItem('tiq_theme',dark?'light':'dark');}catch(e){}
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
const TL = {
|
| 173 |
+
en:{title:'Detection Logs',s1:'Detections',s2:'Scenes',s3:'Classes',s4:'Latest video',search:'Search video or scene',scene:'Scene',type:'Class',direction:'Direction',clear:'Clear',export:'Save Logs',video:'Video',time:'Time',frame:'Frame',track:'Track',class:'Class',confidence:'Confidence',bbox:'Bounding Box',position:'Center'},
|
| 174 |
+
fr:{title:'Logs de détection',s1:'Détections',s2:'Scènes',s3:'Classes',s4:'Dernière vidéo',search:'Recherche vidéo ou scène',scene:'Scène',type:'Classe',direction:'Direction',clear:'Effacer',export:'Sauvegarder',video:'Vidéo',time:'Temps',frame:'Frame',track:'Track',class:'Classe',confidence:'Confiance',bbox:'Boîte englobante',position:'Centre'},
|
| 175 |
+
wo:{title:'Logs yi',s1:'Réy',s2:'Scène yi',s3:'Classe yi',s4:'Video mujj',search:'Seet video walla scène',scene:'Scène',type:'Classe',direction:'Direction',clear:'Baň',export:'Sauvegarder',video:'Video',time:'Waxtu',frame:'Frame',track:'Track',class:'Classe',confidence:'Confiance',bbox:'Bounding Box',position:'Centre'}
|
| 176 |
+
};
|
| 177 |
+
|
| 178 |
+
function setLang(l, btn){
|
| 179 |
+
document.querySelectorAll('.lang-btn').forEach(b=>b.classList.remove('active'));
|
| 180 |
+
btn.classList.add('active');
|
| 181 |
+
const t = TL[l];
|
| 182 |
+
const ids = {title:'t-title',s1:'t-s1',s2:'t-s2',s3:'t-s3',s4:'t-s4',search:'t-search',scene:'t-scene',type:'t-type',direction:'t-direction',clear:'t-clear',export:'t-export',video:'th-video',time:'th-time',frame:'th-frame',track:'th-track',class:'th-class',confidence:'th-confidence',bbox:'th-bbox',position:'th-position'};
|
| 183 |
+
Object.entries(ids).forEach(([k,id])=>{const el=document.getElementById(id);if(el)el.textContent=t[k];});
|
| 184 |
+
document.getElementById('th-scene').textContent = t.scene;
|
| 185 |
+
document.getElementById('th-direction').textContent = t.direction;
|
| 186 |
+
try{localStorage.setItem('tiq_lang',l);}catch(e){}
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
async function loadLogs(){
|
| 190 |
+
try{
|
| 191 |
+
const [rowsRes, scenesRes] = await Promise.all([fetch('/api/logs/rows'), fetch('/api/logs')]);
|
| 192 |
+
allRows = await rowsRes.json();
|
| 193 |
+
const scenes = await scenesRes.json();
|
| 194 |
+
fillFilters(scenes);
|
| 195 |
+
fillSummary(scenes, allRows);
|
| 196 |
+
applyFilters();
|
| 197 |
+
}catch(err){
|
| 198 |
+
const tb=document.getElementById('tbody');
|
| 199 |
+
tb.innerHTML = '<tr><td colspan="9" class="empty">Unable to load real logs.</td></tr>';
|
| 200 |
+
console.warn('[TrackIQ] real logs:', err);
|
| 201 |
+
}
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
function fillFilters(scenes){
|
| 205 |
+
const sceneF = document.getElementById('sceneF');
|
| 206 |
+
const classF = document.getElementById('classF');
|
| 207 |
+
sceneF.innerHTML = '<option value="">All</option>' + scenes.map(s=>`<option value="${s.scene_id}">${s.video_name || s.scene_id}</option>`).join('');
|
| 208 |
+
const classes = [...new Set(allRows.map(r=>r.class_name).filter(Boolean))].sort();
|
| 209 |
+
classF.innerHTML = '<option value="">All</option>' + classes.map(c=>`<option value="${c}">${c}</option>`).join('');
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
function fillSummary(scenes, rows){
|
| 213 |
+
document.getElementById('sumDetections').textContent = rows.length.toLocaleString();
|
| 214 |
+
document.getElementById('sumScenes').textContent = scenes.length.toLocaleString();
|
| 215 |
+
document.getElementById('sumClasses').textContent = new Set(rows.map(r=>r.class_name).filter(Boolean)).size.toLocaleString();
|
| 216 |
+
document.getElementById('sumLatest').textContent = scenes[0]?.video_name || '-';
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
function applyFilters(){
|
| 220 |
+
const search = document.getElementById('searchIn').value.toLowerCase();
|
| 221 |
+
const scene = document.getElementById('sceneF').value;
|
| 222 |
+
const cls = document.getElementById('classF').value;
|
| 223 |
+
const dir = document.getElementById('dirF').value;
|
| 224 |
+
filteredRows = allRows.filter(r =>
|
| 225 |
+
(!search || (r.video_name || '').toLowerCase().includes(search) || (r.scene_id || '').toLowerCase().includes(search)) &&
|
| 226 |
+
(!scene || r.scene_id === scene) &&
|
| 227 |
+
(!cls || r.class_name === cls) &&
|
| 228 |
+
(!dir || r.direction === dir)
|
| 229 |
+
);
|
| 230 |
+
renderRows();
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
async function clearAllLogs(){
|
| 234 |
+
if(!confirm('Delete all logs? This cannot be undone.')) return;
|
| 235 |
+
try{
|
| 236 |
+
const scenes = await fetch('/api/logs').then(r=>r.json());
|
| 237 |
+
await Promise.all(scenes.map(s=>fetch(`/api/logs/${s.scene_id}`,{method:'DELETE'})));
|
| 238 |
+
location.reload();
|
| 239 |
+
}catch(e){ alert('Error: '+e.message); }
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
function clearFilters(){
|
| 243 |
+
['searchIn','sceneF','classF','dirF'].forEach(id=>{document.getElementById(id).value='';});
|
| 244 |
+
applyFilters();
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
function renderRows(){
|
| 248 |
+
const tb = document.getElementById('tbody');
|
| 249 |
+
if(!filteredRows.length){
|
| 250 |
+
tb.innerHTML = '<tr><td colspan="10" class="empty">No real detections found for this filter.</td></tr>';
|
| 251 |
+
return;
|
| 252 |
+
}
|
| 253 |
+
const colors = {Vehicle:'#3b82f6',Motorcycle:'#10b981',Truck:'#f59e0b',Bus:'#8b5cf6',Human:'#ec4899',Bicycle:'#06b6d4'};
|
| 254 |
+
tb.innerHTML = '';
|
| 255 |
+
filteredRows.forEach(r=>{
|
| 256 |
+
const tr = document.createElement('tr');
|
| 257 |
+
tr.innerHTML = `
|
| 258 |
+
<td title="${r.video_name || ''}">${(r.video_name || '-').slice(0,28)}</td>
|
| 259 |
+
<td class="mono">${r.scene_id}</td>
|
| 260 |
+
<td class="mono">${Number(r.timestamp_s || 0).toFixed(2)}s</td>
|
| 261 |
+
<td class="mono">${r.frame_id}</td>
|
| 262 |
+
<td class="mono">#${r.track_id}</td>
|
| 263 |
+
<td><span class="pill"><span class="dot" style="background:${colors[r.class_name] || '#888'}"></span>${r.class_name || '-'}</span></td>
|
| 264 |
+
<td class="mono">${Math.round((r.confidence || 0) * 100)}%</td>
|
| 265 |
+
<td>${r.direction || 'unknown'}</td>
|
| 266 |
+
<td class="mono" style="font-size:11px">(${r.x1},${r.y1})→(${r.x2},${r.y2})</td>
|
| 267 |
+
<td class="mono">(${r.cx}, ${r.cy})</td>
|
| 268 |
+
`;
|
| 269 |
+
tb.appendChild(tr);
|
| 270 |
+
});
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
/* ── Helpers CSV ─────────────────────────────────────────────── */
|
| 274 |
+
function getGroupName(){ return (document.getElementById('groupNameIn')?.value||'trackiq').trim().replace(/\s+/g,'_')||'trackiq'; }
|
| 275 |
+
function getCsvFilename(){
|
| 276 |
+
const g=getGroupName();
|
| 277 |
+
const scenes=[...new Set(filteredRows.map(r=>r.scene_id))];
|
| 278 |
+
return `${g}_${scenes.length===1?scenes[0]:'all'}.csv`;
|
| 279 |
+
}
|
| 280 |
+
function buildCsvContent(){
|
| 281 |
+
const hdr = 'group,scene_id,video_name,timestamp_s,frame_id,track_id,class_name,confidence,x1,y1,x2,y2,cx,cy,direction\n';
|
| 282 |
+
const g = getGroupName();
|
| 283 |
+
const rows = filteredRows.map(r => [g,r.scene_id,`"${(r.video_name||'').replace(/"/g,'""')}"`,r.timestamp_s,r.frame_id,r.track_id,r.class_name,r.confidence,r.x1,r.y1,r.x2,r.y2,r.cx,r.cy,r.direction].join(',')).join('\n');
|
| 284 |
+
return hdr + rows;
|
| 285 |
+
}
|
| 286 |
+
function updateCsvPreview(){
|
| 287 |
+
const el = document.getElementById('csvPreview');
|
| 288 |
+
if(el) el.textContent = getCsvFilename();
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
/* ── Save menu ───────────────────────────────────���───────────── */
|
| 292 |
+
function toggleSaveMenu(e){
|
| 293 |
+
e.stopPropagation();
|
| 294 |
+
const m = document.getElementById('saveMenu');
|
| 295 |
+
m.style.display = m.style.display==='none'?'block':'none';
|
| 296 |
+
updateCsvPreview();
|
| 297 |
+
}
|
| 298 |
+
document.addEventListener('click', ()=>{ const m=document.getElementById('saveMenu'); if(m) m.style.display='none'; });
|
| 299 |
+
|
| 300 |
+
/* ── 1. Download local ───────────────────────────────────────── */
|
| 301 |
+
function saveLocal(){
|
| 302 |
+
const a = document.createElement('a');
|
| 303 |
+
a.href = URL.createObjectURL(new Blob([buildCsvContent()], {type:'text/csv'}));
|
| 304 |
+
a.download = getCsvFilename();
|
| 305 |
+
a.click();
|
| 306 |
+
document.getElementById('saveMenu').style.display='none';
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
/* ── 2. GitHub push ──────────────────────────────────────────── */
|
| 310 |
+
function showGithubPanel(){
|
| 311 |
+
document.getElementById('saveMenu').style.display='none';
|
| 312 |
+
const p = document.getElementById('githubPanel');
|
| 313 |
+
p.style.display='flex';
|
| 314 |
+
updateCsvPreview();
|
| 315 |
+
}
|
| 316 |
+
function closeGithubPanel(){ document.getElementById('githubPanel').style.display='none'; }
|
| 317 |
+
|
| 318 |
+
async function pushToGithub(){
|
| 319 |
+
const token = document.getElementById('ghToken').value.trim();
|
| 320 |
+
const repo = document.getElementById('ghRepo').value.trim();
|
| 321 |
+
const branch = document.getElementById('ghBranch').value.trim() || 'main';
|
| 322 |
+
const path = (document.getElementById('ghPath').value.trim().replace(/\/+$/,'')||'logs') + '/' + getCsvFilename();
|
| 323 |
+
const status = document.getElementById('ghStatus');
|
| 324 |
+
|
| 325 |
+
if(!token||!repo){ status.textContent='⚠ Token and repo are required.'; return; }
|
| 326 |
+
status.textContent='Pushing…';
|
| 327 |
+
|
| 328 |
+
const content = btoa(unescape(encodeURIComponent(buildCsvContent())));
|
| 329 |
+
// Check if file exists (to get SHA for update)
|
| 330 |
+
let sha = null;
|
| 331 |
+
try{
|
| 332 |
+
const check = await fetch(`https://api.github.com/repos/${repo}/contents/${path}`,{headers:{Authorization:`token ${token}`}});
|
| 333 |
+
if(check.ok){ const d=await check.json(); sha=d.sha; }
|
| 334 |
+
}catch(e){}
|
| 335 |
+
|
| 336 |
+
try{
|
| 337 |
+
const body = {message:`TrackIQ logs export — ${getCsvFilename()}`, content, branch};
|
| 338 |
+
if(sha) body.sha = sha;
|
| 339 |
+
const res = await fetch(`https://api.github.com/repos/${repo}/contents/${path}`,{
|
| 340 |
+
method:'PUT', headers:{Authorization:`token ${token}`,'Content-Type':'application/json'},
|
| 341 |
+
body: JSON.stringify(body)
|
| 342 |
+
});
|
| 343 |
+
if(res.ok){ status.innerHTML='<span style="color:#10b981">✓ Pushed successfully!</span>'; }
|
| 344 |
+
else{ const e=await res.json(); status.innerHTML=`<span style="color:#ef4444">Error: ${e.message}</span>`; }
|
| 345 |
+
}catch(err){ status.innerHTML=`<span style="color:#ef4444">Network error: ${err.message}</span>`; }
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
/* ── 3. Google Drive ─────────────────────────────────────────── */
|
| 349 |
+
async function saveDrive(){
|
| 350 |
+
document.getElementById('saveMenu').style.display='none';
|
| 351 |
+
// Uses Google Drive File Picker API via OAuth implicit flow
|
| 352 |
+
const CLIENT_ID = ''; // User must fill their own GCP OAuth client ID
|
| 353 |
+
if(!CLIENT_ID){
|
| 354 |
+
const ok = confirm('To save to Google Drive you need a Google OAuth Client ID.\n\nWould you like instructions on how to set it up?\n\n(For now the file will be downloaded locally as a fallback)');
|
| 355 |
+
if(ok){ window.open('https://developers.google.com/drive/api/guides/about-auth','_blank'); }
|
| 356 |
+
saveLocal();
|
| 357 |
+
return;
|
| 358 |
+
}
|
| 359 |
+
// Full Drive upload with OAuth token
|
| 360 |
+
const scope = 'https://www.googleapis.com/auth/drive.file';
|
| 361 |
+
const redirect = location.origin + location.pathname;
|
| 362 |
+
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${CLIENT_ID}&redirect_uri=${encodeURIComponent(redirect)}&response_type=token&scope=${encodeURIComponent(scope)}`;
|
| 363 |
+
const popup = window.open(authUrl,'_blank','width=500,height=600');
|
| 364 |
+
// Listen for token in URL hash after redirect
|
| 365 |
+
const interval = setInterval(()=>{
|
| 366 |
+
try{
|
| 367 |
+
if(popup.closed){ clearInterval(interval); return; }
|
| 368 |
+
const hash = popup.location.hash;
|
| 369 |
+
if(hash && hash.includes('access_token')){
|
| 370 |
+
clearInterval(interval); popup.close();
|
| 371 |
+
const token = new URLSearchParams(hash.replace('#','')).get('access_token');
|
| 372 |
+
uploadToDrive(token);
|
| 373 |
+
}
|
| 374 |
+
}catch(e){}
|
| 375 |
+
},500);
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
async function uploadToDrive(token){
|
| 379 |
+
const csv = buildCsvContent();
|
| 380 |
+
const fname = getCsvFilename();
|
| 381 |
+
const meta = JSON.stringify({name:fname,mimeType:'text/csv'});
|
| 382 |
+
const form = new FormData();
|
| 383 |
+
form.append('metadata', new Blob([meta],{type:'application/json'}));
|
| 384 |
+
form.append('file', new Blob([csv], {type:'text/csv'}));
|
| 385 |
+
try{
|
| 386 |
+
const res = await fetch('https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart',{
|
| 387 |
+
method:'POST', headers:{Authorization:`Bearer ${token}`}, body:form
|
| 388 |
+
});
|
| 389 |
+
if(res.ok){ alert(`✓ "${fname}" saved to Google Drive!`); }
|
| 390 |
+
else{ const e=await res.json(); alert('Drive error: '+e.error?.message); }
|
| 391 |
+
}catch(err){ alert('Drive upload failed: '+err.message); }
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
function exportFiltered(){ saveLocal(); } // legacy alias
|
| 395 |
+
|
| 396 |
+
(function(){
|
| 397 |
+
try{
|
| 398 |
+
const th=localStorage.getItem('tiq_theme');
|
| 399 |
+
if(th){
|
| 400 |
+
document.documentElement.setAttribute('data-theme',th);
|
| 401 |
+
if(th==='dark'){['themeIcon','themeIcon2'].forEach(id=>{const el=document.getElementById(id);if(el)el.className='fas fa-sun';});}
|
| 402 |
+
}
|
| 403 |
+
const lg=localStorage.getItem('tiq_lang');
|
| 404 |
+
if(lg&&lg!=='en'){
|
| 405 |
+
const btn=document.querySelector(`.lang-btn[onclick*="'${lg}'"]`);
|
| 406 |
+
if(btn)setLang(lg,btn);
|
| 407 |
+
}
|
| 408 |
+
}catch(e){}
|
| 409 |
+
loadLogs();
|
| 410 |
+
// Update CSV filename preview when group name changes
|
| 411 |
+
document.addEventListener('input', e=>{ if(e.target.id==='groupNameIn') updateCsvPreview(); });
|
| 412 |
+
})();
|
| 413 |
+
</script>
|
| 414 |
+
</body>
|
| 415 |
+
</html>
|