computer-auditor / src /App.tsx
algorembrant's picture
Upload 28 files
b4143a2 verified
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>
)
}