fixed timeout issue

This commit is contained in:
2026-06-02 14:26:13 -04:00
parent 50cd3ecc6c
commit 64ed8b81c1
6 changed files with 154 additions and 52 deletions
+1 -2
View File
@@ -2,9 +2,8 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>dashboard</title> <title>Test House Dashboard</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+64 -20
View File
@@ -1,8 +1,41 @@
import { useState, useEffect } from 'react' import { useState, useEffect, useRef, useCallback } from 'react'
import { useQueryClient } from '@tanstack/react-query' import { useQueryClient } from '@tanstack/react-query'
import { useConfig, useSaveConfig } from '../hooks/useConfig' import { useConfig, useSaveConfig } from '../hooks/useConfig'
import { apiFetch } from '../lib/api' import { apiFetch } from '../lib/api'
function useScanPoller(onDone) {
const timerRef = useRef(null)
const onDoneRef = useRef(onDone)
onDoneRef.current = onDone
const stop = useCallback(() => {
if (timerRef.current) {
clearInterval(timerRef.current)
timerRef.current = null
}
}, [])
const start = useCallback(() => {
stop()
timerRef.current = setInterval(async () => {
try {
const data = await apiFetch('/scan-status')
if (!data?.scanning) {
stop()
onDoneRef.current()
}
} catch {
stop()
onDoneRef.current()
}
}, 2000)
}, [stop])
useEffect(() => () => stop(), [stop])
return { start, stop }
}
function fmtSeconds(s) { function fmtSeconds(s) {
if (s == null) return '' if (s == null) return ''
const m = Math.round(parseFloat(s) / 60) const m = Math.round(parseFloat(s) / 60)
@@ -15,11 +48,19 @@ export default function ConfigModal({ onClose }) {
const { mutate: save, isPending } = useSaveConfig() const { mutate: save, isPending } = useSaveConfig()
const [form, setForm] = useState({}) const [form, setForm] = useState({})
const [scanResult, setScanResult] = useState(null) // { testCount, completedCount } | null const [scanResult, setScanResult] = useState(null)
const [saveError, setSaveError] = useState(null) const [saveError, setSaveError] = useState(null)
const [isRescanning, setIsRescanning] = useState(false) const [isRescanning, setIsRescanning] = useState(false)
const [isScanning, setIsScanning] = useState(false)
const [isSavingTimes, setIsSavingTimes] = useState(false) const [isSavingTimes, setIsSavingTimes] = useState(false)
const scanPoller = useScanPoller(() => {
queryClient.invalidateQueries({ queryKey: ['stats'] })
queryClient.invalidateQueries({ queryKey: ['tests'] })
setIsRescanning(false)
setIsScanning(false)
})
useEffect(() => { useEffect(() => {
if (config) { if (config) {
setForm({ setForm({
@@ -55,10 +96,9 @@ export default function ConfigModal({ onClose }) {
} }
save(payload, { save(payload, {
onSuccess: (data) => { onSuccess: (data) => {
if (data?.testCount !== null && data?.testCount !== undefined) { if (data?.scanning) {
setScanResult({ testCount: data.testCount, completedCount: data.completedCount }) setIsScanning(true)
// Auto-close after showing result only if tests were found scanPoller.start()
if (data.testCount > 0) setTimeout(onClose, 1500)
} else { } else {
onClose() onClose()
} }
@@ -98,14 +138,15 @@ export default function ConfigModal({ onClose }) {
setIsRescanning(true) setIsRescanning(true)
try { try {
const data = await apiFetch('/config/rescan-results', { method: 'POST' }) const data = await apiFetch('/config/rescan-results', { method: 'POST' })
if (data?.testCount !== null && data?.testCount !== undefined) { if (data?.scanning) {
setScanResult({ testCount: data.testCount, completedCount: data.completedCount }) scanPoller.start()
} } else {
setIsRescanning(false)
queryClient.invalidateQueries({ queryKey: ['stats'] }) queryClient.invalidateQueries({ queryKey: ['stats'] })
queryClient.invalidateQueries({ queryKey: ['tests'] }) queryClient.invalidateQueries({ queryKey: ['tests'] })
}
} catch (err) { } catch (err) {
setSaveError(err?.message ?? 'Rescan failed') setSaveError(err?.message ?? 'Rescan failed')
} finally {
setIsRescanning(false) setIsRescanning(false)
} }
} }
@@ -134,16 +175,19 @@ export default function ConfigModal({ onClose }) {
</div> </div>
)} )}
{/* Scan result banner */} {/* Scanning indicator (save triggered a scan) */}
{scanResult !== null && ( {isScanning && (
<div className={`rounded-lg px-4 py-3 text-sm border ${ <div className="bg-blue-950/50 border border-blue-700 rounded-lg px-4 py-3 text-blue-300 text-sm flex items-center gap-2">
scanResult.testCount === 0 <span className="animate-spin inline-block"></span>
? 'bg-amber-950/50 border-amber-700 text-amber-300' Scan in progress the table will refresh when complete.
: 'bg-emerald-950/50 border-emerald-700 text-emerald-300' </div>
}`}> )}
{scanResult.testCount === 0
? 'No tests found — check that the target directory contains TC_WIFI_*.ini files in subdirectories.' {/* Rescan indicator (rescan button used) */}
: `Found ${scanResult.testCount} tests (${scanResult.completedCount} completed).`} {!isScanning && isRescanning && (
<div className="bg-blue-950/50 border border-blue-700 rounded-lg px-4 py-3 text-blue-300 text-sm flex items-center gap-2">
<span className="animate-spin inline-block"></span>
Results scan in progress
</div> </div>
)} )}
+1 -6
View File
@@ -4,10 +4,5 @@ import tailwindcss from '@tailwindcss/vite'
export default defineConfig({ export default defineConfig({
plugins: [react(), tailwindcss()], plugins: [react(), tailwindcss()],
server: {
host: true,
proxy: {
'/api': 'http://localhost:3001',
},
},
}) })
+17 -8
View File
@@ -2,13 +2,9 @@ services:
backend: backend:
build: ./server build: ./server
container_name: dashboard-backend container_name: dashboard-backend
ports: expose:
- "3001:3001" - "3001" # Remove public port, keep internal only
environment: 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_BROWSE_ROOT=${HOST_BROWSE_ROOT}
- HOST_MOUNT_ROOT=/host - HOST_MOUNT_ROOT=/host
volumes: volumes:
@@ -23,8 +19,21 @@ services:
frontend: frontend:
build: ./dashboard build: ./dashboard
container_name: dashboard-frontend container_name: dashboard-frontend
ports: expose:
- "5173:80" - "5173" # Remove public port, keep internal only
depends_on: depends_on:
- backend - backend
restart: unless-stopped 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
+48
View File
@@ -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;
}
}
}
+19 -12
View File
@@ -1,4 +1,5 @@
import os import os
import threading
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from functools import wraps from functools import wraps
from pathlib import Path from pathlib import Path
@@ -442,10 +443,12 @@ def set_config_route():
if is_scan_in_progress(): if is_scan_in_progress():
return jsonify({"ok": True, "scanning": True, "testCount": None, "completedCount": None}) return jsonify({"ok": True, "scanning": True, "testCount": None, "completedCount": None})
full_scan(target_dir, results_dir, results_dir_ref) threading.Thread(
tests = get_all_tests() target=full_scan,
completed = len([t for t in tests if t.get("completed")]) args=(target_dir, results_dir, results_dir_ref),
return jsonify({"ok": True, "scanning": False, "testCount": len(tests), "completedCount": completed}) daemon=True,
).start()
return jsonify({"ok": True, "scanning": True, "testCount": None, "completedCount": None})
return jsonify({"ok": 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(): if is_scan_in_progress():
return jsonify({"ok": True, "scanning": True, "testCount": None, "completedCount": None}) return jsonify({"ok": True, "scanning": True, "testCount": None, "completedCount": None})
full_scan(target_dir, results_dir, results_dir_ref) threading.Thread(
tests = get_all_tests() target=full_scan,
completed = len([t for t in tests if t.get("completed")]) args=(target_dir, results_dir, results_dir_ref),
return jsonify({"ok": True, "scanning": False, "testCount": len(tests), "completedCount": completed}) daemon=True,
).start()
return jsonify({"ok": True, "scanning": True, "testCount": None, "completedCount": None})
@app.post("/api/config/rescan-results") @app.post("/api/config/rescan-results")
@@ -483,10 +488,12 @@ def rescan_results_route():
if is_scan_in_progress(): if is_scan_in_progress():
return jsonify({"ok": True, "scanning": True, "testCount": None, "completedCount": None}) return jsonify({"ok": True, "scanning": True, "testCount": None, "completedCount": None})
scan_results_only(results_dir, results_dir_ref) threading.Thread(
tests = get_all_tests() target=scan_results_only,
completed = len([t for t in tests if t.get("completed")]) args=(results_dir, results_dir_ref),
return jsonify({"ok": True, "scanning": False, "testCount": len(tests), "completedCount": completed}) daemon=True,
).start()
return jsonify({"ok": True, "scanning": True, "testCount": None, "completedCount": None})
@app.get("/") @app.get("/")