| import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react' | |
| import { | |
| MdApps, | |
| MdDns, | |
| MdFolderOpen, | |
| MdMemory, | |
| MdRefresh, | |
| MdSchedule, | |
| MdSettingsApplications, | |
| MdStorage, | |
| MdViewModule, | |
| MdWarning, | |
| MdOpenInNew, | |
| MdArrowUpward, | |
| MdSearch, | |
| MdNoteAdd, | |
| MdDeleteForever, | |
| MdSave, | |
| MdStopCircle, | |
| MdComputer, | |
| MdCode, | |
| MdContentCopy, | |
| MdDataUsage, | |
| } from 'react-icons/md' | |
| import { IconButton } from './components/IconButton' | |
| import { formatBytes, formatSizeKb, formatUptime } from './utils/format' | |
| import type { | |
| DirEntry, | |
| DriveInfo, | |
| InstalledApp, | |
| NavId, | |
| NetworkRow, | |
| ProcessRow, | |
| ServiceRow, | |
| SystemSnapshot, | |
| } from './types' | |
| const api = typeof window !== 'undefined' ? window.auditor : undefined | |
| function useNotes() { | |
| const [map, setMap] = useState<Record<string, string>>({}) | |
| const refresh = useCallback(async () => { | |
| if (!api) return | |
| setMap(await api.notesGetAll()) | |
| }, []) | |
| useEffect(() => { | |
| void refresh() | |
| }, [refresh]) | |
| const setNote = useCallback( | |
| async (key: string, value: string) => { | |
| if (!api) return | |
| setMap(await api.notesSet(key, value)) | |
| }, | |
| [] | |
| ) | |
| const del = useCallback( | |
| async (key: string) => { | |
| if (!api) return | |
| setMap(await api.notesDelete(key)) | |
| }, | |
| [] | |
| ) | |
| return { map, refresh, setNote, del } | |
| } | |
| const NAV: { id: NavId; label: string; icon: ReactNode }[] = [ | |
| { id: 'overview', label: 'Overview', icon: <MdViewModule /> }, | |
| { id: 'storage', label: 'Storage & volumes', icon: <MdStorage /> }, | |
| { id: 'filesystem', label: 'Folders & files', icon: <MdFolderOpen /> }, | |
| { id: 'processes', label: 'Processes', icon: <MdMemory /> }, | |
| { id: 'services', label: 'Services', icon: <MdSettingsApplications /> }, | |
| { id: 'apps', label: 'Installed software', icon: <MdApps /> }, | |
| { id: 'network', label: 'Network', icon: <MdDns /> }, | |
| { id: 'environment', label: 'Environment', icon: <MdCode /> }, | |
| { id: 'startup', label: 'Startup items', icon: <MdComputer /> }, | |
| { id: 'scheduled', label: 'Scheduled tasks', icon: <MdSchedule /> }, | |
| { id: 'features', label: 'Windows features', icon: <MdWarning /> }, | |
| ] | |
| export default function App() { | |
| const [nav, setNav] = useState<NavId>('overview') | |
| const [status, setStatus] = useState('') | |
| const [err, setErr] = useState('') | |
| const notes = useNotes() | |
| const [sys, setSys] = useState<SystemSnapshot | null>(null) | |
| const [drives, setDrives] = useState<DriveInfo[]>([]) | |
| const [path, setPath] = useState('') | |
| const [entries, setEntries] = useState<DirEntry[]>([]) | |
| const [proc, setProc] = useState<ProcessRow[]>([]) | |
| const [svc, setSvc] = useState<ServiceRow[]>([]) | |
| const [apps, setApps] = useState<InstalledApp[]>([]) | |
| const [net, setNet] = useState<NetworkRow[]>([]) | |
| const [envText, setEnvText] = useState('') | |
| const [startupBlocks, setStartupBlocks] = useState<{ path: string; entries: DirEntry[] }[]>([]) | |
| const [tasks, setTasks] = useState<{ name: string; state: string }[]>([]) | |
| const [feat, setFeat] = useState('') | |
| const [tempAudit, setTempAudit] = useState<{ path: string; bytes: number; files: number; truncated: boolean }[]>( | |
| [] | |
| ) | |
| const [selectedKey, setSelectedKey] = useState<string | null>(null) | |
| const [noteDraft, setNoteDraft] = useState('') | |
| const [largeRoot, setLargeRoot] = useState('') | |
| const [largeMinMb, setLargeMinMb] = useState(100) | |
| const [largeHits, setLargeHits] = useState<{ path: string; sizeBytes: number }[]>([]) | |
| const [folderTotal, setFolderTotal] = useState<{ | |
| bytes: number | |
| files: number | |
| truncated: boolean | |
| } | null>(null) | |
| const [procFilter, setProcFilter] = useState('') | |
| const [svcFilter, setSvcFilter] = useState('') | |
| const [appFilter, setAppFilter] = useState('') | |
| const clearMsg = () => { | |
| setErr('') | |
| setStatus('') | |
| } | |
| const loadSystem = useCallback(async (opts?: { silent?: boolean }) => { | |
| if (!api) return | |
| if (!opts?.silent) clearMsg() | |
| try { | |
| setSys((await api.system()) as SystemSnapshot) | |
| if (!opts?.silent) setStatus('System snapshot updated') | |
| } catch (e) { | |
| if (!opts?.silent) setErr(String((e as Error).message)) | |
| } | |
| }, []) | |
| const loadDrives = useCallback(async () => { | |
| if (!api) return | |
| clearMsg() | |
| try { | |
| setDrives((await api.drives()) as DriveInfo[]) | |
| setStatus('Volumes refreshed') | |
| } catch (e) { | |
| setErr(String((e as Error).message)) | |
| } | |
| }, []) | |
| const loadDir = useCallback(async () => { | |
| if (!api || !path.trim()) return | |
| clearMsg() | |
| try { | |
| setStatus('Listing folder (measuring sizes)…') | |
| setEntries((await api.listDir(path.trim(), { maxEntries: 800 })) as DirEntry[]) | |
| setStatus('Directory listed') | |
| } catch (e) { | |
| setErr(String((e as Error).message)) | |
| } | |
| }, [path]) | |
| const loadProcesses = useCallback(async () => { | |
| if (!api) return | |
| clearMsg() | |
| setStatus('Loading processes…') | |
| try { | |
| const rows = (await api.processes()) as ProcessRow[] | |
| setProc(rows) | |
| setStatus(`Processes loaded (${rows.length})`) | |
| } catch (e) { | |
| setErr(String((e as Error).message)) | |
| } | |
| }, []) | |
| const loadServices = useCallback(async () => { | |
| if (!api) return | |
| clearMsg() | |
| setStatus('Loading services…') | |
| try { | |
| setSvc((await api.services()) as ServiceRow[]) | |
| setStatus('Services loaded') | |
| } catch (e) { | |
| setErr(String((e as Error).message)) | |
| } | |
| }, []) | |
| const loadApps = useCallback(async () => { | |
| if (!api) return | |
| clearMsg() | |
| try { | |
| setApps((await api.installed()) as InstalledApp[]) | |
| setStatus('Installed programs enumerated') | |
| } catch (e) { | |
| setErr(String((e as Error).message)) | |
| } | |
| }, []) | |
| const loadNetwork = useCallback(async () => { | |
| if (!api) return | |
| clearMsg() | |
| try { | |
| setNet((await api.network()) as NetworkRow[]) | |
| setStatus('Network interfaces read') | |
| } catch (e) { | |
| setErr(String((e as Error).message)) | |
| } | |
| }, []) | |
| const loadEnv = useCallback(async () => { | |
| if (!api) return | |
| clearMsg() | |
| try { | |
| const o = (await api.env()) as Record<string, string> | |
| const lines = Object.keys(o) | |
| .sort() | |
| .map((k) => `${k}=${o[k]}`) | |
| setEnvText(lines.join('\n')) | |
| setStatus('Environment loaded') | |
| } catch (e) { | |
| setErr(String((e as Error).message)) | |
| } | |
| }, []) | |
| const loadStartup = useCallback(async () => { | |
| if (!api) return | |
| clearMsg() | |
| try { | |
| setStartupBlocks((await api.startup()) as { path: string; entries: DirEntry[] }[]) | |
| setStatus('Startup folders scanned') | |
| } catch (e) { | |
| setErr(String((e as Error).message)) | |
| } | |
| }, []) | |
| const loadTasks = useCallback(async () => { | |
| if (!api) return | |
| clearMsg() | |
| setStatus('Loading scheduled tasks…') | |
| try { | |
| setTasks((await api.tasks()) as { name: string; state: string }[]) | |
| setStatus('Scheduled tasks loaded') | |
| } catch (e) { | |
| setErr(String((e as Error).message)) | |
| } | |
| }, []) | |
| const loadFeatures = useCallback(async () => { | |
| if (!api) return | |
| clearMsg() | |
| setStatus('Querying optional features (may take a minute)…') | |
| try { | |
| setFeat(await api.features()) | |
| setStatus('Feature list retrieved') | |
| } catch (e) { | |
| setErr(String((e as Error).message)) | |
| } | |
| }, []) | |
| const loadTemp = useCallback(async () => { | |
| if (!api) return | |
| clearMsg() | |
| setStatus('Measuring temp folders…') | |
| try { | |
| setTempAudit( | |
| (await api.temp()) as { path: string; bytes: number; files: number; truncated: boolean }[] | |
| ) | |
| setStatus('Temp audit complete') | |
| } catch (e) { | |
| setErr(String((e as Error).message)) | |
| } | |
| }, []) | |
| useEffect(() => { | |
| if (!api) return | |
| void loadSystem() | |
| const t = setInterval(() => void loadSystem({ silent: true }), 60_000) | |
| return () => clearInterval(t) | |
| }, [loadSystem]) | |
| const [pathSeeded, setPathSeeded] = useState(false) | |
| useEffect(() => { | |
| if (!api || pathSeeded) return | |
| if (sys?.homedir) { | |
| setPath(sys.homedir) | |
| setPathSeeded(true) | |
| } | |
| }, [api, sys, pathSeeded]) | |
| useEffect(() => { | |
| if (selectedKey && notes.map[selectedKey] !== undefined) setNoteDraft(notes.map[selectedKey] ?? '') | |
| else if (selectedKey) setNoteDraft('') | |
| }, [selectedKey, notes.map]) | |
| const selectForNote = (key: string) => { | |
| setSelectedKey(key) | |
| setNoteDraft(notes.map[key] ?? '') | |
| } | |
| const saveNote = async () => { | |
| if (!selectedKey) return | |
| await notes.setNote(selectedKey, noteDraft) | |
| setStatus('Note saved') | |
| } | |
| const copyText = async (text: string) => { | |
| if (!api) return | |
| try { | |
| if (navigator.clipboard?.writeText) { | |
| await navigator.clipboard.writeText(text) | |
| } else { | |
| await api.clipboardWriteText(text) | |
| } | |
| setStatus('Copied to clipboard') | |
| } catch { | |
| try { | |
| await api.clipboardWriteText(text) | |
| setStatus('Copied to clipboard') | |
| } catch (e) { | |
| setErr(String((e as Error).message)) | |
| } | |
| } | |
| } | |
| const computeFolderTotal = async () => { | |
| if (!api || !path.trim()) return | |
| clearMsg() | |
| setStatus('Computing folder size (capped walk)…') | |
| try { | |
| const r = (await api.folderSize(path.trim())) as { bytes: number; files: number; truncated: boolean } | |
| setFolderTotal(r) | |
| setStatus( | |
| `Folder total ${formatSizeKb(r.bytes, { truncated: r.truncated })} — ${r.files} files counted${r.truncated ? ' (hit file cap)' : ''}` | |
| ) | |
| } catch (e) { | |
| setErr(String((e as Error).message)) | |
| } | |
| } | |
| const runLargeScan = async () => { | |
| if (!api) return | |
| const root = largeRoot.trim() || path.trim() | |
| if (!root) return | |
| clearMsg() | |
| setStatus('Scanning for large files…') | |
| try { | |
| const hits = (await api.largeFiles(root, largeMinMb * 1024 * 1024, 80)) as { | |
| path: string | |
| sizeBytes: number | |
| }[] | |
| setLargeHits(hits) | |
| setStatus(`Found ${hits.length} files`) | |
| } catch (e) { | |
| setErr(String((e as Error).message)) | |
| } | |
| } | |
| const killPid = async (pid: number) => { | |
| if (!api) return | |
| if (!window.confirm(`End process ${pid}? Unsaved data in that process may be lost.`)) return | |
| try { | |
| await api.killProcess(pid) | |
| setStatus(`Sent terminate to PID ${pid}`) | |
| void loadProcesses() | |
| } catch (e) { | |
| setErr(String((e as Error).message)) | |
| } | |
| } | |
| const openExplorer = async (p: string) => { | |
| if (!api) return | |
| try { | |
| await api.openExplorer(p) | |
| } catch (e) { | |
| setErr(String((e as Error).message)) | |
| } | |
| } | |
| const filteredProc = useMemo(() => { | |
| const q = procFilter.trim().toLowerCase() | |
| if (!q) return proc | |
| return proc.filter( | |
| (p) => | |
| p.name.toLowerCase().includes(q) || | |
| String(p.pid).includes(q) || | |
| (p.commandLine ?? '').toLowerCase().includes(q) | |
| ) | |
| }, [proc, procFilter]) | |
| const filteredSvc = useMemo(() => { | |
| const q = svcFilter.trim().toLowerCase() | |
| if (!q) return svc | |
| return svc.filter( | |
| (s) => | |
| s.name.toLowerCase().includes(q) || | |
| s.displayName.toLowerCase().includes(q) || | |
| s.state.toLowerCase().includes(q) | |
| ) | |
| }, [svc, svcFilter]) | |
| const filteredApps = useMemo(() => { | |
| const q = appFilter.trim().toLowerCase() | |
| if (!q) return apps | |
| return apps.filter( | |
| (a) => | |
| a.name.toLowerCase().includes(q) || | |
| a.publisher.toLowerCase().includes(q) || | |
| a.installLocation.toLowerCase().includes(q) | |
| ) | |
| }, [apps, appFilter]) | |
| if (!api) { | |
| return ( | |
| <div className="content-scroll"> | |
| <p className="status-msg error"> | |
| This UI must be run inside the Electron shell so it can reach your system. Use{' '} | |
| <span className="mono">npm run dev</span>. | |
| </p> | |
| </div> | |
| ) | |
| } | |
| const memUsed = sys ? sys.totalMem - sys.freeMem : 0 | |
| const memPct = sys && sys.totalMem ? Math.round((memUsed / sys.totalMem) * 100) : 0 | |
| return ( | |
| <div className="app-shell"> | |
| <aside className="sidebar"> | |
| <div className="sidebar-brand"> | |
| <h1>Computer Auditor</h1> | |
| <p>System inspection dashboard</p> | |
| </div> | |
| {NAV.map((item) => ( | |
| <button | |
| key={item.id} | |
| type="button" | |
| className={`nav-item ${nav === item.id ? 'active' : ''}`} | |
| onClick={() => setNav(item.id)} | |
| > | |
| {item.icon} | |
| {item.label} | |
| </button> | |
| ))} | |
| </aside> | |
| <div className="main"> | |
| <div className="toolbar"> | |
| <h2 className="panel-title">{NAV.find((n) => n.id === nav)?.label}</h2> | |
| <span className="toolbar-spacer" /> | |
| <IconButton label="Refresh system snapshot" onClick={() => void loadSystem()}> | |
| <MdRefresh /> | |
| </IconButton> | |
| {status && <span className="status-msg">{status}</span>} | |
| {err && <span className="status-msg error">{err}</span>} | |
| </div> | |
| <div className="content-scroll"> | |
| {nav === 'overview' && ( | |
| <> | |
| <div className="card-grid"> | |
| <div className="card"> | |
| <div className="card-label">Host</div> | |
| <div className="card-value">{sys?.hostname ?? '—'}</div> | |
| <div className="card-sub mono">{sys?.userInfo}</div> | |
| </div> | |
| <div className="card"> | |
| <div className="card-label">Platform</div> | |
| <div className="card-value">{sys?.platform ?? '—'}</div> | |
| <div className="card-sub mono"> | |
| {sys?.release} / {sys?.arch} | |
| </div> | |
| </div> | |
| <div className="card"> | |
| <div className="card-label">Uptime</div> | |
| <div className="card-value">{sys ? formatUptime(sys.uptimeSec) : '—'}</div> | |
| </div> | |
| <div className="card"> | |
| <div className="card-label">Memory</div> | |
| <div className="card-value"> | |
| {sys ? `${memPct}% used` : '—'} | |
| </div> | |
| <div className="card-sub"> | |
| {sys ? `${formatBytes(memUsed)} / ${formatBytes(sys.totalMem)}` : ''} | |
| </div> | |
| {sys && ( | |
| <div className="bar-track"> | |
| <div className="bar-fill" style={{ width: `${memPct}%` }} /> | |
| </div> | |
| )} | |
| </div> | |
| <div className="card"> | |
| <div className="card-label">CPU</div> | |
| <div className="card-value">{sys?.cpuCount ?? '—'} logical</div> | |
| <div className="card-sub">{sys?.cpuModel}</div> | |
| </div> | |
| <div className="card"> | |
| <div className="card-label">Home & temp</div> | |
| <div className="card-sub mono">{sys?.homedir}</div> | |
| <div className="card-sub mono">{sys?.tmpdir}</div> | |
| </div> | |
| </div> | |
| <div style={{ marginTop: 20 }}> | |
| <div className="input-row"> | |
| <IconButton label="Load logical volumes" onClick={() => void loadDrives()}> | |
| <MdStorage /> | |
| </IconButton> | |
| <IconButton label="Measure temp directories" onClick={() => void loadTemp()}> | |
| <MdFolderOpen /> | |
| </IconButton> | |
| </div> | |
| {drives.length > 0 && ( | |
| <table className="data-table"> | |
| <thead> | |
| <tr> | |
| <th>Volume</th> | |
| <th>Label</th> | |
| <th>Used</th> | |
| <th>Free</th> | |
| <th>Total</th> | |
| <th /> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {drives.map((d) => { | |
| const pct = d.totalBytes ? Math.round((d.usedBytes / d.totalBytes) * 100) : 0 | |
| return ( | |
| <tr key={d.letter}> | |
| <td className="mono">{d.letter}</td> | |
| <td>{d.label || '—'}</td> | |
| <td> | |
| {formatBytes(d.usedBytes)} ({pct}%) | |
| </td> | |
| <td>{formatBytes(d.freeBytes)}</td> | |
| <td>{formatBytes(d.totalBytes)}</td> | |
| <td> | |
| <IconButton | |
| label="Open in Explorer" | |
| onClick={() => void openExplorer(d.mount + '\\')} | |
| > | |
| <MdOpenInNew /> | |
| </IconButton> | |
| <IconButton | |
| label="Attach note to this volume" | |
| onClick={() => selectForNote(`drive:${d.letter}`)} | |
| > | |
| <MdNoteAdd /> | |
| </IconButton> | |
| </td> | |
| </tr> | |
| ) | |
| })} | |
| </tbody> | |
| </table> | |
| )} | |
| {tempAudit.length > 0 && ( | |
| <table className="data-table" style={{ marginTop: 16 }}> | |
| <thead> | |
| <tr> | |
| <th>Temp path</th> | |
| <th>Size</th> | |
| <th>Files (sampled)</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {tempAudit.map((t) => ( | |
| <tr key={t.path}> | |
| <td className="mono">{t.path}</td> | |
| <td>{formatBytes(t.bytes)}</td> | |
| <td> | |
| {t.files} | |
| {t.truncated ? ' (cap reached)' : ''} | |
| </td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| )} | |
| </div> | |
| </> | |
| )} | |
| {nav === 'storage' && ( | |
| <> | |
| <div className="input-row"> | |
| <IconButton label="Refresh drive list" onClick={() => void loadDrives()}> | |
| <MdRefresh /> | |
| </IconButton> | |
| </div> | |
| <table className="data-table"> | |
| <thead> | |
| <tr> | |
| <th>Volume</th> | |
| <th>Label</th> | |
| <th>Used</th> | |
| <th>Free</th> | |
| <th>Total</th> | |
| <th /> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {drives.map((d) => { | |
| const pct = d.totalBytes ? Math.round((d.usedBytes / d.totalBytes) * 100) : 0 | |
| return ( | |
| <tr key={d.letter}> | |
| <td className="mono">{d.letter}</td> | |
| <td>{d.label || '—'}</td> | |
| <td> | |
| {formatBytes(d.usedBytes)} ({pct}%) | |
| <div className="bar-track"> | |
| <div className="bar-fill" style={{ width: `${pct}%` }} /> | |
| </div> | |
| </td> | |
| <td>{formatBytes(d.freeBytes)}</td> | |
| <td>{formatBytes(d.totalBytes)}</td> | |
| <td> | |
| <IconButton label="Open volume" onClick={() => void openExplorer(d.mount + '\\')}> | |
| <MdOpenInNew /> | |
| </IconButton> | |
| <IconButton label="Note" onClick={() => selectForNote(`drive:${d.letter}`)}> | |
| <MdNoteAdd /> | |
| </IconButton> | |
| </td> | |
| </tr> | |
| ) | |
| })} | |
| </tbody> | |
| </table> | |
| </> | |
| )} | |
| {nav === 'filesystem' && ( | |
| <> | |
| <div className="input-row"> | |
| <input | |
| type="text" | |
| value={path} | |
| onChange={(e) => setPath(e.target.value)} | |
| placeholder="Folder path" | |
| className="mono" | |
| style={{ flex: 1, minWidth: 280 }} | |
| /> | |
| <IconButton label="List directory" onClick={() => void loadDir()}> | |
| <MdRefresh /> | |
| </IconButton> | |
| <IconButton label="Parent folder" onClick={() => setPath((p) => p.replace(/[/\\][^/\\]*$/, ''))}> | |
| <MdArrowUpward /> | |
| </IconButton> | |
| <IconButton label="Open in Explorer" onClick={() => void openExplorer(path)}> | |
| <MdOpenInNew /> | |
| </IconButton> | |
| <IconButton label="Note on this path" onClick={() => selectForNote(`path:${path}`)}> | |
| <MdNoteAdd /> | |
| </IconButton> | |
| <IconButton label="Copy current path" onClick={() => void copyText(path)}> | |
| <MdContentCopy /> | |
| </IconButton> | |
| <IconButton label="Compute folder size (recursive, capped)" onClick={() => void computeFolderTotal()}> | |
| <MdDataUsage /> | |
| </IconButton> | |
| </div> | |
| {folderTotal && ( | |
| <p className="status-msg" style={{ marginTop: 0 }}> | |
| Aggregated: {formatSizeKb(folderTotal.bytes, { truncated: folderTotal.truncated })} —{' '} | |
| {folderTotal.files} files | |
| {folderTotal.truncated ? ' (enumeration capped for safety)' : ''} | |
| </p> | |
| )} | |
| <div className="input-row"> | |
| <input | |
| type="text" | |
| value={largeRoot} | |
| onChange={(e) => setLargeRoot(e.target.value)} | |
| placeholder="Large-file scan root (defaults to path above)" | |
| className="mono" | |
| style={{ flex: 1, minWidth: 240 }} | |
| /> | |
| <input | |
| type="number" | |
| value={largeMinMb} | |
| min={1} | |
| onChange={(e) => setLargeMinMb(Number(e.target.value) || 100)} | |
| title="Minimum size MB" | |
| /> | |
| <IconButton label="Find large files" onClick={() => void runLargeScan()}> | |
| <MdSearch /> | |
| </IconButton> | |
| </div> | |
| <table className="data-table"> | |
| <thead> | |
| <tr> | |
| <th>Name</th> | |
| <th>Type</th> | |
| <th>Size (KB)</th> | |
| <th /> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {entries.map((e) => ( | |
| <tr | |
| key={e.fullPath} | |
| className={selectedKey === `path:${e.fullPath}` ? 'selected' : ''} | |
| onDoubleClick={() => e.isDirectory && setPath(e.fullPath)} | |
| > | |
| <td className="mono">{e.name}</td> | |
| <td>{e.isDirectory ? 'Folder' : 'File'}</td> | |
| <td> | |
| {e.error | |
| ? e.error | |
| : formatSizeKb(e.sizeBytes, { truncated: e.sizeTruncated })} | |
| </td> | |
| <td> | |
| {e.isDirectory && ( | |
| <IconButton label="Enter folder" onClick={() => setPath(e.fullPath)}> | |
| <MdFolderOpen /> | |
| </IconButton> | |
| )} | |
| <IconButton label="Explorer" onClick={() => void openExplorer(e.fullPath)}> | |
| <MdOpenInNew /> | |
| </IconButton> | |
| <IconButton label="Note" onClick={() => selectForNote(`path:${e.fullPath}`)}> | |
| <MdNoteAdd /> | |
| </IconButton> | |
| </td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| {largeHits.length > 0 && ( | |
| <> | |
| <h3 style={{ marginTop: 24, fontSize: 13 }}>Large files</h3> | |
| <table className="data-table"> | |
| <thead> | |
| <tr> | |
| <th>Path</th> | |
| <th>Size (KB)</th> | |
| <th /> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {largeHits.map((h) => ( | |
| <tr key={h.path}> | |
| <td className="mono">{h.path}</td> | |
| <td>{formatSizeKb(h.sizeBytes)}</td> | |
| <td> | |
| <IconButton label="Open location" onClick={() => void openExplorer(h.path)}> | |
| <MdOpenInNew /> | |
| </IconButton> | |
| <IconButton label="Note" onClick={() => selectForNote(`path:${h.path}`)}> | |
| <MdNoteAdd /> | |
| </IconButton> | |
| </td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </> | |
| )} | |
| </> | |
| )} | |
| {nav === 'processes' && ( | |
| <> | |
| <div className="input-row"> | |
| <IconButton label="Reload process list" onClick={() => void loadProcesses()}> | |
| <MdRefresh /> | |
| </IconButton> | |
| <input | |
| type="search" | |
| placeholder="Filter name, PID, command line" | |
| value={procFilter} | |
| onChange={(e) => setProcFilter(e.target.value)} | |
| style={{ minWidth: 240, flex: 1 }} | |
| /> | |
| </div> | |
| <table className="data-table"> | |
| <thead> | |
| <tr> | |
| <th>PID</th> | |
| <th>Name</th> | |
| <th>Memory</th> | |
| <th>CPU (session est.)</th> | |
| <th /> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {filteredProc.map((p) => ( | |
| <tr | |
| key={p.pid} | |
| className={selectedKey === `proc:${p.pid}` ? 'selected' : ''} | |
| title={p.commandLine} | |
| > | |
| <td className="mono">{p.pid}</td> | |
| <td>{p.name}</td> | |
| <td>{formatBytes(p.memoryBytes)}</td> | |
| <td>{p.cpuSeconds != null ? `${p.cpuSeconds}s` : '—'}</td> | |
| <td> | |
| <IconButton label="Add note for process" onClick={() => selectForNote(`proc:${p.pid}`)}> | |
| <MdNoteAdd /> | |
| </IconButton> | |
| <IconButton label="End process" onClick={() => void killPid(p.pid)}> | |
| <MdStopCircle /> | |
| </IconButton> | |
| </td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </> | |
| )} | |
| {nav === 'services' && ( | |
| <> | |
| <div className="input-row"> | |
| <IconButton label="Reload services" onClick={() => void loadServices()}> | |
| <MdRefresh /> | |
| </IconButton> | |
| <input | |
| type="search" | |
| placeholder="Filter" | |
| value={svcFilter} | |
| onChange={(e) => setSvcFilter(e.target.value)} | |
| style={{ minWidth: 240, flex: 1 }} | |
| /> | |
| </div> | |
| <table className="data-table"> | |
| <thead> | |
| <tr> | |
| <th>Name</th> | |
| <th>Display name</th> | |
| <th>State</th> | |
| <th>Start</th> | |
| <th /> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {filteredSvc.map((s) => ( | |
| <tr key={s.name} className={selectedKey === `svc:${s.name}` ? 'selected' : ''}> | |
| <td className="mono">{s.name}</td> | |
| <td>{s.displayName}</td> | |
| <td>{s.state}</td> | |
| <td>{s.startType}</td> | |
| <td> | |
| <IconButton label="Note" onClick={() => selectForNote(`svc:${s.name}`)}> | |
| <MdNoteAdd /> | |
| </IconButton> | |
| </td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </> | |
| )} | |
| {nav === 'apps' && ( | |
| <> | |
| <div className="input-row"> | |
| <IconButton label="Reload installed programs" onClick={() => void loadApps()}> | |
| <MdRefresh /> | |
| </IconButton> | |
| <input | |
| type="search" | |
| placeholder="Filter" | |
| value={appFilter} | |
| onChange={(e) => setAppFilter(e.target.value)} | |
| style={{ minWidth: 240, flex: 1 }} | |
| /> | |
| </div> | |
| <table className="data-table"> | |
| <thead> | |
| <tr> | |
| <th>Name</th> | |
| <th>Version</th> | |
| <th>Publisher</th> | |
| <th>Location</th> | |
| <th>Est. size</th> | |
| <th /> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {filteredApps.map((a) => ( | |
| <tr | |
| key={a.name + a.version} | |
| className={selectedKey === `app:${a.name}` ? 'selected' : ''} | |
| > | |
| <td>{a.name}</td> | |
| <td className="mono">{a.version}</td> | |
| <td>{a.publisher}</td> | |
| <td className="mono">{a.installLocation || '—'}</td> | |
| <td>{a.estimatedSizeKb ? `${(a.estimatedSizeKb / 1024).toFixed(1)} MB` : '—'}</td> | |
| <td> | |
| {a.installLocation ? ( | |
| <IconButton label="Open install folder" onClick={() => void openExplorer(a.installLocation)}> | |
| <MdOpenInNew /> | |
| </IconButton> | |
| ) : null} | |
| <IconButton label="Note" onClick={() => selectForNote(`app:${a.name}`)}> | |
| <MdNoteAdd /> | |
| </IconButton> | |
| </td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </> | |
| )} | |
| {nav === 'network' && ( | |
| <> | |
| <div className="input-row"> | |
| <IconButton label="Reload interfaces" onClick={() => void loadNetwork()}> | |
| <MdRefresh /> | |
| </IconButton> | |
| </div> | |
| <table className="data-table"> | |
| <thead> | |
| <tr> | |
| <th>Interface</th> | |
| <th>Address</th> | |
| <th>Family</th> | |
| <th>MAC</th> | |
| <th>Internal</th> | |
| <th /> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {net.map((n, i) => ( | |
| <tr key={`${n.name}-${n.address}-${i}`}> | |
| <td>{n.name}</td> | |
| <td className="mono">{n.address}</td> | |
| <td>{n.family}</td> | |
| <td className="mono">{n.mac ?? '—'}</td> | |
| <td>{n.internal ? 'yes' : 'no'}</td> | |
| <td> | |
| <IconButton label="Note" onClick={() => selectForNote(`net:${n.name}:${n.address}`)}> | |
| <MdNoteAdd /> | |
| </IconButton> | |
| </td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </> | |
| )} | |
| {nav === 'environment' && ( | |
| <> | |
| <div className="input-row"> | |
| <IconButton label="Reload environment block" onClick={() => void loadEnv()}> | |
| <MdRefresh /> | |
| </IconButton> | |
| <IconButton label="Note on whole environment view" onClick={() => selectForNote('env:all')}> | |
| <MdNoteAdd /> | |
| </IconButton> | |
| </div> | |
| <textarea className="mono" value={envText} readOnly spellCheck={false} style={{ minHeight: 400 }} /> | |
| </> | |
| )} | |
| {nav === 'startup' && ( | |
| <> | |
| <div className="input-row"> | |
| <IconButton label="Scan startup folders" onClick={() => void loadStartup()}> | |
| <MdRefresh /> | |
| </IconButton> | |
| </div> | |
| {startupBlocks.map((block) => ( | |
| <div key={block.path} style={{ marginBottom: 20 }}> | |
| <div className="card-label mono" style={{ marginBottom: 8 }}> | |
| {block.path} | |
| </div> | |
| <table className="data-table"> | |
| <thead> | |
| <tr> | |
| <th>Item</th> | |
| <th>Type</th> | |
| <th /> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {block.entries.map((e) => ( | |
| <tr key={e.fullPath}> | |
| <td className="mono">{e.name}</td> | |
| <td>{e.isDirectory ? 'Folder' : 'File'}</td> | |
| <td> | |
| <IconButton label="Open" onClick={() => void openExplorer(e.fullPath)}> | |
| <MdOpenInNew /> | |
| </IconButton> | |
| <IconButton label="Note" onClick={() => selectForNote(`path:${e.fullPath}`)}> | |
| <MdNoteAdd /> | |
| </IconButton> | |
| </td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| ))} | |
| </> | |
| )} | |
| {nav === 'scheduled' && ( | |
| <> | |
| <div className="input-row"> | |
| <IconButton label="Reload tasks" onClick={() => void loadTasks()}> | |
| <MdRefresh /> | |
| </IconButton> | |
| </div> | |
| <table className="data-table"> | |
| <thead> | |
| <tr> | |
| <th>Task</th> | |
| <th>State</th> | |
| <th /> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {tasks.map((t) => ( | |
| <tr key={t.name} className={selectedKey === `task:${t.name}` ? 'selected' : ''}> | |
| <td className="mono">{t.name}</td> | |
| <td>{t.state}</td> | |
| <td> | |
| <IconButton label="Note" onClick={() => selectForNote(`task:${t.name}`)}> | |
| <MdNoteAdd /> | |
| </IconButton> | |
| </td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </> | |
| )} | |
| {nav === 'features' && ( | |
| <> | |
| <div className="input-row"> | |
| <IconButton label="Load DISM feature table" onClick={() => void loadFeatures()}> | |
| <MdRefresh /> | |
| </IconButton> | |
| <IconButton label="Note on features output" onClick={() => selectForNote('features:snippet')}> | |
| <MdNoteAdd /> | |
| </IconButton> | |
| </div> | |
| <pre className="features-pre mono">{feat || 'Run refresh to pull optional features snapshot.'}</pre> | |
| </> | |
| )} | |
| </div> | |
| <footer className="notes-panel"> | |
| <header> | |
| <strong>Notes</strong> | |
| <span className="notes-key">{selectedKey ?? 'Select a row or volume to attach a note key.'}</span> | |
| </header> | |
| <textarea | |
| placeholder="Audit notes for the selected item…" | |
| value={noteDraft} | |
| onChange={(e) => setNoteDraft(e.target.value)} | |
| disabled={!selectedKey} | |
| /> | |
| <div className="input-row" style={{ marginBottom: 0 }}> | |
| <IconButton label="Save note" onClick={() => void saveNote()} disabled={!selectedKey}> | |
| <MdSave /> | |
| </IconButton> | |
| <IconButton | |
| label="Clear note for key" | |
| onClick={() => selectedKey && void notes.del(selectedKey)} | |
| disabled={!selectedKey} | |
| > | |
| <MdDeleteForever /> | |
| </IconButton> | |
| <IconButton label="Reload notes from disk" onClick={() => void notes.refresh()}> | |
| <MdRefresh /> | |
| </IconButton> | |
| </div> | |
| </footer> | |
| </div> | |
| </div> | |
| ) | |
| } | |