diff --git a/dashboard/src/App.jsx b/dashboard/src/App.jsx index e77876e..fa49f9f 100644 --- a/dashboard/src/App.jsx +++ b/dashboard/src/App.jsx @@ -3,6 +3,7 @@ import { Settings } from 'lucide-react' import { useStats } from './hooks/useStats' import { useTests } from './hooks/useTests' import { useConfig } from './hooks/useConfig' +import cgw453Image from './assets/CGW453.PNG' import StatCard from './components/StatCard' import CompletionBar from './components/CompletionBar' import TimeDisplay from './components/TimeDisplay' @@ -97,34 +98,44 @@ export default function App() { {/* Stats row */} {!statsLoading && stats && ( -
-
- +
+ CGW453 test object -
- {stats.devices.map(d => ( -
+
+
{ - const typeStats = d.byType?.[type] ?? { completed: 0, total: 0 } - return `${type}: ${typeStats.completed}/${typeStats.total} tests` - })} + label="Overall" + value={`${((stats.overall.completionRate ?? 0) * 100).toFixed(1)}%`} + sub={`${stats.overall.completed} / ${stats.overall.total} tests`} + accent="text-emerald-400" /> - +
- ))} -
+ {stats.devices.map(d => ( +
+ { + const typeStats = d.byType?.[type] ?? { completed: 0, total: 0 } + return `${type}: ${typeStats.completed}/${typeStats.total} tests` + })} + /> + +
+ ))} +
+ +
(
-
- setForm(f => ({ ...f, [key]: e.target.value }))} - placeholder="C:\path\to\folder" - className="flex-1 bg-slate-800 border border-slate-700 text-slate-200 text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-blue-500" - /> - -
+ setForm(f => ({ ...f, [key]: e.target.value }))} + placeholder="C:\path\to\folder" + className="w-full bg-slate-800 border border-slate-700 text-slate-200 text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-blue-500" + />
))}
@@ -204,10 +194,7 @@ export default function ConfigModal({ onClose }) { {/* SMB Credentials */}
-

SMB Credentials

-

- Optional credentials for network shares used by target/results directories. -

+

Credentials for network shares

- - {browser && ( - setForm(f => ({ ...f, [browser]: path }))} - onClose={() => setBrowser(null)} - /> - )} ) } diff --git a/dashboard/src/components/DirectoryBrowser.jsx b/dashboard/src/components/DirectoryBrowser.jsx deleted file mode 100644 index 24afa24..0000000 --- a/dashboard/src/components/DirectoryBrowser.jsx +++ /dev/null @@ -1,139 +0,0 @@ -import { useState } from 'react' -import { browse } from '../lib/api' - -export default function DirectoryBrowser({ onSelect, onClose }) { - const [current, setCurrent] = useState(null) // null = roots - const [parent, setParent] = useState(null) - const [dirs, setDirs] = useState(null) // null = not loaded yet - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - const [manualPath, setManualPath] = useState('') - - async function navigate(path) { - setLoading(true) - setError(null) - try { - const data = await browse(path) - setCurrent(data.path) - setParent(data.parent) - setDirs(data.dirs) - setManualPath(data.path ?? '') - } catch (e) { - setError(e.message) - } finally { - setLoading(false) - } - } - - function goToManualPath() { - const path = manualPath.trim() - if (!path) { - navigate(null) - return - } - navigate(path) - } - - // Load roots on first render - if (dirs === null && !loading && !error) { - navigate(null) - } - - const normalizedCurrent = current ?? '' - const normalizedPath = normalizedCurrent.replace(/\\/g, '/') - const isUnixPath = normalizedCurrent.startsWith('/') - const breadcrumbs = normalizedPath ? normalizedPath.split('/').filter(Boolean) : [] - - function breadcrumbPathAt(index) { - const parts = breadcrumbs.slice(0, index + 1) - if (isUnixPath) { - return `/${parts.join('/')}` - } - return parts.join('\\') + (index === 0 ? '\\' : '') - } - - return ( -
-
- {/* Header */} -
- Browse Folder - -
- - {/* Breadcrumb */} -
- - {breadcrumbs.map((part, i) => { - const path = breadcrumbPathAt(i) - return ( - - / - - - ) - })} -
- - {/* Directory list */} -
- {loading && ( -

Loading…

- )} - {error && ( -

{error}

- )} - {!loading && !error && parent !== null && ( - - )} - {!loading && !error && dirs?.map(d => ( - - ))} - {!loading && !error && dirs?.length === 0 && ( -

No subdirectories

- )} -
- - {/* Footer */} -
-

- {current ?? 'Select a folder'} -

-
- - -
-
-
-
- ) -} diff --git a/dashboard/src/components/TimeDisplay.jsx b/dashboard/src/components/TimeDisplay.jsx index d1397cf..9b1c3d5 100644 --- a/dashboard/src/components/TimeDisplay.jsx +++ b/dashboard/src/components/TimeDisplay.jsx @@ -52,7 +52,7 @@ export default function TimeDisplay({ elapsedSeconds, estimatedRemainingSeconds,

Time Elapsed

-

+

{fmt(elapsedSeconds) ?? '—'}

diff --git a/dashboard/src/lib/api.js b/dashboard/src/lib/api.js index 1b13c43..e69b048 100644 --- a/dashboard/src/lib/api.js +++ b/dashboard/src/lib/api.js @@ -21,4 +21,3 @@ export const getTests = (params = {}) => { } export const getConfig = () => apiFetch('/config') export const saveConfig = (body) => apiFetch('/config', { method: 'POST', body: JSON.stringify(body) }) -export const browse = (path) => apiFetch(`/browse${path ? `?path=${encodeURIComponent(path)}` : ''}`) diff --git a/docker-compose.yml b/docker-compose.yml index bee5b5b..d57de57 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,12 +4,10 @@ services: container_name: dashboard-backend ports: - "3001:3001" - environment: - - BROWSE_ROOTS=/host volumes: - - ./server:/app - # Host file browsing root inside container (adjust HOST_BROWSE_ROOT if needed) - - ${HOST_BROWSE_ROOT:-C:/Users}:/host + - type: bind + source: ./server + target: /app restart: unless-stopped frontend: diff --git a/server/app.py b/server/app.py index a360072..d59c0c6 100644 --- a/server/app.py +++ b/server/app.py @@ -291,89 +291,6 @@ def rescan_results_route(): return jsonify({"ok": True, "testCount": len(tests), "completedCount": completed}) -@app.get("/api/browse") -def browse_route(): - def _normalize_request_path(path): - if not path: - return path - p = path.strip() - if os.name == "nt": - p = p.replace("/", "\\") - return p - - def _configured_roots(): - raw = os.getenv("BROWSE_ROOTS", "").strip() - if not raw: - return [] - roots = [] - for part in raw.split(";"): - p = part.strip() - if not p: - continue - abs_p = os.path.abspath(p) - if os.path.isdir(abs_p): - roots.append(abs_p) - return roots - - def _is_within_allowed_roots(path, roots): - if not roots: - return True - normalized = os.path.normcase(os.path.abspath(path)) - for root in roots: - try: - if os.path.commonpath([normalized, root]) == root: - return True - except ValueError: - continue - return False - - roots = _configured_roots() - req_path = _normalize_request_path(request.args.get("path")) - - if not req_path: - if roots: - dirs = [] - for root in roots: - display_name = os.path.basename(root.rstrip("/\\")) or root - dirs.append({"name": display_name, "path": root}) - return jsonify({"path": None, "parent": None, "dirs": dirs}) - - if os.name == "nt": - dirs = [] - for drive_idx in range(65, 91): - drive = f"{chr(drive_idx)}:\\" - if os.path.exists(drive): - dirs.append({"name": drive, "path": drive}) - else: - dirs = [{"name": "/", "path": "/"}] - - return jsonify({"path": None, "parent": None, "dirs": dirs}) - - if not os.path.exists(req_path): - return jsonify({"error": "Path does not exist"}), 400 - if not os.path.isdir(req_path): - return jsonify({"error": "Path is not a directory"}), 400 - if not _is_within_allowed_roots(req_path, roots): - return jsonify({"error": "Path is outside allowed browse roots"}), 403 - - try: - dirs = [] - for name in os.listdir(req_path): - child = os.path.join(req_path, name) - if os.path.isdir(child) and not name.startswith("."): - dirs.append({"name": name, "path": child}) - - dirs.sort(key=lambda item: item["name"].lower()) - except OSError: - return jsonify({"error": "Cannot read directory"}), 403 - - parent = os.path.dirname(req_path) - at_root = os.path.normcase(parent) == os.path.normcase(req_path) - if roots and parent and not _is_within_allowed_roots(parent, roots): - parent = None - return jsonify({"path": req_path, "parent": None if at_root else parent, "dirs": dirs}) - - @app.get("/api/events") def events_route(): headers = { diff --git a/server/dashboard.db-shm b/server/dashboard.db-shm index 73f4f32..c641ec4 100644 Binary files a/server/dashboard.db-shm and b/server/dashboard.db-shm differ diff --git a/server/dashboard.db-wal b/server/dashboard.db-wal index fb2f5c5..fe16290 100644 Binary files a/server/dashboard.db-wal and b/server/dashboard.db-wal differ