Merge pull request #4 from Mia-Wu_wnc/login

fixed timeout issue
This commit is contained in:
2026-06-02 14:27:27 -04:00
committed by GitHub
6 changed files with 154 additions and 52 deletions
+1 -2
View File
@@ -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>
+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 { 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>
)}
+1 -6
View File
@@ -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
View File
@@ -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
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 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("/")