fixed timeout issue
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -14,12 +47,20 @@ export default function ConfigModal({ onClose }) {
|
|||||||
const { data: config, isLoading } = useConfig()
|
const { data: config, isLoading } = useConfig()
|
||||||
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: ['tests'] })
|
||||||
}
|
}
|
||||||
queryClient.invalidateQueries({ queryKey: ['stats'] })
|
|
||||||
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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 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("/")
|
||||||
|
|||||||
Reference in New Issue
Block a user