diff --git a/dashboard/index.html b/dashboard/index.html
index cc627a4..4ce7dff 100644
--- a/dashboard/index.html
+++ b/dashboard/index.html
@@ -2,9 +2,8 @@
- {scanResult.testCount === 0
- ? 'No tests found — check that the target directory contains TC_WIFI_*.ini files in subdirectories.'
- : `Found ${scanResult.testCount} tests (${scanResult.completedCount} completed).`}
+ {/* Scanning indicator (save triggered a scan) */}
+ {isScanning && (
+
+ ⟳
+ Scan in progress — the table will refresh when complete.
+
+ )}
+
+ {/* Rescan indicator (rescan button used) */}
+ {!isScanning && isRescanning && (
+
+ ⟳
+ Results scan in progress…
)}
diff --git a/dashboard/vite.config.js b/dashboard/vite.config.js
index 3b1b213..f6c5405 100644
--- a/dashboard/vite.config.js
+++ b/dashboard/vite.config.js
@@ -4,10 +4,5 @@ import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [react(), tailwindcss()],
- server: {
- host: true,
- proxy: {
- '/api': 'http://localhost:3001',
- },
- },
})
+
diff --git a/docker-compose.yml b/docker-compose.yml
index f576d7c..2c2b42e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -2,13 +2,9 @@ services:
backend:
build: ./server
container_name: dashboard-backend
- ports:
- - "3001:3001"
+ expose:
+ - "3001" # Remove public port, keep internal only
environment:
- # Root path on the Docker host to mount inside the container.
- # Linux VM: HOST_BROWSE_ROOT=/home/wnc
- # Windows Docker Desktop: HOST_BROWSE_ROOT=C:/Users
- # Then enter paths under /host/... in the dashboard settings.
- HOST_BROWSE_ROOT=${HOST_BROWSE_ROOT}
- HOST_MOUNT_ROOT=/host
volumes:
@@ -23,8 +19,21 @@ services:
frontend:
build: ./dashboard
container_name: dashboard-frontend
- ports:
- - "5173:80"
+ expose:
+ - "5173" # Remove public port, keep internal only
depends_on:
- backend
- restart: unless-stopped
\ No newline at end of file
+ restart: unless-stopped
+
+ nginx:
+ image: nginx:alpine
+ container_name: dashboard-nginx
+ ports:
+ - "80:80" # Single public entry point
+ volumes:
+ - ./nginx.conf:/etc/nginx/nginx.conf:ro
+ depends_on:
+ - backend
+ - frontend
+ restart: unless-stopped
+
diff --git a/nginx.conf b/nginx.conf
new file mode 100644
index 0000000..ddc22c5
--- /dev/null
+++ b/nginx.conf
@@ -0,0 +1,48 @@
+events {
+ worker_connections 1024;
+}
+
+http {
+ upstream backend {
+ server backend:3001;
+ }
+
+ upstream frontend {
+ server frontend:80;
+ }
+
+ server {
+ listen 80;
+
+ # Scan endpoints may run for several minutes on network shares.
+ location ~ ^/api/config(/rescan|/rescan-results)?$ {
+ proxy_pass http://backend;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_read_timeout 600s;
+ proxy_send_timeout 600s;
+ }
+
+ # Frontend (default)
+ location / {
+ proxy_pass http://frontend;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+
+ # Required for Vite HMR (hot reload) if in dev mode
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ }
+
+ # Backend API
+ location /api/ {
+ proxy_pass http://backend;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_read_timeout 60s;
+ }
+ }
+}
diff --git a/server/app.py b/server/app.py
index 8a4bff6..cdbe015 100644
--- a/server/app.py
+++ b/server/app.py
@@ -1,4 +1,5 @@
import os
+import threading
from datetime import datetime, timedelta, timezone
from functools import wraps
from pathlib import Path
@@ -442,10 +443,12 @@ def set_config_route():
if is_scan_in_progress():
return jsonify({"ok": True, "scanning": True, "testCount": None, "completedCount": None})
- full_scan(target_dir, results_dir, results_dir_ref)
- tests = get_all_tests()
- completed = len([t for t in tests if t.get("completed")])
- return jsonify({"ok": True, "scanning": False, "testCount": len(tests), "completedCount": completed})
+ threading.Thread(
+ target=full_scan,
+ args=(target_dir, results_dir, results_dir_ref),
+ daemon=True,
+ ).start()
+ return jsonify({"ok": True, "scanning": True, "testCount": None, "completedCount": None})
return jsonify({"ok": True, "testCount": None, "completedCount": None})
@@ -464,10 +467,12 @@ def rescan_route():
if is_scan_in_progress():
return jsonify({"ok": True, "scanning": True, "testCount": None, "completedCount": None})
- full_scan(target_dir, results_dir, results_dir_ref)
- tests = get_all_tests()
- completed = len([t for t in tests if t.get("completed")])
- return jsonify({"ok": True, "scanning": False, "testCount": len(tests), "completedCount": completed})
+ threading.Thread(
+ target=full_scan,
+ args=(target_dir, results_dir, results_dir_ref),
+ daemon=True,
+ ).start()
+ return jsonify({"ok": True, "scanning": True, "testCount": None, "completedCount": None})
@app.post("/api/config/rescan-results")
@@ -483,10 +488,12 @@ def rescan_results_route():
if is_scan_in_progress():
return jsonify({"ok": True, "scanning": True, "testCount": None, "completedCount": None})
- scan_results_only(results_dir, results_dir_ref)
- tests = get_all_tests()
- completed = len([t for t in tests if t.get("completed")])
- return jsonify({"ok": True, "scanning": False, "testCount": len(tests), "completedCount": completed})
+ threading.Thread(
+ target=scan_results_only,
+ args=(results_dir, results_dir_ref),
+ daemon=True,
+ ).start()
+ return jsonify({"ok": True, "scanning": True, "testCount": None, "completedCount": None})
@app.get("/")