From d239de257d82c5bd21bbc94cd9d8161717ac9c33 Mon Sep 17 00:00:00 2001 From: Mia Wu Date: Thu, 28 May 2026 11:01:47 -0400 Subject: [PATCH] remove browse, add image --- dashboard/src/App.jsx | 53 ++++--- dashboard/src/assets/CGW453.PNG | Bin 0 -> 20905 bytes dashboard/src/components/ConfigModal.jsx | 36 +---- dashboard/src/components/DirectoryBrowser.jsx | 139 ------------------ dashboard/src/components/TimeDisplay.jsx | 2 +- dashboard/src/lib/api.js | 1 - docker-compose.yml | 8 +- server/app.py | 83 ----------- server/dashboard.db-shm | Bin 32768 -> 32768 bytes server/dashboard.db-wal | Bin 4268352 -> 4268352 bytes 10 files changed, 44 insertions(+), 278 deletions(-) create mode 100644 dashboard/src/assets/CGW453.PNG delete mode 100644 dashboard/src/components/DirectoryBrowser.jsx 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` + })} + /> + +
+ ))} +
+ +
o~>(TYBIccAt3D>(r@p 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 (
-
- 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 73f4f32f56cb8ad2e15df1bd628b798cc7724fcd..c641ec4867b6d00496de70e5ed0b90aab9d1a597 100644 GIT binary patch delta 2050 zcmb8wYfw~W7zgm@d3V`emStTItD+);au-n)FQq9)ilP~rp@HEg3?&3aL?GBjbLvyp z@HW$zZc|N>nP%SYM&+&h)w06!el066NmknbIqYHj*0eJ}IOqR9&-1>+>^ZcwsFoHL zj5#`l-t$x7~#4>J({_8i+NOlMVIkOX75DspL01rfh7kYpXqQHP?@Iz1N1-&7LC`f&9 z(HHtbEcAys7y$8*00UtVBtjA-Lkgrq8l=Nu$N>4XX2MVy23as1MnE=Cc$LLgDEf-@?jcGhg)F=+y(`3JIsVTU>4j7g)kfD zV1K#`il7+g!rf2;^I$$KfQ4`ml)@so7s_BUEP-;k4=P|OR6-S0!!oFW1j-Ox~$yM| z=@MP0E95kLd7lhZ!{sJmo5dGoQ20yyGQYwbc@u^uxR28bI!ULfzZBK+q0q2&U6AKd zE(vRRzj>ysc{yr?1mkt%6gOW)xFUQVujdW?D!(SfbmCM)#Yss8|Ij^XM!rLZOIXkE zn}a&@Q&qMEg?!1Z>YV1~Ef~;G51j5-17xY3f3yZDw*&$HzzX=XT=7;JWp4U(ctK4ul!z2r4_<4>$eR~EVW4|J^Lon~K5v0o)h zQ8^#Cii{oL` z`6LMF_IV!uEELAsJoG27oB4I~4E_bfRI-%K;FH~bQDz^1ZUs&|US%Eh7w#MQ4YSWE z?WIzrY_V=FHTYDhG)9kGdvwC&*GDG!}u!6tt4ohl$%51B&DS8KwK#J!Hev+T!r}-Jqkm_2SgMJFt zr|RLgv8uPk3;7#sh*IWOx?_cl-<8Ku4KV}TA^O>BR55162dySuRBtGogcbar9M~6) ze+-#;+0Ug%=yA%FrD{IkU7y)#s6J-B-q7ggZ$k_5%HB13w+!)&!MpM?+Cp1tsKmAW uOZR|`X0PgNPSxw1J^Wp0Ax;+td@4*9DtV6#X$`v6FuAX@Me?sGYyJmArSx3@ delta 805 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 NLhC^sr^SVC{(nWJU