fixed timeout issue
This commit is contained in:
@@ -2,9 +2,8 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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" />
|
||||
<title>dashboard</title>
|
||||
<title>Test House Dashboard</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,8 +1,41 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useConfig, useSaveConfig } from '../hooks/useConfig'
|
||||
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) {
|
||||
if (s == null) return ''
|
||||
const m = Math.round(parseFloat(s) / 60)
|
||||
@@ -15,11 +48,19 @@ export default function ConfigModal({ onClose }) {
|
||||
const { mutate: save, isPending } = useSaveConfig()
|
||||
|
||||
const [form, setForm] = useState({})
|
||||
const [scanResult, setScanResult] = useState(null) // { testCount, completedCount } | null
|
||||
const [scanResult, setScanResult] = useState(null)
|
||||
const [saveError, setSaveError] = useState(null)
|
||||
const [isRescanning, setIsRescanning] = useState(false)
|
||||
const [isScanning, setIsScanning] = useState(false)
|
||||
const [isSavingTimes, setIsSavingTimes] = useState(false)
|
||||
|
||||
const scanPoller = useScanPoller(() => {
|
||||
queryClient.invalidateQueries({ queryKey: ['stats'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['tests'] })
|
||||
setIsRescanning(false)
|
||||
setIsScanning(false)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (config) {
|
||||
setForm({
|
||||
@@ -55,10 +96,9 @@ export default function ConfigModal({ onClose }) {
|
||||
}
|
||||
save(payload, {
|
||||
onSuccess: (data) => {
|
||||
if (data?.testCount !== null && data?.testCount !== undefined) {
|
||||
setScanResult({ testCount: data.testCount, completedCount: data.completedCount })
|
||||
// Auto-close after showing result only if tests were found
|
||||
if (data.testCount > 0) setTimeout(onClose, 1500)
|
||||
if (data?.scanning) {
|
||||
setIsScanning(true)
|
||||
scanPoller.start()
|
||||
} else {
|
||||
onClose()
|
||||
}
|
||||
@@ -98,14 +138,15 @@ export default function ConfigModal({ onClose }) {
|
||||
setIsRescanning(true)
|
||||
try {
|
||||
const data = await apiFetch('/config/rescan-results', { method: 'POST' })
|
||||
if (data?.testCount !== null && data?.testCount !== undefined) {
|
||||
setScanResult({ testCount: data.testCount, completedCount: data.completedCount })
|
||||
}
|
||||
if (data?.scanning) {
|
||||
scanPoller.start()
|
||||
} else {
|
||||
setIsRescanning(false)
|
||||
queryClient.invalidateQueries({ queryKey: ['stats'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['tests'] })
|
||||
}
|
||||
} catch (err) {
|
||||
setSaveError(err?.message ?? 'Rescan failed')
|
||||
} finally {
|
||||
setIsRescanning(false)
|
||||
}
|
||||
}
|
||||
@@ -134,16 +175,19 @@ export default function ConfigModal({ onClose }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Scan result banner */}
|
||||
{scanResult !== null && (
|
||||
<div className={`rounded-lg px-4 py-3 text-sm border ${
|
||||
scanResult.testCount === 0
|
||||
? 'bg-amber-950/50 border-amber-700 text-amber-300'
|
||||
: 'bg-emerald-950/50 border-emerald-700 text-emerald-300'
|
||||
}`}>
|
||||
{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 && (
|
||||
<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>
|
||||
Scan in progress — the table will refresh when complete.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Rescan indicator (rescan button used) */}
|
||||
{!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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -4,10 +4,5 @@ import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
server: {
|
||||
host: true,
|
||||
proxy: {
|
||||
'/api': 'http://localhost:3001',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
+17
-8
@@ -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
|
||||
|
||||
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
@@ -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
@@ -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("/")
|
||||
|
||||
Reference in New Issue
Block a user